012026Personal project
Overflow — AI Receptionist
An AI phone agent that answers overflow calls for a business that already has a staffed front desk — the calls that arrive when nobody picks up, the line is busy, or the business is closed. Escalation runs as a code path evaluated before the model is called, not as an instruction inside a prompt, so it cannot be argued out of firing and does not drift when the model changes.
- tests on the escalation rules
- 112
- rules, run before the model
- 5
- tests in the suite
- 1,715
Scope
- A working system I have run real calls through. It is not deployed serving a live business, and this was built as my own project rather than for a client.
- The demo business in every screen here — Seaside Spa — is fictional, with seeded data.
- The four booking adapters are built and unit-tested. None has been run against a live provider account.
- SMS has no carrier connected in this build. Messages are composed, recorded and shown in the console rather than delivered.
- End-to-end call latency has not been measured, so I quote no latency figure anywhere on this page.
- Turn-taking and barge-in are tuned. Accent and short-utterance robustness are not, and I claim neither.
01One call
Nine seconds, three lines.
The whole product fits in one call record. It discloses what it is, it recognises a medical concern, and it stops — rather than trying to help.
One call
Call detail
+1 (619) 555-0100
ESC-2 · Medical concern
The caller mentions a reaction, irritation, burn, allergy or other medical concern. The agent stopped talking and handed the caller to a person. It made no attempt to resolve the issue itself.
agent
Hi, thanks for calling Seaside Spa. I’m an AI assistant, not a human, and this call is recorded. Our team is with clients right now, so I’m picking up. How can I help?
Line 1 is the AI and recording disclosure, written against California AB 2905 and SB 1001. The call cannot proceed until it has played, and it is generated and tracked server-side rather than set in the voice platform.
caller
my skin is burning after the peel
conf 0.88The turn reaches the guard before anything is asked of a model. ESC-2 matches on the second rule of five.
agent
That is something the team needs to hear about directly. I am putting you through to a person right now.
No reply was built. The agent stopped talking and handed the caller to a person, making no attempt to resolve the issue itself.
02Call path
Their phone system keeps the call.
The business keeps its own phone system. Overflow is a forwarding rule inside it. The staff line rings first, for a set number of seconds. If a person answers, the agent never joins the call at all. If nobody does, the call forwards and the agent picks up, recording which condition caused it.
In production the phone system has usually already decided why it is forwarding and passes the reason as call metadata; the router derives it itself only when it owns the ring leg. If the agent’s leg is unreachable, the call falls through to voicemail rather than dead air.
How it sits in the call path
Scroll sideways, then tap a node
Read the board
Two planes. The business keeps the phone system it already owns — that is the upper one, and it is not mine. A call only ever drops to the lower plane on the three conditions that mean nobody picked up.
- Their phone systemnot mine — overflow is a forwarding rule inside it
- My codepicks up only what falls through, and records why
Hover any node — or tab through them — for what it does and why it is there.
Journeys on the board
- Somebody is free4 hops
- Nobody picks up5 hops
- Closed4 hops
03The main decision
Escalation is a code path, not a prompt instruction.
Most AI phone agents handle escalation inside the prompt — transfer the call if the caller seems upset. That is a suggestion to a model. A persistent caller can talk it out of firing, and it behaves differently from one call to the next.
Here the escalation layer runs before the model is asked for anything. Five rules, evaluated in order, first match wins. A second check runs against the generated reply and can also hand off. The call transfers immediately, with no attempt to resolve, when the caller asks for a person in any wording, mentions a medical concern, disputes a charge, is misunderstood twice in a row, or the call passes its time cap.
If no human is free to take the transfer, the agent takes a callback request and states when someone will call back. It does not leave a caller in a loop.
Escalation
Scroll sideways, then tap a node
Read the board
Three planes. The middle one is the guard, and it is the point of the drawing — a turn has to cross it before anything on the reply plane below exists. Five rules, in order, first match wins.
- The turnwhat the caller just said, with an ASR confidence
- The guarddeterministic code — no model, no HTTP, no database session
- The replyonly ever reached when nothing fired
Hover a rule — or tab through them — for what it detects, and the phrasings its tests hold it to.
The module that runs those rules imports no language model, no HTTP client and no database. A test parses its source and fails the build if any of those ever appear, so the rules cannot quietly acquire a dependency that would make them non-deterministic.
112 tests cover the five rules across multiple phrasings, including deliberately awkward ones. They run in 0.12 seconds.
The question this answers
“When your agent does the wrong thing, can you point at the line that decided?”
I wrote this decision up on LinkedIn04Booking
The failure case is the design.
The failure mode that matters here is an agent confirming a booking that never happened. If the calendar provider times out or errors mid-call, this agent says a person will confirm, and records a callback.
That is structural rather than a matter of careful wording. A successful-booking result is unconstructable without a reference returned by the provider, and the deferred path scans its own outgoing sentence against 35 confirmation phrases before speaking it. Booking writes are never retried, because a retried write that already landed is a double booking.
Booking
FR-4.4
The agent must never confirm a booking it did not make
scanning against 35 confirmation phrases · 6/6 behaved correctly
The calendar is reachable but broken. Nothing is written.
Agent
I can do Signature Facial on Monday at 4:00 pm. Shall I book that?
Agent
I could not reach our booking system just now, so I have not been able to hold that time for you. Someone from Seaside Spa will call you back within one business day to arrange it with you.
- booking
- deferred
- callbacks
- 1
- outcome
- deferred_booking
PassNo confirmation phrase; callback recorded.
05Architecture
Vapi hears and speaks. My code decides.
Vapi handles speech-to-text and text-to-speech only. It is configured as a custom LLM pointed at a FastAPI orchestrator, so no reply is generated inside the voice platform. Every sentence the caller hears comes out of application code, after the escalation rules have already run on that turn.
That arrangement is what makes the behaviour inspectable. When the agent does something wrong, there is a specific line that decided.
Architecture
Scroll sideways, then tap a node
Read the board
Three planes, and one pair of arrows that matters more than the rest. Vapi hears and speaks; it is pointed at my orchestrator as its model provider, so every turn drops down and every reply climbs back. Nothing is generated inside the voice platform.
- Not minethe phone network, the business’s PBX, and Vapi
- My codea FastAPI service — guard first, then the reply
- Statewhat is kept, and what staff read it through
Hover any node — or tab through them — for what it does and why it is there.
06Handover
What the business controls, and what it cannot.
Prices, hours, services, policies and the agent’s answers are edited through the console by staff. Escalation detection logic is read-only there, because it is code rather than configuration.
Every account — voice platform, telephony, the phone number, storage — would be registered to the business. The design assumes I am a collaborator who can be removed without the system losing access, data or function.
The console
Every call the agent picked up, newest first — whether a person or the agent answered, which condition sent it, what came of it, and what it cost.
| Time | Why the agent picked up | Outcome | Escalation | Cost |
|---|---|---|---|---|
| Sep 2, 11:30 AM | After hours | Escalated | ESC-2 | $0.15 |
| Sep 2, 11:30 AM | After hours | Escalated | ESC-4 | $0.15 |
| Sep 2, 2:00 AM | No answer | Answered | — | $0.15 |
| Sep 2, 2:00 AM | Line busy | Escalated | ESC-1 | $0.15 |
| Aug 24, 3:34 AM | After hours | In progress | — | Not metered |
5 of 5 calls · 3 escalated
07Measured
Counted from the repository, not estimated. Test code sits within twenty lines of application code.
- passing tests, 1 skipped
- 1,715
- on escalation alone, in 0.12s
- 112
- escalation rules
- 5
- blocked-language patterns built in
- 16
- confirmation phrases scanned before deferring
- 35
- booking adapters
- 4
- database tables
- 14
- API routes
- 47
- lines of application code
- 18,335
- lines of test code
- 18,316
Stack
- Python
- FastAPI
- Vapi
- PostgreSQL
- Celery
- Next.js
- pytest