Python SDK reference
The Python SDK is an asynchronous, headless session client. Its current version is 0.45.0, independent from TypeScript / JavaScript SDK version 0.55.0. It converts audio frames and exposes events, but it does not open a microphone, play audio or cancel echo.
uv add dialt-sdk
import os
from dialt import DialtMode, DialtSession, ToolDefinition
lookup_tool: ToolDefinition = {
'name': 'lookup_order',
'description': 'Look up an order by ID.',
'parameters': {'type': 'object', 'properties': {'order_id': {'type': 'string'}},
'required': ['order_id']},
'read_only': True,
'recorded_bridge': 'auto',
}
async with await DialtSession.connect(
'wss://api.dialt.com/v1/realtime',
api_key=os.environ['DIALT_API_KEY'],
output_sample_rate=8_000,
mode=DialtMode(
instructions='You are the customer support assistant for [company]. Help customers with [company] products and accounts.',
web_search=False,
tools=[lookup_tool],
),
) as session:
await session.send_audio(processed_frame)
async for event in session.events():
if event.type == 'audio':
play_or_store(event.audio, event.audio_sample_rate)
else:
print(event.type, event.data)
For text, use the same client with mode=DialtMode(modality='text', instructions=...), then call await session.send_text('Hello'). Text mode uses WebSocket only and emits the same transcript, tool and reply lifecycle events without audio. See assistant instructions for the DialtMode.instructions contract.
instructions is required and holds the agent's role: who it is, whose side it is on, what it knows and what it should do on this call. Dialt's platform prompt covers speech output, conversation mechanics, capabilities, the voice's register and call facts, and gives the agent no role. DialtMode() without non-empty instructions raises ValueError, and the server refuses such a start with missing_instructions. voice selects a voice: the TTS voice together with its register line in the prompt. It never sets the role or content.
Select a stock voice
List the current stock voices with VoicesClient on your backend, then pass a returned key in DialtMode.voice or set_voice().
from dialt import VoicesClient
voices = VoicesClient(api_key=os.environ['DIALT_API_KEY'])
roster = voices.list()
# Select a key from roster['voices'], for example roster['default'].
# Later: DialtMode(instructions=..., voice=roster['default'])
Migration: Customer voice conditioning and VoicesClient.create()/delete() are removed. Remove clip-upload and voice-management calls, and replace saved conditioned-voice keys with a current stock key from GET /v1/voices. Stock listing and selection remain supported. API failures raise VoicesError with status and detail; see the catalogue and migration guide.
Quiet updates and spoken results
Keep routine progress quiet. Request speech for a useful intermediate finding, and send one final result when the operation finishes.
# Routine status: no new speech.
await session.send_tool_progress(call_id, 'Checking availability.')
# Structured facts: quiet by default.
await session.send_tool_partial_result(call_id, {'checked': 3})
# A separate finding worth mentioning, only if the conversation is free.
await session.send_tool_partial_result(call_id, {'available_date': '2026-10-01'}, reply=True)
# When the operation finishes, send its final result once.
await session.send_tool_result(call_id, result, outcome='succeeded', verified=True)
reply=True is best effort: it does not interrupt the caller or an active reply. If speech is skipped, the facts stay in context; the announcement is not queued. Progress and partials never complete a call. Set verified=True only after your application establishes successful completion.
For a deferred tool, notify_on_complete=True requests final-result delivery when the floor is free. Set it to False to retain the result quietly without a proactive completion wake; it does not silence ordinary tool answers. See background tools for deferral and caller decisions.
Report the outcome
Send outcome and verified whenever your application can establish them. outcome='succeeded', verified=True lets the assistant confirm the action ("You're booked for Tuesday"); send it when you hold proof, such as a confirmation ID from your booking API. outcome='unknown', the default, makes the assistant say it cannot confirm the action yet, which suits queued or fire-and-forget work. outcome='failed' makes it say the action did not go through.
await session.send_tool_result(call_id, {'confirmation_id': 'BK-4821'}, outcome='succeeded', verified=True)
await session.send_tool_result(call_id, {'status': 'queued'}, outcome='unknown')
await session.send_tool_result(call_id, {'error': 'slot_unavailable'}, outcome='failed')
Each call is resolved once; the three lines show alternatives. See the WebSocket contract for the wire frames.
DialtSession.connect
await DialtSession.connect(url, *, session_id=None, sr=16000, output_sample_rate=16000, api_key=None, mode=None, user=None, timezone=None, capabilities=None, initial_history=None, resume_token=None, connect_timeout_s=15.0)
Caller input remains mono 16 kHz. output_sample_rate selects 8000, 16000 (the default) or 24000 Hz assistant output. Each audio event carries Float32 mono samples in event.audio and the selected rate in event.audio_sample_rate. Use 8 kHz before a telephony codec or 24 kHz for wider-band playback.
Returns after ready. Raises DialtError for a server rejection before ready; inspect its code and retryable attributes rather than matching human-readable text. Raises TimeoutError when the connection deadline expires.
After a transport loss, save session.resume_token and pass it as resume_token=... to the replacement connection. Within the bounded broker resume window, conversation context and deferred jobs are restored and each job emits tool_deferred_resume.
session.session_uuid: str | None is set from ready alongside resume_token. It is the server-minted session identifier, unchanged across successful resumes and broker-instance transfers; a fresh dialogue gets a new one even when the caller reuses the same session_id.
initial_history initializes completed user/assistant messages and tool calls/results atomically, without executing tools or generating a response. Developer instructions remain in mode.instructions; Dialt's internal prompt and reasoning are private. session.conversation returns a detached, read-only retained window with stable identities and revisions. Resume retains accepted history; a fresh reset defaults to empty history. See the wire contract for item shapes and limits.
session.active_response_id tracks logical generation across bridge speech, tool waits and final speech. response_done clears that target; per-segment done does not. Neither event proves physical playback. session.active_speech and acoustic speech_started/speech_stopped are independent of semantic turn completion and close on observation gaps.
Methods and helpers
| API | Description |
|---|---|
request_reply(operation_id=None, instructions=None, timeout_s=10.0) | Requests one response independently of the greeting. Returns the latest authoritative receipt; reuse the same ID and payload after an uncertain acknowledgement. |
interrupt(response_id=None, target_operation_id=None, operation_id=None, timeout_s=10.0) | Stops exactly one response, or cancels one queued request. Defaults to the active logical response. It suppresses that response's subsequent WS PCM/text without cancelling application tool work. Requires the targeted-interruption capability; unavailable on WebRTC. |
playback_stopped(response_id, discarded_ms) | After clearing local buffered playback, reports discarded audio so accepted dialogue can be corrected, including after generation completion. Applications own their playback queue and must preserve newer responses. |
send_audio(chunk) | Sends bytes as PCM16 or converts a NumPy Float32 array to PCM16. |
send_text(text) | Commits one user turn in a DialtMode(modality='text') session; 1–20,000 characters. |
stream_audio(audio, sr=16000, chunk_ms=100, realtime=True) | Chunks and optionally paces a waveform; returns actual send timestamps. |
events() | Async iterator of SessionEvent(type, t_ms, data, audio). Every server event passes through, including working (data["active"]: Dialt is blocking on a tool result with nothing audible) and advisory session_end_requested (the agent called end_call; data["farewell"] is its farewell argument, which is not spoken when the reply text of that turn ended a sentence; stop automatic turn injection but preserve queued output). It does not confirm that the peer heard or acted on the farewell; a client owns any local playback drain after normal close. A headless client that does its own playback can cover a tool wait the way the browser SDK's thinking sound does. |
now_ms() / start_t | Monotonic session timing relative to the start of connect(). |
reset() | Ends the old conversation and returns a fresh session with the current configuration. Use session = await session.reset() and consume its events; the old object stays closed. |
inject_context(text, role='context', reply=False, message_id=None) | Returns the broker's authoritative accepted/rejected acknowledgement; typed ASR echoes the message ID. |
continue_with_agent(operation_id, instructions, tools, voice=None, context=None, tool_choice=None, tool_choice_one_shot=False) | Initializes another agent atomically and returns its applied/rejected acknowledgement within 10 seconds. It does not start speech. handoff_agent() remains a compatibility alias. |
send_tool_result(id, content, outcome='unknown', verified=False, continue_with=None) | Resolves a client tool call. Pass a continuation mapping when this result must atomically end the old agent's plan, initialize the replacement agent and request its reply. The method then waits for handoff_ack. Send outcome and verified whenever your application can establish them; see report the outcome. |
send_tool_progress(id, note) | Adds quiet status to context without requesting speech or resolving the call. |
send_tool_deferred(id, handle, status_label=None) | Releases the voice turn while retaining an eligible background job. |
send_tool_partial_result(id, content, reply=False, interaction=None) | Delivers a structured segment of the eventual answer; interaction={"id": ..., "prompt": ..., "options": [...]} raises a mid-call decision that is asked by voice and never silently dropped. |
send_tool_interaction_update(id, interaction_id, state, note=None) | Closes an open interaction (resolved/cancelled/superseded) without completing the call; returns the deterministic tool_interaction_update_ack payload. |
narration_state(job_id) / interaction_state(interaction_id) / wait_for_narration_state(job_id, states, timeout=None) | Track a raised interaction's tool_job_narration lifecycle by job or stable interaction id. |
set_tool_choice(choice, one_shot=False) | Restricts tool use mid-session ("auto"/"none"/"required"/{"allowed"}/{"tool"}); also available at session start as DialtMode(tool_choice=...). |
set_instructions(text, new_speaker=False) | Replaces the session instructions mid-session, from the next reply on. new_speaker=True remains available for an independent speaker change. Refused with instructions_busy while a reply is in flight; send it after done. |
set_voice(voice) | Switches the roster voice mid-session; applies from the next reply and is confirmed by a voice event. |
set_tools(tools) | Replaces the client tool manifest mid-session (same shape as DialtMode.tools) for an independent capability change; managed tools ride along and tool_choice resets to "auto". |
request_wrap_up(reason='time_limit') | Asks the model for a graceful sign-off in character (a time limit, or a transfer the host is about to make); never cuts a reply in flight, and the session closes with session_end once the sign-off has played. |
resolve_tool_permission(id, decision) | Submits 'approve' or 'decline' for an application permission request. Observe permission_resolution in events() for acceptance or rejection. |
send_tool_cancel(id) | Cancels a client-owned tool call. |
send_client_event(event, **fields) | Sends playback lifecycle events such as playback_stopped. |
close() | Closes the session and its receive task. |
TurnRecorder | Builds an assistant waveform from session events for recording or offline processing. |
Pass message_id to correlate the returned inject_context_ack with a typed turn's final ASR. Typed ASR uses input_source='text'; spoken ASR uses input_source='voice'. Omitted message IDs are generated by the SDK.
To continue with another agent at an idle boundary, await continue_with_agent() until its handoff_ack reports status='applied', then separately call acknowledged inject_context(..., role='context', reply=True) when the initialized agent should speak without a caller turn. When a tool result is the boundary, prefer send_tool_result(..., continue_with={...}); it records the result, initializes the next agent and requests its reply atomically. A continuation can include tool_choice and tool_choice_one_shot=True to constrain the incoming reply before it starts, then restore auto. One continuation may be pending; a caller turn wins a separate reply-request race. Continuations are neither retried nor restored after reconnect, and each sequential continuation needs a new operation ID. handoff_agent() and the nested handoff option remain compatibility aliases.
Pass an IANA timezone when connecting to anchor the model to the current date, weekday and local time. The anchor is a session-start snapshot; a missing or invalid zone leaves local date and time unknown.
DialtMode accepts keyword arguments: instructions (required), modality ('voice', the default, or 'text'), voice, tools, tool_choice, web_search, end_call (declares the managed end_call(farewell) tool so the agent can end the session; off by default), end_call_when (your own ending condition, e.g. "the user asks to end the call"; implies end_call), greeting. ToolDefinition includes read_only, requires_permission, permission_source ("caller", the default, or "application"), resolver_only, recorded_bridge ("auto", "always" or "never"), status_label, timeout, deferred, deferred_timeout and notify_on_complete. The deprecated expected_duration field remains a compatibility alias.
Exported audio helpers are chunk_audio, float32_to_pcm16, pcm16_to_float32, f32le_to_float32 and to_ws_url.
Hosted evals
dialt.evals creates cases and starts runs on the Evals dashboard with your account API key. Cases are JSON files in your repository; upsert_case matches by name, so a re-push updates the hosted case instead of duplicating it. Field reference and checks: the evals guide.
from dialt import EvalsClient, load_cases
evals = EvalsClient(api_key=os.environ["DIALT_API_KEY"])
cases = evals.upsert_cases(load_cases("evals/")) # one case per *.json file
run = evals.start_run([c["id"] for c in cases], modality="text")
print(evals.dashboard_url(run["id"]))
result = evals.wait(run["id"]) # polls until the run is terminal
start_run also takes names=[...], repetitions, modality="voice" and targets=[...] to compare Dialt brains, for example ["dialt", "dialt-smart"]. wait returns the run once its status is passed, failed, cancelled or error, with one entry per case, repetition and target in attempts. A rejected request raises EvalsError: its code is the error code, details["reason"] explains it, and a failed preflight lists every problem in details["errors"].
Create a reusable suite with create_suite, replace its membership with update_suite, and filter history with list_runs(suite_id=...):
suite = evals.create_suite("Customer support", [c["id"] for c in cases])
run = evals.start_run(suite_id=suite["id"], modality="voice", repetitions=1)
print(evals.dashboard_url(run["id"]))
start_run(targets=[...]) also selects openai-live and gemini-live; see the comparison API example for external credit reservations. From Python SDK 0.40.0, pass dry_run=True to EvalsClient.start_run to check every case against every target first: it returns the plan, including ignored-setting warnings and the max_external_charge_micros to accept, and starts nothing. Pass max_external_charge_micros to accept the quoted external charge. Pass a caller-generated idempotency_key and reuse it with the same arguments when retrying after a lost response. A new intentional run needs a new key.
Application tool approval
Set requires_permission: True and permission_source: "application" on a tool. Forward permission_pending events with source == "application" to your authenticated approval application. Keep consuming the session event stream while the reviewer decides. From your approval handler, call await session.resolve_tool_permission(request_id, "approve") or use "decline". Check permission_resolution.accepted and reason in the event stream. The broker releases the exact stored call on acceptance; handle tool_call and return its result normally. The caller's spoken consent cannot satisfy application approval. The authenticated session host is trusted to submit decisions, so authenticate the reviewer in your application.
WebRTC transport (experimental)
Experimental: the API is stable, but this transport is newly shipped and still being hardened on real networks; ws remains the default and recommended fallback.
Pass transport="webrtc" to carry the session over WebRTC (UDP) instead of the default WebSocket, useful when a headless client is bridging a caller on an unreliable network. Requires the optional aiortc dependency:
uv add "dialt-sdk[webrtc]"
session = await DialtSession.connect(
"wss://api.dialt.com/v1/realtime",
api_key=os.environ["DIALT_API_KEY"],
mode=DialtMode(instructions="You are a helpful voice assistant on a phone call. If a request is missing a key detail, ask one concise clarifying question."),
transport="webrtc",
)
Most headless callers (services, telephony bridges) are fine on the default ws transport, since they usually don't sit behind the weak, lossy last-mile links WebRTC is built for.