Inspiration

At a solo-doctor ENT clinic, the patients ask the same four questions all day:

  • Where exactly are you?
  • What time do you open?
  • Are you open on Sunday?
  • Do you do hearing tests?

Over and over, dozens of times a day, every day. The answers never change. They are written on a board on the wall and printed in a leaflet on the desk, and none of that helps, because the person asking is on a phone in an auto-rickshaw. So someone has to say the address out loud again. And again. And they have to say it patiently the fortieth time, because the fortieth caller has never asked before.

That work is genuinely necessary and completely unrewarding. It is also what makes the rest of the phone unmanageable. The doctor is mid-consultation when it rings, so it goes unanswered — and there is no way to tell from the ringing whether that was the fortieth "what are your timings" or a patient trying to move tomorrow's appointment. Both get missed together.

Hiring someone to absorb it costs more than a small clinic makes back. And a receptionist still goes home at six, still takes lunch, and still cannot answer two lines at once — while callers keep ringing at nine at night, because that is when they finally have a moment to think about their ear.

So the real target was never "answer the phone." It was: take the repetition away entirely, and make the repetitive answers exactly as reliable as the wall.

That last part is the whole difficulty. A wall does not improvise. The obvious answer — "put an AI on the phone" — is suspicious precisely because its failure mode is worse than the problem it solves. A tired human answering the address for the fortieth time still gets the address right. A language model that is unsure invents a plausible answer, in a confident voice, on a recorded call, to a patient making a medical decision.

  • Told the wrong closing time, someone arrives to a locked door.
  • Told a slot exists when it does not, they take an afternoon off work for nothing.
  • Told a price that was never set, they expect that price.

The repetitive questions are the easiest ones to answer and therefore the most damaging to answer wrongly, because nobody thinks to double-check the opening hours.

So the goal was an agent that is as helpful as a good receptionist and no more presumptuous than one: endlessly patient about location, timings and services, never bored on the fortieth call, and quick to say "the doctor will confirm that for you" the moment a question becomes a matter of judgement.

Which made the project less about "can it talk" and more about one constraint:

The agent decides what to say. It never decides what is true.

Every fact a caller hears must come from a tool that read the database. The model understands speech and picks a tool. That is all it does. It cannot invent availability, cannot confirm a save that did not happen, and cannot answer a question about the clinic from its training data.

Everything interesting we built came out of defending that line.


What it does

Press Start call and speak. Your microphone streams as 16 kHz mono PCM to Amazon Nova Sonic over a bidirectional Amazon Bedrock stream, and the reply returns at 24 kHz. It is genuine speech-to-speech with no transcribe-then-synthesise hop, so it feels like a conversation rather than a voice assistant. Interrupt it mid-sentence and playback stops in under 500 ms.

A real call from our testing:

agent    Just so you know, this call is recorded for quality and record-keeping.
         How can I help you with your appointment or clinic information today?
caller   I'd like to book a hearing test
agent    I can help with that. Let me check the calendar.
         I have Monday the fourteenth at nine, nine-thirty, or ten. Which suits you?
caller   can you do sunday the thirteenth
agent    The thirteenth falls on a Sunday, which is our clinic holiday — we're closed
         that day. The nearest I have is Monday the fourteenth at nine. Shall I book it?
caller   yes, my name is Sailaja Devi
agent    Thank you. That's Sailaja Devi — S-A-I-L-A-J-A?
caller   yes
agent    And a callback mobile number?
caller   nine nine zero zero zero one two three zero seven
agent    Booked: Hearing Test, Monday the fourteenth of September at nine.
         Your patient ID is S-I-3-0-7. Please quote that next time and I'll find
         you straight away.


On the phone

  • Booking: The caller names a service, matched against the clinic's offered services by exact name — no fuzzy guessing. The agent reads the calendar and offers up to three concrete dated slots, because three is what a person can hold in their head on a phone call. It looks the caller up or creates a record, writes the appointment, and reads back the service, date, time and patient code.
  • Appointment vs. Slot: The appointment records the service the caller asked for, not the label the slot was published under. A slot is the doctor's half hour, published without knowing who will take it. An appointment is one named person coming in for one named thing.
  • Rescheduling and Cancelling: It finds the caller's existing appointments first — that is the only source of an appointment ID, and it never asks a caller for a booking reference, because nobody has one to hand. A reschedule releases the old slot so someone else can take it. Cancellation requires explicit confirmation, enforced by the orchestrator rather than left to the prompt.
  • Calendar Awareness: Availability is a bounded index query. It never offers a slot that has already started, never offers the same minute twice, never offers a slot the doctor did not publish, and treats a requested hour as a floor rather than a filter, so a full afternoon rolls into the next day instead of returning nothing. One hard boundary: the agent can book and release slots, but can never create one.
  • Closed Days Explained: Ask for Sunday the 13th and it names the reason — "that's a Sunday, our clinic holiday" — then offers the nearest working day. Driven by the configured opening hours, so a clinic that opens Sunday and closes Tuesday needs no code change.
  • Patient Code: Format like SI307 (first letter of first name, last letter of last name, last three digits of mobile), read back at the end of every booking.
  • Dictated Numbers: 9900012307 and "nine nine zero zero zero one two three zero seven" resolve to the same patient. So do "double nine...", a +91 prefix, and half-converted transcripts.
  • Names vs. Health Details: Names are spelled out aloud to confirm; blood groups or measurements are never read aloud to protect privacy in public spaces.
  • Clinic FAQ: Hours, address, directions, and the service list are retrieved from PDFs uploaded by the doctor through the portal, indexed with Bedrock embeddings.
  • Waitlist: When nothing suitable is free, it records who wanted what and when for gap-fill suggestions.
  • Recording: A timestamped transcript plus one stereo WAV per call at 24 kHz (patient on left channel, agent on right) laid out on the real timeline to capture barge-ins correctly.

Helpful up to the line, and honest about where the line is

The agent explains what a service involves in plain language, lists every service the clinic offers, and helps callers understand what to expect.

What it does not do is decide which service a person's symptom needs. When a caller describes a symptom, the agent provides known facts, states plainly that the doctor will confirm what is right, and offers to book the consultation.

This boundary is enforced in three independent layers:

  1. The system prompt.
  2. A deterministic guardrail policy over extracted turn signals.
  3. The absence of any code path that could map symptoms to services.

For the doctor

  • Role-Aware Dashboard: Features the day calendar with publish and block controls, a call activity log with transcripts and playback, patient records that can be corrected in place, impact metrics, and a decisions feed. Live updates arrive over Server-Sent Events (SSE) with no polling or reloading required.
  • Practice Intelligence: A background agent runs on a schedule over accumulated appointments, slots, waitlist entries, and calls to surface actionable decisions for the doctor to approve or dismiss:
Decision What triggered it
Gap fill An open slot matches someone on the waitlist
Schedule gap The same weekday is persistently empty at low utilisation
Unmet demand Callers keep asking for a service the clinic does not offer
No-show trend The no-show rate moved against the preceding equal-length period

Approving a gap-fill books the earliest matching waitlisted patient and removes their entry.


How we built it

Four subsystems operate over one shared data layer where agents never touch storage directly:

                        browser (microphone)
                               |
                    wss:// /ws    16 kHz up · 24 kHz down
                               |
        +----------------------v-----------------------+
        |            Voice_Front_Desk                  |
        |  Strands BidiAgent + Nova Sonic              |
        |  11 tools · guardrails · barge-in            |
        |  turn signals -> policy -> tool orchestrator |
        +----------------------+-----------------------+
                               |
        +----------------------v-----------------------+
        |                 Data_Layer                   |
        |   per-entity store interfaces, swappable     |
        |   memory fakes   <->   DynamoDB single table |
        +------+-------------------------+-------------+
               |                         |
   +-----------v-----------+   +---------v-------------+
   | Practice_Intelligence |   |      Dashboard        |
   | scheduled detectors   |   | role-aware BFF + SSE  |
   | -> Decisions          |   | schedule · calls ·    |
   | doctor approves       |   | metrics · decisions   |
   +-----------------------+   +-----------------------+

Amazon Nova Sonic

Speech-to-speech models process audio directly via Bedrock’s persistent bidirectional stream (InvokeModelWithBidirectionalStream API), eliminating traditional STT and TTS hops.

  • Barge-in: Native support via amazon.nova-2-sonic-v1:0 with configurable endpointingSensitivity.
  • Tool Racing: Tool calls race the agent's own speech, necessitating rigorous latency budgets to prevent the model from guessing answers before tool responses arrive.

Strands Agents SDK

  • @tool decorators derive model-facing schemas directly from function signatures and docstrings.
  • Tools return discriminated results (success with value, or failure with typed error).
  • Models are swappable and injectable, letting 1,514 tests run without touching AWS.

Storage & Specifications

  • Swappable data layers: in-memory fakes and AWS DynamoDB (single table design with 4 GSIs; GSI1 uses slot start time as the sort key).
  • Spec-driven design in Kiro (.kiro/specs/), mapping every requirement to tests and code docstrings.
  • Deployed on AWS CloudFront + EC2 (t4g.small) over HTTPS to support secure browser microphone APIs.

Challenges we ran into

  • Speech-to-Text Inconsistency: The same phone number transcribed as 9900012307 on one call and as words "nine nine zero zero zero one two three zero seven" on another, resulting in empty comparison keys. Fixed in the normaliser rather than the prompt.
  • Latency as a Correctness Bug: A slow availability lookup (taking 8.1 seconds across a year of published slots) caused the model to speak before the tool returned, prompting hallucinations. Fixed via date-floor sort-key conditions and query limits.
  • Unrecorded Saves: register_patient returned existing records untouched when details arrived post-booking while reporting success. Fixed by implementing record amendment logic that validates incoming fields without overwriting existing entries.
  • Closed Days Misreported: Availability searches on or after a date returned Monday slots for Sunday requests, making the agent report no slots found. Fixed by passing closed weekday reasons directly in tool results.
  • Deployment & Verification Gotchas: Amazon Linux 2023 ships without cronie, causing cloud-init setup failures until replaced with systemd timers. Furthermore, integration checks initially passed while testing non-existent routes until assertions were made strictly failure-bound.

Accomplishments that we're proud of

  • Publicly Live: Fully functional deployment with a valid certificate, real Nova Sonic audio, and live DynamoDB writes.
  • High Test Coverage: 1,514 tests and mypy --strict clean across 104 source files, running completely offline with Hypothesis property-based testing.
  • Secure by Architecture: Dashboard routes handling sensitive patient details are entirely unrouted on the public deployment rather than guarded, making unauthorized access structurally impossible.
  • Practical Patient Code Collision Analysis: Evaluated codes across $N = 26 \times 26 \times 1000 = 676{,}000$ possible combinations. Using the birthday approximation, the probability of a collision among $n$ patients is calculated as: $$\text{Probability} \approx 1 - e^{-\frac{n^2}{2N}}$$

At $n = 1000$, this yields $\approx 52\%$. Consequently, codes are used strictly to narrow searches rather than authenticate identity.


What's next for Clinic Front Desk

  • Amazon Connect Integration: Real phone line handling, automated warm transfers, and live escalations (flag_for_human) passing contact attributes (transcript, patient details, reason) so doctors pick up fully informed.
  • Outbound Calls: Automated no-show follow-ups, appointment reminders, and waitlist gap-filling calls.
  • Concurrent Dashboard Reads: Transitioning metric snapshots from sequential queries to parallel executions for a 10–20× speedup.

Would you like to explore the implementation details of any specific subsystem, such as the Strands tool orchestrator or the DynamoDB single-table schema design?

Built With

Share this project:

Updates

Submission history