Working demo

Poppy

Poppy is a full-duplex voice receptionist for a demo UK clinic. The browser holds a live WebRTC call. Two agents share the work: one speaks, one calls tools. Bookings land in an in-memory clinic record that the diary on screen is already watching. This is a working prototype, not a clinic deployment.

Poppy, a voice receptionist demo for a UK private clinic

The problem

A front desk has to greet, identify, find a slot and write the booking while the caller is still on the line. Poppy is the engineering of that conversation: speech in the browser, tools on the server, and one clinic state both of them can see.

What the demo does

  • The operator picks who is calling: a returning patient from the seeded record, or a new caller. Start conversation opens the call. Returning patients are greeted by name.
  • The call has four phases: confirm identity, capture a new patient's name, mobile, email and date of birth straight into the record, then availability, booking, reschedule, cancel, history and notes, then a summary and a goodbye. The agent hangs up itself with an end_call tool.
  • A week diary polls every two seconds during a call. An appointment the agent writes appears live, marked as created by the AI. A patient card fills in as details are captured. Reset restores the seed: three doctors, ten patients, about four weeks of appointments.
  • The public marketing page is a separate, scripted demonstration. It makes no API calls, and the browser is not given the microphone. It is a presentation of the intended product. The engineering below is what the code actually runs.

Architecture

One FastAPI service creates the live session and keeps a sideband WebSocket open for tools. Audio does not pass through that server. It flows between the browser and the voice model over WebRTC. The same ClinicService answers both the diary's REST calls and the agent's tools, so the screen and the voice cannot disagree. The OpenAI key stays on the server. The browser only posts an SDP offer and a patient id.

Poppy demo architecture The front-desk page polls an in-memory clinic API and opens a WebRTC call. FastAPI creates the live session, attaches a sideband for tools, and shares one clinic store with the diary. A voice model speaks. A delegated model calls the tools. The marketing page is separate and has no backend. Week diary polls every 2 seconds WebRTC peer mic and events channel FastAPI gate, sessions, sideband, tools Clinic store in memory, mock adapter Voice model speaks and listens Delegated tool model parallel tool calls London diary slots Marketing page: scripted, no API
Audio stays on the WebRTC path. Tools and the clinic record stay on the server.

Two agents, one call

The front agent is a live voice model. It follows a four-phase script, speaks in short phone-friendly phrases, uses Europe/London times, and switches language if the caller does. The back agent is a delegated responses model. It is the only one with tools, it may call them in parallel, and its instructions say to copy timestamps exactly and to treat free text from the record as untrusted data. The Python SDK did not expose this session API, so a small HTTP client creates the session and attaches the sideband.

The tool list is the front desk: current patient, history, upcoming appointments, doctors, free slots, create, reschedule, cancel, notes, profile updates, and end call. Each tool has a risk level, a Pydantic schema, an idempotency flag, a cache time and a timeout. Unknown tools are rejected. Arguments that fail validation are rejected. The patient id comes from the session, not from the model. A repeated write with the same idempotency key returns the stored result. A risky write without confirmation comes back as confirmation_required, and the agent has to ask, then retry with the same key.

The sideband handler is where the call stays smooth. A pickup greeting goes out as soon as the session is ready, and earlier events are buffered rather than dropped. A reconnect does not announce itself. While a tool runs, a short spoken line can play, at most twice, and fast reads skip it. A watchdog at four seconds synthesises an error for a tool that never returned, so the model is not left waiting. Silence after a tool, and silence from the caller, each get a nudge. Emails and phone numbers spotted in the transcript warm the customer context without blocking speech. When the call ends, cached contact details are cleared from the session.

Booking a slot

Poppy booking sequence The browser posts an SDP offer. The API creates a live session and attaches a sideband. The caller asks for a time. The tool model requests slots. The dispatcher checks London availability in the in-memory clinic. After the caller confirms, the same idempotency key writes the appointment, and the diary poll shows it. SDP offer and patient id Gate, origin check, rate limit Live session plus sideband Spoken greeting, then the ask Slots, then a confirmed write same idempotency key Diary shows the AI booking The agent ends the call itself.
Availability and the booking share one in-memory clinic. The diary is only polling it.

Time, security and how it is hosted

Free slots are computed per doctor and treatment, with spoken times and Europe/London offsets for both BST and GMT. That logic has its own regression tests, including a long date suite. There is no database: patients, doctors, treatments, appointments and the conversation itself live in process memory and disappear on restart. An adapter interface is the seam where a real clinic system would sit. The only adapter today is the mock.

Session routes check an origin allowlist. Session creation is rate-limited per IP, 20 a minute by default. The demo API sits behind an access gate: a token derived from a shared password, compared in constant time, sent as a cookie or header. That is a gate for a demo, not account authentication. There is no retrieval layer and no model-eval harness. Quality sits in unit tests: 13 unit modules and 2 integration modules, covering the dispatcher, prompts, London time, availability, the new-caller flow and the sideband's continue logic. The front end is linted with oxlint. There is no GitHub Actions workflow.

The demo is one Docker web service. Node builds the Vite front end, Python serves it and /api from the same origin, and /api/health is the health check. The marketing site is a separate Vite app on Vercel. Logs are structured, with the conversation id on the context, and in-process timers record session creation and each tool. A latency summary is returned when the call ends. Nothing is shipped to an external tracker.

Where it is going

The voice demo was built over a few days in October 2026, and the marketing page followed later that month. The next real steps are the ones the code leaves open: a clinic adapter behind the interface that exists, persistent storage, a telephone line, and proper accounts. Continuous integration is not there yet. What is there is a call you can start, a diary that updates while you talk, and booking rules that refuse a write until the caller has confirmed it.

Open the Poppy page