Async-first Python SDK for Plivo (plivo_agentstack). Covers Voice AI Agents (WebSocket + REST), Messaging (SMS/MMS/WhatsApp), and Numbers. Python 3.10+, built with hatchling.
src/plivo_agentstack/
__init__.py # Public exports: AsyncClient, errors
client.py # AsyncClient entry point
_http.py # HttpTransport — retry, auth, error mapping
errors.py # PlivoError hierarchy
types.py # Shared models
utils.py # Webhook signature validation (v3)
agent/ # Voice AI Agent stack
app.py # VoiceApp WebSocket server
client.py # Agent REST client (agents, calls, numbers, sessions) + semantic_vad presets
events.py # 38 typed event dataclasses + parse_event()
session.py # Per-connection Session handle
tools.py # Prebuilt tools: EndCall, SendDtmf, WarmTransfer, Collect* agent tools
messaging/ # SMS/MMS/WhatsApp
client.py # MessagesClient
templates.py # WhatsApp Template builder
interactive.py # InteractiveMessage + Location builders
numbers/ # Phone number management
client.py # NumbersClient + LookupResource
tests/ # pytest + pytest-asyncio + respx
examples/ # 18 runnable scripts
pip install -e ".[dev]" # install in dev mode
pytest tests/ -v # run all tests (~87)
ruff check src/ tests/ # lint- Async-first: all I/O uses
async/awaitviahttpx.AsyncClient - Type hints: use Python 3.10+ syntax (
dict | None,list[str], notOptional/List) - Dataclasses: for all typed events and models — no Pydantic
- Line length: 100 characters max (ruff enforced)
- Imports:
from __future__ import annotationsfirst, then stdlib → third-party → project. Useruff check --select Ito verify ordering - Naming: PascalCase classes, snake_case functions, UPPER_SNAKE constants, underscore prefix for private (
_http,_handlers) - Ruff rules: E, F, I, W only — keep it minimal
- No bare
except: always catch specific exceptions - asyncio_mode = "auto": all async test functions run without explicit markers
All WebSocket events use dotted naming convention:
| Event | Type string | Dataclass |
|---|---|---|
| Session started | session.started |
AgentSessionStarted |
| Session ended | session.ended |
AgentSessionEnded |
| Session error | session.error |
Error |
| Tool called | tool.called |
ToolCall |
| Tool executed (MCP) | tool.executed |
ToolExecuted |
| Turn completed | turn.completed |
TurnCompleted |
| Turn metrics | turn.metrics |
TurnMetrics |
| User transcription | user.transcription |
Prompt |
| User DTMF | user.dtmf |
Dtmf |
| DTMF sent | dtmf.sent |
DtmfSent |
| User idle | user.idle |
UserIdle |
| User speech started | user.speech_started |
VadSpeechStarted |
| User speech stopped | user.speech_stopped |
VadSpeechStopped |
| User turn completed | user.turn_completed |
TurnDetected |
| User state changed | user.state_changed |
UserStateChanged |
| Agent handoff | agent.handoff |
AgentHandoff |
| Agent speech interrupted | agent.speech_interrupted |
Interruption |
| Agent speech created | agent.speech_created |
AgentSpeechCreated |
| Agent speech started | agent.speech_started |
AgentSpeechStarted |
| Agent speech completed | agent.speech_completed |
AgentSpeechCompleted |
| Agent false interruption | agent.false_interruption |
AgentFalseInterruption |
| Agent state changed | agent.state_changed |
AgentStateChanged |
| Agent tool started | agent_tool.started |
AgentToolStarted |
| Agent tool completed | agent_tool.completed |
AgentToolCompleted |
| Agent tool failed | agent_tool.failed |
AgentToolFailed |
| LLM availability | llm.availability_changed |
LlmAvailabilityChanged |
| Voicemail detected | voicemail.detected |
VoicemailDetected |
| Voicemail beep | voicemail.beep |
VoicemailBeep |
| Participant added | participant.added |
ParticipantAdded |
| Participant removed | participant.removed |
ParticipantRemoved |
| Call transferred | call.transferred |
CallTransferred |
| Play completed | play.completed |
PlayCompleted |
| User backchannel | user.backchannel |
UserBackchannel |
| Session usage | session.usage |
SessionUsage |
Audio stream events use the Plivo protocol: start, media, dtmf, playedStream, clearedAudio, stop.
- Managed mode:
send_tool_result(),send_tool_error() - Text mode (BYOLLM):
send_text(),extend_wait(),send_raw() - Audio stream:
send_media(),send_checkpoint(),clear_audio() - Control:
update(),inject(),handoff(),speak(),play(),transfer(),send_dtmf(),hangup() - Background audio:
play_background(),stop_background()
Simple tools (customer-side): EndCall, SendDtmf, WarmTransfer — each has .tool (schema), .instructions (prompt hint), .match(event), .handle(session, event).
Agent tools (server-side sub-agents): CollectEmail, CollectAddress, CollectPhone, CollectName, CollectDOB, CollectDigits, CollectCreditCard — each has .definition (for agent_tools=[]), .prompt_hint (for system prompt).
| Component | Providers |
|---|---|
| STT | deepgram, google, azure, assemblyai, groq, openai |
| LLM | openai, anthropic, groq, google, azure, together, fireworks, perplexity, mistral |
| TTS | elevenlabs, cartesia, google, azure, openai, deepgram |
| S2S | openai_realtime, gemini_live, azure_openai |
Provider names are case-insensitive. BYOK API keys are validated at agent creation time.
All provider configs (STT, LLM, TTS) accept optional base_url for custom endpoints. ElevenLabs TTS also accepts region ("us" default, "in" for India residency). Azure OpenAI uses azure_deployment, azure_endpoint, api_version.
Agent creation supports semantic_vad as a string preset or dict:
"high"/"medium"/"low"/"auto"— eagerness presets{"eagerness": "high", "min_interruption_duration_ms": 200}— preset + overrides- Raw dict — full manual control
- HTTP mocking: use
respx(notunittest.mockfor HTTP). Fixturemock_apiprovides a router scoped tohttps://api.plivo.com - Fixtures:
http_transportandclientfixtures inconftest.py— useyieldfor cleanup - Async tests: just write
async def test_*— pytest-asyncio auto mode handles it - Request verification: use
mock_api.calls[0].requestto inspect sent requests - Error assertions:
with pytest.raises(ErrorType)and check.status_code,.body
- Sub-clients (
agent,messages,numbers) are lazy-loaded properties onAsyncClient - Session methods are sync-safe — they enqueue to an
asyncio.Queue, sender task drains it - VoiceApp auto-detects sync vs async handlers — sync runs in thread pool via
asyncio.to_thread() - Unknown WebSocket events parse to raw
dict(forward-compatible) - HttpTransport retries on 429 (respects
Retry-After) and 5xx with exponential backoff - Agent REST client auto-expands
semantic_vadpresets to full config dicts
- Do NOT include "Claude", "Anthropic", "AI-generated", "AI-assisted", or similar attribution in commit messages. Write commit messages as if a human developer wrote the code.
- Keep commit messages concise (1-2 lines), focused on the "why" not the "what"
- Do not amend previous commits unless explicitly asked — always create new commits
- Do not force-push to main
- Do not commit
.env, credentials, or API keys - Do not commit
__pycache__/,.ruff_cache/,*.egg-info/,.venv/,dist/,build/— these are in.gitignore
- New REST resources: add to the appropriate sub-client (
agent/client.py,messaging/client.py,numbers/client.py), wire into the parent client, add tests withrespxmocks - New WebSocket events: add a
@dataclasstoagent/events.py, register in_EVENT_REGISTRY, add parse test intest_events.py - New prebuilt tools: add to
agent/tools.py, export inagent/__init__.py - New examples: add to
examples/, usefrom plivo_agentstack import AsyncClientandfrom plivo_agentstack.agent import VoiceApp, ...pattern. Update README Quick start section - Keep dependencies minimal — core deps are
httpx,websockets,starletteonly