GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/Advanced
Advanced/2026-03-17Advanced

Production-Grade Voice AI Agent with Gemini Live API & Google ADK [2026]

Build and deploy a production-grade voice AI agent by combining Gemini Live API with Google ADK, Function Calling, WebSocket management, and Cloud Run. Covers architecture design, connection stability, parallel tool execution, and cost optimization.

gemini102live-api4voice-agent2google-adk2function-calling20cloud-run6websocket4production140

Setup and context: Why Production Voice AI Is Hard

Getting a voice prototype running with Gemini Live API takes only a few dozen lines of Python. But the moment you try to turn that prototype into a real service, most developers hit the same walls:

  • Connection stability: WebSockets drop unexpectedly
  • Function Calling latency: Conversations stall during external API calls
  • Scaling: Balancing cost and quality as concurrent connections grow
  • Deployment: Works locally but breaks on Cloud Run

This guide walks through solving each of these problems to build a voice AI agent that holds up in enterprise environments. It's aimed at intermediate-to-advanced developers who already understand Gemini Live API basics and want to ship to production.

Prerequisite: For a basic introduction to Gemini Live API, see [Gemini Live API Complete Guide]((/articles/gemini-api/gemini-live-api-guide).


Prerequisites & Setup

What You Need

  • Python 3.11+
  • Google Cloud project (Vertex AI API enabled)
  • google-genai SDK 1.10.0+
  • google-adk 0.4.0+
  • Cloud Run / Docker (for production deployment)

Installation

pip install google-genai>=1.10.0 google-adk>=0.4.0 \
    fastapi uvicorn websockets aiohttp python-dotenv

Environment Variables

# .env
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_CLOUD_LOCATION=us-central1
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
 
# Or API key auth (dev only)
GEMINI_API_KEY=your-api-key

Architecture: The 3-Layer Design

A production voice agent works best with a three-layer separation of concerns:

Client (Browser / Mobile)
        ↕ WebSocket (wss://)
  ┌─────────────────────────────┐
  │    FastAPI WebSocket Server  │  ← Layer 1: Connection Management
  │  (Cloud Run / GKE)          │
  └──────────┬──────────────────┘
             │
  ┌──────────▼──────────────────┐
  │   Agent Orchestration Layer  │  ← Layer 2: Agent Control
  │   (Google ADK + Router)     │
  └──────────┬──────────────────┘
             │
  ┌──────────▼──────────────────┐
  │  Gemini Live API + Tools    │  ← Layer 3: AI Inference + Tool Execution
  │  (gemini-live-2.5-flash)   │
  └─────────────────────────────┘

The critical insight is separating connection management from agent logic. If the Gemini Live API session drops, the WebSocket server layer handles retries and fallback without the client ever noticing.


Step 1: Defining the Agent with Google ADK

Google ADK (Agent Development Kit) provides a structured framework for defining and running Gemini agents. Start by defining the agent's role and its tools.

# agent.py
import asyncio
from datetime import datetime
from google.adk.agents import LiveAgent
from google.adk.tools import function_tool
from google.genai.types import LiveConnectConfig, SpeechConfig, VoiceConfig
 
@function_tool
async def get_current_weather(city: str) -> dict:
    """Retrieve current weather data for a given city.
 
    Args:
        city: City name to look up (e.g., "Tokyo", "New York")
 
    Returns:
        Dict with temperature, condition, and humidity
    """
    mock_weather = {
        "Tokyo": {"temp": 18, "condition": "Sunny", "humidity": 55},
        "New York": {"temp": 12, "condition": "Cloudy", "humidity": 70},
    }
    data = mock_weather.get(city, {"temp": 15, "condition": "Unknown", "humidity": 50})
    return {
        "city": city,
        "temperature": data["temp"],
        "condition": data["condition"],
        "humidity": data["humidity"],
        "timestamp": datetime.now().isoformat(),
    }
 
@function_tool
async def search_knowledge_base(query: str, category: str = "general") -> dict:
    """Search the internal knowledge base and return relevant results.
 
    Args:
        query: The search query string
        category: Search scope (general / product / support)
 
    Returns:
        List of matching documents with relevance scores
    """
    return {
        "query": query,
        "category": category,
        "results": [
            {"title": f"Document about '{query}'", "relevance": 0.92},
            {"title": f"{category} category FAQ", "relevance": 0.78},
        ],
    }
 
@function_tool
async def create_support_ticket(
    issue_summary: str,
    priority: str = "medium",
    user_id: str = ""
) -> dict:
    """Create a support ticket for the user's issue.
 
    Args:
        issue_summary: Brief description of the issue
        priority: Ticket priority (low / medium / high / urgent)
        user_id: Optional user identifier
 
    Returns:
        Created ticket details including ID and ETA
    """
    import random
    ticket_id = f"TKT-{random.randint(10000, 99999)}"
    return {
        "ticket_id": ticket_id,
        "summary": issue_summary,
        "priority": priority,
        "status": "created",
        "estimated_response": "Within 2 hours" if priority in ["high", "urgent"] else "Within 1 business day",
    }
 
def create_voice_agent() -> LiveAgent:
    """Instantiate a production-ready voice AI agent."""
    return LiveAgent(
        model="gemini-live-2.5-flash-native-audio",
        system_instruction="""You are a polite and capable customer support AI assistant.
 
Your responsibilities:
- Answer customer questions clearly via voice
- Use weather, knowledge base, and ticket creation tools as needed
- Keep responses concise to maintain natural conversation flow
- Always confirm before taking irreversible actions
 
Constraints:
- Never handle sensitive data like passwords or credit card numbers
- Don't make definitive claims about unverifiable facts""",
        tools=[get_current_weather, search_knowledge_base, create_support_ticket],
        config=LiveConnectConfig(
            response_modalities=["AUDIO"],
            speech_config=SpeechConfig(
                voice_config=VoiceConfig(
                    prebuilt_voice_config={"voice_name": "Aoede"}
                )
            ),
        ),
    )

Key insight: ADK's @function_tool decorator automatically builds the Function Calling JSON schema from your Python type hints and docstrings. No manual schema writing required.


Step 2: WebSocket Server with Reconnection Logic

The biggest challenge in production is connection stability. Gemini Live API sessions have a default limit (~15 minutes), and network issues can cause abrupt drops.

# server.py
import asyncio
import logging
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from agent import create_voice_agent
 
logger = logging.getLogger(__name__)
app = FastAPI(title="Voice Agent API")
 
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
 
class VoiceAgentSession:
    """Manages a single voice agent session with retry logic."""
 
    MAX_RETRIES = 3
    RETRY_DELAY = 1.5  # seconds
 
    def __init__(self, session_id: str):
        self.session_id = session_id
        self.agent = create_voice_agent()
        self._live_session = None
        self._retry_count = 0
 
    async def _connect_live_session(self):
        """Connect to Gemini Live API with exponential backoff."""
        while self._retry_count < self.MAX_RETRIES:
            try:
                self._live_session = await self.agent.connect()
                self._retry_count = 0
                logger.info(f"[{self.session_id}] Live API connected")
                return
            except Exception as e:
                self._retry_count += 1
                wait = self.RETRY_DELAY * (2 ** (self._retry_count - 1))
                logger.warning(f"[{self.session_id}] Retry {self._retry_count}/{self.MAX_RETRIES} in {wait:.1f}s: {e}")
                await asyncio.sleep(wait)
        raise RuntimeError(f"[{self.session_id}] Max retries exceeded")
 
    async def handle(self, client_ws: WebSocket):
        """Bridge between the client WebSocket and Gemini Live API."""
        await client_ws.accept()
        await self._connect_live_session()
 
        send_task = asyncio.create_task(self._forward_client_to_gemini(client_ws))
        recv_task = asyncio.create_task(self._forward_gemini_to_client(client_ws))
 
        try:
            done, pending = await asyncio.wait(
                [send_task, recv_task], return_when=asyncio.FIRST_EXCEPTION
            )
            for task in pending:
                task.cancel()
        except WebSocketDisconnect:
            logger.info(f"[{self.session_id}] Client disconnected")
        finally:
            await self._cleanup()
 
    async def _forward_client_to_gemini(self, client_ws: WebSocket):
        async for message in client_ws.iter_bytes():
            if self._live_session:
                await self._live_session.send_audio(message)
 
    async def _forward_gemini_to_client(self, client_ws: WebSocket):
        async for event in self._live_session.receive():
            if event.type == "audio":
                await client_ws.send_bytes(event.data)
            elif event.type == "text":
                await client_ws.send_json({"type": "transcript", "text": event.text})
 
    async def _cleanup(self):
        if self._live_session:
            try:
                await self._live_session.close()
            except Exception:
                pass
 
@app.websocket("/ws/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
    session = VoiceAgentSession(session_id)
    await session.handle(websocket)
 
@app.get("/health")
async def health_check():
    return {"status": "ok", "model": "gemini-live-2.5-flash-native-audio"}

Step 3: Parallel Function Calling to Minimize Response Gaps

The biggest UX problem with voice agents is silence during tool execution. While Gemini Live API is running a Function Call, audio output pauses. Running tools in parallel dramatically reduces this gap.

# parallel_tools.py
import asyncio
from typing import Any
 
class ParallelToolExecutor:
    """Orchestrates parallel execution of multiple Function Calls."""
 
    def __init__(self, timeout_seconds: float = 5.0):
        self.timeout = timeout_seconds
        self._registry: dict[str, callable] = {}
 
    def register(self, name: str, fn: callable):
        self._registry[name] = fn
 
    async def execute_parallel(self, tool_calls: list[dict]) -> list[dict]:
        """Execute multiple tool calls concurrently.
 
        Args:
            tool_calls: [{"name": "tool_name", "args": {...}}, ...]
 
        Returns:
            Results list with status for each call
        """
        tasks = [
            (call["name"], asyncio.create_task(
                self._run(self._registry[call["name"]], call["args"])
            ))
            for call in tool_calls
            if call["name"] in self._registry
        ]
        results = []
        for name, task in tasks:
            try:
                results.append({"tool": name, "result": await task, "status": "success"})
            except asyncio.TimeoutError:
                results.append({"tool": name, "result": {"error": "Timed out"}, "status": "timeout"})
            except Exception as e:
                results.append({"tool": name, "result": {"error": str(e)}, "status": "error"})
        return results
 
    async def _run(self, fn: callable, args: dict) -> Any:
        return await asyncio.wait_for(fn(**args), timeout=self.timeout)
 
# Usage
executor = ParallelToolExecutor(timeout_seconds=4.0)
executor.register("get_current_weather", get_current_weather)
executor.register("search_knowledge_base", search_knowledge_base)
 
results = await executor.execute_parallel([
    {"name": "get_current_weather", "args": {"city": "Tokyo"}},
    {"name": "search_knowledge_base", "args": {"query": "return policy", "category": "support"}},
])

Expected output:

[
  {"tool": "get_current_weather", "result": {"city": "Tokyo", "temperature": 18, "condition": "Sunny"}, "status": "success"},
  {"tool": "search_knowledge_base", "result": {"query": "return policy", "results": [...]}, "status": "success"}
]

Troubleshooting

Issue 1: WebSocket drops after a few minutes

Cause: Gemini Live API's ~15-minute session limit, or Cloud Run's default 300-second request timeout.

Fix: Set --timeout=3600 in Cloud Run. Implement client-side keepalive pings every 30 seconds:

async def keepalive_ping(websocket):
    while True:
        await asyncio.sleep(30)
        try:
            await websocket.ping()
        except Exception:
            break

Issue 2: Audio cuts out after a Function Call

Cause: Tool execution exceeding 3 seconds can cause Gemini to time out the response cycle.

Fix: Cache heavy API responses in Memorystore (Redis). Design all tool calls to complete within 2–3 seconds. Consider playing a "hold on" audio filler while the tool runs.

Issue 3: PERMISSION_DENIED on Cloud Run

Cause: The service account lacks required Vertex AI roles.

Fix:

gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
  --member="serviceAccount:YOUR-SA@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"

Deploying to Cloud Run

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8080
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]
gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/voice-agent .
 
gcloud run deploy voice-agent \
  --image gcr.io/YOUR_PROJECT_ID/voice-agent \
  --platform managed \
  --region us-central1 \
  --timeout 3600 \
  --concurrency 80 \
  --min-instances 1 \
  --max-instances 20 \
  --set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID \
  --allow-unauthenticated

Important: Always set --min-instances 1 to eliminate cold start latency. In voice agents, the first 100ms of delay is directly felt by users.


Cost & Performance Considerations

Estimated costs (March 2026, 1,000 sessions/month)

ItemRateEstimated Cost
Live API audio input$0.70 / 1M tokens~$14
Live API audio output$2.80 / 1M tokens~$56
Cloud Run (CPU time)$0.00002400/vCPU-sec~$24
Firestore (read/write)$0.06–0.18/1M ops~$5
Total~$99

Three cost optimization strategies

① VAD (Voice Activity Detection): Silence detection that prevents sending non-speech audio to Gemini can reduce input tokens by 30–50%. The webrtcvad library works well here.

② Session reuse: For users who reconnect within a short window, reusing an existing Live API session avoids re-establishing context. Verify carefully that conversation state transfers correctly.

③ Flash vs. Pro: Use gemini-live-2.5-flash-native-audio for general customer support. Reserve gemini-live-2.5-pro-native-audio (when available) for use cases requiring deep domain knowledge or complex reasoning.


Conclusion

This guide covered building a production-grade voice AI agent with Gemini Live API and Google ADK. Key takeaways:

  • A 3-layer architecture (connection, orchestration, inference) isolates failures and simplifies scaling
  • ADK's @function_tool eliminates manual JSON schema authoring with type-driven generation
  • Exponential backoff reconnection keeps sessions alive in unreliable network conditions
  • Parallel Function Calling eliminates audio gaps during tool execution
  • Cloud Run with min-instances 1 removes cold start latency for latency-sensitive voice UX

For next steps, explore [Gemini Agent Production System]((/articles/gemini-advanced/gemini-agent-production-system) and [Gemini Firebase GenKit Guide]((/articles/gemini-dev/gemini-firebase-genkit) to build more sophisticated multi-agent architectures.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Advanced2026-07-11
A Risk-Tiered Approval Gate for Gemini Function Calling
Handing full autonomy to an agent is unnerving. This walks through a Gemini function-calling loop that routes tool calls into auto-run and hold-for-approval by risk tier, then feeds the result back to the model after a human signs off.
Advanced2026-06-27
Your Gemini Completion Event Will Arrive Twice — An Idempotent Sink That Makes Webhook + Reconciliation Effectively Once-Only
Once you receive Gemini long-running operations over a Webhook and back it up with a reconciliation poller, the same completion arrives twice and publishing or billing runs twice. Build an idempotent sink with a normalized key and a claim-run-commit pattern that keeps side effects effectively once-only.
Advanced2026-06-14
Trusting Gemini Structured Output in Production — Schema Design, Double Validation, and Bounded Retries
Gemini's structured output guarantees parseable JSON, not correct values. Notes on schema design with @google/genai, why propertyOrdering matters, a Zod double-validation layer, handling MAX_TOKENS truncation, and a bounded-retry extraction pipeline.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →