Getting started
The sample documents
The 13-document fictional corpus, the four demo documents, and the manifest that defines what "working" means before any model runs.
By the end of this stage you know the data. You have looked at real (fictional) invoices and receipts in four languages, you know which documents are supposed to pass and which are supposed to fail, and you have a manifest that acts as ground truth for the rest of the build.
This is the gather-the-data phase, and it comes before any model. You cannot judge an extraction you have no expectations for.
Why a fictional corpus
Scraped invoices from the internet are a legal and privacy problem, and they rarely cover the failure cases you actually want to teach. The repo ships with a generated corpus instead. It is safe to redistribute, and every document was designed on purpose, including the broken ones.
The corpus lives in samples/generated/ with its ground truth in samples/manifest.json. Count it:
jq '{documents: length, pages: ([.[].pages] | add)}' samples/manifest.json
# {
# "documents": 13,
# "pages": 14
# }Thirteen documents, fourteen pages. Twelve invoices and one imperfect Dutch fuel receipt, spread across English, Dutch, German, and French, with multiple layouts, one two-page invoice, and one scan-quality image.
List what each document is supposed to produce:
jq -r '.[] | [.filename, .document_type, .language, .scenario] | @tsv' \
samples/manifest.jsonHere is the whole corpus at a glance, with the policy outcome each document was designed to produce:
Happy path, classic layout
ReadyHappy path, compact layout
ReadyHappy path, modern layout
ReadyHappy path, classic layout
ReadyMissing vendor VAT
Needs reviewInvalid vendor VAT
Needs reviewWrong customer VAT
Needs reviewTotal mismatch
Needs reviewMissing purchase order
WarningDuplicate invoice
Needs reviewDegraded scan
ReadyHappy path, two pages
ReadyFuel receipt
ReadyFour documents tell the story
You will process all thirteen eventually, but four of them carry the demo:
| Document | What it demonstrates |
|---|---|
02-nl-happy-compact.pdf | A normal Dutch invoice that should sail through |
06-de-invalid-vendor-vat.pdf | A VAT problem the supplier can fix, which triggers the correction email |
08-en-total-mismatch.pdf | A deterministic totals failure that no model should be allowed to wave through |
13-nl-fuel-receipt.png | A degraded petrol-station receipt, which routes to a different model and a different policy |
Open a few of them. Invoices and receipts overlap, but they do not have the same requirements. A paid fuel receipt has no purchase order, invoice number, customer VAT number, or due date, and demanding those would be wrong. That observation becomes a real design decision later, when invoices and receipts get separate validation policies.
The manifest is an evaluation contract
Decide what "working" means before calling Azure. A convincing extraction demo needs more than one good-looking response, so the manifest records, for every document, the expected type, the expected field values, and the expected policy outcome. The evaluators you run later track three kinds of evidence against it:
- Field accuracy, expected normalized values versus extracted values
- Exact documents, where every compared field matches
- Workflow result,
ready,needs_review, or provider failure after the rules run
The corpus is regenerable, so you can always reset to a known state:
cd backend
uv run --locked --no-sync python scripts/generate_samples.pyThe expected data and the evaluation criteria exist independently of Document Intelligence, Azure OpenAI, FastAPI, SQLite, and React. Nothing about this stage would change if we swapped any of those out. That independence is what makes the evaluation trustworthy.
Checkpoint
samples/manifest.jsonreports 13 documents and 14 pages- You can name the four demo documents and say what each one should produce
- You can explain why a receipt has fewer required fields than an invoice
Next: Local setup
Install the toolchain and reproduce the locked backend and frontend environments.