Project
AI Customer Support Agent
A support agent that classifies a ticket, finds the policy that governs it, checks refund eligibility against an order API, drafts a reply, and routes everything to a human before anything reaches a customer.
The idea
Support automation fails in two expensive ways: it refunds money it should not have, or it promises a customer something the company will not honour. Both come from letting the language model be the thing that decides. This project splits the work.
| Question | Decided by |
|---|---|
| What kind of ticket is this? | The model |
| Which policy governs it? | Vector search |
| Is a refund allowed, and for how much? | A deterministic rules engine |
| How should the reply be worded? | The model, bounded by the verdict above |
| Does a human need to see this? | A deterministic gate |
| Does anything actually happen? | A human approver |
The model writes prose around decisions it did not make.
How a ticket moves
- Input guardrails screen for prompt injection, legal threats, fraud and safety issues.
- The model classifies the ticket: category, sentiment, priority and confidence.
- Triage gate. Anything that must never get a model-written reply goes to a human here.
- The governing policy is retrieved from pgvector, the order is fetched, and the eligibility engine computes what is allowed.
- Review gate. Outside policy, above the auto-approval cap, or not grounded in any policy: a human takes it.
- The model drafts a reply and may propose a refund, a replacement, or nothing. Proposals are clamped to the verdict, and the draft is checked for overpromises, leaked personal data and amounts above the ceiling.
- Post-draft gate. A wording problem gets one retry. An overstep goes straight to a human.
-
The run suspends on LangGraph's
interrupt(), checkpointed to Postgres. Only a human approval resumes it into the one node that can issue a refund.
The full reasoning is in the repository's architecture notes.
What the reviewer sees
Evals
Forty labelled tickets. The two suites that need no API key, retrieval and decisions, run in CI, and the runner fails the build if any eligibility or escalation decision is wrong.
| Suite | Metric | Result |
|---|---|---|
| Retrieval | hit rate @4 | 1.00 |
| Retrieval | hit rate @1 | 0.85 |
| Decisions | eligibility accuracy | 1.00 |
| Decisions | escalation F1 | 1.00 |
| Decisions | missed escalations | 0 |
The first run was worse: it missed 5 of the 15 tickets that should have gone to a human. None of those were bad labels. Each traced back to a real defect, and fixing them took escalation F1 from 0.80 to 1.00. The set is small and I wrote it alongside the policies, so it measures internal consistency, not real traffic.
Running it in public
The demo runs on a single EC2 instance behind Caddy, described in Terraform, for roughly $24 a month. GitHub Actions re-runs the test suite, pushes images to ECR using OIDC rather than stored AWS keys, and rolls the instance forward with an SSM command, so port 22 stays closed. It is deliberately not highly available; the deployment notes price that tradeoff.
Stack
| API | FastAPI, Pydantic v2, SQLAlchemy 2 (async), Alembic |
|---|---|
| Agent | LangGraph with a Postgres checkpointer, Claude |
| Storage | Postgres 16 + pgvector, HNSW cosine index |
| Embeddings | fastembed with bge-small-en-v1.5, run locally |
| UI | React, TypeScript, Vite |
| Deploy | Terraform, EC2, Caddy, ECR, SSM, CloudWatch, GitHub Actions |
Known limits
- The order system is a mock. It enforces financial invariants but no company policy, on purpose, so a bug in the rules engine cannot hide behind it.
- There is no real authentication. The approver's identity is a string in the request.
- Escalation is a status, not a routed queue with owners and SLA timers.
Writing about this project
-
There is no code path from a model tool call to a refund
How an LLM support agent can propose refunds without being able to issue one. Four independent barriers, any one of which is enough on its own.