ask
Send one request and receive a normalized reply.
Use the Python SDK when your client or agent runs in Python.
The package name is thalovant. Python 3.10 or newer is required.
Version 0.7.4 supports v3 Noise over WSS, HTTPS, and MQTT over TLS, using the published HiveMind handshake dependencies. Both 25519_ChaChaPoly_SHA256 and 25519_AESGCM_SHA256 are supported, with XXpsk2 on first contact and KKpsk0 for pinned peers.
Pass noise_state_dir to ThalovantClient or a transport to choose a private, persistent directory. The default preserves the existing HiveMind XDG key locations, including keys used by earlier WSS clients. Keep that directory between process restarts and use a different identity for each concurrent client process.
Malformed saved server pins raise ThalovantConnectionError without replacing the trust file. Restore verified state before retrying. An expired HTTPS Noise handshake raises ThalovantTimeoutError after releasing the failed connection.
connect(timeout=None) returns only after authenticated readiness within one caller budget. The default is the configured connection timeout plus handshake timeout plus one second, with a minimum of 0.1 seconds. An explicit positive, finite timeout covers the whole connection attempt; the extra readiness allowance from 0.5.7 is removed. Invalid or expired budgets raise a connection error with a TimeoutError cause.
close(timeout=None) also bounds how long its caller waits. Cleanup retains ownership until it actually finishes. Use wait_closed(timeout=None) to observe that cleanup before reusing the client identity; here None means an unbounded wait. The async client follows the same rules and preserves cleanup ownership after task cancellation.
ask(timeout=12) includes connection, send, and reply collection in one budget. Constructor options reply_settle_seconds=0.25 and empty_reply_wait_seconds=5.0 set fixed windows starting on first nonempty speech or first handled/soft-miss without speech. Speech starts settlement without requiring handled. Ask requires the matching request ID and accepts the runtime’s session ID. Hard failures freeze partial replies; uncertain application writes are never retried automatically.
Version 0.7.4 requires hivemind-bus-client>=1.1.9a1, including the upstream fix for duplicate inbound BUS delivery. WSS subscribers receive each frame once after HiveMind updates session and routing context and removes unverified origin claims. The earlier SDK object-identity workaround is removed; custom transports may reuse a message object for separate events.
Live on() subscriptions survive reconnects. Version 0.7.4 keeps concurrent registration and removal consistent with session restoration: a handler is registered once per session, and subscription.close() prevents it from returning on later reconnects. A failed registration is not retained for a future reconnect.
listen buffers at most 256 events by default; set a positive max_buffered_events to change the limit. Overflow raises ThalovantRuntimeError and retires the subscription. Its timeout includes connection and registration, and retires a paused subscription when it expires. Close the iterator when finished; the async iterator also retires its listener on cancellation.
TLS certificates are verified by default. For a private CA, configure trust for that CA; ThalovantClient(self_signed=True) explicitly opts into a self-signed development endpoint. This disables certificate verification; leave it False for normal use. A connection is usable only after its Noise handshake completes. HTTPS retains affinity when reconnecting; MQTT must complete a fresh handshake after broker loss before sending application messages.
Use fresh request IDs for Ask and fresh query IDs for Query; generated IDs are the default. An overlapping collector with the same ID is rejected on the same client. The namespaces are independent. Do not reuse an ID for a later logical operation, even after cancellation: a delayed reply can still carry it. Reuse a session ID for conversation context. See the correlation contract.
Install with python -m pip install thalovant==0.7.5:
python -m pip install thalovant==0.7.5update_runtime_group_config(id, config) preserves unrelated stored keys by
reading the configuration and its revision, merging your changes, and sending
a conditional PUT. A conflicting update returns HTTP 412; the helper rereads
and reapplies your original changes, up to three write attempts. Lists are
replaced, and personas is replaced only when you supply it.
This requires an API that supports configuration revisions and conditional
PUT /v1/runtime-groups/{id}/config. Older servers fail without a write. Network
failures and other HTTP errors are not retried. A final HTTP failure exposes
ThalovantAPIError.status_code. Coordinate any writers using merge=False,
which retains unconditional PATCH replacement.
control.update_runtime_group_config(runtime_group_id, {"lang": "fr-fr"})This flow discovers a public hub, creates a client identity, and sends one request.
from thalovant import ThalovantClient, ThalovantControlPlane
api = ThalovantControlPlane()
public_hubs = api.list_public_hubs(limit=12)for hub in public_hubs["data"]: print(hub["id"], hub["slug"], hub["title"])
result = api.create_client_identity( "hub-id", name="python-demo-client", preferred_protocols=("wss", "https", "mqtt"),)
with ThalovantClient(result.identity, protocol="wss") as client: reply = client.ask("Tell me a short clean joke.") print(reply.text)ThalovantControlPlane() uses https://api.thalovant.com by default.
Python 0.7.4 provides convenience helpers for voice clients. Both synchronous and asynchronous ask() accept stt_lang, pipeline, and location. stt_lang carries the recognizer’s language hint; pipeline selects the configured intent stages under the request session. build_location() constructs a location hint, and request_context() merges these options with your existing context.
from thalovant import build_location, speakable
location = build_location(city="Toronto", country="Canada", timezone="America/Toronto")reply = client.ask("What is the weather?", stt_lang="en-ca", location=location)print(reply.text, reply.lang)print(speakable("[please] tell me about {city}", slots={"city": "Toronto"}))reply.lang reports the hub’s reply language. reply.media_events preserves speech and embedded mycroft.audio.queue events in arrival order. For an audio event, event.audio_bytes() decodes its hexadecimal clip; it does not fetch paths or URLs from the event. Invalid clips raise ValueError. Collection limits are 4 MiB per clip and 16 MiB per reply; reply.dropped_media counts omitted clips. Keep reply_settle_seconds above zero to collect clips arriving with speech.
HubIntent.examples(speakable=True, slots={...}) and thalovant intents --speakable render example phrases. When limiting results, complete source phrases rank before phrases derived from slots. These Python convenience methods are separate from the shared skill-management capabilities supported by all seven managed SDKs.
Accounts with multi-factor authentication enabled must include a TOTP code or a one-time recovery code with the login. Without one, the API rejects the sign-in with HTTP 401 and code mfa_required. Both parameters are sent only when provided. MFA support needs SDK 0.4.21 or newer.
# Or use a one-time recovery code instead:Device login asks your browser to approve the sign-in, so scripts, CLIs, and agents never handle your password. The SDK prints a short code and the address https://dash.thalovant.com/activate, opens that page when it can, and waits while you approve the request in the dashboard with your normal sign-in, including Google sign-in or MFA. On approval the SDK holds a scoped, revocable API token and behaves exactly like after api.login(...).
Device login needs SDK 0.4.22 or newer. Approving the request needs a paid workspace plan; a free plan gets HTTP 402.
api.login_with_browser()
# Optional: narrow the scopes and label the token in the dashboard.api.login_with_browser(scopes=["hubs:read", "clients:write"], client_name="my-cli")Manage the resulting token on the dashboard’s API Tokens page.
Pass a stored API token when the process should start authenticated, such as CI jobs and services. Mint one on the API Tokens page or with login_with_browser; the page shows scopes, expiry, and last use, and can revoke the token at any time.
import os
from thalovant import ThalovantControlPlane
api = ThalovantControlPlane(access_token=os.environ["THALOVANT_API_TOKEN"])# Ready immediately; no login call needed.Use this when the identity was downloaded from Thalovant, stored in a secret volume, or saved in your protected SDK config.
from thalovant import ThalovantClient
with ThalovantClient.from_config(profile="prod", protocol="wss") as client: reply = client.ask("What can this hub do?") print(reply.text)from thalovant import ThalovantClient
with ThalovantClient.from_identity_file("_identity.json") as client: reply = client.ask("What can this hub do?") print(reply.text)Environment variables work too:
from thalovant import ThalovantClient
with ThalovantClient.from_env(protocol="https") as client: print(client.ask("Say hello.").text)identity = result.identity
print(identity.enabled_protocols())print(identity.endpoint_for("wss"))print(identity.endpoint_for("https"))print(identity.endpoint_for("mqtt"))
for protocol in ("wss", "https", "mqtt"): if not identity.supports_protocol(protocol): continue if protocol == "mqtt" and identity.mqtt is None: continue
with ThalovantClient(identity, protocol=protocol) as client: print(protocol, client.ask(f"Reply over {protocol}.").text)MQTT requires the identity.mqtt broker credentials returned for that client.
For broker details, see MQTT.
Use a conversation when related turns should share one session.
with ThalovantClient.from_identity_file("_identity.json") as client: with client.conversation(lang="en-us") as convo: print(convo.ask("Remember that my favorite color is blue.").text) print(convo.ask("What color did I mention?").text)from thalovant import ThalovantClient, build_client_context
context = build_client_context( user_id="user-42", user_name="Ada", source="checkout-kiosk", platform="kiosk", locale="en-US", metadata={"trace_id": "req-2026-06-09-001"},)
with ThalovantClient.from_identity_file("_identity.json") as client: reply = client.ask("Show the next instruction.", context=context) print(reply.text)from thalovant import EVENT_SPEAK, ThalovantClient
with ThalovantClient.from_identity_file("_identity.json", protocol="wss") as client: for event in client.listen(EVENT_SPEAK, timeout=30, max_events=3): print(event.text)The iterator observes matching events during its lifetime. For one correlated request and response, use ask().
Since SDK 0.4.36 a connected client can list every intent its hub answers, per language, with the sentences a person says to reach each one. The inventory is read over the client’s own session, so no control-plane token is involved. For what the hub returns and which message types the connection must be allowed to publish, see What Can My Hub Be Asked?.
from thalovant import ThalovantClient, ThalovantPolicyDeniedError
with ThalovantClient.from_identity_file("_identity.json") as client: try: inventory = client.intents(["en-us", "fr-fr"]) except ThalovantPolicyDeniedError as denied: print("refused:", denied.denied_type, "allowed:", denied.allowed) raise
print(inventory.source, inventory.languages) for skill in inventory.skills: print(skill.skill_id) for intent in skill.intents: print(" ", intent.id, intent.engine, intent.examples("fr-fr"))intents() asks ovos.intent.list once per language, requesting each row’s definition with it, and describes only the sentence-based intents the listing did not already carry. describe=False stops at the listing and returns names, engines, and enabled state without sentences. examples(lang, limit=2) ranks complete phrases before dangling prefixes and slot patterns, then favors fuller phrases up to eight words. The limit counts returned examples after empty results and duplicates are removed. phrases_for(lang) finds the nearest registered language variant: fr and fr-CA can select fr-FR. Unknown languages return no examples.
A hub that refuses a query sends hive.policy.denied, which the SDK raises at once as ThalovantPolicyDeniedError with denied_type, code, reason, and the allowed list. With fallback=False, a refused ovos.intent.list raises straight away. With the default fallback=True, the SDK asks the engines’ own manifests instead and returns names only: inventory.source is "engine-manifests", inventory.denied is ("ovos.intent.list",), and inventory.has_phrases is false. The error is raised when those queries are refused too, and whenever ovos.intent.describe is refused.
Since 0.5.7, the default fallback also handles an unanswered ovos.intent.list. The result still uses source="engine-manifests" and denied=("ovos.intent.list",); for this case, denied records the unanswered query. With fallback=False, the listing timeout raises ThalovantTimeoutError. The operation also raises a timeout if the engine manifests never answer.
A hub that accepts the query and answers it with ok: false is a different case, and since 0.4.40 the two queries part ways there. A listing that fails raises ThalovantRuntimeError carrying the hub’s own error text, because a failed listing is not an empty hub. A describe that fails returns no definitions and raises nothing: it means the hub does not know that registration, so the intent keeps its place in the inventory with no sentences.
The two queries are available on their own. client.list_intents("en-us") returns the registration rows and client.describe_intent(skill_id, intent_name, "en-us") the definitions behind one intent, each with a timeout keyword. AsyncThalovantClient has all three.
From the command line:
thalovant --identity _identity.json intents --lang en-us --lang fr-frthalovant --identity _identity.json intents --json--all prints every sentence instead of two per intent. When fallback supplies names only, the command writes a warning to stderr. That warning can follow an explicit refusal or an unanswered listing; it does not by itself establish a policy denial.
ask
Send one request and receive a normalized reply.
listen
Wait for hub events with a timeout.
send_action
Send a button, menu, or tool action.
send_code
Send a QR value, serial number, barcode, or typed code.
intents
List what the hub can be asked, per language, with the sentences that reach each intent.
For the full method list, see SDK Functions.
Use api.get_operation(operation_id) to follow an accepted command; see Operations.
Since SDK 0.4.25 the control plane can create hubs, runtime groups, and skill installs. Browsing the catalog with api.list_marketplace_skills() needs hubs:read and works on any plan. create_hub, create_runtime_group, install_runtime_group_skill, release_hub, and release_runtime_group need hubs:write and a paid plan.
group = api.create_runtime_group({"name": "kiosks"})hub = api.create_hub({"name": "joke-garden", "runtime_group_id": group["id"], "spec": {}})
hub = api.get_hub(hub["id"])api.update_hub(hub["id"], {"active": False}, etag=hub["etag"])update_hub and delete_hub require the hub’s current etag. See Provision Hubs for the full flow, the immutable fields, and the error table.
From SDK 0.5.15, list_hub_skills, install_hub_skill, update_hub_skill, and remove_hub_skill manage skills on one hub by id. list_hub_skills returns a HubSkillList whose rows sit under .data (for skill in api.list_hub_skills("hub-id").data:), the writes return a HubSkillOperation, and wait=True polls that operation with a 120-second default polling budget. The same actions are on the command line as thalovant skills list, thalovant skills add, thalovant skills update, and thalovant skills remove, each with --hub <hub-id>. See Add a Skill to a Hub.
Version 0.7.4 rejects malformed skill-list rows instead of silently omitting them. No new status read starts at or after the polling deadline; an HTTP read already in progress keeps its configured request timeout. If a status read fails, the error retains the accepted operation ID so you can resume with get_operation without repeating the write. An underlying sanitized API error is preserved as the cause; unexpected exceptions are omitted from the displayed chain.
| Symptom | Check |
|---|---|
Missing Thalovant API access token |
Call api.login(...) or api.login_with_browser() before private API actions, or pass access_token= to ThalovantControlPlane. |
HTTP 401 with code mfa_required |
Pass otp_code or recovery_code to api.login(...). |
API access requires a paid plan |
Upgrade the workspace before provisioning private resources through the API. |
Unsupported protocol |
Enable that protocol on the hub and create a fresh identity. |
| MQTT fails immediately | Confirm the identity has mqtt broker credentials. |
ThalovantPolicyDeniedError |
The connection may not publish the message type the error names. Add it to the connection’s message types in the dashboard, then reconnect. See What Can My Hub Be Asked?. |
ovos.intent.list failed: ... |
The hub accepted the listing and could not answer it. Check the hub’s live state, then ask again. See What Can My Hub Be Asked?. |
Last reviewed: September 13, 2026. Review this page when package versions, transport support, TLS defaults, or Noise state storage changes.
The inventory includes fallbacks and fallbacks_known. Use inventory.may_answer("en-us") for a conservative language check; unknown fallback support may still answer. The probe takes at most 1.5 seconds including connect, send, and wait; a confirmed empty result differs from refusal, failure, silence, or a malformed reply envelope. Accepted arrays retain usable handlers and skip invalid rows. See fallback discovery.
Control-plane redirects are refused. Authenticated API calls and request bodies require HTTPS except explicit loopback development endpoints. See API credential security.
The SDK can list, install, update, remove and inspect the history of skills on a hub’s runtime group. Every hub sharing that runtime group is affected. A hub UUID selects the group; it does not create an isolated installation for that hub. A restricted token must cover all hubs served by the group.
Reads require hubs:inspect (hubs:read includes it). Writes require hubs:write,
an eligible paid plan and ownership. Use latest or a concrete released version
for installation and updates. An accepted write returns an operation_id;
acceptance does not mean the runtime has finished installing the skill.
| Action | Method |
|---|---|
| List | list_hub_skills |
| History | list_hub_skill_history |
| Install | install_hub_skill |
| Update | update_hub_skill |
| Remove | remove_hub_skill |
History returns the API JSON envelope with newest-first event and operation
entries, preserving nullable actor/version fields. The limit is 1–200.
Use the existing wait option to follow the accepted operation. A polling failure retains its operation ID; inspect that operation instead of repeating the write.
Configuration merges snapshot nested config and personas before the first read, so caller mutation cannot change the payload between conflict retries. The request context helper also copies an existing session when no pipeline is supplied.
Version 0.7.4 requires Requests 2.33.0 and cryptography 50.0.0 or newer,
excluding known vulnerable versions. Python 3.10 CI tests those minimum
versions, and separate CI jobs audit runtime and documentation dependencies.
Version 0.7.5 keeps that cryptography floor everywhere except on Intel
Macs, which install cryptography 48.0.1 or newer: the last releases that
ship x86_64 macOS wheels.
Install the optional language data when examples should read as sentences:
python -m pip install 'thalovant[listing]==0.7.5'from thalovant import as_sentence, speakable
print(as_sentence("quelle heure est-il", "fr-CA")) # Quelle heure est-il?print(speakable("volume {level} pour cent", lang="fr")) # volume cinquante pour centexamples = intent.examples("fr-CA", sentence=True)sentence=True implies speakable rendering, capitalization and locale-aware
punctuation. Omitting lang uses the first registered locale, including its
slot examples. Explicit slots override the language’s sample values.
The optional thalovant-languages package owns these rules; without it,
sentences remain capitalized and bare and slots retain their names. Unknown
languages receive no guessed punctuation. A dangling prefix remains unpunctuated.
Python 0.7.4 suppresses the repeated OVOS warning about the hub’s older nested location format at the upstream logger factory. Other warnings remain visible. This is specific to Python’s OVOS integration and does not change the wire protocol or locale-listing APIs.
The listing data now describes 270 languages, including regional overrides.
Spanish qué hora es becomes Qué hora es?, and French coupe le son
becomes Coupe le son.. Languages without rules, such as tlh, remain bare.
HubSession owns one reusable hub connection. Supply a factory that returns a
connected client and cleans up a failed or cancelled connection attempt. Event
subscriptions survive client replacement. Go and Rust expose a persistent event
stream; the other managed SDKs expose subscription handles. Close the session
when its owner shuts down; close waits for admitted operations and is terminal.
Background connection attempts back off for 10, 20, 40, 80, then 120 seconds. Foreground calls can try immediately. Your application owns probe scheduling: use the reported probe delay (60 seconds while held, 5 seconds while down). The SDK never replays an admitted Ask or Emit after a lost response, because an Ask can trigger an action. A request timeout applies to the underlying operation; waiting for session admission and your connection factory are separate budgets.
from thalovant import HubSession
# connect_client returns a connected client and cleans up failed attempts.session = HubSession(connect_client, warm=False)try: reply = session.ask("What is the weather?")finally: session.close()Inventory, Skill, and Intent provide a presentable view separate from the
runtime’s native intent inventory. Unknown catalogue locales remain unknown;
phrases observed for a language do not prove catalogue support. Examples choose
the closest supported locale. A nonpositive limit returns the raw phrase pool
(Rust uses zero for its unsigned limit). Cache JSON includes explicit intent
language order so serialization cannot change the default example language.
InventoryCache is optional, defaults to a one-hour TTL, and returns a miss for
invalid, expired, or unreadable data. Writes use private, unique scratch files
and atomic replacement. POSIX cache files are owner-readable/writable; Windows
uses the user’s directory ACLs. Cache keys separate mode, identity path, and the full normalized hub hostname.
Existing Python cache files produce a one-time cache miss after this key update.
Never use inventory caches to store credentials.
OriginPreference gives a preferred address its own short handshake budget and
cools it down after a failure. In non-Python SDKs the factory must implement the
address binding on its own transport, retain the public host for TLS/SNI, and
finish failed-attempt cleanup before returning. Transport/platform restrictions
still apply. Python uses a serialized, scoped resolver override; avoid blocking unrelated
resolver work inside that scope. TLS validation remains enabled.
HubSession wraps synchronous clients. Its OriginPreference.close() retries
cleanup of a failed connection attempt; successfully returned clients still
belong to their caller.
Version 0.7.4 exposes reply.pipeline_ids, reply.skill_ids and reply.claimed.
Pipeline and skill IDs are nonempty strings in first-seen order, with duplicates removed. A successful reply with only pipeline IDs containing the case-sensitive text fallback reports an unclaimed reply. Any non-fallback stage makes a successful reply claimed. Successful replies from older hubs without stage stamps remain claimed. Failed or unhandled replies are never claimed. Malformed non-string stamps are ignored.
Use this as a hint when deciding whether to continue a conversation. Fallback text and the existing success status are retained. These stamps are not verified peer identity and must not authorize actions.