ask
Send one request and receive a normalized reply with a timeout you control.
Use the Kotlin SDK when your client or agent runs on the JVM or on Android.
The Maven Central coordinates are com.thalovant:thalovant-sdk. JVM 17 or newer is required, or an Android toolchain that produces Java 17 bytecode. The SDK uses OkHttp, kotlinx-serialization, and kotlinx-coroutines.
Source and releases: thalovant-kotlin-sdk on GitHub.
Add the dependency to your Gradle build:
dependencies { implementation("com.thalovant:thalovant-sdk:0.1.8")}This flow signs in, creates a client identity, and sends one request over WSS.
import com.thalovant.sdk.CreateClientIdentityOptionsimport com.thalovant.sdk.ThalovantClientimport com.thalovant.sdk.ThalovantControlPlaneimport kotlinx.coroutines.runBlocking
fun main() = runBlocking { val api = ThalovantControlPlane()
val result = api.createClientIdentity( "hub-id", CreateClientIdentityOptions(name = "kotlin-demo-client"), )
val client = ThalovantClient(result.identity) try { val reply = client.ask("Tell me a short clean joke.") println(reply.text) } finally { client.close() }}ThalovantControlPlane() uses https://api.thalovant.com by default.
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.
// Or use a one-time recovery code instead:The otpCode and recoveryCode values are sent only when provided.
Device login asks your browser to approve the sign-in, so services and apps 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.
Device login needs SDK 0.1.1 or newer. Approving the request needs a paid workspace plan; a free plan gets HTTP 402.
import com.thalovant.sdk.DeviceLoginOptions
api.loginWithBrowser( DeviceLoginOptions(scopes = listOf("hubs:read"), clientName = "kotlin-demo"),)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 loginWithBrowser; the page shows scopes, expiry, and last use, and can revoke the token at any time.
val api = ThalovantControlPlane(accessToken = System.getenv("THALOVANT_API_TOKEN"))This release connects to the hub over WSS only. Requesting https or mqtt throws ThalovantUnsupportedProtocolException. Endpoint selection still honors the shared preference order wss, https, mqtt, so identities created for multi-protocol hubs keep working.
Use the Python, Node.js, Go, or Rust SDK when the client needs the HTTPS or MQTT data plane today.
Inspect what an identity supports:
import com.thalovant.sdk.HubProtocol
println(identity.enabledProtocols())println(identity.endpointFor(HubProtocol.WSS))println(identity.mqtt?.endpoint)Raw identity files downloaded from Thalovant work directly. The SDK rejects identity files that other users can read or write on Linux and macOS, so protect them with chmod 600 and keep them out of Git.
val client = ThalovantClient.fromIdentityFile("_identity.json")You can also parse an identity payload you already hold in memory:
import com.thalovant.sdk.ThalovantIdentity
val identity = ThalovantIdentity.fromJson(identityJson)Mutating control-plane commands return durable operations. Poll them with the typed helper:
val operation = api.getOperation("operation-id")println(operation.status)See Operations for lifecycle and retry guidance.
import com.thalovant.sdk.ThalovantEvents
val subscription = client.on(ThalovantEvents.SPEAK) { event -> println(event.text)}// Later, when the listener is no longer needed:subscription.close()A connected client can list every intent its hub answers, per language, with the sentences a person says to reach each one, over its own session and with no control-plane token. See What Can My Hub Be Asked? for what the hub returns, which message types the connection must be allowed to publish, and what a refusal or a failed listing looks like.
Since SDK 0.1.8 the calls are client.intents(languages, options), which returns the inventory grouped by skill, client.listIntents(lang, options), which returns the registration rows, and client.describeIntent(skillId, intentName, lang, options), which returns the definitions behind one intent. All three are suspend functions, like ask.
import com.thalovant.sdk.ThalovantClientimport com.thalovant.sdk.ThalovantPolicyDeniedExceptionimport kotlinx.coroutines.runBlocking
fun main() = runBlocking { val client = ThalovantClient.fromIdentityFile("_identity.json") try { val inventory = client.intents(listOf("en-us", "fr-fr")) println("${inventory.source} ${inventory.languages}") for (skill in inventory.skills) { for (intent in skill.intents) { println("${intent.id} ${intent.engine} ${intent.examples("fr-fr")}") } } } catch (denied: ThalovantPolicyDeniedException) { System.err.println("refused ${denied.deniedType}; allowed ${denied.allowed}") throw denied } finally { client.close() }}intents asks ovos.intent.list once per language, then fills in the sentences the listing did not carry, in windows of at most DESCRIBE_BATCH requests so a hub with many intents is never sent the whole burst at once. IntentInventoryOptions carries the deadline and the switches: describe = false stops at names, engines, and enabled state. examples(lang, limit) prefers whole sentences to ones with a {slot}, then shorter ones; phrasesFor(lang) returns all of them, and asJson() writes the same document the other SDKs write. Language tags compare without regard to case or _ and -, so fr-fr and fr_FR are one request.
A hub that refuses a query throws ThalovantPolicyDeniedException at once with deniedType, code, reason, and allowed. With the default IntentInventoryOptions(fallback = true), a refused ovos.intent.list returns names only instead: inventory.source is HubIntentSource.ENGINE_MANIFESTS, inventory.denied names the refused query, and inventory.hasPhrases is false. A hub that accepts the listing and answers it with ok: false throws ThalovantRuntimeException carrying the hub’s own error text, because a failed listing is not an empty hub. describeIntent returns an empty list for the same answer, which is a real one: the hub does not know that registration.
ask
Send one request and receive a normalized reply with a timeout you control.
sendUtterance
Send speech-like or chat-like input without waiting for the reply.
emit
Send a named event with structured data and context.
getOperation
Poll a durable control-plane command with a typed status.
For cross-language recipes, see SDK Functions.
Since SDK 0.1.3 the control plane can create hubs, runtime groups, and skill installs. Browsing the catalog with listMarketplaceSkills() needs hubs:read and works on any plan. createHub, createRuntimeGroup, installRuntimeGroupSkill, releaseHub, and releaseRuntimeGroup need hubs:write and a paid plan.
val group = api.createRuntimeGroup(RuntimeGroupCreatePayload(name = "kiosks"))val hub = api.createHub( HubCreatePayload( name = "joke-garden", spec = buildJsonObject { }, runtimeGroupId = group["id"]!!.jsonPrimitive.content, ),)
val current = api.getHub(hub["id"]!!.jsonPrimitive.content)api.updateHub( current["id"]!!.jsonPrimitive.content, HubUpdatePayload(active = false), etag = current["etag"]!!.jsonPrimitive.content,)updateHub and deleteHub take the hub’s current etag as a required argument. See Provision Hubs for the full flow, the immutable fields, and the error table.
| Symptom | Check |
|---|---|
Missing Thalovant API access token |
Call api.login(...) or api.loginWithBrowser(...) before private API actions, or pass accessToken to ThalovantControlPlane. |
HTTP 401 with code mfa_required |
Pass otpCode or recoveryCode to api.login(...). |
| API access requires a paid plan | Upgrade the workspace before provisioning private resources through the API. |
ThalovantUnsupportedProtocolException |
The hub does not expose WSS, or the code requested https or mqtt, which this release does not connect over. |
| A request times out | Pass a larger timeoutMs to ask(...). |