WhatsApp Booking Agent
An assistant that answers a dental clinic's WhatsApp and books from the patient record. It runs live on this page. Type into it.
The clinic, the patients and the records are all invented. That is deliberate. A public demo cannot use a real clinic's patient list, and hanging fabricated records off a real business's name would imply I hold their data. So the credibility has to come from somewhere else: from the reasoning being visibly correct, rather than from the data being checkable.
The failure that shaped it
The first version was a throwaway script answering one question. Could a small model call tools reliably enough to drive a UI? It could. Then on the fourth test prompt, «сколько стоит чистка?», it replied:
Чистка зубов у нас стоит 15 000 ₸.
That script has no pricing tool and no pricing data anywhere in it. The model produced a specific figure in tenge and stated it as fact, in a business's voice, unprompted.
The invention was not the dangerous part. Where it landed was. Because the model made no tool call, that reply was also the only one on screen with no provenance to show. Every honest answer displayed its lookup. The one fabricated answer displayed nothing, and nothing reads as ordinary conversation rather than as a warning.
What the demo does about it
Every reply carries its provenance, including when there is none. A reply that queried a record shows the call that produced it. A reply that queried nothing carries an amber chip saying exactly that. The UI has no silent option to fall back on.
The chip is the tool call itself, not decoration. The highlighted row in the record panel is whatever the model actually queried, driven by the tool arguments, so it cannot show a lookup that never happened.
Facts sit behind tools rather than behind instructions. The original prompt told the model not to invent dates, procedures or promos. Price was not on that list, so it invented one. Prompt rules enumerate, and enumeration leaves gaps. The fix was structural: give it a pricing tool, and forbid asserting anything a tool did not return.
A second failure, caught by testing
With the live endpoint wired up, the first real answer said a patient's check-up
was "overdue by 223 days." It was overdue by 11. The model had taken
daysSinceLastVisit, a number a tool genuinely returned, and applied it to a
different claim.
The trace was honest and the answer was still wrong. Provenance does not protect against bad derivation. That one was fixed at the tool boundary as well: the follow-up tool now returns the overdue figure itself, so the model never does date arithmetic.
Build
| Model | Claude Haiku 4.5 via OpenRouter. A three-question booking flow does not need frontier reasoning, and latency matters more for the illusion |
| Tools | lookup_client, check_followup_due, list_eligible_promos, get_service_price, get_clinic_info |
| Guards | Server-side key, per-IP rate limit, turn and token caps, tool arguments membership-tested against the fixtures, model output rendered as plain text |
| Degradation | When the endpoint is capped or down it says so. It never falls back to something that reads like a working reply |
That last row is the same principle as the rest. A fallback that hides its own failure is worse than an error.
What this is not
There is no deployed clinic behind it, and no client engagement. The demo is the deliverable: a working argument that an agent handling someone's customers should be able to show its work, and should be legible when it cannot.