This release is a pre-release and may not be stable for production use.
agentduet-adapters
Model adapters for the AgentDuet Python SDK's VoiceAgent layer.
AgentDuet connects an AI agent to real phone calls: it owns the carrier side (SIP trunking, numbers, WhatsApp, web chat) and hands your code a bidirectional PCM audio stream. These adapters connect that stream to a speech-to-speech model, so a working voice agent is three lines:
from agentduet import VoiceAgent
from agentduet_adapters.gemini import GeminiLive
VoiceAgent.from_env().run(
GeminiLive(instruction="You are May, a warm phone concierge. Keep replies short.")
)
Call your number and the agent picks up.
Install
The SDK comes as a dependency; pick the provider you actually call:
pip install "agentduet-adapters[gemini]" # Google Gemini Live
pip install "agentduet-adapters[grok]" # xAI Grok Voice
pip install "agentduet-adapters[qwen]" # Alibaba Qwen-Omni Realtime
pip install "agentduet-adapters[nova-sonic]" # Amazon Nova Sonic
Nothing vendor-specific installs by default, so a Nova Sonic deployment never ships
google-genai. Each adapter module raises ImportError naming its extra if you import it
without one.
The adapters
| Import | Class | Default model | Credentials |
|---|---|---|---|
agentduet_adapters.gemini |
GeminiLive |
models/gemini-3.1-flash-live-preview |
GEMINI_API_KEY |
agentduet_adapters.grok_voice |
GrokVoice |
grok-voice-think-fast-1.0 |
XAI_API_KEY, or the raw key in ~/.x.ai |
agentduet_adapters.qwen |
QwenVoice |
qwen3.5-omni-flash-realtime |
DASHSCOPE_API_KEY, or the raw key in ~/.qwen; DASHSCOPE_REGION selects intl (default) or cn |
agentduet_adapters.nova_sonic |
NovaSonic |
amazon.nova-2-sonic-v1:0 |
standard AWS environment credentials, AWS_REGION |
Every adapter takes the same four arguments: instruction=, tools=, voice= and model=.
Swapping providers is a one-line change. Credentials are the one deliberate exception:
Gemini, Grok and Qwen take api_key=, while Nova Sonic follows the AWS credential chain and
takes region= (plus an optional credentials_resolver=).
Two provider quirks worth knowing, because they are not bugs in the adapters:
- Qwen runs at 16 kHz input and 24 kHz output while the call runs at 24 kHz, so the
adapter resamples inbound audio (that is what the
soxrandnumpydependencies are for). - Qwen tool calling needs a
qwen3.5-omni-*model. The olderqwen3-omni-*models do not support it, which is why the default is 3.5.
Tools, transcripts and usage
VoiceAgent handles these, not the adapters: pass callbacks and every adapter reports through
the same path.
async def tools(name: str, args: dict) -> dict:
if name == "get_balance":
return {"balance": 402.15, "currency": "SGD"}
return {"error": f"unknown tool {name}"}
async def on_transcript(ev): # ev.role is "user" or "agent"
print(f"{ev.role}: {ev.text}")
async def on_usage(ev): # cumulative for this call
print(f"tokens: {ev.total} (in {ev.input} / out {ev.output})")
VoiceAgent.from_env(tools=tools, on_transcript=on_transcript, on_usage=on_usage).run(
GrokVoice(instruction="You are a bank concierge.", tools=[
{"name": "get_balance", "description": "Balance for the caller",
"input_schema": {"type": "object", "properties": {}}},
])
)
Tool declarations are passed in a neutral shape (name, description, input_schema) and
each adapter translates it to its provider's format. A tool that raises, or one the model
calls with no handler registered, returns an error result to the model rather than ending the
call.
Writing your own adapter
You do not need this package for that. The seam is in the SDK: implement VoiceModel and
ModelSession from agentduet and pass your object to VoiceAgent.
from agentduet import AudioOut, Interrupted, ToolCall, TranscriptDelta, Usage
class MyModel:
async def open(self): # once per call
return MySession(await connect_to_my_provider())
class MySession:
async def push_audio(self, pcm: bytes) -> None: # caller audio, 24 kHz PCM16
...
def events(self): # async iterator
# yield AudioOut(pcm=...) to speak, Interrupted() on barge-in,
# ToolCall(id=..., name=..., args={...}), TranscriptDelta(text=..., role=...),
# Usage(total=..., input=..., output=...)
...
async def send_tool_result(self, call_id: str, result: dict) -> None:
...
async def close(self) -> None:
...
The four adapters here are the worked examples: gemini.py is the shortest, nova_sonic.py
the most involved. VoiceAgent's side of the contract (what it guarantees to call, and when)
is specified in the SDK's specs/wire-protocol-spec.md, section 10.5.
Why this is a separate package
The SDK is a stable transport library: its only dependencies are websockets, httpx and
abxbus. Adapters are the opposite kind of code. They track fast-moving provider APIs, they
each drag in a vendor SDK, and they are the part you are most likely to want to read, fork or
fix yourself. Splitting them out means a provider's breaking change ships as a patch here
instead of forcing a core SDK release, and these can be Apache-2.0 while the SDK is not.
Examples
examples/ has a runnable quickstart per provider. Each needs AGENTDUET_API_KEY and
AGENTDUET_CONNECTOR_UUID (from B3) plus that provider's key:
pip install "agentduet-adapters[gemini,examples]"
python examples/voice_agent_gemini.py # then call your number
License
Apache-2.0. The agentduet SDK itself is separately licensed.
Release files for agentduet-adapters 0.1.0b1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agentduet_adapters-0.1.0b1.tar.gz | 41.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agentduet_adapters-0.1.0b1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:86.0 kB
Release files / agentduet_adapters-0.1.0b1.tar.gz
| Download URL | agentduet_adapters-0.1.0b1.tar.gz |
|---|---|
| Size | 41.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
49eced01e98e34f0105c531592840d98a19040f2095a8f96fe91fe4cd80c0352
|
|
BLAKE2b-256 checksum How to use checksums |
fe1358d7c9de012ee093c9a3265cbb7bd43fb304a948cd7c27fcdf42803ad502
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.12
|
Release files / agentduet_adapters-0.1.0b1-py3-none-any.whl
| Download URL | agentduet_adapters-0.1.0b1-py3-none-any.whl |
|---|---|
| Size | 44.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7a36608bb3839a69a10f80a8aac6f346cfe484de8cc83fe0ce957266c80433dd
|
|
BLAKE2b-256 checksum How to use checksums |
370b46331c73385001130dd2028882bddb76ac2f38ab399dcf52b47e8f64f7f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.12
|