Google ADK

View as Markdown

This guide shows how to integrate the AgentDuet SDK with the Google Agent Development Kit (ADK) for complex AI agent behavior, including multi-agent orchestration, long-term memory, and structured tools.

What this integration provides

A phone agent that answers calls, feeds caller audio into ADK’s LiveRequestQueue, plays ADK audio back on the call, clears the outbound buffer when ADK reports interrupted, and cancels bridge tasks on hangup.

Prerequisites

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

Step 1: Create a project folder

$mkdir agentduet-adk-bidi-bridge && cd $_
$python3.12 -m venv .venv && source .venv/bin/activate
$pip install "agentduet==1.0.0" google-genai google-adk 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
$# ADK / genai often also read GOOGLE_API_KEY - set either
$EOF

Step 3: Write adk_bidi_bridge.py

Create the file next to .env:

1import asyncio
2import logging
3import os
4from typing import Optional
5
6from dotenv import load_dotenv
7from google.adk.agents import Agent
8from google.adk.agents.live_request_queue import LiveRequestQueue
9from google.adk.agents.run_config import RunConfig, StreamingMode
10from google.adk.runners import Runner
11from google.adk.sessions import InMemorySessionService
12from google.genai import types
13
14from agentduet import (
15 BufferFullError,
16 Call,
17 CallAudioConfig,
18 CallClosedError,
19 IncomingCallNotification,
20 SessionManager,
21 SessionManagerConfig,
22 new_session_id,
23)
24
25load_dotenv()
26
27logging.basicConfig(
28 level=logging.INFO,
29 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
30)
31logger = logging.getLogger(__name__)
32
33MODEL = "gemini-3.1-flash-live-preview"
34APP_NAME = "agentduet_adk_integration"
35
36agent = Agent(
37 name="agentduet_agent",
38 model=MODEL,
39 instruction="You are a helpful and friendly AI assistant talking over a phone call.",
40)
41
42session_service = InMemorySessionService()
43runner = Runner(app_name=APP_NAME, agent=agent, session_service=session_service)
44
45
46class ADKIntegration:
47 def __init__(self, user_id: str, call: Call):
48 self._call = call
49 self._user_id = user_id
50 self._session_id = call.id
51 self._live_request_queue = LiveRequestQueue()
52 self._send_to_adk_task: Optional[asyncio.Task] = None
53 self._recv_from_adk_task: Optional[asyncio.Task] = None
54 self._terminated = False
55
56 async def _on_hangup(self, evt):
57 logger.info("Call terminated - cancelling ADK bridge tasks")
58 self._terminated = True
59 if self._send_to_adk_task:
60 self._send_to_adk_task.cancel()
61 if self._recv_from_adk_task:
62 self._recv_from_adk_task.cancel()
63
64 async def run(self):
65 self._call.on_hangup(self._on_hangup)
66
67 session = await session_service.get_session(
68 app_name=APP_NAME, user_id=self._user_id, session_id=self._session_id
69 )
70 if not session:
71 await session_service.create_session(
72 app_name=APP_NAME, user_id=self._user_id, session_id=self._session_id
73 )
74
75 self._send_to_adk_task = asyncio.create_task(self.stream_to_adk())
76 self._recv_from_adk_task = asyncio.create_task(
77 self.receive_from_adk(self._user_id, self._session_id)
78 )
79
80 try:
81 await asyncio.gather(self._send_to_adk_task, self._recv_from_adk_task)
82 except asyncio.CancelledError:
83 logger.debug("ADK integration tasks cancelled")
84
85 async def stream_to_adk(self):
86 try:
87 self._live_request_queue.send_content(
88 types.Content(parts=[types.Part(text="Hi")])
89 )
90 async for audio_chunk in self._call.caller.audio_stream():
91 if self._terminated:
92 break
93 audio_blob = types.Blob(
94 data=audio_chunk,
95 mime_type="audio/pcm;rate=24000",
96 )
97 self._live_request_queue.send_realtime(audio_blob)
98 except CallClosedError:
99 logger.debug("Call closed; stopping stream to ADK")
100 except Exception:
101 if not self._terminated:
102 logger.exception("Error in stream to ADK")
103 raise
104 finally:
105 logger.debug("Stream to ADK completed")
106
107 async def receive_from_adk(self, user_id: str, session_id: str):
108 run_config = RunConfig(
109 streaming_mode=StreamingMode.BIDI,
110 response_modalities=["AUDIO"],
111 input_audio_transcription=None,
112 output_audio_transcription=None,
113 realtime_input_config=types.RealtimeInputConfig(),
114 )
115
116 try:
117 async for event in runner.run_live(
118 user_id=user_id,
119 session_id=session_id,
120 live_request_queue=self._live_request_queue,
121 run_config=run_config,
122 ):
123 if self._terminated:
124 break
125
126 if event.interrupted:
127 await self._call.clear_send_audio_buffer()
128
129 if event.content and event.content.parts:
130 part = event.content.parts[0]
131 if part.inline_data and part.inline_data.data:
132 try:
133 await self._call.send_audio(part.inline_data.data)
134 except BufferFullError:
135 logger.warning(
136 "Send buffer full - drop chunk or raise buffer_size"
137 )
138 event.content = None
139 except CallClosedError:
140 logger.debug("Call closed; stopping receive from ADK")
141 except Exception:
142 if not self._terminated:
143 logger.exception("Error in receive from ADK")
144 raise
145 finally:
146 logger.debug("Receive from ADK completed")
147
148
149async def main():
150 config = SessionManagerConfig.create(
151 api_key=os.getenv("AGENTDUET_API_KEY"),
152 connector_uuid=os.getenv("AGENTDUET_CONNECTOR_UUID"),
153 call_audio=CallAudioConfig(sample_rate=24000),
154 )
155
156 async with SessionManager(config) as sm:
157 logger.info("Connected. Waiting for calls...")
158
159 @sm.on_incoming_call
160 async def on_call(noti: IncomingCallNotification):
161 session = await sm.open_session(new_session_id(), noti.subscriber)
162 call = await session.process_call(noti)
163 logger.info("Incoming call: %s", call.id)
164 try:
165 result = await call.answer()
166 if not result:
167 logger.error(
168 "Answer failed %s: %s (%s)",
169 call.id,
170 result.error_message,
171 result.error_code,
172 )
173 return
174 await ADKIntegration(user_id="default_user", call=call).run()
175 except Exception:
176 logger.exception("Error handling call with ADK")
177 await call.close()
178
179 await sm.run_forever()
180
181
182if __name__ == "__main__":
183 asyncio.run(main())

Step 4: Run the bridge

$python adk_bidi_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_callanswer() → 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: ADK/Gemini Live path expects 24 kHz; set mime_type="audio/pcm;rate=24000".
  • Interrupt: On event.interrupted, call await call.clear_send_audio_buffer() - not private interrupt helpers.
  • Session IDs: Using call.id as the ADK session id keeps one ADK session per phone call.
  • event.content = None: Dropping processed content helps avoid retaining large audio blobs in memory.
  • Hangup: ADK owns the model connection via Runner; cancel your tasks and let run_live exit.