ask
Send one request and receive a normalized reply.
Install the Thalovant Python SDK for agents, services, CLIs, notebooks, and test clients.
Use the Python SDK when your client or agent runs in Python.
The package name is thalovant. Python 3.10 or newer is required.
Install the package with pip install thalovant:
python -m pip install thalovantPin the version when you need a reproducible build. The current one is in Supported Versions.
Since 0.9.0 the package installs two libraries, aiohttp and cryptography,
and imports neither until the first connection or API call. Extras add the
rest:
thalovant[mqtt] for MQTT.thalovant[yaml] to read the config file with PyYAML. Without it, a small
built-in reader handles what a config file holds.thalovant[listing] for the language rules the intent listing uses.The SDK is built on asyncio. AsyncThalovantClient and
AsyncThalovantControlPlane do the work, and ThalovantClient and
ThalovantControlPlane run the same code on a private thread, so a script
needs no event loop of its own. Code written for 0.8 works unchanged; see
Upgrading To 0.9.
This flow discovers a public hub, creates a client identity, and sends one request.
from thalovant import ThalovantClient, ThalovantControlPlane
api = ThalovantControlPlane()
hub = api.list_public_hubs(limit=1)["data"][0]print(hub["title"], hub["slug"])
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.
The same flow in an asyncio application:
import asyncio
from thalovant import AsyncThalovantClient, AsyncThalovantControlPlane
async def main(): async with AsyncThalovantControlPlane() as api: result = await api.create_client_identity("hub-id", name="python-demo-client")
async with AsyncThalovantClient(result.identity) as client: reply = await client.ask("Tell me a short clean joke.") print(reply.text)
asyncio.run(main())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.
An application that shows the code in its own screen, such as a setup wizard,
runs the device flow one step at a time. begin_device_login() returns the
code and the address to show. poll_device_login() asks once whether the
person approved it:
import asyncio
from thalovant import ThalovantDeviceLoginPending
async def sign_in(api): authorization = await api.begin_device_login(scopes=["hubs:read", "clients:write"]) show(authorization.verification_uri, authorization.user_code) # your screen
interval = authorization.interval while True: await asyncio.sleep(interval) try: return await api.poll_device_login(authorization) except ThalovantDeviceLoginPending as pending: interval = pending.interval # longer after the API asks to slow downA poll returns the token and stores it on api, or raises
ThalovantDeviceLoginPending, ThalovantDeviceLoginExpired or
ThalovantDeviceLoginDenied. The token lasts 365 days and has no refresh
token. When the person removes your application, call
api.revoke_api_token(): a token can always revoke itself, whatever its
scopes. The sync ThalovantControlPlane has the same methods, called without
await. Both need SDK 0.9.0 or newer.
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)The SDK connects over WSS, HTTPS, or MQTT over TLS. Every connection is encrypted and authenticated before anything is sent.
Pass noise_state_dir to ThalovantClient or a transport to choose the private, persistent directory where the SDK keeps its keys and the hub identities it has recorded. Keep that directory between restarts, and use a different identity for each client process that runs at the same time.
A damaged saved hub identity raises ThalovantConnectionError without replacing the saved state; restore it before retrying. A connection that runs out of time while authenticating over HTTPS 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 authentication 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.
Live on() subscriptions survive reconnects, and concurrent registration and removal stay 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 once it is authenticated. HTTPS keeps its session when reconnecting; after the broker connection is lost, MQTT authenticates again 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.
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.
AsyncThalovantClient, AsyncThalovantControlPlane and AsyncHubSession run
on your event loop and never block it: identity files, key derivation and
certificate loading happen on the loop’s executor. An application that already
has an aiohttp.ClientSession passes it as session=. The SDK uses it as
given and never closes it:
api = AsyncThalovantControlPlane(access_token=token, session=http)client = AsyncThalovantClient(identity, session=http)AsyncHubSession keeps one hub connection up for a long-running application:
from thalovant import AsyncHubSession
session = AsyncHubSession.for_identity(identity, session=http)session.on_state_change(lambda up: print("hub link", "up" if up else "down"))session.on("mycroft.volume.set", on_volume) # follows every reconnect
await session.connect() # one attempt; raises if it failsrunner = asyncio.create_task(session.run()) # keeps that link, redials after a dropreply = await session.ask("What time is it?")
await session.close()connect() makes one attempt, so a setup step can report a failure at once.
run() keeps the link connect() opened and, after a drop, dials again after
10, 20, 40, 80, then every 120 seconds. A new connection is refused until its
hub has admitted it, so run() retries a refusal for ten minutes before it
raises ThalovantHubRefusedError. Every attempt is logged at DEBUG.
A plain client dials back by itself too when the hub drops its link: after
about five seconds, doubling up to a minute, until the link is back or the
client is closed. A client that only listens gets its events back after a hub
restart. auto_reconnect=False turns this off.
A Home Assistant integration holds one connection of type home_assistant to a
hub. A home skill on the hub sends it thalovant.home.request with what the
person said, and the integration answers with thalovant.home.response:
from thalovant import AsyncHubSession, AsyncThalovantControlPlane, HomeAnswer, answer_home_requests
api = AsyncThalovantControlPlane(access_token=token, session=http)result = await api.create_client_identity( hub_id, name="Home Assistant", connection_type="home_assistant")await api.wait_for_admission(result)
async def handle(request): speech = await ask_home_assistant(request.utterance, request.lang) return HomeAnswer(speech=speech, response_type="action_done")
session = AsyncHubSession.for_identity(result.identity, session=http)stop = answer_home_requests(session, handle)await session.connect()runner = asyncio.create_task(session.run())create_client_identity(..., connection_type=...) checks that the API made
that kind of connection. If it did not, the SDK deletes what it made and raises
ThalovantUnsupportedConnectionTypeError. A plan that does not allow the type
raises ThalovantPlanError, and a hub that already has its Home Assistant link
raises ThalovantAlreadyLinkedError.
wait_for_admission() returns once the hub has admitted the connection, which
takes about ninety seconds. It raises ThalovantAdmissionTimeoutError when the
wait runs out; that error is a ThalovantConnectionError, so you can keep the
connection and wait again later.
When the API cannot be reached at all, because of DNS, the connection, TLS or a
timeout, any control-plane call raises ThalovantAPIUnreachableError. It is
both a ThalovantAPIError and a ThalovantConnectionError, and has no status
code, so you can tell “try again later” from a refusal.
answer_home_requests() answers each request at most once, back along the
route the request came in on, and never after the hub’s ten seconds: your
handler gets nine of them, and sending the reply gets what is left. A reply
that could only arrive after the hub gave up is not sent. The speech is sent
as plain text. When your handler raises, runs out of time, or answers outside
the contract, the SDK sends an error answer with an error code, and the hub
speaks its own sentence for it in the device’s language. delete_client()
removes the connection when the person removes the integration.
While waiting for admission, a revoked token raises ThalovantAuthError
rather than a failed admission, and a rate limit is waited out for the time
the API names. A long-lived link started with run() stops at once with
ThalovantHubKeyChangedError if the hub’s key is no longer the one the SDK
pinned on first contact: retrying cannot fix that, and the SDK never replaces
the pin by itself.
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.
Malformed skill-list rows are rejected rather than silently omitted. 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?. |
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.
update_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"})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.
Since 0.9.0 the SDK requires aiohttp 3.11 and cryptography 48.0.1 or newer, so it installs beside Home Assistant 2026.9, which pins cryptography 48.0.1. cryptography 50.0.0 fixes a flaw in PKCS#7 decryption, which the SDK never calls; an application that uses PKCS#7 itself should require 50.0.0. Intel Macs stay below 49.0.0, because later releases ship no x86_64 macOS wheels.
Before cryptography 50.0.0, the key derivation on a first connection holds Python’s interpreter lock for about a tenth of a second, so an asyncio loop pauses that long. The key is cached afterwards, so this happens once per identity and hub.
Install the optional language data when examples should read as sentences:
python -m pip install 'thalovant[listing]==0.7.6'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 to 0.8.7 suppressed a repeated OVOS warning about the hub’s older nested location format. Since 0.9.0 the SDK does not use the OVOS libraries, so the warning never appears.
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 accepted 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 accepted Ask or Emit after a lost response, because an Ask can trigger an action. A request timeout applies to the underlying operation; waiting for the session 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 connection 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.
A reply 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.
Nothing is required. Every public name, signature and behaviour of 0.8.7 is still there. What changes:
aiohttp and cryptography and nothing else. If your
code imported hivemind-bus-client, ovos-bus-client, ovos-utils,
requests, PyYAML, paho-mqtt or ovos-spec-tools without declaring
it, declare it now. MQTT needs thalovant[mqtt].on() still run off that loop, so a handler can call the client while
another thread waits in ask().ThalovantControlPlane(session=...) still takes a requests-style session.
An aiohttp.ClientSession goes to AsyncThalovantControlPlane instead.ThalovantAPIError or ThalovantConnectionError,
the errors 0.8.7 raised in the same places, so existing except clauses
still catch them.