Spaces:
Sleeping
Sleeping
Commit Β·
3a5b10f
0
Parent(s):
seed: specs + loop scaffolding
Browse files- .claude/agents/verifier.md +33 -0
- CLAUDE.md +109 -0
- LOOP.md +103 -0
- PROGRESS.md +152 -0
- docs/01_requirements.md +145 -0
- docs/02_architecture.md +177 -0
- docs/03_data_and_extraction_spec.md +164 -0
- docs/04_project_setup.md +190 -0
- docs/05_build_plan.md +160 -0
.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.
|