Skip to navigation

Gemini Live

View as Markdown

This guide shows how to integrate the AgentDuet SDK with the Google GenAI SDK for bidirectional real-time audio streaming with Gemini’s native audio model.

What this integration provides

A phone agent that answers incoming calls, streams caller audio to Gemini Live at 24 kHz PCM, plays Gemini audio back on the call, clears the outbound buffer on barge-in, and tears down the Live session on hangup.

Prerequisites

pip install "agentduet==1.0.0" google-genai python-dotenv

Step 1: Create a project folder

mkdir agentduet-gemini-live-bridge && cd $_
python3.12 -m venv .venv && source .venv/bin/activate
pip install "agentduet==1.0.0" google-genai python-dotenv

Step 2: Configure .env

cat > .env << 'EOF'
AGENTDUET_API_KEY=your-connector-api-key
AGENTDUET_CONNECTOR_UUID=your-connector-uuid
GEMINI_API_KEY=your-gemini-key
EOF

Step 3: Write gemini_live_bridge.py

Create the file next to .env:

import asyncio
import logging
import os
from typing import Optional
from dotenv import load_dotenv
from google import genai
from google.genai import errors as genai_errors
from google.genai import types
from google.genai.live import AsyncSession
from websockets import ConnectionClosed
from agentduet import (
BufferFullError,
Call,
CallAudioConfig,
CallClosedError,
IncomingCallNotification,
SessionManager,
SessionManagerConfig,
new_session_id,
)
load_dotenv()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
genai_client = genai.Client(vertexai=False, api_key=os.getenv("GEMINI_API_KEY"))
MODEL = "models/gemini-3.1-flash-live-preview"
CONFIG = types.LiveConnectConfig(
response_modalities=[types.Modality.AUDIO],
speech_config=types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Zephyr")
)
),
system_instruction="You are a helpful and friendly AI assistant.",
)
class GeminiLiveIntegration:
def __init__(self, call: Call, gemini_session: AsyncSession):
self._call = call
self._gemini_session = gemini_session
self._send_to_gemini_task: Optional[asyncio.Task] = None
self._recv_from_gemini_task: Optional[asyncio.Task] = None
self._terminated = False
async def _on_hangup(self, evt):
logger.info("Call terminated - closing Gemini session")
self._terminated = True
try:
await self._gemini_session.close()
except Exception:
logger.exception("Error closing Gemini session")
for task in (self._send_to_gemini_task, self._recv_from_gemini_task):
if task:
task.cancel()
try:
await task
except (asyncio.CancelledError, genai_errors.APIError):
pass
async def run(self):
self._call.on_hangup(self._on_hangup)
self._send_to_gemini_task = asyncio.create_task(self.stream_to_gemini())
self._recv_from_gemini_task = asyncio.create_task(
self.receive_audio_from_gemini()
)
await asyncio.gather(
self._send_to_gemini_task,
self._recv_from_gemini_task,
return_exceptions=True,
)
async def stream_to_gemini(self):
try:
await self._gemini_session.send_realtime_input(text="Hello, how are you?")
async for audio_chunk in self._call.caller.audio_stream():
await self._gemini_session.send_realtime_input(
audio=types.Blob(
data=audio_chunk, mime_type="audio/pcm;rate=24000"
)
)
except ConnectionClosed:
pass
except CallClosedError:
logger.debug("Call closed; stopping stream to Gemini")
except asyncio.CancelledError:
raise
except Exception:
logger.exception("Error in stream to Gemini")
raise
async def receive_audio_from_gemini(self):
try:
while True:
async for response in self._gemini_session.receive():
if server_content := response.server_content:
if server_content.interrupted:
# Public barge-in API - clears queued outbound PCM
await self._call.clear_send_audio_buffer()
logger.debug("Gemini interrupted - cleared send buffer")
break
if model_turn := server_content.model_turn:
for part in model_turn.parts:
if part.inline_data and isinstance(
part.inline_data.data, bytes
):
try:
await self._call.send_audio(
part.inline_data.data
)
except BufferFullError:
logger.warning(
"Send buffer full - drop chunk or "
"raise CallAudioConfig.buffer_size"
)
if part.text is not None:
logger.debug("Text: %s", part.text)
except (ConnectionClosed, genai_errors.APIError):
logger.debug("Gemini session closed")
except asyncio.CancelledError:
raise
except CallClosedError:
logger.debug("Call closed; stopping stream from Gemini")
except Exception:
logger.exception("Error in stream from Gemini")
raise
async def main():
config = SessionManagerConfig.create(
api_key=os.getenv("AGENTDUET_API_KEY"),
connector_uuid=os.getenv("AGENTDUET_CONNECTOR_UUID"),
call_audio=CallAudioConfig(
sample_rate=24000,
buffer_size=1024 * 1024,
),
)
async with SessionManager(config) as sm:
logger.info("SessionManager started %s", sm.id)
@sm.on_incoming_call
async def on_call(noti: IncomingCallNotification):
session = await sm.open_session(new_session_id(), noti.subscriber)
call = await session.process_call(noti)
logger.info("Incoming call=%s caller=%s", call.id, call.caller)
try:
async with genai_client.aio.live.connect(
model=MODEL, config=CONFIG
) as gemini_session:
result = await call.answer()
if not result:
logger.error(
"Answer failed %s: %s (%s)",
call.id,
result.error_message,
result.error_code,
)
return
await GeminiLiveIntegration(call, gemini_session).run()
except Exception:
logger.exception("Error in Gemini Live bridge")
await call.close()
raise
await sm.run_forever()
if __name__ == "__main__":
asyncio.run(main())

Step 4: Run the bridge

python gemini_live_bridge.py

Keep the process running. Confirm logs show the SessionManager connected and waiting for calls.

Step 5: Call your number

Dial the connector phone number. Verify two-way audio, then interrupt mid-reply to confirm barge-in clears playback. Hang up and confirm the process remains ready for the next call.

How the pieces fit

SessionManager receives the inbound call → you open_session + process_call → answer() → two tasks move PCM between AgentDuet and the model. Hangup closes the model session and cancels those tasks. Telephony stays AgentDuet’s job; the model never sees SIP.

Notes

  • Sample rate: Gemini Live expects 24 kHz mono PCM. Keep CallAudioConfig.sample_rate=24000.
  • Interrupt: Always use await call.clear_send_audio_buffer() when server_content.interrupted is set. Private methods like _interrupt are not part of the public API.
  • receive() is turn-scoped: Re-enter the async for loop after each turn (or after an interrupt break) so the agent stays live for the whole call.
  • BufferFullError: Raise buffer_size or drop chunks under load; do not block the Gemini receive loop indefinitely.
  • Hangup order: Close Gemini first in on_hangup, then cancel tasks so receive() exits cleanly.

Next Step