<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Naimish</title>
    <description>The latest articles on DEV Community by Naimish (@naimish21).</description>
    <link>https://dev.to/naimish21</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078553%2Ffc255596-a495-47e5-87c7-92f73594554b.png</url>
      <title>DEV Community: Naimish</title>
      <link>https://dev.to/naimish21</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naimish21"/>
    <language>en</language>
    <item>
      <title>I built a Multilingual healthcare voice agent that knows when call or shutup.</title>
      <dc:creator>Naimish</dc:creator>
      <pubDate>Sat, 15 Aug 2026 06:58:08 +0000</pubDate>
      <link>https://dev.to/naimish21/i-built-a-multilingual-healthcare-voice-agent-that-knows-when-call-or-shutup-2ija</link>
      <guid>https://dev.to/naimish21/i-built-a-multilingual-healthcare-voice-agent-that-knows-when-call-or-shutup-2ija</guid>
      <description>&lt;p&gt;Ten days ago I set out to build a voice agent for the &lt;strong&gt;Murf 10 Days of Voice Agents — VoiceForBharat&lt;/strong&gt; challenge. I picked the &lt;strong&gt;Health Access&lt;/strong&gt; track and built &lt;strong&gt;Careva&lt;/strong&gt;: a phone-and-browser helpline that helps someone in India find the nearest working health facility, understand a government scheme, ask what a medicine actually costs, and — when it matters — stop talking and get a human involved.&lt;/p&gt;

&lt;p&gt;This post is the story plus the guide. Everything here is in the repo: &lt;strong&gt;&lt;a href="https://github.com/ace-ify/murf-livekit" rel="noopener noreferrer"&gt;https://github.com/ace-ify/murf-livekit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The problem and the users
&lt;/h2&gt;

&lt;p&gt;The person I built for does not have a problem that a website solves.&lt;/p&gt;

&lt;p&gt;They have a fever at 9pm and don't know if the PHC is open. They have a ₹40 prescription and no idea the generic salt costs ₹6 at a Jan Aushadhi store. They're eligible for Ayushman Bharat and have never read the eligibility page, because the eligibility page is in English, is 2000 words long, and assumes a smartphone with data.&lt;/p&gt;

&lt;p&gt;They can talk, though. Voice is not a nicer interface here — it's the only interface that clears the bar. No app install, no typing in a script your keyboard doesn't have, no literacy assumption. A phone call works on a ₹1500 feature phone in a village with 2G.&lt;/p&gt;

&lt;p&gt;So the design constraint was: &lt;strong&gt;it has to work when spoken, in Hindi, on a bad line, to someone who is scared.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What the agent does
&lt;/h2&gt;

&lt;p&gt;Careva answers in the language you spoke to her in. She can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;find the nearest PHC / CHC / district hospital with OPD timings, from a district name or a pincode&lt;/li&gt;
&lt;li&gt;explain PM-JAY, JSSK and immunisation schedules from a local knowledge base&lt;/li&gt;
&lt;li&gt;look up the generic salt for a branded medicine and what you'd save&lt;/li&gt;
&lt;li&gt;give an air-quality advisory for a district (useful for asthma and elderly callers)&lt;/li&gt;
&lt;li&gt;remember you between calls, if you say it's okay&lt;/li&gt;
&lt;li&gt;call &lt;em&gt;you&lt;/em&gt; — medication and vaccination reminders&lt;/li&gt;
&lt;li&gt;escalate to a human health worker with a spoken reference number&lt;/li&gt;
&lt;li&gt;hand you to &lt;strong&gt;Samar&lt;/strong&gt;, a specialist agent, for detailed clinic/appointment questions&lt;/li&gt;
&lt;li&gt;and above all: recognise a medical emergency and say "call 108 now" as the &lt;em&gt;first&lt;/em&gt; sentence, before any greeting or name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one drove most of the engineering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F77m2xzgpfb0xk8yz7dhn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F77m2xzgpfb0xk8yz7dhn.png" alt="Careva session UI mid-call, with a pushed facility card showing the nearest PHC, its OPD timings and&lt;br&gt;
  address" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3. How the system works
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🎙️ caller ──audio──▶ Deepgram nova-3 (multi) ──text──▶ LLM ──text──▶ Murf Falcon ──audio──▶ 🔊 caller
                                    ▲                                                │
                                    └────────────── LiveKit (WebRTC / SIP) ──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Four moving parts, and you can swap any of them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What I used&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;STT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deepgram &lt;code&gt;nova-3&lt;/code&gt;, &lt;code&gt;language="multi"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;one model that handles Hindi and English without me picking upfront&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Flash → Groq Llama 3.3 70B → Llama 3.1 70B on NVIDIA NIM&lt;/td&gt;
&lt;td&gt;via LiveKit's &lt;code&gt;FallbackAdapter&lt;/code&gt; — a helpline that 502s is worse than a slow one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Murf Falcon&lt;/strong&gt;, voice &lt;code&gt;Anisha&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Indian voice, and fast enough that the pause after you stop speaking doesn't feel like a dropped call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transport&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LiveKit Agents&lt;/td&gt;
&lt;td&gt;one pipeline serves both the browser and a real phone number over SIP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extras&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Silero VAD, LiveKit multilingual turn detector, BVC noise cancellation&lt;/td&gt;
&lt;td&gt;see below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The session, more or less verbatim from &lt;code&gt;backend/src/agent.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deepgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;STT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nova-3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FallbackAdapter&lt;/span&gt;&lt;span class="p"&gt;([...],&lt;/span&gt; &lt;span class="n"&gt;attempt_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;15.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;murf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MURF_VOICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;# "Anisha"
&lt;/span&gt;        &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Conversational&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;CleanSentenceTokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_sentence_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;text_pacing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;turn_detection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;MultilingualModel&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userdata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;preemptive_generation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_away_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SILENCE_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# 12.0
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two small lines with big effects. &lt;code&gt;preemptive_generation=True&lt;/code&gt; starts the LLM on a partial transcript, which removes a visible chunk of the reply gap. And noise cancellation is picked per participant — &lt;code&gt;BVCTelephony()&lt;/code&gt; for SIP callers, &lt;code&gt;BVC()&lt;/code&gt; for browser — because phone audio is band-limited and the generic model does worse on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;noise_cancellation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;noise_cancellation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BVCTelephony&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;participant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParticipantKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PARTICIPANT_KIND_SIP&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;noise_cancellation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BVC&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The features that mattered
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tools that hit real APIs, with a floor under them.&lt;/strong&gt; Facility lookup is OpenStreetMap Nominatim + the India Post pincode API. Medicines are NLM RxNorm. Air quality is Open-Meteo. Schemes are a hand-rolled BM25 index over local markdown — no vector DB, ~150 lines, and it retrieves fine over a corpus this size. Every external call has a 2.5–3.5s budget and degrades to a curated fallback registry of verified facilities. The one thing the agent is never allowed to say is "I can't help" — the last fallback is still 108 and the nearest hospital.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consent-gated memory.&lt;/strong&gt; A &lt;code&gt;callers&lt;/code&gt; table in SQLite keyed on the participant identity (the phone number, for SIP). It stores name, district, age band, ongoing conditions, last triage outcome — and only after the caller says yes. &lt;code&gt;forget_caller&lt;/code&gt; hard-deletes. On reconnect the greeting is rebuilt from that record: &lt;em&gt;"नमस्ते प्रिया जी… पिछली बार हमने बुखार के बारे में बात की थी। आज आप कैसा महसूस कर रहे हैं?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation that a human can actually work.&lt;/strong&gt; The agent asks permission first — &lt;em&gt;"May I send a short summary to a human health worker?"&lt;/em&gt;, in the caller's own language — and a "no" writes literally nothing. What gets stored is six fields, never the transcript, and it goes through one PII scrub choke point so storage and the webhook can't diverge. The caller hears a reference number, &lt;code&gt;ESC-0007&lt;/code&gt;, read digit by digit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specialist handoff.&lt;/strong&gt; &lt;code&gt;transfer_to_clinic_specialist&lt;/code&gt; swaps the agent object on the live session and swaps the Murf voice with it, so the caller hears a different person (&lt;code&gt;Anisha&lt;/code&gt; → &lt;code&gt;Samar&lt;/code&gt;, female → male) rather than the same voice claiming to be someone else. State carries across: caller facts, call id, turn counters, and the emergency flags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dashboard that defines its own metric.&lt;/strong&gt; &lt;code&gt;/admin&lt;/code&gt; shows total / successful / failed / no-answer calls, a failure breakdown, and a success rate that excludes in-progress calls from the denominator. The header states the definition out loud: &lt;em&gt;success = caller received triage guidance or was escalated to a human.&lt;/em&gt; A success rate whose definition lives only in someone's head is a vanity number.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydl8dlpyoy40tpvazbu9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydl8dlpyoy40tpvazbu9.png" alt="Careva call analytics dashboard showing total calls, success rate, failure breakdown by reason, and a recent&lt;br&gt;
  calls table" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  5. The hard parts (all four of these were real bugs)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The agent said goodbye to a heart attack
&lt;/h3&gt;

&lt;p&gt;Worst thing I found in testing. Caller: &lt;em&gt;"my friend is having chest pain"&lt;/em&gt;. Careva gave the 108 line correctly, and then a few turns later cheerfully wrapped up — &lt;em&gt;"aapka din shubh ho"&lt;/em&gt; — and hung up.&lt;/p&gt;

&lt;p&gt;The prompt already said don't do that. Prompts are suggestions.&lt;/p&gt;

&lt;p&gt;The fix was to move safety out of the model entirely. A regex scans every final transcript — in English, Roman Hinglish, &lt;strong&gt;and&lt;/strong&gt; Devanagari — and latches a flag. &lt;code&gt;end_call&lt;/code&gt; then refuses to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Hard guard: the LLM once said goodbye to a caller whose friend was having a
# heart attack. A prompt line is not enough — refuse the hangup outright.
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emergency_flag&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;escalation_created_flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_call BLOCKED: unhandled emergency (reason=%r)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REFUSED — you cannot end this call. ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same guard blocks the specialist handoff. &lt;strong&gt;Lesson: if a behaviour is safety-critical, it belongs in code that the model calls, not in text the model reads.&lt;/strong&gt; The prompt gets you the good path; the guard gets you the bad one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bad accent wasn't the voice, it was the locale
&lt;/h3&gt;

&lt;p&gt;English replies sounded wrong — an Indian-English sentence being pushed through a Hindi voice, mangling the vowels. I spent a while rewriting the prompt, which was the wrong layer entirely.&lt;/p&gt;

&lt;p&gt;Deepgram's &lt;code&gt;multi&lt;/code&gt; model tags every final transcript with the language actually spoken. So: read that tag, and update the TTS locale mid-call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# "hi" or "en", short-circuited if unchanged
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locale fixes the accent. The prompt fixes word choice. Two different bugs that sound like one.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;"consent_given": "true"&lt;/code&gt; killed entire turns
&lt;/h3&gt;

&lt;p&gt;Consent params in my tools are typed &lt;code&gt;str&lt;/code&gt;, not &lt;code&gt;bool&lt;/code&gt;, and that looks like a mistake until you've seen this: Groq's Llama 3.3 intermittently emits the &lt;em&gt;string&lt;/em&gt; &lt;code&gt;"true"&lt;/code&gt; for a boolean parameter, Groq rejects it server-side with &lt;code&gt;tool_use_failed&lt;/code&gt;, and the whole turn dies — mid-sentence, on a health call.&lt;/p&gt;

&lt;p&gt;A string parameter cannot be malformed. So it's a string, parsed fail-closed, and the truthy set speaks Hindi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;haan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;haan ji&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything unrecognised means &lt;strong&gt;no consent&lt;/strong&gt;. Failing closed on a consent check is free; failing open is a privacy incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every call looked like a silent disconnect
&lt;/h3&gt;

&lt;p&gt;My analytics said 100% no-answer. The calls were fine. I was subscribed to &lt;code&gt;user_speech_committed&lt;/code&gt; and &lt;code&gt;agent_speech_committed&lt;/code&gt;, which don't exist in &lt;code&gt;livekit-agents&lt;/code&gt; 1.4 — and a handler for an event that never fires is completely silent. &lt;code&gt;conversation_item_added&lt;/code&gt; is the real one.&lt;/p&gt;

&lt;p&gt;Two more of the same species, both fixed: finalising the call row on the session &lt;code&gt;close&lt;/code&gt; event lost a race with the loop shutting down and left rows stuck at &lt;code&gt;in_progress&lt;/code&gt; (use &lt;code&gt;ctx.add_shutdown_callback&lt;/code&gt;, which LiveKit awaits); and dispatch metadata was being parsed &lt;em&gt;after&lt;/em&gt; &lt;code&gt;record_call_start()&lt;/code&gt;, so that call raised a swallowed &lt;code&gt;NameError&lt;/code&gt; and recorded zero calls. &lt;strong&gt;Wire up your telemetry and then verify it with your own eyes, because broken instrumentation reports success.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Build your own — the short version
&lt;/h2&gt;

&lt;p&gt;Start from &lt;a href="https://github.com/murf-ai/murf-livekit-starter" rel="noopener noreferrer"&gt;Murf's LiveKit starter&lt;/a&gt;. You need Python 3.10+, &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt;, Node 18+, pnpm, and a free LiveKit Cloud project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ace-ify/murf-livekit.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;murf-livekit
&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv run python src/agent.py download-files
&lt;span class="nb"&gt;cd&lt;/span&gt; ../frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keys.&lt;/strong&gt; Copy &lt;code&gt;.env.example&lt;/code&gt; → &lt;code&gt;.env.local&lt;/code&gt; in both &lt;code&gt;backend/&lt;/code&gt; and &lt;code&gt;frontend/&lt;/code&gt;. &lt;code&gt;.env*&lt;/code&gt; is gitignored — keep it that way, and never paste a key into a chat window, an issue, or a screenshot. You need &lt;code&gt;LIVEKIT_URL&lt;/code&gt; / &lt;code&gt;LIVEKIT_API_KEY&lt;/code&gt; / &lt;code&gt;LIVEKIT_API_SECRET&lt;/code&gt;, &lt;code&gt;MURF_API_KEY&lt;/code&gt; (&lt;a href="https://murf.ai/api/dashboard" rel="noopener noreferrer"&gt;murf.ai/api/dashboard&lt;/a&gt;), &lt;code&gt;DEEPGRAM_API_KEY&lt;/code&gt;, and &lt;code&gt;GOOGLE_API_KEY&lt;/code&gt;. In production these are platform env vars (Railway for the agent, Vercel for the UI) — same LiveKit project on both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run and talk to it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run python src/agent.py console          &lt;span class="c"&gt;# talk in your terminal, no frontend needed&lt;/span&gt;
uv run python src/agent.py dev              &lt;span class="c"&gt;# + pnpm dev, then open localhost:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;console&lt;/code&gt; mode is the fastest loop you'll get — it's where I did most of the debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then write one tool.&lt;/strong&gt; Not five. A tool is just a decorated async function; the docstring &lt;em&gt;is&lt;/em&gt; the spec the model reads, so spend your time there, not on the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@function_tool&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_nearest_health_facility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;location_or_pincode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;facility_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Find the nearest Primary Health Centre (PHC), Community Health Centre (CHC), or Hospital.

    Use this tool when a caller asks:
    - &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mera paas ka PHC/hospital kahan hai?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Where is the nearest health centre?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
    - &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPD kitne baje tak khula hai?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What are the hospital timings?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ...&lt;/span&gt;
    &lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;location_or_pincode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;District&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;digit&lt;/span&gt; &lt;span class="n"&gt;Indian&lt;/span&gt; &lt;span class="n"&gt;PIN&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;automatically&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the example questions in both languages — that's not documentation, that's routing. And note the last line: leaving the location empty and resolving it from memory is what makes a second call feel like a continuation instead of a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What I'd do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cut turn latency properly.&lt;/strong&gt; I log &lt;code&gt;reply = llm_ttft + tts_ttfb&lt;/code&gt; per turn but I haven't done a real p50/p95 pass across the fallback chain. (One thing I did measure: NIM's &lt;code&gt;llama-3.3-70b-instruct&lt;/code&gt; is listed but never responds — read timeout past 45s — while &lt;code&gt;3.1-70b&lt;/code&gt; gives ~0.8s TTFT with working tool calls. Measure your fallbacks; a dead fallback is worse than none, because you'll trust it.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqv70lon7rm16ihpjc4o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqv70lon7rm16ihpjc4o.png" alt="Careva session UI mid-call, with a pushed facility card showing the nearest PHC, its OPD timings and&lt;br&gt;
  address" width="799" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put real auth on &lt;code&gt;/admin&lt;/code&gt;.&lt;/strong&gt; It lists health complaints. Status &lt;em&gt;writes&lt;/em&gt; need &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; and fail closed without it, but the GET is open for local dev. That page does not go public as-is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation triage instead of keyword matching.&lt;/strong&gt; The current gate is a word list that errs open. It works; it isn't triage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widen the fallback facility registry&lt;/strong&gt; — the live-API-fails path is the path a rural caller is most likely to hit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/ace-ify/murf-livekit" rel="noopener noreferrer"&gt;https://github.com/ace-ify/murf-livekit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://murf.ai/api/docs/text-to-speech-models/falcon-2" rel="noopener noreferrer"&gt;Murf Falcon docs&lt;/a&gt; · &lt;a href="https://murf.ai/api/docs/voices-styles/voice-library" rel="noopener noreferrer"&gt;Murf voice library&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.livekit.io/agents" rel="noopener noreferrer"&gt;LiveKit Agents&lt;/a&gt; · &lt;a href="https://developers.deepgram.com" rel="noopener noreferrer"&gt;Deepgram Nova-3&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you build one thing from this post, build the guard, not the prompt. The model will be charming and wrong at some point, and on a health line "charming and wrong" has a cost. Put the floor in code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#VoiceForBharat&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>livekit</category>
      <category>python</category>
      <category>agentaichallenge</category>
    </item>
  </channel>
</rss>
