AI-powered analysis of Spanish residential electricity bills (facturas eléctricas). Upload a factura PDF, get a structured breakdown of every charge, audit findings on anomalies, and savings opportunities — all in plain Spanish.
Status: Phase 1 MVP. Extracts, audits, and compares against the PVPC regulated tariff. Datadis hourly integration is Phase 2.
- Extracts every line of a Spanish electricity bill into a typed schema using Claude vision — handles all major comercializadoras (Iberdrola, Endesa, Naturgy, Octopus, Repsol, COR/PVPC, etc.) without per-layout parsers.
- Audits the extracted data against a set of deterministic rules — math integrity, IVA validity, contador rental, PVPC component allocation, otros_servicios cancellation candidates, autoconsumo opportunities, and PVPC tariff comparison.
- Surfaces findings ranked by severity (critical / warning / info) with quantified annual savings where applicable.
PVPC tariff comparison is built in (see "PVPC Comparator" in AGENTS.md). Hourly-consumption analysis remains Phase 2 — see Next Steps.
Requires Python ≥ 3.10, uv, and an Anthropic API key. For PVPC tariff comparison you also need an ESIOS token.
git clone <repo>
cd bill-advisor
uv venv
uv pip install -e .
# Create .env with API keys (unquoted values for Docker compat)
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
echo "ESIOS_TOKEN=your-esios-token" >> .env
# CLI extraction
.venv/bin/python -m bill_advisor.extraction path/to/factura.pdf
# CLI extraction + audit
.venv/bin/python -m bill_advisor.audit path/to/factura.pdf
# Streamlit UI (debug / quick demos)
.venv/bin/streamlit run app.py
# FastAPI server (production UI backend — consumed by the Next.js frontend)
.venv/bin/uvicorn api.main:app --reload --port 8000
# Docker (production)
docker build -t bill-advisor .
docker run -p 80:8501 -p 8001:8001 \
-e ANTHROPIC_API_KEY=sk-ant-... \
-e ESIOS_TOKEN=your-token \
-v ~/bill-advisor-logs:/app/logs \
bill-advisor
# Or via CI/CD: push to main → auto-deploys to LightsailOnce the FastAPI server is running on http://localhost:8000:
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | /api/health |
— | {"status": "ok"} |
| POST | /api/analyze |
multipart/form-data with pdf field |
{"factura": {...}, "findings": [...]} |
Auto-generated docs (Swagger UI) live at http://localhost:8000/docs.
Every push to main triggers a deploy via appleboy/ssh-action:
- SSH into Lightsail
git clonefresh copy of the repodocker buildthe image- Stop & remove old container
- Run new container with port mapping (80→8501, 8001→8001), API keys via
-eflags, and a volume mount for persistent logs
API keys (ANTHROPIC_API_KEY, ESIOS_TOKEN) are stored as GitHub secrets — never written to disk on the server. The .env file is for local development only.
┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ PDF bytes │ ───▶ │ extraction.py │ ───▶ │ Factura │
│ (Streamlit │ │ (Claude vision + │ │ (Pydantic) │
│ or API) │ │ tool use) │ └──────┬───────┘
└──────────────┘ └──────────────────────┘ │
│
▼
┌───────────────┐
│ audit.py │
│ (10 checks) │
│ │
│ ┌──────────┐ │
│ │ PVPC │─│──▶ pvpc_client.py
│ │ compare │ │ │ ESIOS
│ └──────────┘ │ ▼
└───────┬───────┘ ┌──────────────┐
│ │ comparator │
▼ │ .py (recalc) │
┌──────────────┐ └──────┬───────┘
│ Findings │◀─────────┘
│ (sorted by │
│ severity) │
└──────┬───────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌───────────┐ ┌──────────────┐
│ app.py │ │ api/main │ │ rag/query.py│
│ Streamlit│ │ FastAPI │ │ (corpus │
│ UI + │ │ + rate_ │ │ Q&A chat) │
│ chat │ │ limiter │ │ │
└────┬─────┘ └───────────┘ └──────────────┘
│
├─────────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ logger.py │ │ metrics.py │
│ stdout + │ │ Prometheus │
│ /app/logs/ │ │ :8001 │
└──────────────┘ └──────────────┘
Modules:
| Module | Responsibility |
|---|---|
bill_advisor/schemas.py |
Pydantic models for Factura. Single source of truth with 11 models. |
bill_advisor/extraction.py |
Claude API call: PDF → tool-use forcing the record_factura_extraida schema. System prompt + tool schema prompt-cached (~90% cost saving on repeats). |
bill_advisor/audit.py |
10 deterministic anomaly checks against a validated Factura. No LLM. The PVPC comparison check (_check_pvpc_comparison) internally delegates to comparator.py. |
bill_advisor/comparator.py |
PVPC tariff recompute engine: distributes kWh uniformly across hours per period, recomputes cost at hourly PVPC rates, returns ComparisonResult. |
bill_advisor/pvpc_client.py |
ESIOS API client — fetches indicator #1001 (PVPC 2.0TD Península hourly prices). Returns dict[date, dict[int, float]] in €/kWh. |
bill_advisor/logger.py |
Module-level logger with two handlers: stdout (Docker logs) + file (/app/logs/bill-advisor.log). Messages prefixed [Bill Advisor]. |
bill_advisor/metrics.py |
Prometheus counters and histogram with embedded HTTP server on port 8001. Tracks sessions, extractions, errors, PVPC results, chat questions, extraction duration. |
bill_advisor/rag/query.py |
Claude Sonnet 4 Q&A backed by 7 corpus .md files in rag/corpus/. Accepts messages list for conversation memory. Prompt-cached system prompt. |
api/main.py |
FastAPI server with GET /api/health and POST /api/analyze. CORS for localhost:3000. Wraps extraction + audit. |
api/rate_limiter.py |
In-memory sliding-window rate limiter: 2 req/min per IP on POST /api/analyze. Returns 429 with Retry-After. |
app.py |
Streamlit UI. Single-page: upload → extraction → audit → findings + RAG chat. Conversation stored in session state (not backend). |
Key decisions are documented as ADRs in docs/adr/. Highlights:
- ADR-0001 — Vision LLM (not OCR + regex per-layout)
- ADR-0002 — Tool-use API path, not
messages.parse(schema complexity blew the grammar compiler) - ADR-0004 — PVPC sub-block in the schema (the first extraction missed mercado mayorista entirely)
- ADR-0005 — PDF-first input strategy
- ADR-0008 — Caching strategy (~90% input-cost saving on repeated extractions)
- ADR-0009 — Direct ESIOS call (not MCP) for PVPC prices
- ADR-0010 — Uniform kWh distribution in PVPC comparison
- ADR-0011 — Deterministic audit (zero LLM)
- ADR-0012 — Defer tests and CI to post-MVP
- ADR-0013 — In-memory rate limiting (2 req/min per IP)
- ADR-0014 — Structured logging with stdlib
logging
See docs/next-steps.md. Headline items:
- Build Grafana dashboard for usage metrics
- Write unit tests for
audit.pyandcomparator.py - Phase 2 — Datadis integration for hourly consumption profiles
- Outputs are educativos, never financial or regulatory advice.
- "Ahorro estimado €X/año" figures are based on the single bill extracted and assume similar consumption patterns going forward — they're estimates, not guarantees.
- Always verify extracted values against the original factura. The extractor's
notas_extraccionflags anything that needed inference or human review.
MIT.