Skip to content

Evaluating your corpus

The repo ships an evaluation harness (scripts/eval_extraction.py) that measures the platform per stage against your own ground truth: OCR recall in isolation (no LLM cost), then extraction accuracy end-to-end. It is the referee for every pipeline or template change.

  • A running stack (the harness talks to the API for OCR and to your LLM endpoint for extraction).
  • A corpus directory with one *_truth.json per document, e.g. data/in/invoice-001.pdf + data/in/invoice-001_truth.json.
  • Host-side environment: DATABASE_URL pointing at the stack’s Postgres, and CONNECTION_ENCRYPTION_KEYS from your .env if you use --from-infra.

1. Fetch — OCR the corpus once and cache canonicals

Section titled “1. Fetch — OCR the corpus once and cache canonicals”
Terminal window
python scripts/eval_extraction.py fetch \
--api-url http://localhost:8080 --api-key rk_... \
--data-dir data/in --cache-dir data/eval_cache

Submits one canonical-mode batch, polls to terminal, and caches every CanonicalOcrDocument. Re-attach to an interrupted fetch with --batch-id. After a pipeline upgrade, re-OCR the same corpus with POST /v1/batches/{batch_id}/reprocess (or the Reprocess all button on the batch detail page) and re-run fetch --batch-id to refresh the cache — no resubmission, no dedup interference.

2. OCR audit — is the value even in the canonical?

Section titled “2. OCR audit — is the value even in the canonical?”
Terminal window
python scripts/eval_extraction.py ocr-audit --data-dir data/in --cache-dir data/eval_cache

Checks every populated ground-truth value for presence in the cached canonicals with recall-biased matching (European number formats, date separators and prose months, separator-tolerant identifiers). No LLM involved — a MISSING here is an OCR-stage problem, and no template iteration can fix it. Fix OCR first; everything downstream is capped by this table.

3. Run — extraction accuracy against ground truth

Section titled “3. Run — extraction accuracy against ground truth”
Terminal window
python scripts/eval_extraction.py run \
--data-dir data/in --cache-dir data/eval_cache \
--template my_templates/my-invoice.json \
--from-infra --max-concurrent 4 \
--report-out eval/reports/run-001.json

--from-infra resolves the LLM URL, served model, backend, credentials, and max_model_len from the platform infra_endpoints row — the same source of truth the worker uses, so the harness can never silently disagree with production about context-window budgets. (Without it, pass --llm-url, --llm-model, --backend, --llm-api-key, and export LLM_CONTEXT_WINDOW to match your model.)

The report scores every field with schema-aware rules and prints per-field pct_ok, the document PASS/PARTIAL/FAIL split, and the decision distribution (accept vs review).

For production invoice evals, leave the 2.4.1 extraction-quality defaults on: EXTRACTION_POSTPROCESSING_ENABLED=true, EXTRACTION_EVIDENCE_PLANNING_ENABLED=true, EXTRACTION_CHUNK_MERGE_STRATEGY=field_aware, and TAX_ID_COUNTRY_PREFIX_MODE=infer. These match the worker path: long/noisy documents use focused evidence pages, chunked runs merge totals/tax recap fields with field-aware rules, and normalized values keep raw-value audit metadata in the extraction payload.

4. Iterating a template without overfitting (split protocol)

Section titled “4. Iterating a template without overfitting (split protocol)”

Tuning a template against the same documents you score on will overfit — the numbers go up while real-world accuracy doesn’t. Use a train/holdout split:

  1. Split the corpus deterministically (e.g. sort filenames, alternate halves).
  2. Iterate the template only against the train half — by hand, or with the template forge (give the forge only the train files + their ground truth).
  3. Score each candidate on both halves with --only:
Terminal window
python scripts/eval_extraction.py run ... --only "inv-001*,inv-003*,inv-005*" --report-out train.json
python scripts/eval_extraction.py run ... --only "inv-002*,inv-004*,inv-006*" --report-out holdout.json
  1. Accept a candidate only when the holdout improves too. A train-only improvement is memorisation, not a better template.
  • ocr-audit recall low → OCR stage: check layout policy, page ranges, source quality. Template work cannot help.
  • run conversion low on OCR-present values → extraction stage: template semantics, schema, prompt overlay.
  • decisions all review → calibration: look at flag_kinds in the report (confidence thresholds, validation rules, judge flags).