Building SafeReach: A 10-Day Disaster-Response Voice Agent Build
I just wrapped up the "10 Days of Voice Agents — VoiceForBharat Edition" challenge by Murf AI, and I built something I'm genuinely glad exists now, even in prototype form: SafeReach, a voice-based disaster-response assistant for the Disaster Response track.
This post is the honest version of that build. Not the highlight reel — the version with the import errors, the SIP failure I couldn't immediately explain, and the design decisions I second-guessed halfway through. If you're thinking about building a voice agent yourself, I think the debugging is more useful to you than the demo.
The problem I wanted to solve
During a flood or a cyclone, people don't sit down and type. They're stressed, their hands might be full, they might be moving, and sometimes the situation is loud or chaotic. Typing a query into a chatbot is a strange ask in that moment. Speaking is not.
At the same time, I was very aware of a trap that's easy to fall into when building anything "AI + disaster": pretending your side project has capabilities it doesn't. A hackathon agent is not the National Disaster Management Authority. It doesn't have a live feed of shelter occupancy. It can't dispatch rescue teams. If it acts like it can, it's not just unhelpful — it's actively dangerous, because someone might trust it in a moment when trust matters.
So the actual design brief I gave myself was narrower than "build a disaster AI." It was: build something that tells the truth about what it knows, helps with what it safely can, and gets a human into the loop the moment a human is actually needed.
Meet SafeReach
SafeReach is a browser (and, experimentally, phone-call) based voice assistant. It talks in a calm, practical tone, supports Indian English and Tamil/Tanglish code-mixed conversation, and is explicitly instructed to never pretend to be an official emergency authority.
It can:
- Remember useful context about a returning caller (name, location, household size, mobility needs)
- Pull real current weather data instead of guessing
- Detect when a situation needs human intervention, ask permission, and file a structured escalation request
- Hand off shelter-related questions to a narrower specialist agent
- Log call outcomes to a small analytics dashboard
It cannot, and does not claim to:
- Access real government emergency alert systems
- Know real-time shelter availability
- Dispatch anyone
- Guarantee a response time
That gap between "what it sounds like it might do" and "what it actually does" is the thing I spent the most energy on, honestly more than the STT/LLM/TTS plumbing.
Why voice, specifically
Two reasons. First, the practical one above — typing is a bad interface under stress. Second, voice forces you to write a genuinely conversational system prompt. You can't hide behind a bulleted UI with buttons for "report flood" / "check shelter" / "get weather." The agent has to actually understand what someone means when they say something like "the water's coming into the house and my mother can't walk," and route that correctly. That constraint made the personality and safety design work much harder, in a good way.
Architecture
At a high level, the system looks like this:
User
↓
Browser / SIP
↓
LiveKit (real-time transport)
↓
SafeReach Main Agent
├── Memory (SQLite)
├── Weather Tool (Open-Meteo)
├── Human Escalation
├── Specialist Handoff
└── Call Outcome Logging
↓
Gemini (reasoning)
↓
Murf Falcon (TTS)
↓
User
LiveKit is the transport layer — it moves audio in and out in real time, whether the participant is connecting from a browser or over SIP. The agent logic sitting on top of it is what actually coordinates everything: it takes the transcript from Deepgram, decides what to do (sometimes that means calling a tool, sometimes it means handing off to a specialist), asks Gemini to reason about the response, and sends the result to Murf for speech synthesis.
The stack, concretely:
- Frontend: Next.js, based on the Murf LiveKit starter project, run with pnpm
-
Backend: Python, LiveKit Agents (
AgentSession,AgentServer), LiveKit RTC - STT: Deepgram Nova-3
-
LLM: Google Gemini (I used
gemini-3.5-flash-litefor the actual implementation) -
TTS: Murf Falcon, Indian voice "Anisha,"
en-INlocale, conversational style - Turn detection: LiveKit's MultilingualModel
- VAD: Silero
- Noise cancellation: LiveKit's noise cancellation, handled differently for browser vs. SIP participants
- Memory: SQLite
- External data: Open-Meteo weather API
- Telephony: LiveKit SIP for outbound calling
The backend is organized roughly like this:
src/
agent.py
prompts.py
memory.py
escalation.py
dashboard.py
telephony/
One decision I'm glad I made early: the system prompt lives in its own prompts.py file, not inline in agent.py. Once you start layering in personality, safety boundaries, and disaster-response objectives, that prompt gets long. Keeping it separate made it much easier to iterate on without scrolling past agent wiring code every time.
What I built during the 10-day challenge
I'm not going to walk through this day-by-day as a checklist — that gets repetitive fast. Instead, here's the story in the order the pieces actually came together.
Days 1–2: Giving the agent an identity and boundaries
The first real work wasn't code, it was writing down what SafeReach is not allowed to do. Before I wrote a single tool, I defined its identity, personality, responsibilities, and safety boundaries in prompts.py. That included explicit instructions never to claim access to official emergency systems, never to fabricate current alerts, and never to invent shelter availability.
This felt slow at the time, but it paid off later — every subsequent feature (memory, escalation, specialist handoff) had a clear frame to fit into, because the "who is this agent and what is it for" question was already answered.
Day 3: Voice interaction and multilingual behavior
Next I worked on making the conversation actually feel usable — supporting Indian English naturally and handling Tamil / Tanglish code-mixed speech, since that's how a lot of people in Tamil Nadu actually talk day to day, especially under stress, switching between languages mid-sentence.
Memory: not repeating yourself to a machine during a crisis
By Day 4, I added persistent caller memory using SQLite. The idea is simple: if someone calls back, or continues a session, SafeReach shouldn't make them repeat their name, location, household size, or mobility needs from scratch. That's a small thing in a calm context, but in a stressful one, having to re-explain "my mother uses a wheelchair" for the third time is exactly the kind of friction I wanted to remove.
[SCREENSHOT 1 — SafeReach browser conversation]
Notice how the agent references previously saved context instead of asking the same onboarding questions again.
Real weather, not guessed weather
Day 5 was about adding an actual weather tool using the Open-Meteo API, pulling current temperature, relative humidity, and wind speed. The non-negotiable design rule here: if the tool fails, SafeReach says so. It does not fall back to generating a plausible-sounding weather estimate from the LLM. In a disaster-response context, a confidently wrong weather guess is worse than an honest "I couldn't retrieve that right now."
This is a small piece of engineering, but I think it's one of the more important ones in the whole project, because it's the pattern that everything else (escalation, specialist handoff) follows: prefer an honest gap over a fabricated answer.
Human escalation: the most important feature I built
Day 7 is where the project stopped feeling like a chatbot demo and started feeling like something with real stakes.
SafeReach can detect situations that need a human — someone trapped, injured, or in immediate danger, or someone who needs urgent local assistance it can't provide itself. When that happens, it doesn't just fire off a report. The flow is:
User reports serious situation
↓
SafeReach identifies need for human help
↓
SafeReach explains what information it wants to share
↓
SafeReach asks for explicit permission
↓
Caller gives permission
↓
create_human_help_request tool is called
↓
Request is saved
↓
Reference ID is returned
↓
Caller receives an honest next step
The permission step was a deliberate design choice, not an afterthought. The tempting shortcut is: detect a serious situation, immediately package up everything you know, and send it. I didn't want that. The workflow is detect → explain → ask permission → create request, not detect → automatically send. Even in an emergency, a person should know what's being shared about them before it's shared.
The escalation summary itself is intentionally narrow — who needs help, what happened, what the agent already checked, urgency, language, and preferred follow-up method. It explicitly excludes passwords, OTPs, PINs, account numbers, or any other sensitive personal data that has no business being in a help request.
Escalation requests are stored in a local JSON file with a structure like:
reference_id
created_at
status
who_needs_help
what_happened
checked
urgency
language
follow_up_method
During testing, the system generated reference IDs like SR-F423BE3E, which the caller receives so they have something concrete to reference. (I'm not sharing real caller data here — anything shown is either structural or anonymized.)
[SCREENSHOT 2 — Human escalation permission flow]
This is the moment where SafeReach explains what it wants to share and waits for explicit confirmation before proceeding.
[SCREENSHOT 3 — Human-help reference ID]
The reference ID the caller receives after a request is filed — their proof that something was actually logged.
Call analytics: closing the feedback loop
Day 8 was the dashboard. I wanted it to reflect real call data, not hardcoded placeholder numbers, so it tracks total calls, successful calls, and failed calls, pulled from actual logged outcomes.
Defining "success" for a disaster-response agent isn't obvious, so I settled on: a successful call is one where the caller receives verified information or an appropriate human-help request is created. That definition matters because it keeps "success" tied to genuinely useful outcomes rather than just "the call didn't crash."
I tested this by making an actual call through the system and confirming the total call count increased afterward — small, but it proved the loop actually works end to end: real call → outcome recorded → dashboard updated. I want to be clear this is not a production analytics system with historical trends or large sample sizes. It's a working feedback loop, tested at the scale you'd expect from a 10-day build.
[SCREENSHOT 4 — Call analytics dashboard]
Watch the total call counter — this number only moves because of an actual completed call, not a static mock value.
Specialist handoff: not making one agent do everything
Day 9 added a second agent: the SafeReach Shelter Information Specialist. Its job is deliberately narrower than the main agent's — it focuses on shelter-specific questions.
When the conversation turns toward needing shelter, the main agent tells the user before handing off — something like "I will connect you to our shelter information specialist" — and the specialist picks up from there, introducing itself ("Hello, I'm your SafeReach Shelter Information Specialist...") while retaining relevant context from the conversation so far, like location, household size, mobility difficulties, and the shelter requirement itself. The user shouldn't have to re-explain their whole situation to a "new" agent that just joined.
This was the point where the project stopped being one big prompt trying to do everything and became something closer to a small multi-agent system, where each agent has a defined, limited scope.
[SCREENSHOT 5 — Shelter Specialist handoff]
Look for the explicit handoff message and the specialist's introduction — context from the main conversation carries over instead of resetting.
Telephony: outbound calling over SIP
Day 6, chronologically, was outbound calling using LiveKit SIP. I'm putting it last in the story because it's the one area where I don't have a clean success story — and I'd rather tell you that honestly than pretend otherwise.
Real-time voice-over-SIP involves several moving pieces at once: LiveKit itself, the SIP trunk configuration, the destination number, and the network/provider behavior in between. During testing I hit a 486 Busy Here response on an outbound call attempt. The lesson that stuck with me: a correctly configured application-side SIP trunk does not, on its own, guarantee that a destination will accept an outbound SIP INVITE. The failure can live entirely outside your codebase, in infrastructure you don't control and can't fully debug from your terminal.
I'm not going to claim every telephony test in this project went smoothly, because it didn't. What I can say is that I got outbound calling working and understood, concretely, where the failure modes live — which is arguably more useful than a clean success would have been.
Challenges and debugging lessons
Since this section matters more to me than the feature list, here are the real problems, in the order they annoyed me.
1. The Python relative import error.
Running the agent directly with something like:
uv run python .\src\agent.py dev
got me:
ImportError: attempted relative import with no known parent package
The root cause was running a package module directly instead of executing it as part of the package structure. The fix was to run it as a module instead:
uv run python -m src.agent dev
Small fix, but it's a good reminder that Python's import system cares about how you invoke a file, not just what's in it — especially once your project has internal package structure like src/.
2. Deepgram streaming language configuration.
I initially ran into an issue with Deepgram's streaming STT and language handling — trying to lean on automatic language detection didn't behave the way I expected. The fix was to explicitly configure the streaming language behavior instead of relying on automatic detection.
3. SIP outbound calling — the 486 Busy Here issue described above. Telephony debugging is a different discipline from application debugging, because the point of failure might be a provider or destination behavior you can't inspect directly.
4. Finding the handoff mechanism.
When I started on the specialist handoff, I actually checked whether the base Agent class exposed some obvious handoff() or transfer() method — it didn't show up doing a dir(Agent) on it. There's no single built-in method that does this for you. The handoff has to be implemented through LiveKit's agent/tool architecture directly, which took some reading through the framework before I got it working end to end.
5. Getting the escalation permission flow right.
This wasn't really a code bug, more a design bug I caught myself making. My first instinct was to have the agent detect a serious situation and immediately compile the escalation summary. I changed it to detect → explain → ask permission → create request, because skipping the permission step felt wrong for something built specifically for a disaster-response context, even though it added an extra conversational turn.
How to build your own voice agent (starting point)
If you want to build something similar, the rough order that worked for me:
- Define the agent's identity and hard boundaries before writing tools — decide what it must never claim to be able to do.
- Get the basic STT → LLM → TTS loop working with the simplest possible prompt.
- Add one real tool (in my case, weather) and enforce "fail honestly" behavior for it.
- Add memory only once the core loop is stable — it's much easier to debug memory issues when you're not also debugging the pipeline.
- Treat any "needs a human" pathway as a first-class feature, not an edge case bolted on at the end.
- Only split into multiple agents once a single prompt is genuinely trying to do too much.
Project setup
If you want to run this yourself:
Backend:
uv sync
uv run python -m src.agent dev
Frontend:
pnpm install
pnpm dev
Environment variables live in .env.local — never commit this file to GitHub. You'll need to provide your own credentials for LiveKit, Murf, Deepgram, and Google/Gemini, along with any other services the project depends on. None of my actual keys are in the repository or in this post.
Once both are running, you open the frontend locally and it connects to your running LiveKit agent session.
[SCREENSHOT 6 — Project architecture]
A visual of the pipeline described above — useful for seeing how STT, LLM reasoning, tools, memory, and TTS fit around the LiveKit transport layer.
Security and privacy considerations
I want to be direct about this: SafeReach is a challenge prototype, not a production emergency-dispatch system. A few decisions I made with that in mind:
- No API keys, phone numbers, or real caller data are published anywhere in this post or the repository.
- No OTPs, PINs, or passwords are ever included in escalation summaries — that's enforced by design, not just convention.
- I'm not publishing full conversation transcripts or exposing sensitive caller information on the dashboard.
- Escalation summaries are scoped to only what's operationally necessary.
If this were ever going to move beyond a prototype, it would need real authentication and authorization, encryption in transit and at rest, audit logging, defined data retention policies, stronger PII minimization, proper secret management, actual integration with verified emergency services, and real reliability monitoring. None of that exists here, and I don't want to imply otherwise.
What I learned
The biggest shift for me over these 10 days was realizing that a voice agent isn't really "STT + LLM + TTS glued together." That's the easy 20%. The part that actually makes it useful — memory, tool use with honest failure handling, human escalation with a permission model, specialist routing, basic analytics, and privacy boundaries — is the other 80%, and none of it comes from the LLM being smarter. It comes from the system around the LLM being designed carefully.
The other thing that stood out: most of my actual debugging time went into the integration layer — imports, SIP configuration, STT language settings — not into prompt engineering or getting Gemini to "reason better." If you're planning your own build, budget your time accordingly. The glue code will fight you more than the model will.
What I'd build next
To be clear, none of the following exists yet — these are honest next steps, not hidden features:
- Verified integrations with actual government/emergency information sources
- Real shelter availability data instead of general guidance
- Additional specialist agents beyond shelter information
- Automatic duplicate-escalation detection
- A human-facing support dashboard with status tracking for open escalations
- More reliable SIP handling and monitoring for outbound calls
- Automatic callback after an escalation is resolved
- Better handling of Tamil/Tanglish speech patterns
- More detailed call analytics beyond the current basic counts
- Production-grade authentication and privacy controls
- Latency monitoring across the pipeline
- Automated tests for agent routing and safety behavior
Screenshots / evidence
The screenshots referenced throughout this post (conversation flow, escalation permission step, reference ID, dashboard, and specialist handoff) show the actual working system as tested during the challenge. Any caller information visible in them is either test data or anonymized.
GitHub and resources
- [GitHub Repository — SafeReach] (link to follow)
- Murf LiveKit Starter project
- LiveKit documentation
- Murf Falcon documentation
Final thoughts
SafeReach isn't a finished product, and it was never trying to be one in 10 days. What I hope comes through in this post is that building something for a high-stakes domain like disaster response forces a different kind of discipline than building a general-purpose chatbot. You spend as much time deciding what the agent shouldn't do as what it should. That constraint made this the most interesting build I've done, and honestly, the most useful thing I've learned isn't Deepgram config syntax or LiveKit's SIP setup — it's that "I don't know, let me get you a real person" is sometimes the most responsible thing a voice agent can say.
LinkedIn Post — Day 10
I just wrapped up the "10 Days of Voice Agents — VoiceForBharat Edition" challenge by Murf AI, building SafeReach — a voice-based disaster-response assistant for the Disaster Response track.
Over 10 days, SafeReach grew from a basic voice loop into a system with persistent caller memory, a real-time weather tool, a human escalation workflow with explicit permission-based consent, a call analytics dashboard, and a specialist-agent handoff for shelter-related questions — all running on LiveKit, Deepgram Nova-3, Gemini, and Murf Falcon (Indian voice, en-IN).
One real challenge worth mentioning: testing outbound telephony over LiveKit SIP, I hit a "486 Busy Here" response and learned firsthand that a correctly configured SIP trunk on your side doesn't guarantee the other side will accept the call — telephony debugging lives partly outside your own codebase.
The biggest lesson from this build: a useful voice agent isn't just STT + LLM + TTS. It's memory, honest error handling, human escalation, and clear boundaries about what it can and can't actually do — especially in a disaster-response context, where pretending to know something you don't is worse than admitting you don't know it.
I wrote up the full technical journey, including the debugging that didn't go smoothly, here: [blog URL]
Thanks to Murf AI for putting together this challenge.
10DaysofAIVoiceAgents #MurfFalcon #VoiceForBharat
@murf AI
Day 10 Submission Summary
Project: SafeReach
Track: Disaster Response
What I built: A browser-based disaster-response voice agent using LiveKit, Gemini, Deepgram Nova-3, and Murf Falcon, with persistent caller memory, a real-time weather tool, a human escalation workflow with explicit user permission, a call analytics dashboard, and a specialist-agent handoff for shelter-related questions.
Build process: Built incrementally over the 10-day challenge, starting with agent identity and safety boundaries, then adding multilingual voice interaction, memory, weather, outbound telephony via LiveKit SIP, human escalation, call analytics, and specialist handoff.
Status: This is a challenge prototype, not a production emergency-dispatch system. It has no live integration with official emergency services or real shelter-availability data.
Suggested Blog Tags
voiceagents livekit murf disasterresponse ai conversationalai python speechtotext texttospeech buildinpublic
Suggested Title Alternatives
- Building SafeReach: A 10-Day Disaster-Response Voice Agent Build (current, 67 chars)
- SafeReach: What I Learned Building a Disaster-Response Voice Agent in 10 Days (80 chars)
- I Built a Disaster-Response Voice Agent in 10 Days — Here's What Broke (73 chars)
- Voice Agents Aren't Just STT + LLM + TTS: Lessons from Building SafeReach (76 chars)
- 10 Days, One Voice Agent, and a
486 Busy HereI Didn't See Coming (70 chars)
All options are well under the 128-character limit for full-post titles.
Final Pre-Publication Checklist
- [ ] Insert real GitHub repository URL where
[GitHub Repository — SafeReach]appears - [ ] Insert real blog URL into the LinkedIn post before publishing
- [ ] Replace all six screenshot placeholders with actual images
- [ ] Double-check no real phone numbers, API keys, or caller data appear in any screenshot
- [ ] Confirm reference ID example (
SR-F423BE3E) is test/anonymized data, not a real record - [ ] Verify tech stack names against actual code one more time (Gemini model name, Murf voice name, Deepgram model)
- [ ] Confirm "what I'd build next" list stays clearly separated from what's actually built
- [ ] Read once more for any accidental marketing language before publishing
- [ ] Add live demo link if/when available
- [ ] Tag Murf AI correctly on LinkedIn post before publishing
Top comments (0)