<?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: Aditya Nagalkar</title>
    <description>The latest articles on DEV Community by Aditya Nagalkar (@aditya_nagalkar_e38db4d7b).</description>
    <link>https://dev.to/aditya_nagalkar_e38db4d7b</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3875371%2F9e546982-ce90-445e-8ef1-48c712ddd37d.png</url>
      <title>DEV Community: Aditya Nagalkar</title>
      <link>https://dev.to/aditya_nagalkar_e38db4d7b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aditya_nagalkar_e38db4d7b"/>
    <language>en</language>
    <item>
      <title>I Built a Voice-Controlled AI Agent in Python</title>
      <dc:creator>Aditya Nagalkar</dc:creator>
      <pubDate>Sun, 12 Apr 2026 18:44:55 +0000</pubDate>
      <link>https://dev.to/aditya_nagalkar_e38db4d7b/i-built-a-voice-controlled-ai-agent-in-python-2045</link>
      <guid>https://dev.to/aditya_nagalkar_e38db4d7b/i-built-a-voice-controlled-ai-agent-in-python-2045</guid>
      <description>&lt;p&gt;Here is the complete, final article ready to be copied and published on Substack, Dev.to, or Medium.&lt;/p&gt;

&lt;p&gt;I Built a Voice-Controlled AI Agent in Python — Here's What Actually Went Wrong&lt;br&gt;
When I got the assignment to build a voice-controlled local AI agent, my first thought was — how hard can it be? Record audio, transcribe it, run some code. Three days later, I had a much more honest answer.&lt;/p&gt;

&lt;p&gt;This is a write-up of what I built, what broke, and what I learned along the way.&lt;/p&gt;

&lt;p&gt;What the Project Does&lt;br&gt;
The final system works like this: you speak a command into the browser, it transcribes your voice using Whisper, an LLM figures out what you meant and which tool to call, and then the agent actually does it — creates files, writes code, summarizes text, or just chats with you. Everything is surfaced in a web UI built with Gradio.&lt;/p&gt;

&lt;p&gt;The pipeline looks like this:&lt;/p&gt;

&lt;p&gt;Plaintext&lt;br&gt;
Your Voice → Whisper (STT) → LLaMA 3.3 70B (Intent) → Tool Execution → UI&lt;br&gt;
Simple on paper. Less simple in practice.&lt;/p&gt;

&lt;p&gt;The Architecture: How It Actually Fits Together&lt;br&gt;
I knew from the start that I wanted to keep the codebase modular. Tying LLM reasoning logic directly to Gradio UI components is a fast track to spaghetti code.&lt;/p&gt;

&lt;p&gt;I split the system into a clean frontend and a dedicated core/ backend directory. The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Interface (app.py)&lt;br&gt;
This is the Gradio frontend. It handles the multi-modal audio input (microphone, file upload, or raw file paths), manages the session state (like the 8-turn conversation memory and the human-in-the-loop toggle), and renders the final output alongside the latency benchmarks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Ears (core/stt.py)&lt;br&gt;
When app.py captures an audio payload, it passes it here. This module handles routing the audio to the Groq API to hit the whisper-large-v3 model. Because it's API-based, we get sub-second transcription instead of waiting 10–15 seconds for local processing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Brain &amp;amp; Dispatcher (core/agent.py)&lt;br&gt;
This is where the magic happens. It takes the transcribed text and the conversation history, packages them into a highly opinionated prompt, and queries llama-3.3-70b-versatile. Its job isn't to execute commands—its only job is to understand the intent and return that strictly formatted JSON array of commands. Once it parses the JSON, it dispatches the instructions to the appropriate tools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Hands (core/tools.py)&lt;br&gt;
This is the execution layer. It maps the LLM's intents to actual Python functions: create_file, write_code, summarize, or chat. This is also exactly where the safe_filepath() security gatekeeper lives. Before any write_code or create_file intent is executed, this module sanitizes the request to guarantee nothing ever gets written outside the designated output/ folder.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Stack &amp;amp; Model Choices&lt;br&gt;
Speech-to-Text: Whisper large-v3 via Groq API&lt;/p&gt;

&lt;p&gt;LLM: LLaMA 3.3 70B Versatile via Groq API&lt;/p&gt;

&lt;p&gt;UI: Gradio&lt;/p&gt;

&lt;p&gt;Language: Python&lt;/p&gt;

&lt;p&gt;I chose Groq over running models entirely locally for one crucial reason: speed. Running Whisper large-v3 locally on my machine requires about 3GB of VRAM and takes 5–15 seconds per clip. LLaMA 70B locally requires 40+ GB of RAM. Groq's LPU hardware delivers sub-second transcription and sub-3-second LLM responses, making the agent feel instant. For an agent that's supposed to feel responsive and conversational, that difference makes or breaks the user experience.&lt;/p&gt;

&lt;p&gt;Challenge 1: Getting the LLM to Return Consistent JSON&lt;br&gt;
The first real problem was making the LLM reliably output structured data. I needed it to return a specific schema:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;JSON&lt;br&gt;
{&lt;br&gt;
  "commands": [&lt;br&gt;
    {&lt;br&gt;
      "intent": "write_code",&lt;br&gt;
      "filename": "retry.py",&lt;br&gt;
      "code": "...",&lt;br&gt;
      "reply": "Created retry.py"&lt;br&gt;
    }&lt;br&gt;
  ]&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
Early versions of my prompt would randomly return extra conversational text before the JSON, wrap it in Markdown blocks, or just spit out a plain sentence. Every time the format broke, the entire pipeline crashed.&lt;/p&gt;

&lt;p&gt;The fix was a three-pronged approach:&lt;/p&gt;

&lt;p&gt;Using Groq's response_format={"type": "json_object"} parameter to force JSON output.&lt;/p&gt;

&lt;p&gt;Lowering the temperature to 0.3 for more deterministic responses.&lt;/p&gt;

&lt;p&gt;Rewriting the system prompt to be ruthlessly explicit — spelling out exact field names, types, and fallback behaviors for edge cases.&lt;/p&gt;

&lt;p&gt;Challenge 2: Compound Commands&lt;br&gt;
The assignment included a bonus feature: supporting multiple commands in a single breath. If a user says, "Create a Python file with a retry function and summarize what it does," that is two distinct intents at once.&lt;/p&gt;

&lt;p&gt;The problem? The LLM would often collapse them into one command, execute them out of order, or just drop the second half entirely.&lt;/p&gt;

&lt;p&gt;I had to structure the prompt to heavily emphasize that the commands array is ordered and every distinct action requires its own entry. Upgrading from llama-3.1-8b-instant to llama-3.3-70b-versatile was the real game-changer here. The smaller model was fast but sloppy with complex, multi-step instructions. The 70B model parses compound commands flawlessly.&lt;/p&gt;

&lt;p&gt;Challenge 3: The Frontend Was the Hardest Part&lt;br&gt;
I did not expect this going in. Gradio is fantastic for quick demos, but fighting its default styling is genuinely painful.&lt;/p&gt;

&lt;p&gt;I wanted an interface that felt intentional, but Gradio has a persistent dark theme that bleeds into components like the audio widget and buttons. CSS overrides alone don't fully fix it because Gradio relies heavily on internal CSS variables that take priority.&lt;/p&gt;

&lt;p&gt;After about six iterations, here is what kept going wrong:&lt;/p&gt;

&lt;p&gt;The audio widget stayed dark even after setting the background to white because it renders its own internal styles.&lt;/p&gt;

&lt;p&gt;The theme toggle I built changed the body element's class, but Gradio scopes its CSS to .gradio-container, rendering the toggle useless.&lt;/p&gt;

&lt;p&gt;Buttons kept reverting to Gradio's default orange accent regardless of my CSS.&lt;/p&gt;

&lt;p&gt;The actual solution: Passing a gr.themes.Base(...).set(...) object directly into gr.Blocks(). This sets Gradio's internal design tokens (input_background_fill, button_primary_background_fill, etc.) before any rendering happens. Once those tokens were aligned, my CSS layer on top worked cleanly.&lt;/p&gt;

&lt;p&gt;Challenge 4: File Safety&lt;br&gt;
The assignment required restricting file operations to an output/ folder. My initial implementation just used open(filename, "w"). It didn't take long to realize a user could theoretically say "create a file at ../../.env" and overwrite sensitive system files.&lt;/p&gt;

&lt;p&gt;The fix was building a safe_filepath() function in tools.py. It strips any path separators using os.path.basename(), validates the filename against a regex allowing only safe characters, and checks the extension against a strict allowlist. Nothing gets written outside the designated output/ sandbox.&lt;/p&gt;

&lt;p&gt;Bonus Features I Added&lt;br&gt;
Human-in-the-loop: A toggleable checkpoint that pauses the pipeline after transcription. You can read exactly what the agent heard before it executes anything — incredibly useful when audio quality is spotty.&lt;/p&gt;

&lt;p&gt;Session Memory: The agent retains the last 8 conversation turns and passes them as context to the LLM. You can say "now make that async" after generating code, and it understands exactly what "that" refers to.&lt;/p&gt;

&lt;p&gt;Performance Benchmarking: The UI breaks down STT time, LLM time, and total execution time for every single request.&lt;/p&gt;

&lt;p&gt;What I'd Do Differently&lt;br&gt;
If I were starting over, I'd respect prompt engineering from day one. I spent hours debugging JSON parsing errors in Python that were really just ambiguous instruction problems. The model does exactly what you tell it — if your instructions are vague, your output will be too.&lt;/p&gt;

&lt;p&gt;I'd also look into streaming the LLM output directly to the UI. For longer code generation tasks, there's a noticeable pause before the file appears, and streaming would make the agent feel instantly responsive.&lt;/p&gt;

&lt;p&gt;The Code&lt;br&gt;
The full project is open-source on GitHub: Adii22-22/voice-ai-agent&lt;/p&gt;

&lt;p&gt;Setup takes about five minutes — clone the repo, install the dependencies, drop a Groq API key into a .env file, and run python app.py.``&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
