Skip to content

Repository files navigation

TrustLoop

TrustLoop cover

Odi — TrustLoop Compliance Guardian mascot

Case 1 — AI-Powered Banking Dispute Automation Pipeline Tencent Cloud × UTM Hackathon 2026, AI Agent Track

Self-auditing AI Expert Team that resolves bank disputes in minutes, not hours.

TrustLoop automates the full lifecycle of a Malaysian bank complaint — from email intake to compliant customer communication — using six specialized AI Experts orchestrated behind a state machine that an independent Compliance Guardian Expert can veto at every step. Every automated decision is written to a tamper-evident, SHA-256 hash-chained ledger.

1. Architecture

Demo - https://youtu.be/VEk_3OrWTtw

TrustLoop Architecture

The 6 Experts (Skill + MCP + Data, per the WorkBuddy "Expert" formula)

# Expert Module Skill MCP / Data
1 Intake & Security backend/app/experts/intake.py Regex + OCR field extraction, AES-256-GCM PII encryption Mailtrap (mailbox), Tencent Cloud OCR
2 Classification & Governance backend/app/experts/classification.py 7-category classification, BNM urgency/SLA rules Gemini (structured JSON output)
3 Core Verification backend/app/experts/verification.py Deterministic account / transaction / NRIC / CRM name matching Mock Core Banking + CRM (Supabase tables) via MCP, Gemini (narrative only)
4 Financial Resolution backend/app/experts/resolution.py Journal-entry posting with boundary checks Supabase (journal_entries)
5 Compliance Guardian backend/app/experts/compliance_guardian.py State-machine enforcement, hash-chain ledger, similarity veto Supabase pgvector (experience_repository)
6 Customer Communication backend/app/experts/communication.py BNM/FMOS-compliant templating Mailtrap (send), WeasyPrint (PDF certificate)

Why a 5th "Compliance Guardian" Expert

In banking disputes, getting a decision wrong is worse than being a bit slow — a wrong credit damages the bank's books; a wrong rejection damages the customer relationship and can trigger FMOS escalation. Case 1's surface ask is speed (90 min → stretch target under 5 min); TrustLoop's bet is that regulators and banks actually need faster + safe to supervise. So the pipeline defaults conservative: when confidence is low, similar past cases were overturned, or identity/core checks disagree, the Guardian forces MANUAL_REVIEW instead of gambling on an auto-PASS.

Most dispute-automation prototypes treat "audit trail" as a passive database table. Here it's an active agent with veto power:

  1. State machine enforcement (backend/app/core/state_machine.py, built on transitions) — a case can never skip stages (e.g. jump straight to FINANCIALLY_RESOLVED from INTAKE). Illegal transitions are rejected and logged as VIOLATION entries, not silently ignored.
  2. Memory Flywheel read-before-write (experience_repository + pgvector) — before letting any decision through, the Guardian embeds the case summary, retrieves similar historical cases, and forces MANUAL_REVIEW if similarity confidence is low or if similar past decisions were frequently overturned on appeal. Every closed case writes its outcome back (Share × Iterate: build once, reuse N times — Expert skills and settled experience are reused across the 7 dispute categories instead of re-deriving judgment from scratch each time). Move 3: INSTALL — "read first, write back, a closed loop".
  3. Tamper-evident decision ledger (backend/app/core/ledger.py) — every decision is SHA-256-chained to the previous one. GET /cases/{id}/ledger recomputes the whole chain and returns chain_intact: true/false — this is the dashboard's "decision ledger browser" demo feature.

2. Tech stack

Layer Technology Where
AI agent build tool CodeBuddy Entire backend was built inside CodeBuddy — see submitted conversation history / screenshots for proof of usage.
Agent orchestration / NL ops console WorkBuddy (custom MCP server) workbuddy-mcp/server.py — investigators ask natural-language questions about live cases directly from WorkBuddy; see §5.
Core banking/CRM verification Custom MCP server (core-banking-mcp/, built with fastmcp) core-banking-mcp/server.py + backend/app/services/core_banking_client.py. Case 1 explicitly names MCP for this step; see §6 for why it's a subprocess bridge, not an in-process import.
OCR Tencent Cloud OCRGeneralBasicOCR, MLIDCardOCR backend/app/services/ocr.py. MLIDCardOCR natively recognizes Malaysian ID documents (MyKad/MyPR/MyTentera/MyKAS/POLIS/i-Kad), a direct fit for NRIC extraction.
LLM Google Gemini (google-genai) backend/app/services/llm.py. Structured JSON output (not free text) for classification; free-tier friendly.
Embeddings Gemini gemini-embedding-001 Memory Flywheel similarity search.
Database Supabase (Postgres + pgvector + Vault) backend/app/db.py, backend/db/schema.sql. Chosen because CodeBuddy IDE's own docs list Supabase as a first-class BaaS target.
Email (intake and outbound) Mailtrap (official Python SDK, sandbox mode) backend/app/services/mailer.py. One tool covers both directions — no domain verification needed for a hackathon demo.
PII encryption AES-256-GCM (cryptography) backend/app/security/encryption.py. Envelope encryption for NRIC/account numbers before they ever reach the database.
State machine transitions backend/app/core/state_machine.py.
PDF generation WeasyPrint Compliance certificate (GET /cases/{id}/certificate.pdf).
Backend framework FastAPI backend/app/main.py.
Frontend Next.js + React + Tailwind frontend/ — management dashboard, case detail, ledger browser, Ops Console.
Deploy Current: Render (Docker). Target: Tencent Serverless Cloud Function + KMS + TencentDB + VectorDB (see §7). Root Dockerfile for the hackathon demo runtime (bundles Pango/Cairo for WeasyPrint).

Note on the WorkBuddy Agent Mode naming: WorkBuddy's documented capability is called Work Mode (plus Skills / MCP / Expert Center), not "Agent Mode" — this README and the codebase use the correct terminology throughout.

3. How prompts drive the AI generation

Every LLM call is structured, not free-form chat, and lives in a single reviewable place per Expert:

  • Classification (backend/app/experts/classification.py): system instruction pins the model to the 7 official BNM categories and instructs conservative confidence scoring; response_schema forces machine-parsable JSON (category, confidence, reasoning) — verified live against the real Gemini API before integration.
  • Verification narrative (backend/app/experts/verification.py): the PASS/FAIL/MANUAL_REVIEW decision itself is rule-based, not LLM-based — the LLM is only asked to phrase a one-sentence audit-trail explanation of a decision that was already made deterministically. This keeps the highest-stakes decision reproducible.
  • Ops Console intent parsing (backend/app/services/ops_query.py): natural language ("this week's high-urgency mis-selling cases") is parsed into a structured intent (list_cases / explain_case / sla_status / general_stats) via response_schema; the actual data retrieval is a deterministic Supabase query — the LLM never invents numbers, only narrates real query results back in plain English.

4. QuickStart

Environment variables

External accounts needed: Supabase, Google AI Studio (Gemini), Tencent Cloud International (OCR), Mailtrap. Full credential setup steps are in backend/.env.example / frontend/.env.example comments.

  • Backend: copy backend/.env.examplebackend/.env and fill in values.
  • Core banking MCP: copy core-banking-mcp/.env.examplecore-banking-mcp/.env (same SUPABASE_URL / SUPABASE_SECRET_KEY as the backend).
  • WorkBuddy MCP (optional NL console): copy workbuddy-mcp/.env.exampleworkbuddy-mcp/.env.
  • Frontend: copy frontend/.env.examplefrontend/.env.local.

Run the backend locally

cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # then fill in real values, see `.env.example` comments

# ONE-TIME: open Supabase Dashboard -> SQL Editor -> paste db/schema.sql -> Run

# REQUIRED: set up the core-banking MCP server in its OWN venv (must stay
# separate from backend/.venv — see §6 for why mixing them breaks FastAPI)
cd ../core-banking-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # same SUPABASE_URL / SUPABASE_SECRET_KEY as backend/.env
deactivate
cd ../backend

# macOS only: WeasyPrint needs Pango/Cairo, not bundled with the Python wheel
brew install pango

# macOS only: point the dynamic linker at Homebrew's lib folder
export DYLD_LIBRARY_PATH="/opt/homebrew/lib"

source .venv/bin/activate   # back to the backend's own venv
uvicorn app.main:app --reload --port 8000
# -> http://localhost:8000/docs for interactive API docs

Linux/Render (Docker) does not need the DYLD_LIBRARY_PATH step — the Dockerfile installs the equivalent apt packages instead. The Dockerfile also sets up core-banking-mcp's venv inside the image.

Try the pipeline end-to-end

# 1. Simulate an incoming complaint email straight into the Mailtrap sandbox inbox
#    (or send one for real via the Mailtrap dashboard / API)
curl -X POST http://localhost:8000/cases/intake/poll

# 2. Run a case through the full Expert Team
curl -X POST http://localhost:8000/cases/<case_id>/process

# 3. Inspect its tamper-evident decision trail
curl http://localhost:8000/cases/<case_id>/ledger

# 4. Ask the Ops Console a natural-language question
curl -X POST http://localhost:8000/ops/query \
  -H "X-Internal-Ops-Key: <INTERNAL_OPS_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"question": "how many cases are close to breaching SLA?"}'

5. WorkBuddy runtime integration (workbuddy-mcp/)

Case 1 explicitly requires the solution to be "operable through natural language commands without requiring programming expertise". workbuddy-mcp/ is a small MCP server (built with fastmcp) exposing one tool, ask_ops_console, that forwards a question to this backend's /ops/query endpoint — the exact same endpoint the web dashboard's search box calls.

To wire it into WorkBuddy: add it as a local MCP server (stdio) pointing at workbuddy-mcp/server.py, after filling workbuddy-mcp/.env. An investigator can then ask WorkBuddy things like:

  • "What are this week's high-urgency mis-selling cases?"
  • "Why was case 4521 auto-approved?"
  • "How many cases are close to breaching their SLA?"

and get answers computed from the live case database — not a canned dev-time demo.

6. Development process

Build notes from iterating on the pipeline — integrations that took a few tries, hardening that landed once the dashboard needed real KPIs and auth, and bugs that only showed up on a live end-to-end run against real Supabase / Gemini / Tencent OCR / Mailtrap (not mocks).

Core Verification via MCP + CRM identity cross-check

Verification talks to core banking / CRM through a real MCP boundary, not an in-process DB query. A standalone core-banking-mcp/ server (built with fastmcp, own venv) exposes get_account, get_disputed_transactions, and verify_dispute. backend/app/experts/verification.py calls it via backend/app/services/core_banking_client.py.

verify_dispute also cross-checks intake NRIC and complainant name against CRM fields on the mock account (matched_nric, matched_customer_name). A mismatched identity forces MANUAL_REVIEW instead of a silent PASS.

Dependency wrinkle: importing fastmcp into the backend venv as an in-process MCP client broke FastAPIfastmcp's dependencies (starlette>=1.0, websockets>=15) hard-conflict with fastapi==0.115.6 (confirmed live: Router.__init__() got an unexpected keyword argument 'on_startup' after starlette was silently upgraded). Fix: keep core-banking-mcp/ in its own isolated venv and shell out via core-banking-mcp/mcp_client_cli.py (stdin/stdout JSON bridge). The MCP protocol call is real; the dependency trees never collide. That is also why backend/requirements.txt does NOT list fastmcp.

While wiring this, MCP-unreachable used to look identical to "account not found" (both returned matched_account: False) and would have wrongly returned FAIL on an infra outage. Those paths are now distinguished — outage → MANUAL_REVIEW.

Quantifiable metrics + /metrics/summary

Added backend/app/core/metrics.py so every case response carries processing_seconds (intake → communication sent) and age_seconds (still- open cases). GET /metrics/summary aggregates average processing time, % of PASS cases under the 5-minute stretch target, avg_processing_seconds_pass_under_5min (stretch-target cohort — excludes overnight queue-wait outliers), average classification confidence, SLA-at-risk/overdue counts, investigator workload distribution, and estimated investigator-minutes-freed (baseline 90 min/case). Feeds the frontend dashboard KPI cards, Before/After impact, and SLA alert deep-links.

AI Workload Rebalancer + /metrics/workload-rebalance

Additive, read-only endpoint (does not change /metrics/summary or perform assigns). backend/app/core/workload_rebalance.py compares open-case loads across investigators, picks move candidates from the heaviest desk (preferring overdue / at-risk / High urgency), and returns:

  • Odi-facing summary (odi_summary)
  • from_investigatorto_investigator with open counts
  • concrete move_cases (id, short id, SLA bucket)
  • expected_sla_improvement_pct (heuristic headroom estimate)

GET /metrics/workload-rebalance powers the dashboard Workload card’s info icon hover tip. Actual reassignment still goes through the existing POST /cases/{id}/assign.

Role-based access control

Auth is enforced on the API, not just stored as a profiles.role field:

  • Every /cases/* and /metrics/* endpoint requires Depends(require_identity) (Supabase JWT or internal-service key) — no credentials → 401.
  • Role is looked up from profiles by the JWT sub claim, not trusted from a custom claim (Supabase does not populate one without an Auth Hook we have not configured).
  • The load-bearing boundary: POST /cases/{id}/manual-review requires compliance_officer or managerinvestigator403; compliance officer succeeds, posts the journal entry, and writes a MANUAL_REVIEW_DECISION ledger entry with who and why.
  • backend/scripts/seed_test_users.py seeds investigator / compliance_officer Auth users + matching profiles rows for the login flow.

Read endpoints authenticate the caller but do not yet scope rows by role (e.g. investigator → only assigned cases) — deferred as a product decision, not missing plumbing.

Holiday-aware BNM working-day SLA

BNM SLA windows are working days, not calendar days — skipping only Sat/Sun under-counts Malaysian public holidays and can stamp an optimistic sla_due_at. Fixed by wiring a gazette-backed Malaysia Calendar API into the deterministic rules engine (not the LLM, not MCP):

  • backend/app/services/malaysia_calendar.py fetches and caches public holidays for the configured state (default KL), including state weekend patterns where they differ.
  • backend/app/core/urgency_rules.py advances High/Medium/Low windows only on days the calendar marks as working days.
  • Verified live against the API (e.g. Fri 2026-03-20 + 1 WD → Mon 2026-03-23, skipping Aidilfitri on Sat 21).
  • If the API is unreachable, falls back to Sat/Sun-only so classification never blocks; toggle via SLA_USE_MALAYSIA_CALENDAR / SLA_CALENDAR_STATE in env.

Dashboard SLA countdown / overdue / 48h-risk cards keep using sla_due_at — no UI change; newly classified cases simply get accurate deadlines.

Low-urgency SLA extension (request → approve)

BNM Low urgency is “20 working days + extension”. TrustLoop models that as a two-step workflow (not a silent date edit):

  1. Investigator POST /cases/{id}/sla-extension/request with reason + extra working days (capped by SLA_EXTENSION_MAX_DAYS) — only when urgency=Low, case still open, and no pending request.
  2. Compliance officer / manager POST /cases/{id}/sla-extension/decide approve/reject. Approve recalculates sla_due_at via holiday-aware extend_sla_due_at; reject leaves the deadline unchanged.
  3. Both steps write case_events + hash-chained ledger entries (SLA_EXTENSION_REQUEST / SLA_EXTENSION_DECISION). Pending state is exposed on GET /cases/{id} as pending_sla_extension.

Case detail UI: investigators see “Request extension”; officers see Approve/Reject when a request is pending. SLA countdown updates after approval with no extra dashboard wiring.

Live end-to-end run — what broke and how it was fixed

Full path on the real stack: Mailtrap sandbox email → intake → classify → verify → resolve → communicate → PDF certificate. Issues that only showed up under that run:

  • transitions auto-transitions footgun: the library auto-generates a to_<STATE>() method per state that jumps there from ANY current state — would have silently let any Expert bypass the Compliance Guardian entirely. Fixed with auto_transitions=False.
  • Hash-chain timestamp drift risk: hashing a timestamptz column's round-tripped value (instead of the exact string used to compute the hash) would cause verify_chain_integrity() to false-negative after a Postgres round trip. Fixed by storing the exact hashed string verbatim in a dedicated hashed_at text column.
  • Official mailtrap Python SDK bug (reproduced on both 2.6.0 and 2.6.1): testing_api.messages.get_list() / related typed methods crash with a Pydantic ValidationError because Mailtrap's live API sometimes returns blacklists_report_info: {"result": "error"}, a shape the SDK's model doesn't cover. Worked around by reading inbox messages via direct, schema-tolerant HTTP calls (backend/app/services/mailer.py) instead of the SDK's typed models; sending still uses the SDK (unaffected).
  • Mailtrap testing-plan rate limit: rapid-fire sends during testing hit "Too many emails per second". Added retry-with-backoff around send_compliance_email.
  • Postgres uuid has no ILIKE operator: the Ops Console's "explain case X" query originally tried partial-UUID matching via ilike, which Postgres rejects outright for uuid columns (42883). Fixed by matching the prefix in Python over a bounded recent-cases window instead.
  • PII leakage in an API response: the Ops Console's explain_case path was returning the full case row, including the encrypted NRIC/account ciphertext blobs, to the caller. Stripped before returning — least- privilege by default even for ciphertext.
  • Regex over-capture: complainant-name extraction (backend/app/experts/intake.py) originally captured up to 60 characters after "my name is", swallowing the rest of the sentence. Fixed to stop at the first clause boundary.
  • Memory Flywheel cold start: the very first case in a category will always have zero similar-case history, and the Guardian conservatively forces MANUAL_REVIEW in that situation — correct behavior, but it means a from-scratch demo needs a few historical entries seeded first to show the full auto-resolution happy path. backend/scripts/seed_experience_repository.py does this (kept as a permanent demo utility, not a throwaway script).

7. Documented simplifications (hackathon scope, not hidden gaps)

  • Mock Core Banking/CRM: no real bank system is available for a hackathon; mock_bank_accounts / mock_bank_transactions in backend/db/schema.sql is a seeded, controllable stand-in with the exact same PASS/FAIL/MANUAL_REVIEW contract Case 1 asks for. CRM identity fields (nric, customer_name) are cross-checked via MCP when intake supplies them.
  • Tencent Cloud–native target stack (not wired for this demo): the intended production shape is Serverless Cloud Function (API/runtime), KMS (PII key custody), TencentDB (relational case store), and VectorDB (Memory Flywheel embeddings). The hackathon build uses Render + Supabase Postgres/pgvector + app-level AES-256-GCM instead, because those Tencent managed services are not free-tier friendly for a short contest window. Interfaces are already isolated (db.py, encryption.py, OCR already on Tencent Cloud), so swapping onto the full Tencent stack is an infra cutover, not a redesign.
  • Bilingual (BM/EN) templates: only English templates are implemented in backend/app/templates/; Bahasa Malaysia translation via Gemini is a straightforward follow-up (LlmService.generate already supports it).

8. License

This project is licensed under the MIT License.

About

Six-agent banking dispute automation with OCR, MCP-powered verification, compliance guardrails, and auditable decision ledgers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages