ProjectBuild an AI Voice Receptionist: the explanation · Project overview: the Medilane voice receptionist · part 2 of 23
Part 2 · Project overview: the Medilane voice receptionist

Project overview: the Medilane voice receptionist

12 min read·
Start here · About 15 minutes · No code · Every screenshot below is the real app, running locally with Docker, answering with real models.

Medilane Clinic is a fictional clinic in Pune with five doctors. Its callers do not fill in forms: they talk. In the browser they press a green button and speak in English, Hindi or Hinglish (Hindi and English mixed in one sentence). An AI receptionist listens, answers questions from the clinic's own documents, and books, moves or cancels appointments, but only after checking who is calling and only after the caller says "yes" to a read-back. It never gives medical advice. When a caller describes an emergency, it tells them to call 112 straight away and alerts the front desk, without waiting for a language model. Front-desk staff watch every call live and take over whenever the receptionist hands a caller to them; admins manage doctors, documents, prompts and users.

This page is a guided tour of the finished product. The 21 modules after it take every part apart and show how to build it.

Medilane Voice Receptionist System architecture Legend request flow background future, not built 1 step CLINIC KNOWLEDGE INGESTION worker, Redis queue Clinic docs English + Hindi Admin upload PDF, MD, TXT Upload size and type Parse PDF, MD, HTML Chunk by heading Embed multilingual OCR scanned PDFs retries passing errors 1 2 3 STORAGE Postgres + pgvector, Redis PostgreSQL + pgvector clinic, calls, chunks, agent state Redis desk events, limits, queue File storage local disk or Cloud Storage index chunks CALLER browser SPEECH IN WebSocket SAFETY Microphone 16 kHz audio Speaker plays 24 kHz VAD Silero: end of turn Whisper on Groq speech to text, language Guards PII masking, scope, injection Emergency check before any model, 112 4 5 6 7 11 BRAIN LangGraph OPEN MODELS Groq Language en, hi, Hinglish Agent loop router, tools Clinic tools verify, slots, book Confirm first read back, then yes Small model gpt-oss-20b Large model gpt-oss-120b Fallback Qwen 3.8 27B Prompt Guard 2 injection check 8 9 SPEECH OUT Kokoro, local CPU Sentence split speak early Normalise numbers to words Kokoro TTS en + hi voices Barge-in caller talks, we stop reply text, sentence by sentence 10 audio plays on the caller's speaker (11) OPERATIONS Langfuse, Prometheus FUTURE Front desk live calls, hand-offs Traces every stage Metrics latency, outcomes Evals WER, safety, gate Prompt versions rollback Audit log every change Phone line SIP or Twilio Streaming STT lower latency SMS reminders after booking 12
The whole receptionist by layer. Steps 1 to 3 are a clinic document being ingested; 4 to 11 are one spoken turn, from the caller's microphone to the reply on their speaker; 12 is the front desk and the traces. Blue is our code, amber a model, green safety; dashed purple parts are planned, not built.

Open the full-size architecture diagram

The tour

The tour follows one synthetic patient, Rahul Verma (born 12 March 1988), a few other callers, one front-desk member (frontdesk@medilane.local) and one admin (admin@medilane.local). All data is synthetic: the doctors, the 40 patients, their appointments and the clinic documents were written for this project.

1. The call page

The call page before a call: language badge, the green call button and things you can ask

The public call page. Callers need no account. The badge says the language is detected automatically, the notice under the buttons says calls are recorded and that personal details are masked, and the cards suggest things to say in each language. The transcript on the right fills in as the call goes on. The browser page is Module 19; the short-lived voice token that starts each call is Module 6.

2. A real voice turn

These two screenshots are a real spoken turn. A recorded English question, "Is the clinic open on Sunday?", was played into the page as its microphone. Everything after that was the live system: the page's audio worklet sent 16 kHz audio over the WebSocket, Silero VAD noticed when the question ended, Whisper on Groq turned it into text, the agent searched the clinic documents and wrote the reply, and Kokoro spoke it on the CPU, sentence by sentence.

The receptionist speaking its answer while the text is still streaming in

While it speaks. The orb shows "Speaking...", the hint says "Just start talking to interrupt", and the answer is still arriving word by word. Above it, the tool line shows that the receptionist searched the document "Clinic timings and holidays" before answering (Modules 2, 8, 9 and 14).

The finished answer with a source pill and first audio 2.3 s

The finished answer: open on Sunday 10 AM to 1 PM, General Physician only, walk-in, no bookings. The pill under the answer names the document it came from (document search is Module 12), and the chip at the top says the caller heard the first audio 2.3 seconds after they stopped speaking.

From here on, the caller's words were typed into the message box of the same live call. A typed message skips only the microphone, VAD and Whisper. It goes through the same WebSocket to the same agent, with the same tools and the same safety checks, and Kokoro still speaks the reply. The transcript marks each such message "typed".

3. Booking an appointment

Rahul verified by name and date of birth, three free slots found, and a read-back of Monday 09:00 with the fee

Rahul types the running example: "Mujhe kal Dr. Mehta ka appointment chahiye, morning mein." The receptionist first asks for his full name and date of birth. "Rahul Verma, 12 March 1988" matches a patient, so verify_patient marks him verified (the green chip at the top). Only then does it look for slots (find_slots: three free for Dr. Ananya Mehta from Monday 26 October, because Sunday is walk-in only) and prepare a booking. It reads back the first free slot, Monday 26 October at 09:00, with the fee, ₹600, and asks him to confirm. Nothing is booked yet (Modules 7 and 11).

Rahul picks ten o'clock, hears a new read-back, says yes, and the booking card APT-SCZ3 appears

Rahul wants another time: "दस बजे वाला" (the ten o'clock one). The receptionist prepares the booking again and reads back the new time, Monday 26 October at 10:00, ₹600; the language badge has switched to Hindi. Only after his "हाँ, कर दो" (yes, go ahead) does confirm_pending book it. The green card shows Dr. Ananya Mehta, 26 Oct, 10:00 AM, ₹600 and the booking ID APT-SCZ3, and the reply repeats the reference. This last answer reached him 1.8 seconds after he finished.

The booking only happens when the caller says yes to a read-back they have just heard: when Rahul chose another time, the 09:00 read-back was dropped and a new one prepared. An earlier take of these screenshots caught a real bug in exactly this confirm rule (a new time read back, the old one booked), now fixed with tests; Module 11 tells that story.

4. Hindi, emergencies and interruptions

A Hindi question about Sunday answered in Hindi with two source pills

Hindi. "क्या रविवार को क्लिनिक खुला रहता है?" The receptionist searched the clinic documents and answered in Hindi, written in Devanagari so the Hindi voice can read it aloud. The two source pills are the Hindi and the English version of the timings document (the search is Module 12; Hindi and Devanagari are Module 10).

A Hindi chest pain message answered with a red emergency banner and call 112 now

An emergency. "मेरे पापा को सीने में बहुत दर्द हो रहा है और पसीना आ रहा है" (my father has bad chest pain and is sweating). A rule check catches it before any model runs: the reply tells the caller to call 112 or go to the nearest hospital emergency, the page shows a red "Call 112 now" banner, and the call is handed to the front desk as an emergency. First audio here was 0.30 seconds (Module 15).

A long answer cut off with (you interrupted) and the parking question answered next

Barge-in. The caller asked what to bring and where to park, then cut in with "Sorry, just tell me about parking." The receptionist stopped mid-sentence (the answer ends with "(you interrupted)"), dropped the rest, and answered only the parking question from the documents: the basement of Sai Arcade, free for the first two hours with a stamped ticket. Here the interruption was typed; typing while the receptionist speaks interrupts it, just as speaking does (Module 14).

5. On a phone

The call page at phone width before a call
The call page at phone width during a call, listening

The same page at phone width: before a call, and during one, with the language badge switched to English, the receptionist listening, and the hang-up, microphone and speaker buttons. The same React page fits the narrow screen (Module 19).

6. The front desk

The front desk signs in at /staff/login. Here the signed-in user is Asha Pillai, with the front_desk role.

Live calls with counters, the masked call list topped by Rahul's booking, and a hand-off queue of four

Live calls. The counters: 10 calls today, 85 bookings by voice and 95 by staff, 4 calls handed to staff, and first audio 1.5 s at the median (3.2 s at p95) over the last 7 days. The call list shows each caller's language and outcome; Rahul's booking, APT-SCZ3 with Dr. Ananya Mehta, is at the top. Unverified callers are never named, and the list is labelled "masked". On the right, 4 hand-offs wait: an unverified Hinglish caller who asked about lab report values (medical advice is out of scope), an English caller whose identity did not match, and two of Rahul's calls from that morning, when the models had run out (step 8), asking for 10:30 with Dr. Mehta. Staff can open or claim each one. Updates arrive live over a server-sent event stream for the front desk, without a page reload (Module 18).

Call detail of Rahul's booking call: masked transcript with timings per turn and a summary panel

Opening Rahul's call. The transcript is masked: his date of birth shows as [DOB]. Each receptionist turn lists its timings; the 09:00 read-back reached him 3.2 s after he finished, the 10:00 one after "दस बजे वाला" 1.9 s. The panel on the right sums up the call: booked APT-SCZ3 with Dr. Ananya Mehta for 26 Oct at 10:00 AM, verified by name and date of birth, Hindi, 58 seconds, 9 turns, first audio 1.9 s at the median (1.4 to 3.2 s), prompt version receptionist-system@v1, and no hand-offs: the receptionist handled this call alone, so the front desk had nothing to do (Module 18).

Appointments for Saturday 24 October on a timeline, coloured by doctor, with a list below

Appointments for Saturday 24 October: 26 booked, 13 by voice (coloured) and 13 by staff (grey). Dr. Kulkarni and Dr. Iyer are off on Saturdays. Staff can add or cancel bookings by hand; the same safe booking code runs for them and for the receptionist (Module 7).

7. The admin side

Clinic settings with five doctors, their days, fees and languages, and three holidays

Clinic settings: the five doctors with their days and fees, and the holidays (Diwali, Christmas, Republic Day) on which no slots are offered. The note at the bottom says it plainly: the receptionist reads doctors, fees and hours through its tools on every call, so a change applies to the next question (Module 7).

Ten clinic documents ready with 44 chunks, and a search debugger answering a Hinglish question

Knowledge: ten documents, ready, split into 44 chunks. An admin can upload a new one in English or Hindi. The search debugger shows what the receptionist would find for a question. Here the Hinglish "kya Sunday ko clinic khula hai" finds the "Opening hours" section of the timings document first, with its vector rank, keyword rank and fused score (Module 12).

One prompt version, v1, active, used by 5 calls

Prompts. The receptionist's instructions are versioned; here v1 is active and 5 calls have used it. Every call records the version it used, activating a version is audited, and rolling back means activating an older one. Facts such as hours, fees and slots must come from tools, never from the prompt (Module 16).

Quality page in a fresh database: empty tiles and no evaluation runs yet

Quality: the evaluation runs stored in this database. It is a fresh one, so the four tiles (booking success, emergencies caught, word error rate, details shared unverified) show dashes and the page names the command that fills it, python -m evals.run --suite all. The results of the runs so far are in the table below and in Module 17.

Audit log with sign-ins, a booking, verifications, hand-offs, prompt changes and new users

The audit log: an append-only record of sign-ins, bookings, verifications, hand-offs, prompt changes and new users. The appointment.booked row is Rahul's booking APT-SCZ3 with Dr. Ananya Mehta, starting 26 October at 10:00 India time (+05:30). Patients appear as IDs, never as names or dates of birth. Sign-ins and the audit log are Module 6.

8. When the models run out

The receptionist unsure, offering the front desk, and handing Rahul over with the time he wanted

An honest failure. During testing, the large model used up its free daily quota on Groq and the fallback model hit its per-minute limit. Rahul had heard a read-back for 09:00 and asked for "साढ़े दस बजे" (half past ten). With no model able to answer, the receptionist did not guess: it said it was not sure and offered the front desk. His "हाँ, कर दो" answered that offer, so handoff_to_staff ran, not a booking. The front desk was alerted that the caller wants an appointment at 10:30. The new slot search had already cancelled the 09:00 read-back, so a yes could never book it. The fallback model is Module 16; the confirm rule is Module 11.

9. Behind one turn

Every turn records how long each stage took: speech to text, the model's first words, the first spoken sentence, and the first audio the caller hears. These are real turns from live calls through the Docker stack.

BEHIND ONE TURN Where the time goes before the caller hears a reply "Is the clinic open on Sunday?" first audio 2,812 ms 281 ms Whisper 1,275 ms model, first words 706 ms Kokoro, first sentence 550 ms not split out FIRST AUDIO IN OTHER LIVE TURNS Hindi emergency (no model at all) 1.1 s Booking: "हाँ, कर दो" confirmed 1.9 s Booking: asked for name and birth date 2.1 s Booking: read back 10:30, Rs 600 2.6 s Booking: verified, found slots 3.7 s
Real calls through the Docker stack: of the three models (amber shades), the language model takes the biggest share of a turn, so the emergency path, which skips it (green), is the fastest reply of all.

What is inside

WHAT IS IN THE BOX One project, built and measured languages 3 English, Hindi, Hinglish doctors 5 and 40 synthetic patients clinic documents 10 8 English, 2 Hindi; 44 chunks word error rate 2.0% 25 synthetic test clips retrieval hit@3 0.97 33 questions, by section first audio 1.1 to 1.5 s simple live turns emergencies caught 26/26 0 false alarms on 18 backend tests 464 plus 31 frontend tests modules 21 problem to deployment
The project in nine numbers: blue is what it holds, amber what was measured on models, green what the tests and safety checks prove. The speech and retrieval numbers come from synthetic voices and test questions, not real callers.
PartWhat it doesModule
The problem and the solutionwhy a clinic phone line needs this, and the listen, think, act, speak loop1, 2
Designmodel and infrastructure choices, the high-level and the low-level design3, 4, 5
Sign-in and voice sessionsstaff passwords, roles, two-step codes, short-lived single-use call tokens6
Clinic and bookingdoctors, slots in Asia/Kolkata time, bookings that never double-book7
Listeningvoice activity detection, Whisper speech to text8, 9
LanguagesEnglish, Hindi and Hinglish, scripts, text made ready for speech10
The agenta LangGraph graph with clinic tools, identity checks and confirm-before-change11
Clinic knowledgemultilingual search over the clinic documents, searched before speaking12
SpeakingKokoro text to speech, sentence by sentence, on the CPU13
The live callstreaming both ways, barge-in, reconnects14
Safetyemergency rules, scope limits, injection checks, Indian personal-data masking15
Models and promptsrule routing, a fallback model, free-tier limits, prompt versions16
Evaluationconversation, safety, language, retrieval, speech and latency suites with a release gate17
Watching callstraces, metrics, the live front-desk stream, masked transcripts18
Frontendthe call page and the staff app19
Containersone command runs the whole stack locally20
CloudGoogle Cloud with Terraform, keyless CI/CD21

Three things are not part of this project, and the modules say so where it matters: a real phone line (callers use the browser), tests with real human callers (all test audio is synthetic voices), and a deploy that was actually run on Google Cloud (the Terraform validates, but it was never applied).

Results in numbers

Measured on 23 and 24 October 2026 with the evaluation suites; Module 17 explains every formula and lists every run, including the failed ones.

What was measuredResultPass mark
33 scripted calls in three languages (code around the model)all correct, 0 disclosures before verification, 0 changes without a yes0.85 correct, 0 slips
11 calls with the real gpt-oss-120b (large tier)11 of 11 correct; gpt-oss-20b managed 6 of 110.85
12 real replies in three languages, right script (gpt-oss-120b)12 of 12 on 24 October, after a fix (11 of 12 the day before)95%
26 emergencies and 18 look-alikesall 26 caught, 0 false alarmsevery emergency, at most 10% false alarms
33 clinic questions, right section in the top 397% (MRR 0.831)85%
Whisper on 25 synthetic clipsword error rate 2.0% (Hindi 4.0%)8% English, 15% Hindi and Hinglish
Silence to first audio, real agent, 4 turnsmedian 2.1 s, slowest 2.8 s2.5 s and 4 s

The tech stack

LayerChoice
FrontendReact, TypeScript, Vite, served by nginx; an AudioWorklet microphone at 16 kHz and 24 kHz playback
APIPython, FastAPI with one WebSocket per call, SQLAlchemy, Alembic
DataPostgreSQL 17 with pgvector and full-text search, Redis
ListeningSilero VAD on the CPU; Whisper large-v3 on Groq
ThinkingLangGraph; open models on Groq: gpt-oss-20b (small), gpt-oss-120b (large), Qwen 3.8 27B (fallback)
SpeakingKokoro-82M on the CPU, an English and a Hindi voice
Retrievala multilingual MiniLM embedding model, keyword search, fused by RRF
Quality and safetyemergency rules before any model, Prompt Guard 2, a central personal-data masker, evaluation suites with a release gate
ObservabilityLangfuse traces, Prometheus metrics, a live event stream for the front desk
DeliveryDocker Compose locally; Cloud Run, Cloud SQL, Memorystore and Terraform on Google Cloud; GitHub Actions

Run it yourself

You need Docker Desktop and one free API key from Groq, which runs both Whisper and the language models. Create it at console.groq.com (sign in, API Keys, Create API Key; it is shown only once). Langfuse keys are optional, for tracing. Copy .env.example to .env and paste the keys there, never into code:

bash
GROQ_API_KEY=gsk_****
LANGFUSE_PUBLIC_KEY=pk-lf-****     # optional
LANGFUSE_SECRET_KEY=sk-lf-****     # optional

What you should see

Nothing yet: the file only holds settings, and git ignores it. Then one command starts everything:

bash
cd source-code
docker compose up -d --build
docker compose ps

What you should see

The first build downloads the voice and embedding models and bakes them into the image, so it takes a while. When docker compose ps shows the api and web services as healthy, open http://localhost:8090, allow the microphone and press the green button. Say you are Rahul Verma, born 12 March 1988, and ask for an appointment with Dr. Mehta. Staff sign in at http://localhost:8090/staff/login as frontdesk@medilane.local (password FrontDesk-Demo-2026) or admin@medilane.local (password ChangeMe-Admin-2026). These are published demo passwords; set SEED_FRONTDESK_PASSWORD and SEED_ADMIN_PASSWORD before the first start for anything public. Module 20 explains every container, and Module 21 puts the same system on Google Cloud.

Where to go next

Read the modules in order. Module 1 starts with the problem a clinic phone line has, and each module after it opens one box of the system map at the top of this page.