Architecture
A LangGraph agent drives four MCP servers. Every model call goes through one cost-aware gateway; every step is written to a trace the UI streams live.
System diagram
- classify
- extract
- validate
- reextract
- human_review
- validate_final
- save
- po_match
- summarize
ask: plan SQL → run_select → (fix SQL ≤ 2×) → answer
MCP servers
Streamable HTTP on a private network, each request authenticated with a shared secret. Never exposed publicly.
extraction
LLM via gateway:8101Reads the document with a vision model through the gateway. Every value comes back with a confidence, source page and evidence snippet. The model gets no tools, so text in a document cannot trigger actions.
classify_documentpurchase order, invoice or unknownextract_fieldstyped record with per-field confidencereextract_fieldsre-read only the flagged fields (strong tier)
validation
no LLM:8102Deterministic checks and the PO ↔ invoice two-way match in pure Python: no LLM, no database, no network. The same input always gives the same answer.
validate_recordarithmetic, dates, formats, low confidencematch_invoice_to_poline pairing, price/qty tolerance, remaining valuelist_rulesthe rule catalogue
sqlstore
no LLM:8103Postgres access. Reads go through a SELECT-only pipeline under a read-only role with row-level security and a timeout; writes only through typed, parameterised tools.
get_schematables and columns the reader can seerun_selectone guarded SELECT, row-cappedsave_recordtyped insert of a reviewed recordget_purchase_orderPO plus earlier invoices against itsave_match_resulttyped insert of a match result
reference
no LLM:8104Static reference data bundled with the server: vendor master, illustrative FX rates and tax rates. No external APIs, so the demo has no live dependency here.
lookup_vendorfuzzy name / tax-ID match against the vendor masterconvert_currencystatic illustrative rateslookup_tax_ratestandard / reduced VAT and sales tax
Key design decisions
Deterministic validation, not an LLM judge
Arithmetic, dates, formats and PO matching are plain Python rules with tests. They are cheap, reproducible and auditable, and a model cannot talk its way past them. The LLM only reads the document; deciding whether the numbers add up is code.
Selective re-extraction instead of full retries
When validation flags fields, only those field paths go back to the stronger (and pricier) model tier. Most documents never escalate, and the ones that do pay for a few fields, not a whole page. If the strong tier still isn’t sure, a human decides.
Row-level security for isolation
Every visitor gets an anonymous session; every query runs with the session id set and Postgres RLS policies filter by it. Model-written SQL runs as a separate read-only role with a statement timeout, so even a bad query can neither see another session’s data nor change anything.
Replay mode
The six samples have recorded runs. When the daily LLM budget is used up or live processing is off, the demo replays them with their original timing instead of failing — the public site keeps working at zero marginal cost.
MCP servers as separate processes
Each capability is its own service with its own credentials: of the tools, only sqlstore touches the database and only extraction sends a document to a model. The agent gets per-node tool allowlists (classify and extract cannot reach sqlstore), so a prompt-injected document has nothing dangerous to call.
What changes on-prem
Swap the hosted model for a vision-language model on a local GPU behind the same gateway, and drop all external APIs. The MCP boundaries, validation, review flow, RLS and evals stay as they are; SSO, an audit log and PII redaction would be added.