A runnable version of the clinic customer-service triage system: classify an incoming message, resolve it automatically where the rules allow, or escalate to staff with a short case summary. FastAPI backend, SQLite storage, single-page web UI.
Money, VIP status, and slot availability are never left to an LLM to decide. The system is split into three layers:
- Classification (
backend/app/classify.py) - picks one ofpricing_inquiry,new_booking,vip_cancellation,billing_dispute,urgent_complaint,other. Uses Claude ifANTHROPIC_API_KEYis set (better at reading intent), otherwise falls back to keyword matching. Either way, only a category label comes out of this step. - Business rules (
backend/app/rules.py) - the actual decision. Checks the VIP list, refund limit, transaction history, and slot table directly in the database and decides resolved-or-escalated, priority, refund amounts, reference numbers, etc. This part runs identically whether or not an API key is configured, so money and identity decisions are always deterministic and auditable. - Phrasing (
backend/app/phrasing.py) - turns the decision into the actual customer-facing text and staff summary. If Claude is available it's asked to word things naturally, but it's given the already-decided facts and told not to invent anything beyond them. Plain-English templates are the fallback (and are what runs by default with no API key), so the app works fully offline.
This means: no API key -> the whole thing still works, just with templated wording instead of Claude's phrasing. Add a key and the classification and wording both get noticeably better, without changing what gets refunded or who gets treated as VIP.
cd backend
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # optional: add your ANTHROPIC_API_KEY herecd backend
uvicorn app.main:app --reload --port 8000Then open http://localhost:8000 for the UI, or http://localhost:8000/docs for the interactive API docs.
Troubleshooting: "disk I/O error" on startup. This happens if SQLite can't get a
proper file lock on clinic.db, which can occur when the project folder sits inside a
cloud-synced directory (OneDrive, Dropbox, etc.) that intercepts file locks. If you hit
this, set DATABASE_URL in .env (or as an environment variable) to a path outside the
synced folder, e.g. DATABASE_URL=sqlite:///C:/clinic-data/clinic.db on Windows, and
re-run.
On first run the database is empty. Click "Load demo data" in the UI (or
POST /api/seed-demo) to seed the scenario this was designed around: 3 slots today,
a BHD 50 refund limit, a 4-name VIP list, a 3-item pricing KB, and a duplicate BHD 40
charge on Sara Ahmed's account for testing the billing-dispute flow.
The New customer message panel on the left is the main entry point: type a message, optionally give a name and customer type, and hit Run triage. You'll get back the classification, priority, whether it was resolved automatically, the exact customer reply, and (if escalated) the staff case summary.
The right column is the editable clinic state: today's slots, refund limit, VIP list, and pricing KB. Edit these and re-run messages to see the decisions change (e.g. drop the refund limit to BHD 30 and re-run the billing dispute test case to see it escalate instead of auto-resolving).
The Staff queue shows every case still needing a human, sorted by priority (urgent complaints first), with a one-click "Mark resolved."
| Endpoint | What it does |
|---|---|
POST /api/triage |
Submit a message, get back the full decision |
GET /api/state |
Current slots, refund limit, VIP list, pricing KB |
PUT /api/state/refund-limit |
Change the refund approval limit |
POST /api/vips / DELETE /api/vips/{id} |
Manage the VIP list |
POST /api/pricing / DELETE /api/pricing/{id} |
Manage pricing KB |
POST /api/slots / DELETE /api/slots/{id} |
Manage today's slots |
POST /api/transactions / GET /api/transactions |
Add/view transaction history |
GET /api/cases |
List cases (?status=open for the staff queue) |
POST /api/cases/{id}/resolve |
Mark a case as handled |
POST /api/seed-demo |
Reset and reload the demo scenario |
These match the five scenarios this system was originally spec'd against:
- VIP cancellation -
"Hi, I need to cancel my 11am appointment today, something came up."with name "Layla Hassan" (or any seeded VIP) -> cancels, offers remaining slots, flags a staff follow-up call. - New booking -
"I'd like to book the earliest slot you have today please."-> books the earliest open slot. - Billing dispute -
"I was charged twice for my last visit, BHD 80 came out of my account instead of 40."with name "Sara Ahmed" -> finds the duplicate BHD 40 charge, refunds it (under the BHD 50 limit), returns a reference number. - Pricing inquiry -
"How much is a full consultation and does it include follow up?"-> answers from the pricing KB. - Urgent complaint -
"This is the second time my appointment got pushed with no warning... leaving a 1 star review..."-> never auto-resolved, escalated as priority 1, sends an acknowledgment without p