# SDK Python

> Installez le SDK Python de Thalovant pour vos agents, services, outils en ligne de commande, carnets et clients de test.

Source: https://docs.thalovant.com/fr/developers/sdks/python/

---
{/* Last Edit: Claude Opus 5.5 - 2026-09-28 - Motive: Canadian French translation of developers/sdks/python.mdx; terms per docs/french-glossary.md (AMF, serveur), and review fixes. */}

Utilisez le SDK Python quand votre client ou votre agent s'exécute en Python.

Le nom du paquet est `thalovant`. Python 3.10 ou plus récent est requis.

## Installation

Installez le paquet avec `pip install thalovant` :

```bash
python -m pip install thalovant
```

Épinglez la version quand vous avez besoin d'une compilation reproductible. La version actuelle figure dans
[Versions prises en charge](/fr/developers/sdks/#versions-prises-en-charge).

Depuis la version 0.9.0, le paquet installe deux bibliothèques, `aiohttp` et `cryptography`,
et n'importe ni l'une ni l'autre avant la première connexion ou le premier appel à l'API. Des extras ajoutent
le reste :

- `thalovant[mqtt]` pour MQTT.
- `thalovant[yaml]` pour lire le fichier de configuration avec PyYAML. Sans lui, un petit
  lecteur intégré gère ce que contient un fichier de configuration.
- `thalovant[listing]` pour les règles linguistiques qu'utilise la liste des intentions.

Le SDK est bâti sur asyncio. `AsyncThalovantClient` et
`AsyncThalovantControlPlane` font le travail, et `ThalovantClient` et
`ThalovantControlPlane` exécutent le même code sur un fil d'exécution privé; un script
n'a donc pas besoin de sa propre boucle d'événements. Le code écrit pour 0.8 fonctionne sans changement; consultez
[Passer à 0.9](#passer-à-09).

## Première requête

Ce flux découvre un hub public, crée une identité de client et envoie une requête.

```python
from thalovant import ThalovantClient, ThalovantControlPlane

api = ThalovantControlPlane()

hub = api.list_public_hubs(limit=1)["data"][0]
print(hub["title"], hub["slug"])

api.login("you@example.com", "password")

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()` utilise `https://api.thalovant.com` par défaut.

Le même flux dans une application asyncio :

```python

from thalovant import AsyncThalovantClient, AsyncThalovantControlPlane

async def main():
    async with AsyncThalovantControlPlane() as api:
        await api.login("you@example.com", "password")
        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())
```

  Gardez `result.identity` secret. Les actions authentifiées de l'API exigent l'accès à l'API sur l'espace de travail.

## Contexte et médias des requêtes vocales

Python `0.7.4` offre des utilitaires pratiques pour les clients vocaux. Les versions synchrone et asynchrone de `ask()` acceptent `stt_lang`, `pipeline` et `location`. `stt_lang` transmet l'indice de langue du moteur de reconnaissance; `pipeline` choisit les étapes d'intention configurées sous la session de la requête. `build_location()` construit un indice d'emplacement, et `request_context()` fusionne ces options avec votre contexte existant.

```python
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` indique la langue de réponse du hub. `reply.media_events` conserve la parole et les événements `mycroft.audio.queue` intégrés dans leur ordre d'arrivée. Pour un événement audio, `event.audio_bytes()` décode son extrait hexadécimal; il ne va chercher ni chemin ni URL à partir de l'événement. Les extraits invalides lèvent `ValueError`. Les limites de collecte sont de 4 Mio par extrait et de 16 Mio par réponse; `reply.dropped_media` compte les extraits omis. Gardez `reply_settle_seconds` au-dessus de zéro pour collecter les extraits qui arrivent avec la parole.

`HubIntent.examples(speakable=True, slots={...})` et `thalovant intents --speakable` produisent des phrases d'exemple. Quand vous limitez les résultats, les phrases sources complètes passent avant les phrases dérivées des emplacements (slots). Ces méthodes pratiques propres à Python sont distinctes des fonctionnalités communes de gestion des compétences que prennent en charge les sept SDK gérés.

## Se connecter avec l'authentification multifacteur

Les comptes où l'authentification multifacteur (AMF) est activée doivent fournir un code TOTP ou un code de récupération à usage unique avec la connexion. Sans l'un ou l'autre, l'API refuse la connexion avec HTTP 401 et le code `mfa_required`. Les deux paramètres ne sont envoyés que s'ils sont fournis. La prise en charge de l'AMF exige le SDK 0.4.21 ou plus récent.

```python
api.login("you@example.com", "password", otp_code="123456")

# Or use a one-time recovery code instead:
api.login("you@example.com", "password", recovery_code="abcd-efgh-ijkl")
```

## Se connecter sans mot de passe

La connexion par appareil demande à votre navigateur d'approuver la connexion; vos scripts, vos outils en ligne de commande et vos agents ne manipulent donc jamais votre mot de passe. Le SDK affiche un code court et l'adresse `https://dash.thalovant.com/activate`, ouvre cette page quand il le peut, puis attend que vous approuviez la demande dans le tableau de bord avec votre connexion habituelle, y compris la connexion Google ou l'AMF. Après l'approbation, le SDK détient un jeton d'API révocable à portée limitée et se comporte exactement comme après `api.login(...)`.

La connexion par appareil exige le SDK 0.4.22 ou plus récent. L'approbation de la demande exige un forfait payant pour l'espace de travail; un forfait Free reçoit HTTP 402.

```python
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")
```

Gérez le jeton obtenu sur la [page **API Tokens**](/fr/account/api-tokens/) du tableau de bord.

Une application qui affiche le code dans son propre écran, comme un assistant de configuration,
exécute le flux par appareil une étape à la fois. `begin_device_login()` retourne le
code et l'adresse à afficher. `poll_device_login()` demande une fois si la
personne a approuvé la demande :

```python

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 down
```

Une interrogation retourne le jeton et le conserve sur `api`, ou lève
`ThalovantDeviceLoginPending`, `ThalovantDeviceLoginExpired` ou
`ThalovantDeviceLoginDenied`. Le jeton dure 365 jours et n'a pas de jeton de
rafraîchissement. Quand la personne retire votre application, appelez
`api.revoke_api_token()` : un jeton peut toujours se révoquer lui-même, quelles que soient ses
portées. Le `ThalovantControlPlane` synchrone offre les mêmes méthodes, appelées sans
`await`. Les deux exigent le SDK 0.9.0 ou plus récent.

## S'authentifier avec un jeton d'API

Passez un jeton d'API enregistré quand le processus doit démarrer déjà authentifié, comme les tâches de CI et les services. Créez-en un sur la [page **API Tokens**](/fr/account/api-tokens/) ou avec `login_with_browser`; la page affiche les portées, l'expiration et la dernière utilisation, et peut révoquer le jeton en tout temps.

```python

from thalovant import ThalovantControlPlane

api = ThalovantControlPlane(access_token=os.environ["THALOVANT_API_TOKEN"])
# Ready immediately; no login call needed.
```

## Utiliser une identité enregistrée

Utilisez cette méthode quand l'identité a été téléchargée depuis Thalovant, conservée dans un volume secret ou enregistrée dans votre configuration SDK protégée.

```python
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)
```

```python
from thalovant import ThalovantClient

with ThalovantClient.from_identity_file("_identity.json") as client:
    reply = client.ask("What can this hub do?")
    print(reply.text)
```

Les variables d'environnement fonctionnent aussi :

```python
from thalovant import ThalovantClient

with ThalovantClient.from_env(protocol="https") as client:
    print(client.ask("Say hello.").text)
```

## Où le SDK conserve ses clés

Le SDK se connecte par WSS, par HTTPS ou par MQTT sur TLS. Chaque connexion est chiffrée et authentifiée avant que quoi que ce soit ne soit envoyé.

Passez `noise_state_dir` à `ThalovantClient` ou à un transport pour choisir le répertoire privé et persistant où le SDK conserve ses clés et les identités de hub qu'il a enregistrées. Gardez ce répertoire d'un redémarrage à l'autre, et utilisez une identité différente pour chaque processus client qui s'exécute en même temps.

Une identité de hub enregistrée endommagée lève `ThalovantConnectionError` sans remplacer l'état enregistré; restaurez-la avant de réessayer. Une connexion dont le délai expire pendant l'authentification par HTTPS lève `ThalovantTimeoutError` après avoir libéré la connexion en échec.

`connect(timeout=None)` ne retourne qu'une fois la connexion authentifiée et prête, dans un seul budget de l'appelant. La valeur par défaut est le délai de connexion configuré, plus le délai d'authentification, plus une seconde, avec un minimum de 0,1 seconde. Un délai explicite, positif et fini couvre toute la tentative de connexion; la marge supplémentaire de préparation introduite dans `0.5.7` est retirée. Les budgets invalides ou expirés lèvent une erreur de connexion dont la cause est `TimeoutError`.

`close(timeout=None)` borne aussi la durée d'attente de son appelant. Le nettoyage conserve la propriété de la connexion jusqu'à ce qu'il soit réellement terminé. Utilisez `wait_closed(timeout=None)` pour observer ce nettoyage avant de réutiliser l'identité de client; ici, `None` signifie une attente sans limite. Le client asynchrone suit les mêmes règles et conserve la propriété du nettoyage après l'annulation d'une tâche.

`ask(timeout=12)` regroupe la connexion, l'envoi et la collecte de la réponse dans un même budget. Les options de constructeur `reply_settle_seconds=0.25` et `empty_reply_wait_seconds=5.0` fixent des fenêtres qui commencent à la première parole non vide ou au premier événement « handled » ou « soft-miss » sans parole. La parole démarre la stabilisation sans exiger « handled ». Ask exige l'ID de requête correspondant et accepte l'ID de session du runtime. Les échecs graves figent les réponses partielles; les écritures incertaines de l'application ne sont jamais réessayées automatiquement.

Les abonnements `on()` actifs survivent aux reconnexions, et les inscriptions et retraits simultanés restent cohérents avec la restauration de la session : un gestionnaire est inscrit une fois par session, et `subscription.close()` l'empêche de revenir lors des reconnexions suivantes. Une inscription qui échoue n'est pas conservée pour une reconnexion future.

`listen` met au plus 256 événements en mémoire tampon par défaut; donnez à `max_buffered_events` une valeur positive pour changer la limite. Un débordement lève `ThalovantRuntimeError` et retire l'abonnement. Son délai inclut la connexion et l'inscription, et retire un abonnement en pause à son expiration. Fermez l'itérateur quand vous avez terminé; l'itérateur asynchrone retire aussi son écouteur en cas d'annulation.

Les certificats TLS sont vérifiés par défaut. Pour une autorité de certification privée, configurez la confiance envers cette autorité; `ThalovantClient(self_signed=True)` accepte explicitement un point de terminaison de développement autosigné. Cette option désactive la vérification des certificats; laissez-la à `False` pour un usage normal. Une connexion n'est utilisable qu'une fois authentifiée. HTTPS conserve sa session lors d'une reconnexion; après la perte de la connexion au courtier, MQTT s'authentifie de nouveau avant d'envoyer des messages de l'application.

Utilisez de nouveaux ID de requête pour Ask et de nouveaux ID de query pour Query; des ID générés sont la valeur par défaut. Un collecteur concurrent qui porte le même ID est refusé sur le même client. Les deux espaces de noms sont indépendants. Ne réutilisez pas un ID pour une opération logique ultérieure, même après une annulation : une réponse retardée peut encore le porter. Réutilisez un ID de session pour le contexte d'une conversation.

## Choisir un protocole

```python
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 exige les identifiants de courtier `identity.mqtt` retournés pour ce client.

Pour les détails du courtier, consultez [Connexion MQTT](/fr/developers/mqtt/).

## Ajouter une conversation

Utilisez une conversation quand des tours liés doivent partager une même session.

```python
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)
```

## Ajouter du contexte

```python
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)
```

## Écouter des événements

```python
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)
```

L'itérateur observe les événements correspondants pendant toute sa durée de vie. Pour une requête et une réponse corrélées, utilisez `ask()`.

## Lister ce qu'on peut demander au hub

Depuis le SDK 0.4.36, un client connecté peut lister chaque intention à laquelle son hub répond, par langue, avec les phrases qu'une personne prononce pour atteindre chacune d'elles. L'inventaire est lu par la propre session du client; aucun jeton de plan de contrôle n'entre donc en jeu. Pour savoir ce que le hub retourne et quels types de messages la connexion doit être autorisée à publier, consultez [Que peut-on demander à mon hub?](/fr/concepts/hub-intents/).

```python
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()` envoie `ovos.intent.list` une fois par langue, en demandant la définition de chaque ligne en même temps, et ne décrit que les intentions à base de phrases que la liste ne contenait pas déjà. `describe=False` s'arrête à la liste et retourne les noms, les moteurs et l'état d'activation, sans les phrases. `examples(lang, limit=2)` classe les phrases complètes avant les préfixes incomplets et les motifs à emplacements, puis privilégie les phrases plus complètes, jusqu'à huit mots. La limite compte les exemples retournés après le retrait des résultats vides et des doublons. `phrases_for(lang)` trouve la variante de langue inscrite la plus proche : `fr` et `fr-CA` peuvent sélectionner `fr-FR`. Les langues inconnues ne retournent aucun exemple.

Un hub qui refuse une query envoie `hive.policy.denied`, que le SDK lève aussitôt sous forme de `ThalovantPolicyDeniedError` avec `denied_type`, `code`, `reason` et la liste `allowed`. Avec `fallback=False`, un refus de `ovos.intent.list` lève l'erreur tout de suite. Avec la valeur par défaut `fallback=True`, le SDK interroge plutôt les manifestes des moteurs et ne retourne que les noms : `inventory.source` vaut `"engine-manifests"`, `inventory.denied` vaut `("ovos.intent.list",)` et `inventory.has_phrases` est faux. L'erreur est levée quand ces queries sont refusées elles aussi, et chaque fois que `ovos.intent.describe` est refusée.

Depuis `0.5.7`, le repli par défaut gère aussi une requête `ovos.intent.list` restée sans réponse. Le résultat utilise toujours `source="engine-manifests"` et `denied=("ovos.intent.list",)`; dans ce cas, `denied` consigne la query restée sans réponse. Avec `fallback=False`, l'expiration du délai de la liste lève `ThalovantTimeoutError`. L'opération lève aussi une erreur de délai si les manifestes des moteurs ne répondent jamais.

Un hub qui accepte la query et y répond avec `ok: false` est un cas différent, et depuis la version 0.4.40, les deux queries se comportent différemment à cet égard. Une liste qui échoue lève `ThalovantRuntimeError` avec le texte d'erreur du hub lui-même, parce qu'une liste en échec n'est pas un hub vide. Une description qui échoue ne retourne aucune définition et ne lève rien : cela signifie que le hub ne connaît pas cette inscription, et l'intention garde donc sa place dans l'inventaire, sans phrases.

Les deux queries sont offertes séparément. `client.list_intents("en-us")` retourne les lignes d'inscription, et `client.describe_intent(skill_id, intent_name, "en-us")` les définitions derrière une intention; chacune accepte le mot-clé `timeout`. `AsyncThalovantClient` offre les trois.

Depuis la ligne de commande :

```bash
thalovant --identity _identity.json intents --lang en-us --lang fr-fr
thalovant --identity _identity.json intents --json
```

`--all` affiche chaque phrase au lieu de deux par intention. Quand le repli ne fournit que les noms, la commande écrit un avertissement sur stderr. Cet avertissement peut suivre un refus explicite ou une liste restée sans réponse; il n'établit pas à lui seul un refus par politique.

## Applications asynchrones

`AsyncThalovantClient`, `AsyncThalovantControlPlane` et `AsyncHubSession` s'exécutent
sur votre boucle d'événements et ne la bloquent jamais : les fichiers d'identité, la dérivation de clés et
le chargement des certificats se font dans l'exécuteur de la boucle. Une application qui possède déjà
une `aiohttp.ClientSession` la passe avec `session=`. Le SDK l'utilise telle quelle
et ne la ferme jamais :

```python
api = AsyncThalovantControlPlane(access_token=token, session=http)
client = AsyncThalovantClient(identity, session=http)
```

`AsyncHubSession` maintient une connexion au hub pour une application de longue durée :

```python
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 fails
runner = asyncio.create_task(session.run())  # keeps that link, redials after a drop
reply = await session.ask("What time is it?")

await session.close()
```

`connect()` fait une seule tentative, pour qu'une étape de configuration puisse signaler un échec tout de suite.
`run()` maintient le lien ouvert par `connect()` et, après une coupure, recompose après
10, 20, 40 et 80 secondes, puis toutes les 120 secondes. Une nouvelle connexion est refusée tant que son
hub ne l'a pas admise; `run()` réessaie donc après un refus pendant dix minutes avant de
lever `ThalovantHubRefusedError`. Chaque tentative est journalisée au niveau DEBUG.

Un client ordinaire se reconnecte aussi de lui-même quand le hub coupe son lien : après
environ cinq secondes, puis en doublant l'intervalle jusqu'à une minute, jusqu'à ce que le lien revienne ou que le
client soit fermé. Un client qui ne fait qu'écouter récupère ses événements après un redémarrage
du hub. `auto_reconnect=False` désactive ce comportement.

## Connecter Home Assistant

Une intégration Home Assistant détient une connexion de type `home_assistant` à un
hub. Une compétence domotique du hub lui envoie `thalovant.home.request` avec ce que la
personne a dit, et l'intégration répond avec `thalovant.home.response` :

```python
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=...)` vérifie que l'API a bien créé
ce type de connexion. Sinon, le SDK supprime ce qu'il a créé et lève
`ThalovantUnsupportedConnectionTypeError`. Un forfait qui ne permet pas ce type
lève `ThalovantPlanError`, et un hub qui a déjà son lien Home Assistant
lève `ThalovantAlreadyLinkedError`.

`wait_for_admission()` retourne une fois que le hub a admis la connexion, ce qui
prend environ quatre-vingt-dix secondes. Elle lève `ThalovantAdmissionTimeoutError` quand
l'attente expire; cette erreur est une `ThalovantConnectionError`, ce qui vous permet de garder la
connexion et d'attendre de nouveau plus tard.

Quand l'API est complètement injoignable, à cause du DNS, de la connexion, de TLS ou d'un
délai expiré, tout appel au plan de contrôle lève `ThalovantAPIUnreachableError`. Cette erreur est
à la fois une `ThalovantAPIError` et une `ThalovantConnectionError`, et n'a pas de code
d'état; vous pouvez ainsi distinguer « réessayez plus tard » d'un refus.

`answer_home_requests()` répond à chaque requête au plus une fois, par le même
chemin que celui de la requête, et jamais après les dix secondes du hub : votre
gestionnaire en obtient neuf, et l'envoi de la réponse obtient le reste. Une réponse
qui ne pourrait arriver qu'après l'abandon du hub n'est pas envoyée. La parole est envoyée
en texte brut. Quand votre gestionnaire lève une exception, manque de temps ou répond hors
du contrat, le SDK envoie une réponse `error` avec un code d'erreur, et le hub
prononce sa propre phrase pour ce cas dans la langue de l'appareil. `delete_client()`
retire la connexion quand la personne retire l'intégration.

Pendant l'attente de l'admission, un jeton révoqué lève `ThalovantAuthError`
plutôt qu'un échec d'admission, et en cas de limite de débit, le SDK attend le temps
indiqué par l'API. Un lien de longue durée démarré avec `run()` s'arrête aussitôt avec
`ThalovantHubKeyChangedError` si la clé du hub n'est plus celle que le SDK a
épinglée au premier contact : une nouvelle tentative ne peut rien y changer, et le SDK ne remplace jamais
l'épinglage de lui-même.

  Créer une connexion `home_assistant` exige une API qui connaît ce type. Une
  API qui ne le connaît pas lève `ThalovantUnsupportedConnectionTypeError`.

## Utilitaires pratiques

  
    Envoyer une requête et recevoir une réponse normalisée.
  
  
    Attendre des événements du hub avec un délai d'expiration.
  
  
    Envoyer une action de bouton, de menu ou d'outil.
  
  
    Envoyer une valeur de code QR, un numéro de série, un code-barres ou un code saisi.
  
  
    Lister ce qu'on peut demander au hub, par langue, avec les phrases qui atteignent chaque intention.
  

Pour la liste complète des méthodes, consultez [Fonctions des SDK](/fr/developers/sdk-functions/).
Utilisez `api.get_operation(operation_id)` pour suivre une commande acceptée; consultez [Opérations](/fr/developers/operations/).

## Provisionner des hubs

Depuis le SDK 0.4.25, le plan de contrôle peut créer des hubs, des groupes de runtime et des installations de compétences. Parcourir le catalogue avec `api.list_marketplace_skills()` exige `hubs:read` et fonctionne avec n'importe quel forfait. `create_hub`, `create_runtime_group`, `install_runtime_group_skill`, `release_hub` et `release_runtime_group` exigent `hubs:write` et un forfait payant.

```python
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` et `delete_hub` exigent l'etag actuel du hub. Consultez [Provisionner des hubs](/fr/developers/provisioning/) pour le flux complet, les champs immuables et le tableau des erreurs.

À partir du SDK `0.5.15`, `list_hub_skills`, `install_hub_skill`, `update_hub_skill` et `remove_hub_skill` gèrent les compétences d'un hub selon son identifiant. `list_hub_skills` retourne une `HubSkillList` dont les lignes se trouvent sous `.data` (`for skill in api.list_hub_skills("hub-id").data:`), les écritures retournent une `HubSkillOperation`, et `wait=True` interroge cette opération avec un budget d'interrogation par défaut de 120 secondes. Les mêmes actions existent en ligne de commande : `thalovant skills list`, `thalovant skills add`, `thalovant skills update` et `thalovant skills remove`, chacune avec `--hub <hub-id>`. Consultez [Ajouter une compétence à un hub](/fr/manage/add-a-skill-to-a-hub/).

Les lignes mal formées de la liste des compétences sont rejetées plutôt qu'omises en silence. Aucune nouvelle lecture d'état ne commence à l'échéance d'interrogation ou après; une lecture HTTP déjà en cours conserve son délai de requête configuré. Si une lecture d'état échoue, l'erreur conserve l'ID de l'opération acceptée pour que vous puissiez reprendre avec `get_operation` sans répéter l'écriture. Une erreur d'API sous-jacente assainie est conservée comme cause; les exceptions inattendues sont omises de la chaîne affichée.

## Problèmes courants

| Symptôme | Vérification |
| --- | --- |
| `Missing Thalovant API access token` | Appelez `api.login(...)` ou `api.login_with_browser()` avant les actions privées de l'API, ou passez `access_token=` à `ThalovantControlPlane`. |
| HTTP 401 avec le code `mfa_required` | Passez `otp_code` ou `recovery_code` à `api.login(...)`. |
| `API access requires a paid plan` | Passez l'espace de travail à un forfait payant avant de provisionner des ressources privées par l'API. |
| `Unsupported protocol` | Activez ce protocole sur le hub et créez une nouvelle identité. |
| MQTT échoue immédiatement | Confirmez que l'identité possède des identifiants de courtier `mqtt`. |
| `ThalovantPolicyDeniedError` | La connexion n'a peut-être pas le droit de publier le type de message que nomme l'erreur. Ajoutez-le aux types de messages de la connexion dans le tableau de bord, puis reconnectez-vous. Consultez [Que peut-on demander à mon hub?](/fr/concepts/hub-intents/). |
| `ovos.intent.list failed: ...` | Le hub a accepté la liste, mais n'a pas pu y répondre. Vérifiez l'état en direct du hub, puis redemandez. Consultez [Que peut-on demander à mon hub?](/fr/concepts/hub-intents/). |

1. Commencez par WSS.
2. N'ajoutez HTTPS que pour les clients requête-réponse.
3. N'ajoutez MQTT que si le client a besoin de trafic qui passe par un courtier.

## Découverte des gestionnaires de repli

L'inventaire comprend `fallbacks` et `fallbacks_known`. Utilisez `inventory.may_answer("en-us")` pour une vérification prudente de la langue; une prise en charge du repli inconnue peut tout de même répondre. La sonde prend au plus 1,5 seconde, connexion, envoi et attente compris; un résultat vide confirmé diffère d'un refus, d'un échec, d'un silence ou d'une enveloppe de réponse mal formée. Les tableaux acceptés conservent les gestionnaires utilisables et ignorent les lignes invalides. Consultez la [découverte des replis](/fr/concepts/hub-intents/#gestionnaires-de-repli-et-disponibilité-des-langues).

Les redirections du plan de contrôle sont refusées. Les appels authentifiés à l'API et les corps de requête exigent HTTPS, sauf pour les points de terminaison de développement explicites en boucle locale (loopback). Consultez [Sécurité des identifiants d'API](/fr/developers/sdks/#sécurité-des-identifiants-dapi).

## Mises à jour simultanées de la configuration du runtime

`update_runtime_group_config(id, config)` préserve les clés enregistrées sans rapport avec votre changement
en lisant la configuration et sa révision, en fusionnant vos changements, puis en envoyant
un `PUT` conditionnel. Une mise à jour en conflit retourne HTTP 412; l'utilitaire relit
la configuration et réapplique vos changements d'origine, jusqu'à trois tentatives d'écriture. Les listes sont
remplacées, et `personas` n'est remplacé que si vous le fournissez.

Cela exige une API qui prend en charge les révisions de configuration et le
`PUT /v1/runtime-groups/{id}/config` conditionnel. Les serveurs plus anciens échouent sans écrire. Les
échecs réseau et les autres erreurs HTTP ne sont pas réessayés. Un échec HTTP final expose
`ThalovantAPIError.status_code`. Coordonnez tout processus d'écriture qui utilise `merge=False`,
qui conserve le remplacement inconditionnel par `PATCH`.

```python
control.update_runtime_group_config(runtime_group_id, {"lang": "fr-fr"})
```

## Gérer les compétences par un hub

Le SDK peut lister, installer, mettre à jour et retirer les compétences du groupe de runtime d'un
hub, et en consulter l'historique. **Chaque hub qui partage ce groupe de runtime est touché.** L'UUID
d'un hub sélectionne le groupe; il ne crée pas d'installation isolée pour ce hub.
Un jeton restreint doit couvrir tous les hubs desservis par le groupe.

Les lectures exigent `hubs:inspect` (`hubs:read` l'inclut). Les écritures exigent `hubs:write`,
un forfait payant admissible et la propriété. Utilisez `latest` ou une version publiée précise
pour l'installation et les mises à jour. Une écriture acceptée retourne un `operation_id`;
l'acceptation ne signifie pas que le runtime a fini d'installer la compétence.

| Action | Méthode |
|---|---|
| Lister | `list_hub_skills` |
| Historique | `list_hub_skill_history` |
| Installer | `install_hub_skill` |
| Mettre à jour | `update_hub_skill` |
| Retirer | `remove_hub_skill` |

L'historique retourne l'enveloppe JSON de l'API avec des entrées `event` et `operation`,
de la plus récente à la plus ancienne, en préservant les champs acteur et version qui peuvent être nuls. La limite va de 1 à 200.

Utilisez l'option d'attente existante pour suivre l'opération acceptée. Un échec d'interrogation
conserve l'ID de son opération; examinez cette opération au lieu de répéter l'écriture.

Les fusions de configuration prennent un instantané de la configuration imbriquée et des personas avant la première lecture;
une modification faite par l'appelant ne peut donc pas changer la charge utile entre les nouvelles tentatives après un conflit. L'utilitaire
de contexte de requête copie aussi une session existante quand aucun pipeline n'est fourni.

Depuis la version 0.9.0, le SDK exige aiohttp 3.11 et cryptography 48.0.1 ou plus récents; il
s'installe donc à côté de Home Assistant 2026.9, qui épingle cryptography 48.0.1.
cryptography 50.0.0 corrige une faille dans le déchiffrement PKCS#7, que le SDK n'appelle
jamais; une application qui utilise elle-même PKCS#7 devrait exiger 50.0.0. Les Mac Intel
restent sous 49.0.0, parce que les versions suivantes ne fournissent aucune wheel macOS x86_64.

Avant cryptography 50.0.0, la dérivation de clé lors d'une première connexion retient
le verrou de l'interpréteur Python pendant environ un dixième de seconde; une boucle asyncio
s'interrompt donc pendant ce temps. La clé est ensuite mise en cache : cela se produit une fois par
identité et par hub.

## Listes d'intentions adaptées aux paramètres régionaux

Installez les données linguistiques facultatives quand les exemples doivent se lire comme des phrases :

```sh
python -m pip install 'thalovant[listing]==0.7.6'
```

```python
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 cent
examples = intent.examples("fr-CA", sentence=True)
```

`sentence=True` implique le rendu prononçable, la mise en majuscule et une ponctuation
adaptée aux paramètres régionaux. Omettre `lang` utilise les premiers paramètres régionaux inscrits, y compris leurs
exemples d'emplacements. Des `slots` explicites remplacent les valeurs d'échantillon de la langue.
Le paquet facultatif `thalovant-languages` détient ces règles; sans lui,
les phrases restent capitalisées et sans ponctuation, et les emplacements gardent leur nom. Les langues
inconnues ne reçoivent aucune ponctuation devinée. Un préfixe incomplet reste sans ponctuation.

Les versions 0.7.4 à 0.8.7 de Python supprimaient un avertissement OVOS répété au sujet de
l'ancien format d'emplacement imbriqué du hub. Depuis la version 0.9.0, le SDK n'utilise plus les
bibliothèques OVOS; l'avertissement n'apparaît donc jamais.

Les données de la liste décrivent maintenant 270 langues, y compris des variantes régionales.
L'espagnol `qué hora es` devient `Qué hora es?`, et le français `coupe le son`
devient `Coupe le son.`. Les langues sans règles, comme `tlh`, restent sans ponctuation.

## Sessions gérées et caches d'inventaire

`HubSession` détient une connexion au hub réutilisable. Fournissez une fabrique qui retourne un
client connecté et qui nettoie une tentative de connexion échouée ou annulée. Les abonnements aux
événements survivent au remplacement du client. Go et Rust exposent un flux d'événements
persistant; les autres SDK gérés exposent des descripteurs d'abonnement. Fermez la session
quand son propriétaire s'arrête; la fermeture attend les opérations acceptées et est définitive.

Les tentatives de connexion en arrière-plan attendent 10, 20, 40, 80, puis 120 secondes.
Les appels au premier plan peuvent essayer immédiatement. Votre application gère l'ordonnancement des sondes :
utilisez le délai de sonde signalé (60 secondes pendant la rétention, 5 secondes pendant une panne).
Le SDK ne rejoue jamais un Ask ou un Emit accepté après la perte d'une réponse, parce qu'un
Ask peut déclencher une action. Le délai d'une requête s'applique à l'opération sous-jacente;
l'attente de la session et votre fabrique de connexions ont des budgets distincts.

```python
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` et `Intent` offrent une vue présentable distincte de l'inventaire
natif des intentions du runtime. Les paramètres régionaux inconnus du catalogue restent inconnus;
des phrases observées pour une langue ne prouvent pas que le catalogue la prend en charge. Les exemples choisissent
les paramètres régionaux pris en charge les plus proches. Une limite non positive retourne le bassin brut de phrases
(Rust utilise zéro pour sa limite non signée). Le JSON du cache inclut l'ordre explicite des langues
des intentions, pour que la sérialisation ne puisse pas changer la langue d'exemple par défaut.

`InventoryCache` est facultatif, utilise par défaut une durée de vie (TTL) d'une heure, et retourne un échec de cache pour
les données invalides, expirées ou illisibles. Les écritures utilisent des fichiers temporaires privés et uniques
et un remplacement atomique. Sous POSIX, les fichiers de cache sont lisibles et modifiables par leur propriétaire seulement; Windows
utilise les ACL du répertoire de l'utilisateur. Les clés de cache distinguent le mode, le chemin de l'identité et le nom d'hôte complet normalisé du hub.
Les fichiers de cache Python existants produisent un échec de cache unique après ce changement de clé.
N'utilisez jamais les caches d'inventaire pour conserver des identifiants.

`OriginPreference` donne à une adresse préférée son propre court budget de connexion et
la met en pause après un échec. Dans les SDK autres que Python, la fabrique doit implémenter la
liaison à l'adresse sur son propre transport, conserver l'hôte public pour TLS/SNI et
terminer le nettoyage des tentatives échouées avant de retourner. Les restrictions de transport et de plateforme
s'appliquent toujours. Python utilise une substitution de résolveur sérialisée et limitée à une portée; évitez de bloquer
du travail de résolution sans rapport à l'intérieur de cette portée. La validation TLS reste activée.

`HubSession` enveloppe des clients synchrones. Sa méthode `OriginPreference.close()` réessaie
le nettoyage d'une tentative de connexion échouée; les clients retournés avec succès
appartiennent toujours à leur appelant.

## Revendications des réponses

Une réponse expose `reply.pipeline_ids`, `reply.skill_ids` et `reply.claimed`.

Les ID de pipeline et de compétence sont des chaînes non vides, dans l'ordre de première apparition, sans doublons. Une réponse réussie dont les seuls ID de pipeline contiennent le texte `fallback` (sensible à la casse) est signalée comme non revendiquée. Toute étape autre qu'un repli rend revendiquée une réponse réussie. Les réponses réussies des anciens hubs sans estampilles d'étape restent revendiquées. Les réponses en échec ou non traitées ne sont jamais revendiquées. Les estampilles mal formées qui ne sont pas des chaînes sont ignorées.

Servez-vous-en comme d'un indice pour décider s'il faut poursuivre une conversation. Le texte de repli et l'état de réussite existant sont conservés. Ces estampilles ne constituent pas une identité de pair vérifiée et ne doivent pas autoriser d'actions.

## Passer à 0.9

Rien n'est obligatoire. Chaque nom public, chaque signature et chaque comportement de 0.8.7 est
toujours là. Ce qui change :

- Le paquet installe `aiohttp` et `cryptography`, et rien d'autre. Si votre
  code importait `hivemind-bus-client`, `ovos-bus-client`, `ovos-utils`,
  `requests`, `PyYAML`, `paho-mqtt` ou `ovos-spec-tools` sans les
  déclarer, déclarez-les maintenant. MQTT exige `thalovant[mqtt]`.
- Les clés et les identités de hub enregistrées restent dans les mêmes fichiers et le même format.
  Rien n'est migré, et les hubs épinglés se reconnectent sans poignée de main de
  premier contact.
- Chaque client synchrone possède un fil d'exécution privé pour sa boucle d'événements. Les gestionnaires passés
  à `on()` s'exécutent toujours hors de cette boucle; un gestionnaire peut donc appeler le client pendant
  qu'un autre fil attend dans `ask()`.
- `ThalovantControlPlane(session=...)` accepte toujours une session de type requests.
  Une `aiohttp.ClientSession` va plutôt à `AsyncThalovantControlPlane`.
- Les nouvelles erreurs sont des sous-classes de `ThalovantAPIError` ou de `ThalovantConnectionError`,
  les erreurs que 0.8.7 levait aux mêmes endroits; les clauses `except` existantes
  les interceptent donc toujours.
