Skip to main content

dialt-sdk

The headless Python SDK for the Dialt realtime voice API. Dialt manages speech recognition, turn-taking, interruption handling, speech generation, reasoning and tool orchestration through a single API; this SDK connects telephony bridges, services, evaluations and custom devices to it.

dialt-sdk replaces the deprecated converse-sdk distribution. New code imports from dialt. Existing applications can install the final converse-sdk compatibility release while migrating imports from converse_sdk to dialt.

uv add dialt-sdk

The session-loop fragment below assumes your media layer supplies mic_frame and play_audio, and your application supplies run_tool:

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"}}},
    "read_only": True,
    "expected_duration": "instant",
    "status_label": "order lookup",
    "deferred": True,
    "deferred_timeout": 7200,
    "notify_on_complete": True,
}

session = await DialtSession.connect(
    "wss://dialt.com/ws",
    api_key=os.environ["DIALT_API_KEY"],
    mode=DialtMode(
        instructions="Help callers with their orders.",
        greeting="Hello, how can I help?",
        tools=[lookup_tool],
    ),
)

async with session:
    await session.send_audio(mic_frame)  # PCM16 little-endian mono at 16 kHz
    await session.inject_context(
        "Claude Code finished. Tell the user briefly.", role="context", reply=True)

    async for event in session.events():
        if event.type == "audio":
            play_audio(event.audio)  # Float32 mono at 16 kHz
        elif event.type == "tool_call":
            result = await run_tool(event.data["name"], event.data["args"])
            await session.send_tool_result(
                event.data["id"], result, outcome="succeeded", verified=True)
        elif event.type == "tool_deferred_resume":
            resume_host_job(event.data["handle"])

A string greeting is the conversation's first assistant turn, not presentation-only audio. It enters model context and is bargeable in voice mode; when interrupted, playback stops and only the heard prefix remains in context. Use greeting=False to disable it.

DialtMode(turn_end_threshold=0.2) opts one voice session into faster, less conservative Ink finalization. The accepted range is 0.05 through 0.5; lower values are more conservative. Leave it unset for the broker's deployment default. Use an override only when the application's pause distribution has been evaluated, because faster finalization can increase mid-thought ends.

deferred: True makes the tool a background job: the call can outlive the voice turn, the conversation carries on while the host works, and with notify_on_complete the agent speaks the result when it lands — even if the caller has moved on to another topic. For work that should release the voice turn immediately, call send_tool_deferred(id, handle, status_label=...) before continuing it in the background. The handle identifies that call, not your worker: mint a fresh one per call (e.g. f"job-{call_id}") and check the returned acknowledgement — re-using a live handle is rejected as handle_in_use, and a rejected defer leaves the call on the ordinary tool timeout, so it expires while you think it is backgrounded. Keep a handle-to-worker map if several calls should feed one long-running worker. For proactive host announcements, call await session.inject_context(text, role="context", reply=True, message_id="job-42"). It returns the broker's authoritative acknowledgement (accepted plus optional retryable/detail). For a typed user turn, the same message_id and input_source="text" appear on the final ASR event; spoken input uses input_source="voice". The role defaults to "context", reply defaults to False, and omitted message IDs are generated by the SDK. Text is limited to 1–2000 characters. An accepted message_id is an idempotency key for the logical session. After an acknowledgement loss, retry the identical payload and ID (including after resume) to replay delivery proof without injecting a duplicate turn. Reusing an accepted ID with different content is rejected; the broker retains up to 512 accepted receipts per logical session.

Save session.resume_token from the connected session; after a transport loss, pass it as resume_token=... to the replacement DialtSession.connect(...) call so pending jobs are re-announced as tool_deferred_resume events within the broker's bounded resume window.

The SDK deliberately does not own capture, playback, pacing or echo cancellation. Live playback integrations must implement the playback contract.

For a text session, keep the same conversation configuration and replace the media loop with committed turns:

async with await DialtSession.connect(
    "wss://dialt.com/ws",
    api_key=os.environ["DIALT_API_KEY"],
    mode=DialtMode(modality="text", instructions="Help customers with their orders."),
) as session:
    await session.send_text("Where is order A123?")
    async for event in session.events():
        if event.type == "utterance":
            print(event.data["text"])

Text mode is WebSocket-only and emits no audio. Model behavior, tools, greeting, history and conversation lifecycle events remain the same.

DialtMode(end_call=True) lets the agent end the session itself through the managed end_call(farewell) tool: the farewell is spoken, session_end_requested arrives with the farewell, and the server closes after a short grace unless the user speaks. Off by default; the host then ends the session with wrap_up or by closing. DialtMode(end_call_when="the user asks to end the call") states your own ending condition instead of Dialt's default (the caller asks you to end the call); the condition is declared to the agent as part of the tool.

Mid-session voice switch, tool swap and wrap-up

set_voice(key) switches the roster voice from the next reply on, confirmed by a voice event. set_tools(tools) replaces the client tool manifest for an agent whose capabilities change by call phase. With the conversation history intact, the two are enough to hand a call from one agent persona to another on the same session: start with only a hand-off tool declared, and when the agent calls it, declare the second persona's tools, switch the voice and return the handover note as the tool result.

async for event in session.events():
    if event.type == "tool_call" and event.data["name"] == "handoff_to_agent":
        await session.set_tools(SPECIALIST_TOOLS)
        await session.set_voice("southern_us_female")
        await session.send_tool_result(event.data["id"], "Intake complete; continue as the account specialist.",
                                       outcome="succeeded", verified=True)

request_wrap_up(reason) asks the model for a graceful sign-off in persona, for a host-enforced time limit or a transfer the host is about to make. It never cuts a reply in flight; the session closes with session_end once the sign-off has played.

See the complete Python reference and the Twilio bridge quickstart.

A policy agent beside the call

DialtMode(policy=...) declares rules the broker's judge watches the transcript for, each with the instruction it injects when the rule applies and how it lands: speak_now makes the agent reply at once, next_turn lets it act at its next turn. The judge sees only the policy and the transcript, each rule is raised once per session, and every raised rule arrives as a policy_flag event (rule, action, evidence, delivered). The agent's instructions and tools are untouched; the injected context is the whole effect, so a flag never invalidates the prompt cache. Contract and timing: docs/policy-agent.md.

mode = DialtMode(instructions=..., tools=..., policy={
    "subject": "a clinic appointment call",
    "rules": [
        {"id": "emergency", "action": "speak_now",
         "when": "The caller says they have, right now, symptoms that may need urgent care.",
         "do": "Tell the caller to hang up and call emergency services now, then end the call."},
        {"id": "clinical_advice", "action": "next_turn",
         "when": "The caller asks whether symptoms are serious or whether to change a medicine.",
         "do": "Do not give medical advice. Say a clinician has to answer that; offer a callback."},
    ],
})
async for event in session.events():
    if event.type == "policy_flag":
        log(event.data["rule"], event.data["delivered"])

Hosted evals

dialt.evals creates cases and starts runs on the Dialt evals dashboard with your account API key (dk_...; existing ck_... keys remain valid). Cases are JSON files in your repository; upsert_case keeps the hosted copy in step by name, so a re-push updates rather than duplicates.

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 terminal
print(result["status"], [a["status"] for a in result["attempts"]])

A case has name, starter (or target.greeting, when the agent opens the call), target and simulator (each a session mode document, the shape DialtMode.to_wire() produces, so every session option is a case option), fixtures, checks and limits; the field reference is in the evals guide. converse-recipes ships a converse-evals push evals/ command built on this client.

Relaying two sessions (simulations)

dialt.relay cross-pipes two sessions so one can play the user for the other: the building blocks behind converse-recipes' converse-sim and the hosted evals in the Dialt webapp.

from dialt.relay import TextTurnRelay, VoiceTurnRelay

# text: forward each committed target utterance to the simulator once tool work has settled
relay = TextTurnRelay(simulator.send_text)
relay.utterance(event.data["text"])      # on the target's `utterance`
relay.working(active)                    # on `working`
relay.done()                             # on `done`: forwards after TURN_RELAY_SETTLE_S

# voice: the simulator's virtual microphone, a paced stream that never stops
relay = VoiceTurnRelay(simulator)
relay.start()                            # line noise flows from now on, like a live line
await relay.audio(event.audio)           # on each target `audio` event: queued, played at real time
await target.send_client_event(          # on the target's `interrupted`: what the simulator
    "playback_stopped", **relay.interrupted(event.data))   # never heard, as a real client reports it
relay.canceled()                         # on the target's `canceled`: drop the rescinded audio
await relay.close()                      # at the end; a line that hangs up first just stops the mic

The broker measures turn-end silence in received audio, so a simulated caller's mic must keep streaming between turns and after an interrupted clip, not only while someone speaks; the receiving side's own endpointer then closes each turn on the trailing silence. Both relays take on_error to surface background failures.

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 DialtSession.connect(...) to carry the session over WebRTC (UDP) instead of the default WebSocket. Requires uv add "dialt-sdk[webrtc]". ws remains the default; most headless callers are fine on it. See the Python guide's WebRTC section.

Licensed under the Apache License 2.0. This license applies to the SDK, not to the hosted Dialt service, its models, or its server-side implementation. Runtime dependencies remain under their own licenses.

Application tool approval

The permission source vocabulary is caller (the default) or application; the old conversation and external values are not accepted.

Set requires_permission: True and permission_source: "application" on a tool to require approval from your application. Omit the source for caller consent verified by AI. Forward permission_pending events whose source is "application" to your approval application, displaying the exact tool, arguments, status_label and expires_at. Keep the session event consumer running while the reviewer decides. In your authenticated approval callback:

await session.resolve_tool_permission(request_id, "approve")  # or "decline"

The authenticated session host is trusted to submit decisions; authenticate the reviewer in your application. Observe permission_resolution in session.events() and check event.data["accepted"] and any rejection reason. Sending is not acceptance or completion. An accepted approval releases the exact stored tool call; handle tool_call and return its result as usual. Caller consent does not satisfy application approval, and the assistant does not ask the caller for it.

Release files for dialt-sdk 0.25.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dialt-sdk 0.25.0
File Size Uploaded
dialt_sdk-0.25.0.tar.gz 49.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dialt-sdk 0.25.0
File Interpreter ABI Platform
dialt_sdk-0.25.0-py3-none-any.whl Python 3 none any Details

Total release size: 89.9 kB

Release files / dialt_sdk-0.25.0.tar.gz

Download URL dialt_sdk-0.25.0.tar.gz
Size 49.2 kB
Tags Source
SHA-256 checksum
How to use checksums
affdd4dc0ec3e4320bb7884b2de7c6d46b5eba73a842e1f83bbc9c378d8b2d8a
BLAKE2b-256 checksum
How to use checksums
013012e4feaa70007f96d715df213af98a327ea6e99f7965999e90d45bd2bf9b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / dialt_sdk-0.25.0-py3-none-any.whl

Download URL dialt_sdk-0.25.0-py3-none-any.whl
Size 40.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9bf3a8cfda63aaa39486ffdc90e93bca84feb37061460ca77951c9e37fcb03e6
BLAKE2b-256 checksum
How to use checksums
f4687d5091784954a1c331d2650889ccf846e3c7a4d250d8a54bd332521e2c5a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page