← Projects Built · in testing Lead engineer & architect, Global AI team 2026–

Disputes & Complaints

Every letter investigated and evidenced before a colleague opens it.

  • 5parts to every case pack
  • 2lanes, one graph
  • 3engines in parallel
  • 0target false-accepts
in a letter a dispute or a complaint, as it arrived
classifydocumentsverify the sender three engines, in parallel · nothing routed by a model
out · the case pack
  1. 01 classification against a governed knowledge base
  2. 02 documents the account, checked
  3. 03 verification who wrote, and may they
  4. 04 flags what needs an investigation
  5. 05 an action what to do, and why
every item a click from its evidence
The pack is assembled before a colleague opens the case. Their job is no longer to investigate; it is to verify what the pack says, item by item, against the evidence it cites.

The colleague's job collapses from investigate to verify.

Two things that broke. What would you have done?

What broke

Hand-marked forms were being misrouted.

What would you have done?Why, the fix, and what it changed
Why

A fair share of correspondence is a pre-printed form with checkboxes ticked by hand, and the model's opinion of which form it was looking at was being trusted over the geometry on the page.

Fix

One form type read deterministically (selection marks, polygon geometry, label proximity) and made authoritative over the model; the others read by vision under an observe-only prompt.

Result

A whole class of misroutes removed.

What broke

The classification drifted between versions of the knowledge base.

What would you have done?Why, the fix, and what it changed
Why

It's grounded on business-owned workbooks, and the early ones drifted between versions with nothing pinned.

Fix

One canonical folder, one single-purpose workbook per axis, the model prompt versioned alongside it.

Result

Organisational as much as technical. It held.

The deep dive
  • Python
  • Google ADK
  • GPT vision
  • Azure Document Intelligence
  • Microsoft Fabric
  • Azure Service Bus

A collections business receives a steady volume of letters and forms saying, in some form, this is wrong: the debt isn’t mine, the amount is wrong, I never had this account, I’m in hardship, I want to complain. Each one has to be read, matched to an account, checked against the documents on file, verified as coming from the right person, worked through an investigation checklist, and classified, before anyone can act on it. That reading and checking is where the handling time goes.

This system does the investigation asynchronously, so that when a colleague opens a case the work is already done and evidenced. They verify; they don’t investigate.

A case pack with five parts, every item a click from its evidence

For every piece of correspondence, a case pack with five parts:

  1. Classification: what the letter is (the dispute reason, the customer’s requests, the complaint type, the form type, the identity and representation fields). Each value drawn from a governed list, each with a citation into the letter.
  2. Documentation check: does the account hold at least one of the original documents that proves it can be collected, and does that document carry the fields it needs to?
  3. Verification: does the correspondent match the system of record, and if a third party is writing, do they have authority to?
  4. Investigation flags: which signals from the investigation matrix apply, split into those computable from account data and those read from statements.
  5. Summary, deadlines, and a recommended action: two lines of summary, a status chip per component, the regulatory clocks that apply, and a next action from a deterministic decision table. Every item click-through to its evidence.

The first of these is the piece that shipped first and is covered on stage one. The middle three are the investigation engines. The last is the case pack.

Two lanes behind one topic, three engines in parallel, nothing routed by a model

 letters / email ──► ingestion (normalise, dedup, PDF→page images)
                          │
                    Service Bus topic
          ┌───────────────┴───────────────┐
    priority sub                       bulk sub
    real-time lane                     bulk lane (batch API, overnight)
    (Container Apps + queue scaler)    (Container Apps Job)
          └───────────────┬───────────────┘
                   CaseOrchestrator  (deterministic ADK graph)
                          │
     DocPipeline: page prioritiser → vision extractor → schema validator (OCR fallback)
                          │
     Parallel:   documentation check ∥ verification ∥ flags
                          │
     CasePackAgent → keyword write-back to the document system → warehouse

Two lanes behind one topic because the volume is bimodal: a small stream of cases that need an answer within hours, and a large backlog that can run overnight at half the cost through the batch API. Both lanes run the same graph. The three analysis agents run in parallel because they are independent and each is latency-bound on a different dependency; this is the single biggest latency win in the design.

Orchestration is deterministic. The agents are ADK agents, but nothing routes with a model. Session state carries the case envelope; component results accumulate under namespaced keys; page images live in the artifact store and travel by reference. A colleague-facing conversational agent exists, and it is the only place a model chooses what to call.

It wraps the running stage one instead of replacing it

It’s a migration, not a greenfield build. Stage one (reading, form detection, classification, idempotent persistence) was already running as a batch processor when the full design was written. The design wraps it rather than replacing it, and a set of migration watch-outs from the stage-one code review became the first iteration’s backlog: a marker file whose shape downstream depended on, a shared database cursor that needed pooling, and paths that would not survive a container.

The document store is the flakiest dependency, so it’s isolated. Session reuse, retries with backoff and jitter, a circuit breaker, and a defined degraded state: needs review, never a stalled queue. Its authentication mechanism is due to change; the client is the only thing that knows.

One warehouse, read and written through one pattern. Account data is fetched through the platform’s registry: one SELECT per account per table, a short-TTL cache in front for customers who write more than once, and results are written back to the same warehouse. One system of record on the hot path means one connection, one credential, and one class of failure to isolate.

Regulatory clocks are config. The jurisdiction is fixed; the specific timelines are set with compliance and live in versioned config next to the decision table, so the deadline panel and the recommended action can change without a release.

Every safety-critical check fails closed. Documentation completeness, verification and fraud escalation default to the non-passing state on missing or low-confidence input. Why this matters more than it sounds.

Built end to end; the false-accept target is zero

Built end to end and in testing: the pipeline is replayed against a labelled golden set with per-component scorecards (extraction, classification, verification false-accept and false-reject, documentation false-complete, flag recall) ahead of a pilot with a manual holdout. The metrics that matter most are the asymmetric ones: the verification false-accept rate has a target of zero, because a miss there is a data breach rather than a UX inconvenience; the documentation false-complete rate has a compliance floor of one percent; high-severity flag recall, which includes a separate high-recall pass for vulnerability, is a non-negotiable.

Hand-marked forms, drifting workbooks, and fraud signals hiding in statements

Hand-marked forms. A fair share of correspondence is a pre-printed form with checkboxes ticked by hand. One form type is read deterministically: selection marks, polygon geometry and label proximity from the document reader. Two others are read by vision under an “observe only” prompt. Getting the deterministic path to be authoritative over the model’s opinion of which form it was looking at removed a whole class of misroutes.

Knowledge-base drift. The classification is grounded on business-owned workbooks, and the early ones drifted between versions with nothing pinned. The fix was organisational as much as technical: one canonical folder, one single-purpose workbook per axis, the model prompt versioned alongside it.

Where the fraud signals actually live. Most of them are in statement PDFs, not in the account data. Reading a statement once into a normalised transaction list, and having every rule consume that list, was the design move that made the flag engine tractable. It also moved the model firmly back into extraction.

Pages