Request a consultationContact

[ Demonstration ]

Human-reviewed document extraction: a working demonstration with its failures

A demonstration of the kind of AI workflow we build: software reads invoices, ordinary code checks the result, and a person reviews anything uncertain before it is used. It ran on documents we invented, not on client data.

Prepared by Wagenberg Technology Group LLC, 17 October 2026. Copyright Wagenberg Technology Group LLC, all rights reserved.

Headline (held-out test set, 60 documents)

The problem

Accounts-payable teams retype vendor, invoice number, date, PO number, subtotal, tax, total and currency from invoices that arrive in many layouts. Automating that is easy to demo and hard to trust: the costly failure is not a document the system cannot read, it is a document it reads wrongly and accepts without anyone looking. This demonstration measures that number directly and publishes every failure.

Design: where code checks, where a person reviews

  1. Extraction (code, deterministic). Rule-based parsing of the document text layer: labelled fields, several date formats, US and European number formats, currency codes and symbols, tolerance for common OCR character confusions in labels. Each field gets a confidence score and records which rule produced it. Vendor names are matched against the buyer's vendor master list; PO numbers are checked against the buyer's own PO formats. No AI model, no network, no paid service.
  2. Validation (code). Required fields present; subtotal + tax = total to the cent; effective tax rate between 0% and 30%; date between 2024-01-01 and 30 days after a fixed reference date; duplicate by vendor + invoice number (and a softer check on vendor + total + date); credit notes flagged; positive totals on invoices.
  3. Routing (code). A document is auto-accepted only if every one of the 8 fields has confidence of at least 0.85 and every check passes. Anything else goes to the human review queue with the reasons written out. Some amounts are deliberately held below the threshold: OCR-repaired characters (0.60), ambiguous separators (0.50), totals on documents that also show another total-like figure such as a previous balance (0.75), a bare '$' from a vendor that bills in Canadian dollars (0.50).
  4. Review (a person). A local web page shows the document text beside the extracted fields, the confidence and rule for each field, the failing checks, and three keyboard-accessible actions: approve, save corrections and approve, reject. Corrected values are re-checked. Every decision, by the extractor or a reviewer, is appended to out/audit_log.jsonl.
  5. Nothing is sent anywhere. The outputs are out/accepted.jsonl, out/review_queue.jsonl, out/review_decisions.jsonl and the audit log.

Data

120 invented documents from 14 fictional vendors in the master list plus vendors deliberately left out of it, in 5 layouts (classic invoice, European invoice, supplier-portal table export, statement of charges, plain-text email). Each document has a ground-truth JSON file. Messiness was injected on purpose: several date formats, tax line present, absent or written as 'exempt', missing PO numbers, OCR-style character swaps, duplicate resubmissions in a different layout, credit notes, documents with two totals (page totals and previous balances), printed totals that do not add up, implausible dates, and UK vendors billing in US dollars with day-first dates.

Before any extractor rule was written, the documents were split with a fixed seed into 60 development and 60 held-out test documents (data/split.json). Rules were tuned by looking only at development results. The test set was first scored only after tuning stopped and the code hashes were recorded (evidence/frozen_code_sha256_*.txt). After that, the only change to src/extract.py was opening the audit log in append mode; the extraction output did not change.

Held-out set composition by layout: classic 12, email 12, euro 5, statement 7, table 24. Messiness tags in the held-out set (a document can have several): cad_written_as_bare_dollar 2, credit_note 4, duplicate_resubmission 5, implausible_date 1, no_po 12, no_tax_line 8, ocr_noise 8, printed_total_does_not_add_up 2, tax_exempt_note 7, two_totals_page_total 1, two_totals_prev_balance 2, uk_vendor_billing_usd_dayfirst_date 1.

Results on the held-out test set

MeasureCountDenominatorRate
Field correct: vendor586096.7%
Field correct: invoice_number6060100.0%
Field correct: date576095.0%
Field correct: po_number6060100.0%
Field correct: subtotal596098.3%
Field correct: tax6060100.0%
Field correct: total6060100.0%
Field correct: currency6060100.0%
All 8 fields correct546090.0%
Auto-accepted346056.7%
Sent to human review266043.3%
Wrong data among auto-accepted0340.0%
Documents that policy says need a person, routed to review1212100.0%
Reviews that turned out unnecessary82630.8%

The extraction output scored here is byte-identical (sha256 e5097ddcc9cfd41a) to the output scored the first time the test set was evaluated with the frozen code, so these are first-look held-out numbers.

Reading the same documents from their PDF renderings (via pdftotext -layout) instead of the text files gave: all fields correct 54/60; auto-accepted 34/60; wrong data among auto-accepted 0/34. The PDFs are clean digital renderings, not scans, so this only shows the PDF path works.

Every held-out failure, published

A failure is any document with at least one wrong or missing field, or any auto-accepted document that should have gone to a person.

DocLayoutRouteStopped before acceptance?What went wrong (automatic)Diagnosis
DOC-058tablehuman_reviewyesvendor: got None, expected 'Placeholder Office Goods Ltd' (conf 0.00, none)OCR noise changed the table label 'Supplier' to 'Supp1ier' and the name to 'Placeholdcr Office Goods Ltd'. The vendor-label patterns are not OCR-tolerant and the portal-export layout has no usable first line, so no vendor candidate was found. Caught: required field missing.
DOC-070classichuman_reviewyesdate: got None, expected '2026-09-13' (conf 0.00, none)Code bug, clean document. The OCR digit-repair step treats a letter next to '-' as a misread digit, so '13-Sep-2026' became '13-5ep-2026' before parsing and no date was found. Caught: required field missing.
DOC-092tablehuman_reviewyesvendor: got None, expected 'Example Supply Co.' (conf 0.00, none)Same pattern: label printed as '5upplier'. No vendor candidate. Caught: required field missing.
DOC-102classichuman_reviewyessubtotal: got '810.64', expected '8810.64' (conf 0.95, label:5ubtotal@line18)The most important failure. OCR printed '8,810.64' as 'B,810.64'. The amount pattern needs a leading digit, so it read '810.64' with 0.95 confidence: a confident, wrong value. Only the arithmetic check (810.64 + 528.64 != 9339.28) and the tax-rate check stopped it from being accepted.
DOC-110eurohuman_reviewyesdate: got None, expected '2026-04-13' (conf 0.00, none)Real OCR noise inside the month name ('13 Apri1 2026'). The parser has no fuzzy month matching. Caught: required field missing.
DOC-115statementhuman_reviewyesdate: got None, expected '2025-07-16' (conf 0.00, none)Same code bug: '16-Jul-2025' became '16-Ju1-2025'. The date line itself had no OCR noise. Caught: required field missing.

Two of these (DOC-070 and DOC-115) are a plain bug found only on the held-out set. It is left unfixed so the published numbers reproduce from the published code. The fix is to stop the digit-repair step from touching letters inside month names.

Unnecessary reviews (cost, not error)

DocLayoutWhy it was sent to a person
DOC-008emaillow confidence date (0.60)
DOC-011tablelow confidence subtotal (0.60)
low confidence total (0.60; ocr_char_fix)
DOC-012classiclow confidence currency (0.50; '$' is ambiguous for vendor default CAD)
DOC-031emaillow confidence date (0.60)
DOC-038emaillow confidence date (0.60)
DOC-071emaillow confidence total (0.75; other total-like amounts present: With your previous balance of USD 747.38 the total outstanding is USD 54,813.02.)
DOC-075tablelow confidence total (0.75; other total-like amounts present: / Total balance / 10,459.89 /)
DOC-080emaillow confidence total (0.75; other total-like amounts present: Lines 1-2 total 238.41 USD.)

The 'low confidence date (0.60)' rows on email documents come from the same digit-repair bug: the repair runs over the 30 characters after the date label, so the 'O' in a following 'PO-95594' is "repaired" and the date is marked as OCR-corrected even though it was read correctly. The bug costs reviews and missing dates; in this held-out run it did not produce a wrong accepted value.

Development history (development set only)

The development set reached 100% because the rules were tuned on it. The held-out numbers above are the honest ones.

Limits

Cost to run

$0. Python 3 standard library for extraction, evaluation and the review page. The optional PDF renderings use the reportlab library if it is installed, and the optional PDF reading path uses pdftotext. No network calls, no cloud or paid AI. One extraction pass over all 120 documents took 0.33 seconds on the machine used (Darwin arm64).

What would change with real documents

The code, the documents and the raw evidence are kept privately by Wagenberg Technology Group. For the service itself, see AI workflows and automation. For how we test software and record failures, see the sample launch-readiness review.

Talk it through

Request a consultation through the contact form on this website, or email thomas@wagenbergtech.com.