<?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: Bhavesh M</title>
    <description>The latest articles on DEV Community by Bhavesh M (@itzbhavs).</description>
    <link>https://dev.to/itzbhavs</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%2F4079091%2Fd08e2d38-a1a9-4a71-8b21-0cdc84037b53.jpg</url>
      <title>DEV Community: Bhavesh M</title>
      <link>https://dev.to/itzbhavs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itzbhavs"/>
    <language>en</language>
    <item>
      <title>Arbiter - we taught an open-weight model to decide when not to wake you up</title>
      <dc:creator>Bhavesh M</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:31:24 +0000</pubDate>
      <link>https://dev.to/itzbhavs/arbiter-we-taught-an-open-weight-model-to-decide-when-not-to-wake-you-up-39p6</link>
      <guid>https://dev.to/itzbhavs/arbiter-we-taught-an-open-weight-model-to-decide-when-not-to-wake-you-up-39p6</guid>
      <description>&lt;p&gt;Most ML monitoring tools are very good at telling you something changed. None of them tell you whether you should care... That gap is why the engineer on call gets twenty drift alerts a week, learns that most are noise, and stops reading them. Then the one that mattered goes past unread!.&lt;/p&gt;

&lt;p&gt;At Hacktoberfest Hack Day Coimbatore 2026, we built Arbiter — a reasoning layer for production machine learning, powered by Gemma 4..&lt;/p&gt;

&lt;p&gt;Where it started:&lt;/p&gt;

&lt;p&gt;One of us maintains driftguard-ai-sdk, an open-source Python SDK on PyPI that detects model drift in real time. It works. But running it surfaced a line that is, in hindsight, an admission:&lt;/p&gt;

&lt;p&gt;drift_threshold=0.15&lt;/p&gt;

&lt;p&gt;Where does it come from? Nobody knows... Every monitoring tool has a constant like this, and every one is a placeholder for judgment nobody automated. A threshold cannot tell a seasonal spike from a broken data pipeline, because both produce identical numbers.&lt;/p&gt;

&lt;p&gt;So, We built the layer it was missing.&lt;/p&gt;

&lt;p&gt;What Arbiter does?? &lt;/p&gt;

&lt;p&gt;It answers the three questions a number cannot:&lt;/p&gt;

&lt;p&gt;Why did this happen, broken pipeline, genuine population shift, or model decay? Does it matter, or is it the month-end batch that spikes every cycle?&lt;/p&gt;

&lt;p&gt;What should we do? retrain, roll back, investigate, or nothing?&lt;/p&gt;

&lt;p&gt;Arbiter ships verdicts, not metrics. Each carries an action, a confidence score, and a written explanation naming the specific features and magnitudes behind it. Low confidence escalates to a human instead of acting. On a replay of [N] drift events, it returned [X] actionable verdicts and suppressed [Y] as benign ; a [Z]% reduction in alert volume.The integration is one environment variable. Arbiter implements the SDK's existing wire protocol, so it's a drop-in replacement backend. No code change, no fork, no second SDK to learn.&lt;/p&gt;

&lt;p&gt;export DRIFTGUARD_API_URL=&lt;a href="https://arbiter-api.example.com" rel="noopener noreferrer"&gt;https://arbiter-api.example.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why open weights, not an API&lt;/p&gt;

&lt;p&gt;Production ML telemetry often cannot leave the network it was generated in. Feature distributions are business data, and in regulated industries they are not going to a closed API. Gemma 4's open weights mean the same model we demo can be self-hosted inside a customer's VPC. A closed model would have made Arbiter undeployable for exactly the companies that have this problem worst.&lt;/p&gt;

&lt;p&gt;The idea underneath:&lt;/p&gt;

&lt;p&gt;Detecting drift is a capability. Deciding what to do about it, and standing behind that decision, is an outcome.&lt;/p&gt;

&lt;p&gt;The industry is shifting from the first to the second. SkySync, this event's sponsor, builds their whole model on being accountable for returns rather than just shipping AI. Arbiter applies that one layer down — which is why every verdict carries a confidence score and an explanation. A recommendation you cannot audit is not a decision anyone can be accountable for. Built in four hours. Open source, MIT licensed.&lt;/p&gt;

&lt;p&gt;Demo: &lt;a href="https://youtu.be/LNeg_N4Y3AA" rel="noopener noreferrer"&gt;Watch here&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/bhaveshmadisetty/hacktoberfest-hack-day-coimbatore-x-init-club-and-idea-club-team-lastofus" rel="noopener noreferrer"&gt;Click here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to INIT Club, iDEA Club and Major League Hacking for running it!!&lt;/p&gt;

&lt;h1&gt;
  
  
  Hacktoberfest #OpenSource #MLOps #Gemma #MachineLearning
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a voice agent that answers a kirana shop's phone in Hinglish — here's everything I learned</title>
      <dc:creator>Bhavesh M</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:19:23 +0000</pubDate>
      <link>https://dev.to/itzbhavs/i-built-a-voice-agent-that-answers-a-kirana-shops-phone-in-hinglish-heres-everything-i-learned-3765</link>
      <guid>https://dev.to/itzbhavs/i-built-a-voice-agent-that-answers-a-kirana-shops-phone-in-hinglish-heres-everything-i-learned-3765</guid>
      <description>&lt;p&gt;&lt;em&gt;10 Days of Voice Agents — VoiceForBharat Edition&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Sharma ji runs a kirana store in Pune. His phone rings all day, and most calls are the same four questions: &lt;em&gt;pyaz ka bhav kya hai?&lt;/em&gt; (what's the onion rate), &lt;em&gt;do kilo cheeni bhej dena&lt;/em&gt;, &lt;em&gt;kal ka order aadha hi aaya&lt;/em&gt;, &lt;em&gt;kya aap Amul butter rakhte ho?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;He is usually weighing something with one hand when the phone rings. Half the calls he misses. The ones he takes cost him a customer standing at the counter.&lt;/p&gt;

&lt;p&gt;The people calling him are not going to install an app. Many won't type an order in English. They are comfortable doing exactly one thing: talking on a phone, in whatever mix of Hindi and English they normally speak. That is why this had to be a voice agent and not a chatbot — voice is not a nicer interface here, it is the &lt;em&gt;only&lt;/em&gt; interface these users already have.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Meera&lt;/strong&gt;, a voice agent for &lt;strong&gt;Sharma Kirana Store&lt;/strong&gt;, on the &lt;strong&gt;Local Commerce&lt;/strong&gt; track. She answers the shop's phone, quotes real mandi rates, remembers regulars, takes orders, escalates to Sharma ji when she should not decide, calls customers back, and hands complaints to a returns specialist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/bhaveshmadisetty/voice-for-bharat-bhavesh" rel="noopener noreferrer"&gt;https://github.com/bhaveshmadisetty/voice-for-bharat-bhavesh&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What the agent actually does
&lt;/h2&gt;

&lt;p&gt;A real call sounds like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Caller:&lt;/strong&gt; Hello, pyaz ka kya bhav hai?&lt;br&gt;
&lt;strong&gt;Meera:&lt;/strong&gt; Namaste, main Meera bol rahi hoon Sharma Kirana Store se. Onion thirty-two rupaye kilo hai — aaj ka mandi rate.&lt;br&gt;
&lt;strong&gt;Caller:&lt;/strong&gt; Do kilo bhej dena. Main Raj.&lt;br&gt;
&lt;strong&gt;Meera:&lt;/strong&gt; Ji Raj bhai. Do kilo pyaz. Aapka naam aur order yaad rakh loon agli baar ke liye?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;She is not reciting a price list from her prompt. &lt;code&gt;get_market_price&lt;/code&gt; hits the Government of India's daily mandi feed on every single ask, because an onion rate moves several rupees a day and a shop assistant who quotes yesterday's number is worse than one who says "I'll check."&lt;/p&gt;




&lt;h2&gt;
  
  
  How the system works
&lt;/h2&gt;

&lt;p&gt;Four moving pieces, and every voice agent is some version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🎙️ user speaks
   ↓ audio
Deepgram STT  →  Gemini LLM (+ tools)  →  Murf Falcon TTS
   ↑                                          ↓ audio
   └──────────── LiveKit (WebRTC / SIP) ──────┘
                       ↓
              🔊 user hears the reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STT&lt;/strong&gt; — Deepgram &lt;code&gt;nova-3&lt;/code&gt;, which handles code-mixed Hindi-English input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt; — Gemini Flash, with tools for prices, memory, escalation, delivery slots, opt-out, and handoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS&lt;/strong&gt; — &lt;strong&gt;Murf Falcon&lt;/strong&gt;, voice &lt;code&gt;Anisha&lt;/code&gt; (Indian English female). 55ms model latency, ~130ms time to first audio. On a phone call this is the whole product: a 900ms gap before the agent speaks makes a caller say "hello?" and talk over her.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport&lt;/strong&gt; — LiveKit, which carries browser audio over WebRTC &lt;em&gt;and&lt;/em&gt; real telephone audio over a SIP trunk. Same agent code for both.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend is Next.js and shows the agent's live state — listening, thinking, speaking — so you can see where a delay came from instead of guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The features that tell the story
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Live mandi prices with a three-source fallback
&lt;/h3&gt;

&lt;p&gt;Prices come from three places, tried in order, and the caller is always told how fresh the number is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Live?&lt;/th&gt;
&lt;th&gt;Answers when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;data.gov.in daily mandi prices&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Live&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Normally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;A CSV the shop uploaded at &lt;code&gt;/prices&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;API timed out or rate-limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The shop's standing list of 14 staples&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Nothing else answered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mandis quote &lt;strong&gt;wholesale per quintal&lt;/strong&gt;; a kirana sells &lt;strong&gt;retail per kilo&lt;/strong&gt;. That conversion is done in Python, never by the LLM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;₹2,400/quintal → ₹24/kg wholesale → ×1.35 margin → ₹32/kg shelf rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because a caller is listening to silence while a request runs, the whole lookup has a &lt;strong&gt;6-second budget&lt;/strong&gt;. Stale data is labelled out loud — &lt;em&gt;"yesterday's closing rate"&lt;/em&gt; — and an unknown item is never guessed. She says Sharma ji will call back with that rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Memory, with consent
&lt;/h3&gt;

&lt;p&gt;A returning caller is greeted by name, in their preferred language, with their usual items ready. It's a SQLite file, it stores a first name, language, delivery preference and past items — and it is written &lt;strong&gt;only&lt;/strong&gt; after the customer says yes, and deleted on request.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Escalation to a human, with a spelled-out reference
&lt;/h3&gt;

&lt;p&gt;Two triggers only: a &lt;strong&gt;complaint&lt;/strong&gt;, or a &lt;strong&gt;price the tool genuinely could not fetch&lt;/strong&gt;. An item the shop doesn't stock is &lt;em&gt;not&lt;/em&gt; an escalation — nothing was blocked.&lt;/p&gt;

&lt;p&gt;Before anything is filed, two sentences are required: name what would be sent, then ask.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Main sirf itna bhejungi — aapka naam Raj, aur ki do kilo sugar mein se ek hi kilo aaya. Sharma ji ko bhej doon?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;create_escalation&lt;/code&gt; &lt;strong&gt;refuses to file&lt;/strong&gt; unless the customer's own last turn contains a real yes, checked against the session transcript. UPI handles, OTPs, PINs, Aadhaar and card numbers are scrubbed &lt;em&gt;before&lt;/em&gt; the row is written, not on the way out — a secret that reached storage has already leaked.&lt;/p&gt;

&lt;p&gt;The caller gets a reference like &lt;code&gt;SK-4F2B&lt;/code&gt;. The alphabet has no I, O, 0 or 1, because this gets said down a phone line and written on the back of a receipt.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Outbound calls, and the do-not-call list
&lt;/h3&gt;

&lt;p&gt;Meera also rings customers back to confirm orders. This inverts the consent story — an inbound caller chose to ring the shop; nobody chose this one. So two rules live in code, not in the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The opening line must state &lt;strong&gt;who, why, and how to stop&lt;/strong&gt; in the first two sentences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Stop calling me" hits the disk the moment it's said&lt;/strong&gt;, before the confirmation is even spoken. The list is checked three times: when queuing, before dialling, and again once the room is live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retries are decided from the actual SIP status, not from vibes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;SIP&lt;/th&gt;
&lt;th&gt;Retried?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;busy&lt;/code&gt; / &lt;code&gt;no_answer&lt;/code&gt; / &lt;code&gt;unreachable&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;486, 480, 404…&lt;/td&gt;
&lt;td&gt;Yes, up to 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;declined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;603, 403&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — they pressed reject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;voicemail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;answered, silence&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opted_out&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Never&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Voicemail gets a shorter message that names the shop but &lt;strong&gt;not&lt;/strong&gt; the items or the total — a recording can be played back by anyone.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A dashboard that can show failure
&lt;/h3&gt;

&lt;p&gt;Three numbers at &lt;code&gt;/dashboard&lt;/code&gt;. A call counts as successful only if a &lt;strong&gt;tool actually ran&lt;/strong&gt;: a rate was quoted, an order was saved with consent, or a ticket reached a human with a reference. Never by asking the model how it thought the call went — &lt;em&gt;a verdict the model grades itself on drifts towards flattering.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Failures are recorded with a reason: &lt;code&gt;unresolved&lt;/code&gt;, &lt;code&gt;opted_out&lt;/code&gt;, &lt;code&gt;no_conversation&lt;/code&gt;. A shop that only counts the calls it likes has a dashboard that always looks fine.&lt;/p&gt;

&lt;p&gt;The table stores a truncated room id, browser-or-phone, verdict, reason, duration. No name, no number, no transcript — because &lt;code&gt;call_outcomes&lt;/code&gt; &lt;strong&gt;has no column to put them in&lt;/strong&gt;, rather than filtering them out. A filter is one refactor away from being removed.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Handoff to a specialist
&lt;/h3&gt;

&lt;p&gt;When a customer complains about an order that already arrived, Meera announces one sentence and puts them through to &lt;strong&gt;Kavita&lt;/strong&gt;, the returns and refunds desk. The chat context travels with them, so the customer does not explain themselves twice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hard parts (this is the useful section)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The agent never joined the room, and the logs said nothing
&lt;/h3&gt;

&lt;p&gt;Day 1. The frontend connected to LiveKit fine. No agent ever appeared. Session ended with &lt;code&gt;agent didn't join the room&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AgentServer&lt;/code&gt; defaults &lt;code&gt;initialize_process_timeout&lt;/code&gt; to &lt;strong&gt;10 seconds&lt;/strong&gt;. That's the budget for prewarming — which on my machine meant Silero VAD &lt;em&gt;plus&lt;/em&gt; a 394 MB multilingual turn-detector model. Measured cold start: &lt;strong&gt;16.76s&lt;/strong&gt;. The worker hit the deadline, logged a bare &lt;code&gt;worker failed&lt;/code&gt;, and exited &lt;strong&gt;before registering&lt;/strong&gt; with LiveKit Cloud. A leftover prewarm subprocess kept &lt;code&gt;python.exe&lt;/code&gt; visible in Task Manager, so it looked alive.&lt;/p&gt;

&lt;p&gt;The fix was one line:&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initialize_process_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; the giveaway was the &lt;em&gt;absence&lt;/em&gt; of the &lt;code&gt;registered worker&lt;/code&gt; log line. When debugging a worker, check for the line that should be there, not just the errors that are.&lt;/p&gt;

&lt;h3&gt;
  
  
  She said the wrong number out loud, and every test passed
&lt;/h3&gt;

&lt;p&gt;Day 5. The price tool returned ₹32 for onion and Meera said &lt;em&gt;"tees"&lt;/em&gt; (30). ₹38 became &lt;em&gt;"aath tees"&lt;/em&gt;, which is not a number in any language.&lt;/p&gt;

&lt;p&gt;Hindi numerals between twenty and a hundred are irregular — you cannot compose &lt;em&gt;battees&lt;/em&gt; the way you compose &lt;em&gt;thirty-two&lt;/em&gt;. The model was assembling them from parts.&lt;/p&gt;

&lt;p&gt;Fix attempt 1: list the correct Hindi words in the prompt, and tell her to fall back to English digits &lt;em&gt;if unsure&lt;/em&gt;. It got most of the way there, then produced &lt;code&gt;"Oniontees rupees kilo hai"&lt;/code&gt; — &lt;em&gt;battees&lt;/em&gt; losing its first syllable and merging with the word before it. &lt;strong&gt;Being confident was not the same as being right, so the "if you are certain" escape hatch was itself the bug.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rule is now unconditional: &lt;strong&gt;rupee figures are spoken in English, inside the Hindi sentence.&lt;/strong&gt; &lt;em&gt;"Onion thirty-two rupaye kilo hai."&lt;/em&gt; Quantities stay Hindi (&lt;em&gt;"do kilo pyaz"&lt;/em&gt;). Every Pune shopkeeper talks this way. Measured over five runs: 5/5 correct, versus wrong roughly one time in three before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; remove a failure mode, don't narrow it. And forty passing unit tests told me nothing — I found this by driving four real conversations through the agent and &lt;em&gt;reading the transcript&lt;/em&gt;. Do that before you record a demo, not after.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding one prompt section silently broke a feature from three days earlier
&lt;/h3&gt;

&lt;p&gt;Day 7. The escalation block is the longest section in the prompt. Adding it broke a Day 4 test that had passed for two days: Meera stopped calling &lt;code&gt;lookup_caller&lt;/code&gt;, and greeted a two-call regular as a stranger.&lt;/p&gt;

&lt;p&gt;I verified it was a real regression by stashing the change — passed 3/3 before, failed 3/3 after. Strengthening the memory instruction got it to 2/3, and &lt;em&gt;that&lt;/em&gt; was the tell: the real conflict was with the &lt;strong&gt;greeting&lt;/strong&gt; rule at the top. On turn one with a name, both rules fire and the greeting wins. Naming the precedence explicitly — look them up first, &lt;em&gt;then&lt;/em&gt; introduce yourself in the same breath — got it to 4/4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; a prompt is one document, not a pile of independent sections. A new feature can break a finished one at a distance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The prompt fix that failed twice
&lt;/h3&gt;

&lt;p&gt;Day 9. The handoff was announced in Hindi to an English-speaking customer. The instruction had a language rule plus a worked example in each language — and the model copied the &lt;em&gt;Hindi example&lt;/em&gt;. The example outweighed the rule it existed to illustrate. Two rounds of stronger wording did not move it.&lt;/p&gt;

&lt;p&gt;The fix was to stop writing prompt. The model has &lt;strong&gt;already&lt;/strong&gt; classified the language to fill in the tool's &lt;code&gt;language&lt;/code&gt; argument. So the announcement's language variant is now selected &lt;strong&gt;in code&lt;/strong&gt;, and only the matching one is ever put in front of the model. There is nothing left to copy from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; when a prompt fix fails twice and the information already exists in code, decide it in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  A test can be wrong in the agent's favour
&lt;/h3&gt;

&lt;p&gt;My degradation test asked about imported olive oil and demanded the escalation line. Meera instead said the shop doesn't stock it — honest, useful, and exactly what a real shop assistant says. The test was asserting a &lt;em&gt;script&lt;/em&gt; instead of the &lt;em&gt;requirement&lt;/em&gt;. I rewrote it to assert the narrower thing that must hold: no invented number, and no dead end.&lt;/p&gt;

&lt;h3&gt;
  
  
  One more, for anyone doing PII redaction
&lt;/h3&gt;

&lt;p&gt;A sixteen-digit card number written in groups of four &lt;strong&gt;opens with a valid twelve-digit Aadhaar match.&lt;/strong&gt; With the Aadhaar pattern running first, the leading twelve got replaced and the last four — the part a card is identified by — sat in the ticket looking harmless. &lt;strong&gt;Longest shape has to win.&lt;/strong&gt; There is now a test named after that failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build your own — start here
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The four components
&lt;/h3&gt;

&lt;p&gt;Every voice agent is the same loop. Pick one provider per box and you have a working agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;STT&lt;/strong&gt; — turns speech into text (Deepgram, AssemblyAI, Whisper)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt; — decides what to say and which tools to call (Gemini, GPT, Claude)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS&lt;/strong&gt; — turns text back into speech (&lt;strong&gt;Murf Falcon&lt;/strong&gt; — 55ms, 150+ voices, 35+ languages)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time transport&lt;/strong&gt; — moves audio both ways with low latency (LiveKit, over WebRTC for browsers and SIP for phones)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trick is that these run &lt;em&gt;streaming and overlapping&lt;/em&gt;, not one after another. TTS starts speaking the first sentence while the LLM is still writing the second. That's why TTS latency dominates perceived responsiveness, and why Falcon matters more than it looks on a spec sheet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; 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 &lt;a href="https://cloud.livekit.io/" rel="noopener noreferrer"&gt;LiveKit Cloud&lt;/a&gt; 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/bhaveshmadisetty/voice-for-bharat-bhavesh.git
&lt;span class="nb"&gt;cd &lt;/span&gt;voice-for-bharat-bhavesh

&lt;span class="c"&gt;# Backend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;backend
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python src/agent.py download-files

&lt;span class="c"&gt;# Frontend&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../frontend
pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Where API keys go — and where they do not
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;backend/.env.local&lt;/code&gt; (copy from &lt;code&gt;.env.example&lt;/code&gt;). &lt;strong&gt;It is gitignored. Keep it that way.&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;&lt;span class="nv"&gt;LIVEKIT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wss://your-project.livekit.cloud
&lt;span class="nv"&gt;LIVEKIT_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;LIVEKIT_API_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;MURF_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...          &lt;span class="c"&gt;# murf.ai/api/dashboard&lt;/span&gt;
&lt;span class="nv"&gt;DEEPGRAM_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...      &lt;span class="c"&gt;# deepgram.com&lt;/span&gt;
&lt;span class="nv"&gt;GOOGLE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...        &lt;span class="c"&gt;# or OPENAI_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules I'd give anyone starting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never commit &lt;code&gt;.env.local&lt;/code&gt;.&lt;/strong&gt; Check your &lt;code&gt;.gitignore&lt;/code&gt; covers &lt;code&gt;.env.*&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; your first push, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The frontend only needs three of these&lt;/strong&gt; — &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; — for its token endpoint. I initially had all six in &lt;code&gt;frontend/.env.local&lt;/code&gt;. Nothing leaked (Next.js does not ship unprefixed vars to the browser), but there was no reason for those keys to be in that file. Give each service only the keys it uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never put a secret in a &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; variable.&lt;/strong&gt; That prefix means "send this to the browser."&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Run and test a conversation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1 — the agent&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv run python src/agent.py dev
&lt;span class="c"&gt;#   ⚠️ wait for the line: registered worker&lt;/span&gt;
&lt;span class="c"&gt;#   If you don't see it, the agent is NOT connected — see the prewarm bug above.&lt;/span&gt;

&lt;span class="c"&gt;# Terminal 2 — shop service (prices, escalations, dashboard)&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv run python src/price_server.py

&lt;span class="c"&gt;# Terminal 3 — frontend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;strong&gt;&lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;&lt;/strong&gt;, click &lt;strong&gt;Start talking&lt;/strong&gt;, allow the mic, and say &lt;em&gt;"pyaz ka bhav kya hai?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then try the other pages: &lt;code&gt;/prices&lt;/code&gt; to upload the shop's CSV, &lt;code&gt;/escalations&lt;/code&gt; for the open-case queue, &lt;code&gt;/dashboard&lt;/code&gt; for how the calls are going.&lt;/p&gt;

&lt;p&gt;To make a real phone ring, &lt;a href="https://linphone.org/" rel="noopener noreferrer"&gt;Linphone&lt;/a&gt; is the free path — a softphone plus one script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend
uv run python setup_trunk.py    &lt;span class="c"&gt;# writes SIP_OUTBOUND_TRUNK_ID for you&lt;/span&gt;
uv run python call_me.py        &lt;span class="c"&gt;# your phone rings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twilio works too if you need an actual PSTN number; setup is in the README.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change it into your own agent
&lt;/h3&gt;

&lt;p&gt;The whole personality lives in &lt;code&gt;backend/src/prompts.py&lt;/code&gt;, deliberately kept out of the pipeline code in &lt;code&gt;agent.py&lt;/code&gt;. Rewrite that file and you have a different agent — a clinic receptionist, a language tutor, a delivery tracker — on the same plumbing. Swap the Murf voice with one line: &lt;code&gt;tts=murf.TTS(voice="Samar")&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd improve next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A shopkeeper-facing view of the day.&lt;/strong&gt; The dashboard answers "are the calls working"; Sharma ji also wants "what did people ask for today that I don't stock."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More languages.&lt;/strong&gt; Marathi is the obvious next one for Pune. The pipeline supports it; the prompt's numeral rules would need the same treatment Hindi got.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Barge-in tuning.&lt;/strong&gt; Callers in a noisy shop interrupt constantly, and the turn detector is tuned for a quieter room than a kirana at 7pm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer judge-backed tests, more transcript reviews.&lt;/strong&gt; Every real bug I shipped was found by reading a transcript, not by a green suite.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The one thing I'd tell someone starting Day 1
&lt;/h2&gt;

&lt;p&gt;Your unit tests will pass while your agent says something wrong out loud. Voice agents fail in the gap between "the tool returned 32" and "the customer heard thirty-two." Read the transcripts. Say the numbers out loud yourself. That gap is where the whole product lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/bhaveshmadisetty/voice-for-bharat-bhavesh" rel="noopener noreferrer"&gt;https://github.com/bhaveshmadisetty/voice-for-bharat-bhavesh&lt;/a&gt;&lt;br&gt;
Built with &lt;a href="https://murf.ai/api" rel="noopener noreferrer"&gt;Murf Falcon&lt;/a&gt; · &lt;a href="https://docs.livekit.io" rel="noopener noreferrer"&gt;LiveKit&lt;/a&gt; · &lt;a href="https://deepgram.com" rel="noopener noreferrer"&gt;Deepgram&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;#VoiceForBharat&lt;/em&gt;&lt;br&gt;
`&lt;/p&gt;

</description>
      <category>voiceagent</category>
      <category>ai</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
