kennethzychew commited on
Commit
3a5b10f
Β·
0 Parent(s):

seed: specs + loop scaffolding

Browse files
.claude/agents/verifier.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: verifier
3
+ description: Independent reviewer for completed build tasks. Re-runs the test
4
+ suite and checks finished work against the specs and the acceptance criteria,
5
+ reporting discrepancies. Use as a final adversarial pass after a batch of
6
+ tasks, or whenever you want a second opinion that did not write the code.
7
+ tools: Read, Bash, Grep, Glob
8
+ ---
9
+
10
+ You are a verifier. You did not write this code, and your job is to find where
11
+ it falls short β€” not to be agreeable. The maker is too generous grading its own
12
+ work; you are the check.
13
+
14
+ When invoked, do the following and report concisely:
15
+
16
+ 1. Run the full suite and linter yourself: `uv run pytest -q` and
17
+ `uv run ruff check .`. Paste the real output. Do not trust a prior claim that
18
+ they passed β€” run them.
19
+ 2. For each task marked complete in `PROGRESS.md` (TONIGHT section), open the
20
+ corresponding code and confirm it actually satisfies that task's acceptance
21
+ criterion in `docs/05_build_plan.md` and the relevant spec in `docs/`. Look
22
+ specifically for the hard part being skipped: a function that returns a
23
+ placeholder, a test that asserts nothing meaningful, a rule that is declared
24
+ but never applied, an arithmetic check that does not actually reconcile.
25
+ 3. Confirm scope was respected: no task under **TOMORROW** was started, and no
26
+ dependency outside task **N1**'s night set was added (check `pyproject.toml`
27
+ / `uv.lock`).
28
+ 4. Confirm the core stays decoupled: `core.py` must not import from `ingest/`,
29
+ `web/`, or `store/`, and no provider SDK is called outside a backend adapter.
30
+
31
+ Report as: what genuinely passes, what is incomplete or wrong (with file and
32
+ line), and anything out of scope. If everything holds, say so plainly. If not,
33
+ list the specific gaps so the maker can fix them. Do not edit code yourself.
CLAUDE.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md β€” Working Conventions for This Project
2
+
3
+ ## What this project is
4
+
5
+ An autonomous document-extraction agent for invoices/receipts. A reusable core
6
+ pipeline (`process_document`) turns a document into a validated, structured
7
+ record and decides whether to **auto-accept** it or route it to **review**. The
8
+ core is invoked by two thin entry points (a folder watcher and a Gradio web
9
+ demo) and depends on a swappable model backend (Gemini free tier or local
10
+ Ollama). Full detail in `docs/`.
11
+
12
+ Read `docs/01_requirements.md`, `docs/02_architecture.md`,
13
+ `docs/03_data_and_extraction_spec.md`, and `docs/04_project_setup.md` before
14
+ making design decisions. Follow `docs/05_build_plan.md` for task order.
15
+
16
+ ## Tech stack
17
+
18
+ Python **3.11** (pinned via `.python-version`; `requires-python = ">=3.11"`),
19
+ **uv** (package/venv management β€” `uv add`, `uv sync`, `uv run`; commit
20
+ `uv.lock` and `.python-version`), Docling (parsing), Pydantic v2
21
+ (contract/validation), google-genai (Gemini) + local Ollama, Gradio (demo),
22
+ SQLite + CSV (storage), watchdog (watcher), pytest (tests).
23
+
24
+ ## Architectural rules (do not violate)
25
+
26
+ 1. **Core stays decoupled.** `core.py` must not import from `ingest/`, `web/`,
27
+ or `store/`. Entry points depend on the core, never the reverse. The core
28
+ returns a result object; it performs no file moves and no DB writes.
29
+ 2. **Backends sit behind the interface.** All model access goes through
30
+ `ExtractionBackend`. No entry point or core code calls a provider SDK
31
+ directly. Adding a backend = implement the interface + register in the
32
+ factory; nothing else changes.
33
+ 3. **Model identifiers are config, never literals.** Free model catalogs change
34
+ without notice β€” a hardcoded model name is a latent outage. Read names from
35
+ config; treat a missing/renamed model as a recoverable config error with a
36
+ clear message.
37
+ 4. **Structured output is enforced, not parsed.** Use schema/grammar-constrained
38
+ output (Pydantic schema for Gemini, JSON-schema/grammar for Ollama). Never
39
+ regex JSON out of free-form text.
40
+ 5. **Validation gates auto-accept.** A hard-rule failure (especially an
41
+ arithmetic cross-check) forces `review` regardless of model confidence. The
42
+ model is treated as fallible by design.
43
+ 6. **The loop never dies on one document.** Every document is processed in
44
+ isolation with try/except; failures log full context and route to review,
45
+ then processing continues.
46
+ 7. **Pure functions stay pure.** `validation/rules.py` and `routing/score.py`
47
+ do no I/O and are fully unit-tested.
48
+
49
+ ## Precision posture (important)
50
+
51
+ Optimize **precision on the auto-accepted path** for the critical fields
52
+ `total`, `tax`, `invoice_number`. A confidently-wrong number is the costly
53
+ error because it is written and propagates silently; a missing field is caught
54
+ by review. When in doubt, route to review. Recall is measured and traded
55
+ against precision via the single `CONFIDENCE_THRESHOLD`, set empirically in
56
+ evaluation. Arithmetic cross-checks are the mechanism that keeps precision high
57
+ without destroying recall.
58
+
59
+ ## Coding conventions
60
+
61
+ - Type-hint everything; keep functions small and single-purpose.
62
+ - Pydantic models are the single source of truth for the data contract.
63
+ - Fail fast on misconfiguration at startup with actionable messages.
64
+ - Structured logging (one record per document: inputs, backend, decision,
65
+ validation failures, timings). No secrets in logs.
66
+ - Bounded retries + timeouts on all network/model calls.
67
+ - No secrets in code or git. Config via `.env` / Space secrets only.
68
+ - Manage dependencies with uv; commit `uv.lock` so installs are reproducible.
69
+ Add deps via `uv add`, never by hand-editing pins. Run commands via `uv run`.
70
+
71
+ ## Testing & definition of done
72
+
73
+ - A task is done when its build-plan **[AC]** is met and tests pass.
74
+ - Pure logic (schema, validation, routing) must have unit tests before the
75
+ pipeline is wired together.
76
+ - `test_core_smoke.py` runs the pipeline end-to-end with a stub backend (no
77
+ network) so the core is testable offline.
78
+ - The watcher must survive a deliberately corrupt file (routes to review,
79
+ loop continues).
80
+
81
+ ## Privacy & cost guardrails
82
+
83
+ - Development and demo must remain **free**: local Ollama (no quota) or Gemini
84
+ free tier; Hugging Face Spaces free CPU tier for hosting.
85
+ - The public demo is **stateless** and must show a "synthetic/public documents
86
+ only" notice. Free hosted backends may train on inputs β€” never send real
87
+ financial data through the demo or a free API. Sensitive data is handled only
88
+ via the local Ollama backend.
89
+
90
+ ## Autonomous / overnight runs
91
+
92
+ - The authoritative task ledger is `PROGRESS.md`. Do the next unchecked task in
93
+ its **TONIGHT** section; **never** start a task under **TOMORROW**; **never**
94
+ add a dependency outside the night set in `PROGRESS.md` task **N1**.
95
+ - Commit cadence: **one commit per completed task**, using the message given in
96
+ `PROGRESS.md`. Never commit failing tests. Tick the task's box in the same or
97
+ an immediately following commit.
98
+ - Prove completion in the transcript: run the task's acceptance check and paste
99
+ the output β€” an unattended loop's evaluator only sees what you print.
100
+ - If blocked, record a one-line note under **BLOCKED** in `PROGRESS.md` and move
101
+ to the next task rather than halting the whole run.
102
+ - The specs in `docs/` are inputs, not work products β€” do not edit them.
103
+
104
+ ## When unsure
105
+
106
+ Prefer the choice that (a) keeps the core independent of entry points and
107
+ backends, (b) routes uncertain results to review rather than auto-accepting,
108
+ and (c) keeps the project runnable for free. Surface assumptions in code
109
+ comments and the README rather than silently deciding.
LOOP.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LOOP.md β€” How to Run This Overnight
2
+
3
+ This runs the **TONIGHT** scope in `PROGRESS.md` unattended and stops at the
4
+ `β›” STOP` boundary. It builds the deterministic core (schema, validation,
5
+ routing, backend interface + offline stub, stub-backed core pipeline). It needs
6
+ **no Gemini key and no Ollama** β€” those are tomorrow.
7
+
8
+ ## Before you start
9
+
10
+ - Be on a throwaway branch or an isolated worktree so the night's work is
11
+ contained and easy to review:
12
+ `git switch -c overnight-build` (or `git worktree add ../overnight overnight-build`).
13
+ - Have these in the repo root: `CLAUDE.md`, `PROGRESS.md`, `LOOP.md`,
14
+ `.claude/agents/verifier.md`, and the specs in `docs/`.
15
+ - Be logged into Claude Code as usual. The loop uses your existing auth; on a
16
+ subscription it counts against your normal weekly rate limit, on an API key it
17
+ bills pay-as-you-go (the `--max-budget-usd` guard below applies in that case).
18
+
19
+ ## Recommended: the driver loop (one task per iteration, commit each)
20
+
21
+ A fresh context per iteration means no context-window drift over a long night,
22
+ each task is its own commit, and a single crashed iteration just resumes from
23
+ the ledger next loop.
24
+
25
+ ```bash
26
+ #!/usr/bin/env bash
27
+ # run-overnight.sh β€” stops itself at the PROGRESS.md β›” STOP boundary
28
+ set -u
29
+ for i in $(seq 1 20); do
30
+ echo "=== iteration $i ==="
31
+ claude -p "Read PROGRESS.md, CLAUDE.md, and the specs in docs/. Do the NEXT
32
+ unchecked task in the TONIGHT section only β€” exactly one. Implement it per
33
+ docs/05_build_plan.md. Run that task's Check command and paste the output.
34
+ If it passes: commit just that task's changes with its Commit message, then
35
+ tick its box in PROGRESS.md and commit that. If it fails after reasonable
36
+ attempts: add a one-line note under BLOCKED in PROGRESS.md, commit, and move
37
+ on. NEVER start a TOMORROW task. NEVER add a dependency outside task N1's
38
+ night set. NEVER edit files in docs/. When every TONIGHT box is checked, run
39
+ 'uv run pytest -q' and 'uv run ruff check .'; if both are clean, print
40
+ exactly DONE_ALL." \
41
+ --allowedTools "Read,Edit,Write,Bash" \
42
+ --permission-mode acceptEdits \
43
+ --max-turns 25 \
44
+ --max-budget-usd 0.75 \
45
+ --output-format json | tee "run_$i.json"
46
+ if grep -q "DONE_ALL" "run_$i.json"; then
47
+ echo "All TONIGHT tasks complete."; break
48
+ fi
49
+ done
50
+ ```
51
+
52
+ Launch it and walk away:
53
+
54
+ ```bash
55
+ chmod +x run-overnight.sh
56
+ nohup ./run-overnight.sh > overnight.log 2>&1 &
57
+ ```
58
+
59
+ (`nohup ... &` keeps it running if the terminal closes. For a laptop that
60
+ sleeps, run it in `tmux` on a machine that stays awake.)
61
+
62
+ ## Alternative: a single `/goal` session
63
+
64
+ Simpler, one process. `/goal` keeps the session going until a small evaluator
65
+ model confirms the condition from the transcript, so the condition ends by
66
+ running the proof commands. It has no built-in budget, hence the turn cap.
67
+
68
+ ```bash
69
+ claude -p "/goal Every task in the TONIGHT section of PROGRESS.md is checked
70
+ off, and 'uv run pytest -q' exits 0, and 'uv run ruff check .' reports no
71
+ errors. Work one TONIGHT task at a time, top to bottom; after each task's
72
+ Check passes, commit it with its PROGRESS.md message and tick its box. Never
73
+ start a TOMORROW task. Never add a dependency outside task N1's night set.
74
+ Never edit docs/. Prove completion by running pytest and ruff and printing
75
+ 'cat PROGRESS.md' at the end. Stop after 60 turns regardless." \
76
+ --allowedTools "Read,Edit,Write,Bash" \
77
+ --permission-mode acceptEdits
78
+ ```
79
+
80
+ The driver loop is the safer choice for a long unattended night; `/goal` is
81
+ fine if you prefer one process and will glance at it.
82
+
83
+ ## Guardrails (why this is safe to leave)
84
+
85
+ - **Hard scope:** both the ledger and the prompt forbid starting TOMORROW work
86
+ or adding model dependencies, so it cannot wander into Gemini/Ollama.
87
+ - **Isolation:** running on a branch/worktree means the morning review is a
88
+ clean diff and nothing touched `main`.
89
+ - **Caps:** `--max-turns` (and `--max-budget-usd` on API billing) stop a runaway
90
+ iteration; the loop caps total iterations.
91
+ - **Broad Bash is granted** so it can run `uv`, `pytest`, and `git` β€” which is
92
+ exactly why you keep it on an isolated branch.
93
+
94
+ ## In the morning
95
+
96
+ 1. `git log --oneline` β€” you should see roughly one commit per N-task.
97
+ 2. **Read the diffs.** Agents are good at *looking* done; the test suite is the
98
+ real gate, but skim the code. `uv run pytest -q` yourself.
99
+ 3. Check `PROGRESS.md`: which TONIGHT boxes are ticked, and read any **BLOCKED**
100
+ entries β€” those are your first fixes.
101
+ 4. Then continue with TOMORROW: set up the Gemini key (and/or Ollama), implement
102
+ the Docling/OCR parsing and the two backends, wire the real `acquire` into
103
+ `core.py`, then persistence, the watcher, the web demo, and the eval harness.
PROGRESS.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROGRESS.md β€” Autonomous Build Ledger
2
+
3
+ This file is the memory for the overnight run. It outlives any single agent
4
+ session. The loop reads it at the start of every iteration, does the **next
5
+ unchecked task in the TONIGHT section only**, proves the task's acceptance
6
+ check, commits, and ticks the box.
7
+
8
+ ## Protocol (read every iteration)
9
+
10
+ 1. Do the next unchecked task under **TONIGHT**, exactly one per iteration.
11
+ 2. Implement it per `docs/05_build_plan.md` and the other specs in `docs/`.
12
+ 3. Run the task's **Check** command and paste the output (the loop's evaluator
13
+ only sees what is printed).
14
+ 4. If it passes: commit just that task's changes with the listed **Commit**
15
+ message; tick the box; commit the ledger update.
16
+ 5. If it fails after reasonable attempts: add a one-line note under **BLOCKED**,
17
+ commit, and move to the next task. Do not halt the whole run.
18
+ 6. **Never** start a task under **TOMORROW**.
19
+ 7. **Never** add a dependency outside the night set in task **N1**.
20
+ 8. The specs already exist in `docs/` β€” do not regenerate or edit them.
21
+
22
+ When every TONIGHT box is checked, run `uv run pytest -q` and
23
+ `uv run ruff check .`; if both are clean, print `DONE_ALL` and stop.
24
+
25
+ ---
26
+
27
+ ## Night dependency set (the ONLY packages to add tonight)
28
+
29
+ - Runtime: `pydantic`, `pydantic-settings`
30
+ - Dev: `pytest`, `ruff`
31
+
32
+ Everything else (docling, paddleocr/pytesseract, google-genai, ollama, gradio,
33
+ watchdog) belongs to TOMORROW and must NOT be added tonight.
34
+
35
+ ---
36
+
37
+ ## TONIGHT
38
+
39
+ - [ ] **N1 β€” Project scaffold** (build plan 0.1)
40
+ Run `uv init`; `uv python pin 3.11`; set `requires-python = ">=3.11"`. Create
41
+ the `src/doc_agent/` package tree and `tests/` from the setup-doc layout (the
42
+ `docs/` specs are already present β€” leave them). Add `.gitignore` (ignore
43
+ `data/`, `.env`, `.venv/`, caches; **commit** `uv.lock` and `.python-version`)
44
+ and an empty `README.md`. Add night deps: `uv add pydantic pydantic-settings`
45
+ and `uv add --dev pytest ruff`.
46
+ Check: `uv sync && uv run python -c "import sys; assert sys.version_info[:2]==(3,11)" && cat .python-version`
47
+ Commit: `phase 0.1: project scaffold (uv, py3.11, package layout)`
48
+
49
+ - [ ] **N2 β€” Config loader** (0.2)
50
+ `src/doc_agent/config.py` with pydantic-settings: load env, validate combos
51
+ (gemini requires key; `vision_direct` requires a multimodal backend), fail
52
+ fast with clear messages. Add `tests/test_config.py`.
53
+ Check: `uv run pytest tests/test_config.py -q`
54
+ Commit: `phase 0.2: config loader with validation`
55
+
56
+ - [ ] **N3 β€” Document schema** (1.1)
57
+ `src/doc_agent/schema/models.py`: `Document` and `LineItem` per
58
+ `docs/03_data_and_extraction_spec.md`, with money→float and date→ISO
59
+ normalizers. Add `tests/test_schema.py`.
60
+ Check: `uv run pytest tests/test_schema.py -q`
61
+ Commit: `phase 1.1: pydantic document schema + normalizers`
62
+
63
+ - [ ] **N4 β€” Validation rules** (1.2)
64
+ `src/doc_agent/validation/rules.py`: hard rules H1–H4, soft rules S1–S4,
65
+ monetary epsilon, returning a structured report. Pure functions, no I/O. Add
66
+ `tests/test_validation.py` (reconciling totals pass H2/H3; mismatches fail;
67
+ soft failures recorded without forcing review).
68
+ Check: `uv run pytest tests/test_validation.py -q`
69
+ Commit: `phase 1.2: validation rules (hard/soft + arithmetic checks)`
70
+
71
+ - [ ] **N5 β€” Confidence & routing** (1.3)
72
+ `src/doc_agent/routing/score.py`: pure `score(data, report, model_signal)` and
73
+ `route(score, report)` with hard-failure short-circuit. Add
74
+ `tests/test_routing.py` (hard fail β‡’ review regardless of score; threshold
75
+ boundary; missing required fields lower score).
76
+ Check: `uv run pytest tests/test_routing.py -q`
77
+ Commit: `phase 1.3: confidence scoring + routing decision`
78
+
79
+ - [ ] **N6 β€” Modality detection** (2.1)
80
+ `src/doc_agent/parsing/detect.py`: map a file to `native_pdf | image` by
81
+ extension/MIME. Add `tests/test_detect.py`.
82
+ Check: `uv run pytest tests/test_detect.py -q`
83
+ Commit: `phase 2.1: modality detection`
84
+
85
+ - [ ] **N7 β€” Backend interface + offline stub** (2.4 + stub)
86
+ `src/doc_agent/backends/base.py`: the `ExtractionBackend` protocol,
87
+ `BackendResult`, and a factory built from config.
88
+ `src/doc_agent/backends/stub.py`: a `StubBackend` returning deterministic,
89
+ schema-valid `Document` data with no network (this is the stub the smoke test
90
+ and CLAUDE.md reference). **Do NOT implement `gemini.py` or `ollama.py`
91
+ tonight.** Add `tests/test_backends.py` (factory returns configured backend;
92
+ unknown backend β‡’ clear error; stub returns schema-valid data).
93
+ Check: `uv run pytest tests/test_backends.py -q`
94
+ Commit: `phase 2.4: backend interface + factory + offline stub backend`
95
+
96
+ - [ ] **N8 β€” Core pipeline orchestration** (3.1, stub-backed)
97
+ `src/doc_agent/core.py`: `process_document(path) -> ExtractionResult` chaining
98
+ detect β†’ acquire β†’ `backend.extract` β†’ validate β†’ score β†’ route. Pure of
99
+ side-effects (no file moves, no DB). For tonight, define the `acquire`
100
+ interface and a minimal injectable path so the smoke test can supply a fake
101
+ payload β€” **real Docling/OCR `acquire` is a TOMORROW task**. Define the
102
+ `ExtractionResult` type here. Add `tests/test_core_smoke.py` running
103
+ end-to-end with `StubBackend` + an injected payload (no network), asserting a
104
+ populated result with a decision.
105
+ Check: `uv run pytest tests/test_core_smoke.py -q`
106
+ Commit: `phase 3.1: core pipeline orchestration (stub-backed smoke test)`
107
+
108
+ - [ ] **N9 β€” Idempotency helper** (3.2)
109
+ A content-hash helper so the same file is not reprocessed across runs. Add
110
+ `tests/test_hash.py` (same file β†’ same hash).
111
+ Check: `uv run pytest tests/test_hash.py -q`
112
+ Commit: `phase 3.2: content-hash idempotency helper`
113
+
114
+ - [ ] **N-FINAL β€” Full green gate**
115
+ Run the whole suite and linter; fix anything red until clean.
116
+ Check: `uv run pytest -q && uv run ruff check .`
117
+ Commit: `chore: full suite + lint green` (only if any fixes were needed)
118
+
119
+ ---
120
+
121
+ ## β›” STOP β€” END OF AUTONOMOUS SCOPE
122
+
123
+ Do not proceed past this line. Everything below requires API keys, a local
124
+ model server, large/finicky native dependencies, or live datasets β€” all for the
125
+ morning, supervised.
126
+
127
+ ---
128
+
129
+ ## TOMORROW (DO NOT START β€” supervised, needs setup)
130
+
131
+ - [ ] Add deferred deps: `docling`, (`paddleocr` or `pytesseract`),
132
+ `google-genai`, `ollama`, `gradio`, `watchdog`.
133
+ - [ ] 2.2 Docling parser (downloads layout models on first run).
134
+ - [ ] 2.3 OCR path (PaddleOCR/Tesseract β€” watch the Paddle/3.11 wheel risk;
135
+ fall back to pytesseract if it won't resolve).
136
+ - [ ] Wire the real Docling/OCR `acquire` into `core.py`.
137
+ - [ ] 2.5 Gemini backend β€” **needs `GEMINI_API_KEY`**.
138
+ - [ ] 2.6 Ollama backend β€” **needs a local Ollama server + pulled model**.
139
+ - [ ] 4.1 persistence (SQLite + CSV); 4.2 watcher; 4.3 Gradio web demo.
140
+ - [ ] 5 evaluation harness β€” needs datasets + a real backend; **counts against
141
+ the Gemini free-tier daily limit**, so run deliberately.
142
+ - [ ] 6 deploy to Hugging Face Spaces.
143
+
144
+ ---
145
+
146
+ ## BLOCKED
147
+
148
+ (none yet β€” the loop appends one-line entries here)
149
+
150
+ ## RUN LOG
151
+
152
+ (the loop may append short per-iteration notes here)
docs/01_requirements.md ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Requirements Specification β€” Document Extraction Agent
2
+
3
+ ## 1. Overview
4
+
5
+ An autonomous agent that ingests invoices, receipts, and similar
6
+ semi-structured financial documents, extracts their key fields into a
7
+ validated structured record, and routes anything it is not confident about
8
+ to a human review queue. The agent runs unattended over a stream of incoming
9
+ documents and is also exposed through a small public web demo.
10
+
11
+ The reasoning step (turning document text/images into structured fields) is
12
+ performed by a swappable LLM backend. Everything around it β€” triggering,
13
+ parsing, validation, confidence scoring, routing, persistence, logging β€” is
14
+ application code. The engineering value of this project is the system around
15
+ the model, not the model itself.
16
+
17
+ ## 2. Problem statement
18
+
19
+ Manually keying fields off invoices and receipts is slow and error-prone, and
20
+ the documents arrive in inconsistent formats and qualities (clean PDFs,
21
+ flatbed scans, phone photos). We want a pipeline that processes them
22
+ automatically, is confident only when it should be, and surfaces the rest for
23
+ a human β€” with measurable accuracy.
24
+
25
+ ## 3. Goals
26
+
27
+ - Ingest documents from a watched location with no human trigger per document.
28
+ - Support three input modalities: native-text PDFs, scanned images, and phone
29
+ photos.
30
+ - Extract a defined set of fields into a strict JSON schema.
31
+ - Validate extracted values, including arithmetic consistency checks.
32
+ - Assign a confidence to each document and route low-confidence documents to
33
+ review rather than auto-accepting them.
34
+ - Persist accepted records and export them to CSV.
35
+ - Provide a public demo URL where a single uploaded document is processed and
36
+ its result shown.
37
+ - Run entirely on free infrastructure and free model access.
38
+ - Be measurable against ground-truth datasets (precision, recall, F1).
39
+
40
+ ## 4. Non-goals (explicitly out of scope for v1)
41
+
42
+ - Fine-tuning or training a model. Off-the-shelf models only.
43
+ - A full review *application* with auth, multi-user workflows, or audit trails.
44
+ The review queue is a directory plus a CSV, not a product.
45
+ - Persistent multi-tenant storage in the cloud demo. The demo is
46
+ presentation-only and stateless.
47
+ - Handling non-financial document types (contracts, IDs, medical records).
48
+ - Real-time / low-latency guarantees. This is a background batch system.
49
+ - Production hardening (SLAs, horizontal scale, queue infrastructure).
50
+
51
+ ## 5. Users and usage modes
52
+
53
+ 1. **Autonomous batch mode (primary).** Operator drops files into an `inbox/`
54
+ directory (local or mounted). The agent processes each, writes accepted
55
+ records to storage, and moves uncertain ones to `review/`. No interaction
56
+ per document.
57
+ 2. **Demo mode (secondary).** A visitor uploads one document to the public web
58
+ UI and sees the extracted fields, per-field confidence, validation results,
59
+ and the accept/review decision. Nothing is persisted.
60
+
61
+ Both modes call the same core pipeline.
62
+
63
+ ## 6. Functional requirements
64
+
65
+ - **FR-1 Ingestion.** Detect new files in `inbox/` (file-watcher or poll) and
66
+ enqueue them for processing. Supported types: `.pdf`, `.png`, `.jpg`,
67
+ `.jpeg`, `.webp`, `.tif/.tiff`.
68
+ - **FR-2 Parsing / text acquisition.** For native-text PDFs, extract text and
69
+ layout. For scans/photos, obtain content either via OCR or via a multimodal
70
+ model that reads the image directly. The chosen path is backend-dependent
71
+ (see architecture).
72
+ - **FR-3 Field extraction.** Produce a JSON object conforming to the schema in
73
+ the data spec, using the active model backend with structured-output
74
+ enforcement.
75
+ - **FR-4 Validation.** Apply type/format checks and arithmetic cross-checks.
76
+ Each field carries a validation status.
77
+ - **FR-5 Confidence + routing.** Compute a document-level confidence from
78
+ model signal, validation results, and required-field completeness. If it
79
+ clears the threshold, auto-accept; otherwise route to review.
80
+ - **FR-6 Persistence.** Append accepted records to a local SQLite database and
81
+ export to CSV. Move source files to `processed/` or `review/` accordingly.
82
+ - **FR-7 Logging.** Emit structured logs for every document: inputs, backend
83
+ used, decision, validation failures, and timings. Never crash the loop on a
84
+ single bad document β€” isolate, log, and continue.
85
+ - **FR-8 Web demo.** Accept one uploaded document, run the core pipeline, and
86
+ render fields, confidence, validation, and decision. Stateless.
87
+ - **FR-9 Backend selection.** The model backend is chosen by configuration at
88
+ startup with no code change (Gemini free tier or local Ollama).
89
+ - **FR-10 Evaluation.** A harness runs the pipeline over a labelled dataset and
90
+ reports field-level precision, recall, and F1, plus document-level routing
91
+ statistics.
92
+
93
+ ## 7. Non-functional requirements
94
+
95
+ - **NFR-1 Cost.** Zero spend for development and demo. Local model = no quota;
96
+ hosted model = free tier only.
97
+ - **NFR-2 Privacy.** Free hosted backends may use inputs for training; the
98
+ public demo must process only synthetic/public documents. This must be
99
+ stated in the demo UI. Sensitive data is handled only via the local backend.
100
+ - **NFR-3 Swappability.** Adding or replacing a backend requires implementing
101
+ one interface and changing config β€” nothing else.
102
+ - **NFR-4 Robustness.** A malformed or unreadable document produces a logged
103
+ failure and a review routing, never a crash.
104
+ - **NFR-5 Reproducibility.** Pinned dependencies; deterministic config;
105
+ documented setup that runs from a clean checkout.
106
+ - **NFR-6 Portability.** The core pipeline is independent of both entry points
107
+ and of any specific host.
108
+
109
+ ## 8. Success criteria
110
+
111
+ The project is successful when:
112
+
113
+ - The agent processes a mixed batch (native PDFs + scans + phone photos)
114
+ end-to-end with no per-document intervention, persisting accepted records and
115
+ correctly diverting uncertain ones to review.
116
+ - On a held-out labelled set, **auto-accept precision on the critical fields
117
+ (`total`, `tax`, `invoice_number`) is β‰₯ 0.98**, with recall reported at that
118
+ operating point. (Rationale and method in the data spec.)
119
+ - A public demo URL processes an uploaded document of each modality and
120
+ displays a correct, validated result.
121
+ - Swapping between the Gemini and Ollama backends requires only a config
122
+ change.
123
+
124
+ ## 9. Key assumptions
125
+
126
+ - Documents are predominantly English. Multilingual handling is best-effort.
127
+ - Volume during development is low (tens to low hundreds of documents), well
128
+ within free-tier limits.
129
+ - The operator's local machine or chosen free host can run lightweight Python
130
+ continuously; the model itself runs locally (Ollama) or via free API.
131
+ - Free-tier quotas and free hosting behaviour (idle sleep, CPU-only) are
132
+ acceptable for a portfolio demo.
133
+
134
+ ## 10. Glossary
135
+
136
+ - **Auto-accept:** a document whose confidence clears the threshold and whose
137
+ record is persisted without human review.
138
+ - **Review:** a document routed to a human because confidence is below
139
+ threshold or a hard validation rule failed.
140
+ - **Critical fields:** fields where a confidently-wrong value is most costly β€”
141
+ `total`, `tax`, `invoice_number`.
142
+ - **Backend:** an implementation of the model interface that turns a document
143
+ into structured fields.
144
+ - **Core pipeline:** the host- and entry-point-independent function that takes
145
+ a document and returns an extraction result.
docs/02_architecture.md ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Technical Architecture β€” Document Extraction Agent
2
+
3
+ ## 1. Guiding principle
4
+
5
+ One reusable core; thin replaceable edges. The core is a pure-ish function:
6
+
7
+ ```
8
+ process_document(path) -> ExtractionResult
9
+ ```
10
+
11
+ Everything that triggers it (folder watcher, web upload) and everything it
12
+ depends on (the model backend) sits behind interfaces so the core never knows
13
+ which entry point invoked it or which model produced the fields. This is what
14
+ keeps the project portable across local and cloud, and it is the main thing
15
+ that makes the design read as engineering rather than prompting.
16
+
17
+ ## 2. System overview
18
+
19
+ ```
20
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
21
+ inbox/ (watcher) ─────▢ β”‚
22
+ β”‚ CORE PIPELINE │────▢ SQLite + CSV
23
+ web upload (demo) ────▢ parse β†’ extract β†’ validate β”‚ (batch mode)
24
+ β”‚ β†’ score β†’ route β”‚
25
+ β”‚ │────▢ review/ (batch)
26
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
27
+ β”‚
28
+ Model Backend (interface)
29
+ β”œβ”€β”€ GeminiBackend (free API, multimodal)
30
+ └── OllamaBackend (local, offline)
31
+ ```
32
+
33
+ In batch mode the pipeline persists and moves files. In demo mode the same
34
+ pipeline returns a result that is rendered and then discarded.
35
+
36
+ ## 3. Core pipeline stages
37
+
38
+ 1. **Intake & type detection.** Identify modality from extension/MIME:
39
+ `native_pdf`, `image` (scan or photo). This determines the parse path.
40
+ 2. **Text/representation acquisition.**
41
+ - `native_pdf`: parse with Docling to get text + layout. Optionally also
42
+ keep the page image for backends that prefer vision.
43
+ - `image`: two supported strategies, backend-dependent β€”
44
+ (a) *vision-direct*: pass the image to a multimodal backend (Gemini),
45
+ no separate OCR; (b) *ocr-then-text*: run OCR (e.g. PaddleOCR/Tesseract)
46
+ to text, then pass text to a text-only backend (Ollama small models).
47
+ 3. **Extraction.** Call `backend.extract(document_payload, schema)` and get
48
+ back a structured object plus whatever confidence signal the backend
49
+ exposes. Structured output is enforced (JSON schema / grammar), not parsed
50
+ out of free text.
51
+ 4. **Validation.** Run the rule set (types, formats, required fields,
52
+ arithmetic cross-checks). Produce a per-field validation status and a list
53
+ of failures.
54
+ 5. **Confidence scoring.** Combine model signal, validation outcome, and
55
+ required-field completeness into a single document-level score.
56
+ 6. **Routing.** Compare score to threshold and apply hard-fail overrides
57
+ (a failed critical cross-check forces review regardless of score). Emit an
58
+ `ExtractionResult` with `decision ∈ {accept, review}`.
59
+
60
+ ## 4. Components and responsibilities
61
+
62
+ - **Watcher / runner** (`ingest/`): detects new files, calls the core,
63
+ performs file moves and persistence side-effects in batch mode. Owns retry
64
+ and isolation so one document never halts the loop.
65
+ - **Parser** (`parsing/`): modality detection and text/layout acquisition.
66
+ Wraps Docling and the OCR option behind a single `acquire(path) -> Payload`.
67
+ - **Backend interface** (`backends/base.py`): defines `extract()`. Concrete
68
+ adapters: `gemini.py`, `ollama.py`. Selected by config via a factory.
69
+ - **Schema & models** (`schema/`): the Pydantic models that define the output
70
+ contract and are used to enforce structured output and to validate types.
71
+ - **Validation** (`validation/`): pure functions over the parsed object β†’
72
+ validation report. No I/O.
73
+ - **Confidence & routing** (`routing/`): pure functions β†’ score and decision.
74
+ - **Persistence** (`store/`): SQLite writer + CSV exporter. Batch mode only.
75
+ - **Web demo** (`web/`): a Gradio app that uploads one file, calls the core,
76
+ and renders the result. No persistence.
77
+ - **Eval** (`eval/`): runs the core over a labelled dataset and computes
78
+ metrics.
79
+
80
+ ## 5. Model backend abstraction
81
+
82
+ ```python
83
+ class ExtractionBackend(Protocol):
84
+ name: str
85
+ def extract(self, payload: DocumentPayload, schema: type[BaseModel]) -> BackendResult: ...
86
+
87
+ # BackendResult: { data: dict, field_confidence: dict | None, raw: Any }
88
+ ```
89
+
90
+ - **GeminiBackend.** Calls the Gemini free tier. Multimodal: accepts the page
91
+ image directly for scans/photos and text for native PDFs. Requests
92
+ schema-constrained JSON output. Used by the cloud demo (CPU host can't run a
93
+ local model) and available locally.
94
+ - **OllamaBackend.** Calls a local Ollama server (e.g. a 3B–7B model). Text-in
95
+ only, so images must go through the OCR path first. Uses grammar/JSON-schema
96
+ constrained decoding for reliable structure. Used for offline, private, and
97
+ no-quota local runs.
98
+
99
+ **Backend rule:** never hardcode a single remote model *name* in logic β€” model
100
+ identifiers are config, because free catalogs change without notice. Treat a
101
+ missing/renamed model as a recoverable config error.
102
+
103
+ ## 6. Dual entry points
104
+
105
+ - **Watcher (autonomous).** A long-running process using a filesystem watcher
106
+ (or a poll loop for portability). On a new file: `process_document()`, then
107
+ persist + move. This is the "runs on its own" capability.
108
+ - **Web demo (URL).** A Gradio interface with a single upload control. On
109
+ upload: `process_document()`, render fields + confidence + validation +
110
+ decision, then discard. Carries an explicit "synthetic/public documents
111
+ only" notice (see NFR-2).
112
+
113
+ Both are ~50–100 lines of glue. All real logic lives in the core.
114
+
115
+ ## 7. Data flow for one document
116
+
117
+ ```
118
+ file ─▢ detect modality ─▢ acquire payload (Docling | OCR | raw image)
119
+ ─▢ backend.extract(payload, schema) ─▢ raw structured data
120
+ ─▢ validate(data) ─▢ validation report
121
+ ─▢ score(data, report, model_signal) ─▢ confidence
122
+ ─▢ route(confidence, report) ─▢ {accept | review}
123
+ ─▢ [batch] persist + move file / [demo] render + discard
124
+ ```
125
+
126
+ ## 8. Confidence and routing logic
127
+
128
+ Document confidence blends three inputs:
129
+
130
+ - **Model signal** β€” backend-exposed token/field confidence where available;
131
+ otherwise treated as neutral.
132
+ - **Validation** β€” a hard-failed critical cross-check (e.g. totals don't
133
+ reconcile) forces `review` regardless of score. Soft failures reduce score.
134
+ - **Completeness** β€” missing required fields reduce score.
135
+
136
+ Routing:
137
+
138
+ ```
139
+ if any(critical_hard_failures): decision = review
140
+ elif confidence >= THRESHOLD: decision = accept
141
+ else: decision = review
142
+ ```
143
+
144
+ `THRESHOLD` is a single tunable constant. The evaluation harness exists
145
+ precisely to set it (see data spec β€” optimize auto-accept precision on
146
+ critical fields, accept the resulting recall, and report both).
147
+
148
+ ## 9. Error handling
149
+
150
+ - Per-document `try/except` in the runner; failures log full context and route
151
+ to `review/` with a reason. The loop continues.
152
+ - Backend calls are wrapped with bounded retries and timeouts; exhausted
153
+ retries route to review, they do not crash.
154
+ - Idempotency: a content hash prevents reprocessing the same file twice across
155
+ restarts.
156
+
157
+ ## 10. Technology choices (and why)
158
+
159
+ - **Python** β€” ecosystem fit for parsing/ML.
160
+ - **Docling** β€” open-source, free, structured PDF/scan parsing with layout and
161
+ tables, agent-framework friendly.
162
+ - **Pydantic** β€” the output contract doubles as a validation and
163
+ structured-output schema.
164
+ - **Gradio** β€” minimal code to a public demo UI; native to Hugging Face Spaces.
165
+ - **SQLite + CSV** β€” zero-infra local persistence and a portable export.
166
+ - **Gemini free tier + local Ollama** β€” two free inference paths covering
167
+ cloud-demo and offline/private use behind one interface.
168
+
169
+ ## 11. Deployment topology
170
+
171
+ - **Local / batch:** watcher + core + (Gemini or Ollama) + SQLite/CSV on the
172
+ operator's machine. Fully free; Ollama gives no quotas and full privacy.
173
+ - **Cloud / demo:** Gradio app + core + Gemini backend on Hugging Face Spaces
174
+ (free, CPU-only, sleeps when idle). No local model in the cloud; no
175
+ persistence. Public URL for the portfolio.
176
+
177
+ See the setup doc for concrete structure, configuration, and deployment steps.
docs/03_data_and_extraction_spec.md ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data & Extraction Specification
2
+
3
+ ## 1. Datasets
4
+
5
+ All free, all on Hugging Face and/or Kaggle. data.gov.sg is **not** a source β€”
6
+ it publishes statistical open data, not document images. The set below is
7
+ chosen to cover every required input modality *and* to provide ground-truth
8
+ labels, which the evaluation harness needs.
9
+
10
+ | Dataset | What it is | Modality covered | Labels | Where |
11
+ |---|---|---|---|---|
12
+ | **SROIE (ICDAR 2019)** | ~1,000 real scanned receipts from shops/restaurants; variable print & scan quality | Scans | company, address, date, total | HF: `Voxel51/scanned_receipts`; also `priyank-m/SROIE_2019_text_recognition` |
13
+ | **CORD** | ~11,000 Indonesian receipts captured in the wild; noisy, low quality; multi-level labels (store/menu/subtotal/total + subclasses) | Scans / in-the-wild | rich key-value + line items | HF: search `CORD` (naver-clova-ix) |
14
+ | **MC-OCR** | 2,436 receipts captured on mobile devices | Phone photos | quality + key fields | Search "MC-OCR RIVF 2021" (HF mirrors / challenge page) |
15
+ | **High Quality Invoice Images for OCR** | ~6,700 synthetic but realistic invoices | Native-style invoices | invoice fields | Kaggle: `osamahosamabdellatif/high-quality-invoice-images-for-ocr`; HF: `Voxel51/high-quality-invoice-images-for-ocr` |
16
+ | **invoices-and-receipts_ocr_v1** | Mixed invoices + receipts with OCR/structure | Mixed | structured | HF: `mychen76/invoices-and-receipts_ocr_v1` |
17
+ | **invoice-ocr-json** | Invoices with structured JSON ground truth | Native invoices | JSON key-values | HF: `GokulRajaR/invoice-ocr-json` |
18
+ | **FUNSD** | 199 noisy scanned forms | Messy scans/forms | entities + relations | HF: search `funsd` |
19
+ | **DocILE** (optional, large) | Large invoice benchmark for key-info localization & extraction | Native invoices | KILE/LIR | DocILE project (HF/registration) |
20
+
21
+ ### Recommended working split
22
+
23
+ - **Messy real receipts:** SROIE (baseline real) + CORD (noisy in-the-wild).
24
+ - **Phone photos:** MC-OCR.
25
+ - **Native invoices:** High Quality Invoice Images + `invoice-ocr-json` (has
26
+ clean JSON labels, convenient for the eval harness).
27
+ - **Messy forms (stretch):** FUNSD.
28
+
29
+ Hold out a fixed evaluation slice per source and never tune against it.
30
+
31
+ ### Licensing note
32
+
33
+ These are research/benchmark datasets with their own terms; several invoice
34
+ sets are synthetic. Use them for development, evaluation, and demo only, and
35
+ keep real/sensitive documents off the public demo and off free hosted
36
+ backends (see NFR-2). Confirm each dataset's license before any redistribution.
37
+
38
+ ## 2. Output schema
39
+
40
+ A single unified schema spans receipts and invoices; fields not present in a
41
+ given document are `null`. Define with Pydantic so the same model enforces
42
+ structured output, validates types, and serializes to storage.
43
+
44
+ ```python
45
+ class LineItem(BaseModel):
46
+ description: str | None
47
+ quantity: float | None
48
+ unit_price: float | None
49
+ amount: float | None
50
+
51
+ class Document(BaseModel):
52
+ doc_type: Literal["receipt", "invoice", "other"]
53
+ vendor_name: str | None
54
+ vendor_address: str | None
55
+ invoice_number: str | None # critical
56
+ document_date: date | None # ISO 8601
57
+ due_date: date | None
58
+ currency: str | None # ISO 4217 where detectable
59
+ line_items: list[LineItem]
60
+ subtotal: float | None
61
+ tax: float | None # critical
62
+ total: float | None # critical
63
+ # populated by the pipeline, not the model:
64
+ field_confidence: dict[str, float] = {}
65
+ validation: dict = {}
66
+ decision: Literal["accept", "review"] | None = None
67
+ ```
68
+
69
+ **Field requirements**
70
+
71
+ - Always attempt: `doc_type`, `vendor_name`, `document_date`, `total`.
72
+ - Critical (precision-prioritised): `total`, `tax`, `invoice_number`.
73
+ - Monetary fields are numbers (no currency symbols/thousands separators);
74
+ normalize during extraction.
75
+ - Dates are ISO 8601 (`YYYY-MM-DD`); store raw string alongside if parsing is
76
+ ambiguous.
77
+
78
+ ## 3. Validation rules
79
+
80
+ Validation is pure functions over the parsed `Document` β†’ a report. Two
81
+ classes of rule:
82
+
83
+ **Hard rules (a failure forces `review`):**
84
+
85
+ - `H1` All critical fields parse to the correct type when present.
86
+ - `H2` Arithmetic reconciliation, when the inputs exist:
87
+ `subtotal + tax β‰ˆ total` within a small epsilon (rounding tolerance).
88
+ - `H3` Line-item reconciliation, when line items exist:
89
+ `sum(line_items.amount) β‰ˆ subtotal` (or `total` if no subtotal).
90
+ - `H4` `total` is present and non-negative.
91
+
92
+ **Soft rules (reduce confidence, do not force review):**
93
+
94
+ - `S1` `document_date` present and plausible (not in the far future).
95
+ - `S2` `currency` resolves to a known code.
96
+ - `S3` `vendor_name` non-empty.
97
+ - `S4` Per-line arithmetic: `quantity * unit_price β‰ˆ amount`.
98
+
99
+ Epsilon for monetary comparisons accommodates rounding (e.g. Β±0.02 absolute or
100
+ a small relative tolerance, whichever is larger).
101
+
102
+ ## 4. Confidence scoring
103
+
104
+ Document confidence ∈ [0, 1] blends:
105
+
106
+ - **Model signal** (weighted) β€” backend field/token confidence where exposed;
107
+ neutral (0.5) when unavailable.
108
+ - **Validation** β€” start from model signal; subtract penalties for each soft
109
+ failure; any hard failure short-circuits to `review`.
110
+ - **Completeness** β€” penalty proportional to missing required fields.
111
+
112
+ Exact weights live in config and are set empirically via the eval harness. Keep
113
+ the function pure and unit-tested with hand-built cases.
114
+
115
+ ## 5. Routing
116
+
117
+ ```
118
+ decision = review if any hard rule fails
119
+ = accept if confidence >= THRESHOLD
120
+ = review otherwise
121
+ ```
122
+
123
+ `THRESHOLD` is one constant, tuned in evaluation.
124
+
125
+ ## 6. Evaluation methodology
126
+
127
+ This is what turns "seems to work" into evidence, and it is how the
128
+ precision/recall question is answered concretely.
129
+
130
+ **Definitions (field level, against ground truth):**
131
+
132
+ - *Precision* = correct extracted values / all values the system produced
133
+ (and auto-accepted).
134
+ - *Recall* = correct extracted values / all values present in ground truth.
135
+ - *F1* = harmonic mean.
136
+
137
+ **Procedure:**
138
+
139
+ 1. Run the core over each held-out dataset slice.
140
+ 2. Normalize predicted and gold values (numbers, dates, casing/whitespace)
141
+ before comparison.
142
+ 3. Compute precision, recall, F1 **per field** and **per critical field**.
143
+ 4. Compute document-level routing stats: % auto-accepted, % to review, and β€”
144
+ crucially β€” **precision on the auto-accepted subset** for critical fields.
145
+ 5. Sweep `THRESHOLD` and report the precision/recall trade-off curve.
146
+
147
+ **Target / operating point:**
148
+
149
+ - Optimize so **auto-accept precision on `total`, `tax`, `invoice_number` β‰₯
150
+ 0.98**, then report recall at that point. Recall "lost" to the threshold is
151
+ simply review-queue volume β€” acceptable, because the asymmetric cost favours
152
+ not writing wrong numbers. Arithmetic cross-checks (H2/H3) are the lever that
153
+ raises precision without collapsing recall, since they let confident,
154
+ internally-consistent values through while catching the inconsistent ones.
155
+
156
+ Report a small table per dataset (precision/recall/F1 per field, plus routing
157
+ stats) in the project README β€” this is the portfolio's evidence of rigor.
158
+
159
+ ## 7. Modality handling summary
160
+
161
+ - **Native PDF:** Docling β†’ text/layout β†’ backend (text or vision).
162
+ - **Scan:** vision-direct (Gemini reads the image) **or** OCR β†’ text β†’ backend.
163
+ - **Phone photo:** same as scan; vision-direct is more robust to skew/lighting,
164
+ which is why the Gemini path is preferred for the demo.
docs/04_project_setup.md ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Setup, Stack & Deployment
2
+
3
+ ## 1. Repository layout
4
+
5
+ ```
6
+ doc-extraction-agent/
7
+ β”œβ”€β”€ CLAUDE.md # conventions & guardrails for the coding agent
8
+ β”œβ”€β”€ README.md # quickstart + results table (eval evidence)
9
+ β”œβ”€β”€ pyproject.toml # project + dependency declarations (managed by uv)
10
+ β”œβ”€β”€ uv.lock # resolved, pinned dependency lock (committed)
11
+ β”œβ”€β”€ .python-version # uv interpreter pin: 3.11 (committed)
12
+ β”œβ”€β”€ .env.example # config template (no secrets committed)
13
+ β”œβ”€β”€ docs/
14
+ β”‚ β”œβ”€β”€ 01_requirements.md
15
+ β”‚ β”œβ”€β”€ 02_architecture.md
16
+ β”‚ β”œβ”€β”€ 03_data_and_extraction_spec.md
17
+ β”‚ └── 05_build_plan.md
18
+ β”œβ”€β”€ src/doc_agent/
19
+ β”‚ β”œβ”€β”€ __init__.py
20
+ β”‚ β”œβ”€β”€ config.py # loads env/config; selects backend
21
+ β”‚ β”œβ”€β”€ core.py # process_document(): the reusable pipeline
22
+ β”‚ β”œβ”€β”€ schema/
23
+ β”‚ β”‚ └── models.py # Pydantic Document, LineItem
24
+ β”‚ β”œβ”€β”€ parsing/
25
+ β”‚ β”‚ β”œβ”€β”€ detect.py # modality detection
26
+ β”‚ β”‚ β”œβ”€β”€ docling_parser.py # native PDF β†’ text/layout
27
+ β”‚ β”‚ └── ocr.py # image β†’ text (optional path)
28
+ β”‚ β”œβ”€β”€ backends/
29
+ β”‚ β”‚ β”œβ”€β”€ base.py # ExtractionBackend protocol + factory
30
+ β”‚ β”‚ β”œβ”€β”€ gemini.py # free-tier multimodal adapter
31
+ β”‚ β”‚ └── ollama.py # local model adapter
32
+ β”‚ β”œβ”€β”€ validation/
33
+ β”‚ β”‚ └── rules.py # hard/soft rules β†’ report
34
+ β”‚ β”œβ”€β”€ routing/
35
+ β”‚ β”‚ └── score.py # confidence + decision (pure)
36
+ β”‚ β”œβ”€β”€ store/
37
+ β”‚ β”‚ β”œβ”€β”€ db.py # SQLite writer
38
+ β”‚ β”‚ └── export.py # CSV export
39
+ β”‚ β”œβ”€β”€ ingest/
40
+ β”‚ β”‚ └── watcher.py # folder watcher / poll loop (batch entry)
41
+ β”‚ └── web/
42
+ β”‚ └── app.py # Gradio demo (URL entry)
43
+ β”œβ”€β”€ eval/
44
+ β”‚ β”œβ”€β”€ run_eval.py # metrics over labelled datasets
45
+ β”‚ └── datasets/ # download scripts / loaders (no data in git)
46
+ β”œβ”€β”€ data/ # gitignored: inbox/ processed/ review/ exports/
47
+ β”‚ β”œβ”€β”€ inbox/
48
+ β”‚ β”œβ”€β”€ processed/
49
+ β”‚ β”œβ”€β”€ review/
50
+ β”‚ └── exports/
51
+ └── tests/
52
+ β”œβ”€β”€ test_validation.py
53
+ β”œβ”€β”€ test_routing.py
54
+ β”œβ”€β”€ test_schema.py
55
+ └── test_core_smoke.py
56
+ ```
57
+
58
+ ## 2. Stack
59
+
60
+ - **Runtime:** Python **3.11**, pinned via `.python-version` (`uv python pin
61
+ 3.11`). Chosen over 3.12 for broadest wheel coverage across the Torch-based
62
+ Docling stack and PaddleOCR/PaddlePaddle, which lags newest Pythons.
63
+ Declared range: `requires-python = ">=3.11"`.
64
+ - **Package manager:** `uv` (manages the venv, resolves and locks deps via
65
+ `uv.lock`; add deps with `uv add`, run with `uv run`).
66
+ - **Parsing:** `docling` (native PDF/scan structure). Optional OCR:
67
+ `paddleocr` or `pytesseract` + system Tesseract.
68
+ - **Modeling:** `google-genai` (Gemini free tier) and a local `ollama` server
69
+ (e.g. `qwen2.5:7b` or a 3B variant) reached over HTTP.
70
+ - **Contract/validation:** `pydantic` v2.
71
+ - **Web demo:** `gradio`.
72
+ - **Storage:** stdlib `sqlite3` + `csv`.
73
+ - **Watcher:** `watchdog` (or a stdlib poll loop for max portability).
74
+ - **Config:** `pydantic-settings` / `python-dotenv`.
75
+ - **Testing:** `pytest`.
76
+
77
+ Dependencies are declared in `pyproject.toml` and pinned via the committed
78
+ `uv.lock` (`uv sync` installs exactly that lock). Do not float the model
79
+ identifier in code β€” it is config (see guardrails).
80
+
81
+ ## 3. Configuration (`.env.example`)
82
+
83
+ ```
84
+ # Backend selection: "gemini" | "ollama"
85
+ EXTRACTION_BACKEND=gemini
86
+
87
+ # Gemini (free tier via Google AI Studio key; no card required)
88
+ GEMINI_API_KEY=
89
+ GEMINI_MODEL=gemini-flash-latest # identifier is config, not hardcoded
90
+
91
+ # Ollama (local)
92
+ OLLAMA_HOST=http://localhost:11434
93
+ OLLAMA_MODEL=qwen2.5:7b
94
+
95
+ # Image handling: "vision_direct" | "ocr_then_text"
96
+ IMAGE_STRATEGY=vision_direct # vision_direct requires a multimodal backend
97
+
98
+ # Routing
99
+ CONFIDENCE_THRESHOLD=0.85 # tuned via eval
100
+
101
+ # Paths (batch mode)
102
+ INBOX_DIR=./data/inbox
103
+ PROCESSED_DIR=./data/processed
104
+ REVIEW_DIR=./data/review
105
+ EXPORT_DIR=./data/exports
106
+ DB_PATH=./data/agent.db
107
+ ```
108
+
109
+ `config.py` validates these at startup and fails fast with a clear message if,
110
+ e.g., `gemini` is selected with no key, or `vision_direct` is selected with a
111
+ text-only backend.
112
+
113
+ ## 4. Local setup
114
+
115
+ ```bash
116
+ # 1. Pin the interpreter to 3.11 (writes .python-version; uv fetches it if absent)
117
+ uv python pin 3.11
118
+
119
+ # 2. Install (uv creates the venv on 3.11 and installs from pyproject.toml + uv.lock)
120
+ uv sync
121
+
122
+ # 3a. Gemini path: get a free AI Studio key, put it in .env
123
+ # (free tier, no credit card; quota resets daily)
124
+
125
+ # 3b. Ollama path (offline/private):
126
+ # install Ollama, then:
127
+ ollama pull qwen2.5:7b
128
+ # set EXTRACTION_BACKEND=ollama and IMAGE_STRATEGY=ocr_then_text
129
+
130
+ # 4. Create working dirs
131
+ mkdir -p data/{inbox,processed,review,exports}
132
+ ```
133
+
134
+ ## 5. Running
135
+
136
+ **Autonomous batch mode:**
137
+
138
+ ```bash
139
+ uv run python -m doc_agent.ingest.watcher
140
+ # drop files into data/inbox/ β€” accepted records land in SQLite + data/exports/,
141
+ # uncertain ones move to data/review/
142
+ ```
143
+
144
+ **Web demo (local):**
145
+
146
+ ```bash
147
+ uv run python -m doc_agent.web.app
148
+ # opens a Gradio URL; upload one document to see fields + confidence + decision
149
+ ```
150
+
151
+ **Evaluation:**
152
+
153
+ ```bash
154
+ uv run python eval/run_eval.py --dataset sroie --split holdout
155
+ # prints per-field precision/recall/F1 and auto-accept precision on critical fields
156
+ ```
157
+
158
+ ## 6. Deployment to Hugging Face Spaces (free public demo URL)
159
+
160
+ 1. Create a new **Space** β†’ SDK: **Gradio** (free CPU tier). Set the Space's
161
+ Python to **3.11** (the `python_version: "3.11"` field in the Space README
162
+ metadata) so the deployed runtime matches the pinned local interpreter.
163
+ 2. Add `app.py` at the Space root that imports and launches
164
+ `doc_agent.web.app` (or copy the web entry there), plus a `requirements.txt`
165
+ the Gradio builder can read β€” generate it from the uv-managed project rather
166
+ than hand-maintaining it: `uv export --no-hashes --no-dev -o requirements.txt`.
167
+ 3. Set **Repository secrets** in the Space: `GEMINI_API_KEY`,
168
+ `EXTRACTION_BACKEND=gemini`, `IMAGE_STRATEGY=vision_direct`,
169
+ `GEMINI_MODEL=gemini-flash-latest`.
170
+ 4. Push; the Space builds and serves a public URL.
171
+
172
+ **Free-tier realities to design around (and to note in the UI):**
173
+
174
+ - CPU-only and the Space **sleeps when idle** β†’ first request after idle has a
175
+ cold start. This is why the cloud demo uses the **Gemini API** for inference
176
+ rather than a local model, and why `vision_direct` (no heavy OCR in the
177
+ Space) is the demo's image path.
178
+ - **Stateless:** no persistent DB in the demo. Show the result; don't store it.
179
+ - **Privacy:** the free Gemini tier may use inputs for training, so the demo
180
+ must display a "synthetic/public documents only" notice and must not be used
181
+ for real financial data.
182
+
183
+ ## 7. What stays free
184
+
185
+ - **Inference:** local Ollama (no quota, private) or Gemini free tier
186
+ (~1,500 req/day, resets daily, no card) β€” far above dev volume.
187
+ - **Hosting:** Hugging Face Spaces free CPU tier for the public demo.
188
+ - **Storage:** local SQLite/CSV; nothing paid.
189
+
190
+ No component requires a credit card or paid plan for development or demo.
docs/05_build_plan.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build Plan β€” Ordered Tasks for Claude Code
2
+
3
+ Execute phases in order. Each task lists what to build and its acceptance
4
+ criteria. Every phase ends in something runnable or testable. Keep the core
5
+ decoupled from entry points and backends throughout (see CLAUDE.md).
6
+
7
+ Legend: **[AC]** = acceptance criteria.
8
+
9
+ ---
10
+
11
+ ## Phase 0 β€” Scaffolding
12
+
13
+ **0.1 Project skeleton.** Create the repo layout from the setup doc and
14
+ initialize the project with **uv** (`uv init`). Pin the interpreter to 3.11
15
+ (`uv python pin 3.11`) and set `requires-python = ">=3.11"` in `pyproject.toml`.
16
+ Add dependencies via `uv add` (committing `uv.lock`); add `.env.example` and
17
+ `.gitignore` (ignore `data/`, `.env`, `.venv/`, caches β€” but **commit**
18
+ `uv.lock` and `.python-version`); add an empty `README.md`.
19
+ **[AC]** `.python-version` reads `3.11`; `uv sync` succeeds and
20
+ `uv run python -c "import sys, doc_agent; assert sys.version_info[:2]==(3,11)"`
21
+ passes.
22
+
23
+ **0.2 Config loader.** Implement `config.py` using pydantic-settings: load env,
24
+ validate combinations (gemini requires key; vision_direct requires multimodal
25
+ backend), fail fast with clear messages.
26
+ **[AC]** Unit test: invalid combos raise; valid config parses.
27
+
28
+ ---
29
+
30
+ ## Phase 1 β€” Contract & validation (pure, no model yet)
31
+
32
+ **1.1 Schema.** Implement `schema/models.py` (`Document`, `LineItem`) per the
33
+ data spec, with normalization helpers (money β†’ float, date β†’ ISO).
34
+ **[AC]** `test_schema.py`: valid dicts parse; malformed money/date handled.
35
+
36
+ **1.2 Validation rules.** Implement `validation/rules.py`: hard rules H1–H4,
37
+ soft rules S1–S4, monetary epsilon, returning a structured report.
38
+ **[AC]** `test_validation.py`: reconciling totals pass H2/H3; mismatches fail;
39
+ soft failures recorded without forcing review.
40
+
41
+ **1.3 Confidence & routing.** Implement `routing/score.py`: pure
42
+ `score(data, report, model_signal) -> float` and
43
+ `route(score, report) -> decision`, with hard-failure short-circuit.
44
+ **[AC]** `test_routing.py`: hard failure β‡’ review regardless of score;
45
+ threshold boundary behaves; missing required fields lower score.
46
+
47
+ These three modules are fully testable before any model exists.
48
+
49
+ ---
50
+
51
+ ## Phase 2 β€” Parsing & backends
52
+
53
+ **2.1 Modality detection.** `parsing/detect.py`: map file β†’ `native_pdf` |
54
+ `image` by extension/MIME.
55
+ **[AC]** Correctly classifies the supported extensions.
56
+
57
+ **2.2 Docling parser.** `parsing/docling_parser.py`: native PDF β†’ text/layout
58
+ payload; optionally retain page image.
59
+ **[AC]** A sample text PDF yields non-empty structured text.
60
+
61
+ **2.3 OCR path (optional strategy).** `parsing/ocr.py`: image β†’ text via
62
+ PaddleOCR/Tesseract, behind the same payload interface.
63
+ **[AC]** A sample receipt image yields text; absence of the OCR engine degrades
64
+ gracefully with a clear error.
65
+
66
+ **2.4 Backend interface + factory.** `backends/base.py`: the
67
+ `ExtractionBackend` protocol, `BackendResult`, and a factory that builds the
68
+ backend from config.
69
+ **[AC]** Factory returns the configured backend; unknown backend β‡’ clear error.
70
+
71
+ **2.5 Gemini backend.** `backends/gemini.py`: multimodal call to the free tier;
72
+ text for native PDFs, image for scans/photos; schema-constrained JSON output;
73
+ bounded retries + timeout; model id from config.
74
+ **[AC]** Given a sample document, returns schema-valid JSON; transient errors
75
+ retry then route to review (don't crash).
76
+
77
+ **2.6 Ollama backend.** `backends/ollama.py`: local call with JSON-schema/grammar
78
+ constrained decoding; text-in (pairs with OCR path).
79
+ **[AC]** With a local model present, returns schema-valid JSON for a text
80
+ payload. Skipped/marked if no local server (documented).
81
+
82
+ ---
83
+
84
+ ## Phase 3 β€” Core pipeline
85
+
86
+ **3.1 Assemble core.** `core.py`: `process_document(path) -> ExtractionResult`
87
+ chaining detect β†’ acquire β†’ extract β†’ validate β†’ score β†’ route. Pure of
88
+ side-effects (no file moves, no DB) β€” returns a result object.
89
+ **[AC]** `test_core_smoke.py`: runs end-to-end on a couple of sample files with
90
+ a stub backend; returns a populated result with a decision.
91
+
92
+ **3.2 Idempotency.** Content-hash helper so the same file isn't reprocessed.
93
+ **[AC]** Same file hashed identically across runs.
94
+
95
+ ---
96
+
97
+ ## Phase 4 β€” Entry points
98
+
99
+ **4.1 Persistence.** `store/db.py` (SQLite append of accepted records) and
100
+ `store/export.py` (CSV export).
101
+ **[AC]** Accepted record persists and appears in CSV; schema columns match.
102
+
103
+ **4.2 Watcher / batch runner.** `ingest/watcher.py`: watch (or poll) `inbox/`,
104
+ call core, persist accepted, move source to `processed/` or `review/`,
105
+ per-document try/except with structured logging.
106
+ **[AC]** Dropping a batch (mixed PDFs/scans/photos) processes all; one
107
+ deliberately corrupt file routes to review with a logged reason and does not
108
+ stop the loop.
109
+
110
+ **4.3 Web demo.** `web/app.py`: Gradio single-upload UI rendering fields,
111
+ per-field confidence, validation report, and decision; explicit
112
+ "synthetic/public only" notice; stateless.
113
+ **[AC]** Local Gradio URL processes one upload of each modality and displays a
114
+ correct, validated result.
115
+
116
+ ---
117
+
118
+ ## Phase 5 β€” Evaluation
119
+
120
+ **5.1 Dataset loaders.** `eval/datasets/`: scripts to fetch a held-out slice of
121
+ SROIE and the labelled invoice JSON set; map gold labels to the schema. No data
122
+ committed to git.
123
+ **[AC]** Loader yields (document, gold) pairs for a slice.
124
+
125
+ **5.2 Metrics harness.** `eval/run_eval.py`: run core over a slice; normalize;
126
+ compute per-field and per-critical-field precision/recall/F1; compute
127
+ auto-accept precision on critical fields; sweep threshold.
128
+ **[AC]** Prints a metrics table; produces the precision/recall trade-off across
129
+ thresholds; recommends a `CONFIDENCE_THRESHOLD`.
130
+
131
+ **5.3 Tune & record.** Set `CONFIDENCE_THRESHOLD` so auto-accept precision on
132
+ critical fields β‰₯ 0.98; record the resulting recall.
133
+ **[AC]** README contains a results table per dataset (precision/recall/F1 +
134
+ routing stats) β€” the portfolio evidence.
135
+
136
+ ---
137
+
138
+ ## Phase 6 β€” Deploy & document
139
+
140
+ **6.1 Hugging Face Space.** Add Space `app.py` and a `requirements.txt` for the
141
+ Gradio builder, generated from uv (`uv export --no-hashes --no-dev -o
142
+ requirements.txt`); set secrets (`GEMINI_API_KEY`, backend=gemini,
143
+ vision_direct). Deploy.
144
+ **[AC]** Public URL processes an uploaded document of each modality.
145
+
146
+ **6.2 README.** Quickstart (both modes), the swappable-backend explanation, the
147
+ results table, the demo URL, and the free-tier/privacy caveats.
148
+ **[AC]** A reader can run locally from scratch and understands the design and
149
+ the evidence.
150
+
151
+ ---
152
+
153
+ ## Build order rationale
154
+
155
+ Pure logic first (Phases 1) so the hard-to-test parts (validation, routing) are
156
+ locked down before any model variance enters. Parsing and backends next
157
+ (Phase 2), then the core that composes them (Phase 3). Entry points (Phase 4)
158
+ are thin glue added only once the core is proven. Evaluation (Phase 5) sets the
159
+ one tunable threshold with evidence. Deployment (Phase 6) is last and small
160
+ because the core was kept host-independent from the start.