You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make extraction the first step of the Mastra workflow
Extraction was running in the API route before the workflow — an arbitrary split
that left "extraction" outside the orchestration. It's now the workflow's intake
step, so the pipeline is one Mastra flow end to end (PDF → extract → match →
investigate → approve → reconcile).
- intakeStep renders the PDF + runs vision extraction, writes an intake-document
chunk early so the reveal shows the page immediately, and passes the input
through unchanged (the pipeline still runs on the trusted seeded record).
- Fails open: a timeout/error proceeds on the record (now proven offline too,
since the mock-model integration test runs intake with no API key).
- A phase-2 resume sets skipExtraction so approve/reject doesn't re-run the
vision call (3s vs 15s).
- Route no longer does extraction; trace adapter surfaces the intake step's
document + extracted result. README/comments updated — nothing says extraction
is outside Mastra.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -63,7 +63,7 @@ The split-view dashboard shows the **invoice queue** (color-coded by outcome) an
63
63
64
64
**AI at the edges, deterministic code in the core.** The two ends of the pipeline are language/perception problems — reading a messy PDF, judging a fuzzy exception — so they use a model. The middle (is this a 9%-over variance? a duplicate? which approval tier?) is arithmetic and policy, so it's pure, unit-tested functions ([`lib/matching.ts`](lib/matching.ts), [`lib/policy.ts`](lib/policy.ts), [`lib/erp.ts`](lib/erp.ts)): exact, auditable, identical on every run. An LLM never decides a payment amount.
65
65
66
-
**Extraction reads the document; the record decides.**The vendor's PDF is rendered ([`lib/invoice-pdf.ts`](lib/invoice-pdf.ts)) and read by a vision model into a schema-validated `Invoice` ([`lib/extract.ts`](lib/extract.ts), `Invoice.safeParse`). Here's the deliberate part: the extracted invoice is *shown* (the reveal proves the read happened), but the downstream pipeline runs on the **trusted seeded record**, not the model's output. So a misread degrades the reveal, never the verdicts — the demo's edge cases stay reliable. It also fails open: if the model is slow or down, the run notes it and proceeds on the record. In production you'd reconcile extraction against the PO and surface the diff; the seam is the same.
66
+
**Extraction reads the document; the record decides.**Extraction is the **first step of the Mastra workflow** (`intake`): the vendor's PDF is rendered ([`lib/invoice-pdf.ts`](lib/invoice-pdf.ts)) and read by a vision model into a schema-validated `Invoice` ([`lib/extract.ts`](lib/extract.ts), `Invoice.safeParse`) — so it's one pipeline end to end, from PDF to ERP, not a step bolted on outside. Here's the deliberate part: the extracted invoice is *shown* (the reveal proves the read happened), but the downstream steps run on the **trusted seeded record**, not the model's output. So a misread degrades the reveal, never the verdicts — the demo's edge cases stay reliable. It also fails open: if the model is slow or down, the step notes it and the pipeline proceeds on the record. In production you'd reconcile extraction against the PO and surface the diff; the seam is the same.
67
67
68
68
**The investigator agent.** [`src/mastra/agents/investigator.ts`](src/mastra/agents/investigator.ts) is a Mastra [`Agent`](https://mastra.ai) with three tools — `get-vendor-price-history`, `get-po-notes`, `get-receipt-notes` — returning deliberately unstructured, free-text records ([`lib/vendor-context.ts`](lib/vendor-context.ts)). It runs an open-ended loop: it picks which tools to call and in what order (a quantity problem pulls the receipt notes first; a price problem pulls the price history), reads them, and writes a recommendation (`likely_legitimate` / `likely_overcharge` / `unclear`). It only *recommends* — the deterministic routing and the human gate own the outcome, so a wrong call is caught by the reviewer. Each tool call and the recommendation appear live on the trace. Tools read the trusted vendor from `requestContext`, not model-generated args, so the agent can't pull the wrong vendor's file. Model id lives in one place ([`src/mastra/model.ts`](src/mastra/model.ts)), resolved by Mastra's router (`"anthropic/claude-haiku-4-5"`).
0 commit comments