Skip to content
Selected work

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

AI answeredAfter hoursEscalated · ESC-2

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.

  1. 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.

  2. 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.

  3. 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.

Duration
9s
Turns
3
Cost
$0.15
Final state
Ended

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

THEIR PHONE SYSTEMMY CODEINBOUNDOPEN NOW?RING STAFF18 secondsA PERSON ANSWERShuman_answeredCLOSEDafter_hoursNO ANSWERno_answerLINE BUSYbusyTHE AGENT PICKS UPVOICEMAILif my leg failsTHE AI STAYS SILENT

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

THE TURNTHE GUARDTHE REPLYIN ORDER · FIRST MATCH WINSCALLER TURNtext + confESC-1PERSONESC-2MEDICALESC-3COMPLAINTESC-4LOW ASR ×2ESC-5TIME CAPBUILD REPLYfrom the dbOUTPUT CHECKevery sentenceSPEAK ITSTOPno reply is builtHUMAN FREE?WARM TRANSFERescalatedCALLBACKname and number

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 LinkedIn

04Booking

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

NOT MINEMY CODESTATEEACH TURN ↓↑ THE REPLYPHONE CALLPSTNBUSINESS PBXrings firstBROWSERno phoneVAPI WEB SDKVAPIspeech in and outFASTAPI ORCHESTRATORtelephony/routing + hoursguard/escalationkb/answers from dbbooking/four adapterscalls/turns and logscosts/cost per callPOSTGRESQL14 tablesOBJECT STORErecordingsNEXT.JS CONSOLEwhat staff see

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.

TimeWhy the agent picked upOutcomeEscalationCost
Sep 2, 11:30 AMAfter hoursEscalatedESC-2$0.15
Sep 2, 11:30 AMAfter hoursEscalatedESC-4$0.15
Sep 2, 2:00 AMNo answerAnswered—$0.15
Sep 2, 2:00 AMLine busyEscalatedESC-1$0.15
Aug 24, 3:34 AMAfter hoursIn 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
The repositorygithub.com/asadurrehmandev/Vapi-Voice-Agent