Garder votre compétence réactive
SkillKit gère ressources en cache, sons variés, appels de service bornés, actualisation en arrière-plan et persistance facultative à votre place.
Une compétence devrait passer son temps à répondre à la personne, plutôt qu’à retrouver les mêmes fichiers ou à attendre trop longtemps un service. SkillKit fournit de petits outils pour ce travail répétitif. Choisissez seulement ceux dont votre compétence a besoin.
Commencez par Créer un quiz éducatif si vous
n’avez pas encore écrit de compétence. Ces exemples prolongent un projet existant. Installez avec
python -m pip install --pre "thalovant-skillkit==0.24.2", et déclarez thalovant-skillkit>=0.24.2
dans les dépendances de votre projet.
Choisir un outil
Section intitulée « Choisir un outil »| Ce que vous ajoutez | Utilisez | Votre compétence décide toujours |
|---|---|---|
| De nombreux sons empaquetés | bundled_files et ShuffleBagPool | Quel son convient au jeu et quel haut-parleur l’entend |
| Plusieurs réponses traduites | self.speak_varied_dialog | Les mots eux-mêmes et le moment d’inviter une réponse |
| Un service en ligne | JsonServiceClient et RequestBudget | La requête, la validation et la réponse d’indisponibilité |
| Un catalogue qui s’actualise de temps en temps | PeriodicWorker | La fraîcheur, l’annulation et le remplacement sûr du cache |
| Un état Redis/PostgreSQL facultatif | JsonStateStore | La validation des enregistrements, leur propriété et la récupération locale |
| Une partie ou un brouillon par joueur | SessionStateStore | Ce qu’un tour modifie, et quand une partie est terminée |
| Des alias ou des listes de blocage régionaux | SkillResources.combined_lines | Les langues que la fonctionnalité doit accepter |
Ces outils n’ajoutent pas d’appel de service à une compétence hors ligne. Les importer ne démarre aucune tâche de fond et ne se connecte à aucun stockage. OVOS gère toujours la parole, l’audio, l’acheminement des intentions, les conversations et les alarmes planifiées.
Trouver les sons une fois, puis choisir vite
Section intitulée « Trouver les sons une fois, puis choisir vite »Imaginez une boîte de cartes de sons d’animaux. Vous faites d’abord la liste des cartes, puis vous piochez dans cette liste chaque fois que quelqu’un demande. Vous ne déballez pas toute la boîte à chaque tour.
from pathlib import Pathfrom thalovant_skillkit.assets import bundled_filesfrom thalovant_skillkit.selection import ShuffleBagPool
sound_dir = Path(__file__).parent / "sounds"sounds = bundled_files(sound_dir, "*.ogg")choices = ShuffleBagPool()
# Run inside an intent handler after adding at least two sounds to your package:# chosen = choices.draw(sounds)[0]# self.play_audio(str(chosen))Créez choices une seule fois par instance de la compétence. bundled_files mémorise un tuple trié
de noms de fichiers; il ne charge pas l’audio en mémoire. Le cache contient au plus 128 combinaisons
répertoire/motif. Le premier appel parcourt le répertoire; les appels suivants
réutilisent la liste. N’oubliez pas d’inclure vos sons dans la wheel construite.
Vérifiez votre résultat : appelez bundled_files(sound_dir, "*.ogg") deux fois, puis examinez
bundled_files.cache_info(). Le deuxième appel fait augmenter hits. Piochez deux fois dans une
réserve qui contient au moins deux fichiers distincts : les choix diffèrent. Une réserve d’un seul fichier
ne peut renvoyer que ce fichier. La sélection seule ne fait pas jouer d’audio.
Pour les réponses, ajoutez plusieurs lignes au fichier .dialog traduit habituel et appelez :
# Inside an intent handler; translate quiz.correct.dialog in each supported language.self.speak_varied_dialog("quiz.correct", {"name": "Sam"})Kit mémorise les choix, tandis qu’OVOS gère le rendu, les remplacements de ressources et
la parole. Si vous rechargez un remplacement avec des lignes différentes, son sac de sélection est
remplacé. Le speak_dialog ordinaire garde son comportement normal. Pour plusieurs joueurs,
conservez le dernier son de chaque joueur dans l’état de session et passez-le sous la forme avoid=last_sound.
Kit évite ce choix quand un autre est disponible.
Donner un budget de temps au travail en ligne
Section intitulée « Donner un budget de temps au travail en ligne »Un délai d’expiration est la durée pendant laquelle une opération peut attendre. Un budget de réponse est le temps partagé dont disposent toutes les opérations de cette réponse. Sans budget, trois appels d’une seconde pourraient faire attendre quelqu’un environ trois secondes.
from thalovant_skillkit.network import JsonServiceClient, RequestBudget
# Create these once in your skill instance.budget = RequestBudget()service = JsonServiceClient("garden-watering", "1.0.0", budget=budget)
# Use your configured service URL. An empty URL means no service is configured.service_url = ""with budget.limit(2.0): result = service.post(service_url, {"question": "does the garden need water?"}, timeout=1.0)
if result is None: answer = "I cannot check the garden right now."else: # Validate the service's fields before using them in your real skill. answer = str(result.get("answer", "I could not find an answer."))Vérifiez votre résultat : avec l’URL vide, result vaut None immédiatement et le
texte de repli est sélectionné. Avec un service configuré, un objet JSON valide devient
un dictionnaire. Une requête échouée, une réponse invalide ou un budget épuisé renvoie
None; votre compétence décide comment répondre.
Le client envoie des en-têtes d’identification et d’identifiant de requête, plafonne les réponses à 2 Mio par défaut et tente chaque requête une seule fois. Il ne réessaie pas les requêtes POST qui ont peut-être déjà effectué une action. Après un échec, il ignore cette URL pendant 30 secondes par défaut, pour que les tours suivants n’attendent pas à répétition le même service en panne. Les autres URL restent disponibles. Le délai de récupération compte au plus 128 entrées. Les réponses réussies ne sont ni mises en cache ni partagées entre haut-parleurs.
Les échéances sont distinctes pour les fils d’exécution concurrents, et un budget imbriqué ne peut pas prolonger son parent. Elles sont vérifiées entre les lectures; elles ne peuvent pas interrompre de force un socket bloqué ni une résolution DNS. Utilisez toujours aussi un délai d’expiration de transport fini. Une lecture bloquée peut se terminer jusqu’à un délai d’expiration de socket après l’épuisement du budget.
Pour une compétence qui se charge hors ligne mais qui a besoin d’Internet pour certaines fonctionnalités, utilisez ces
attributs de classe au lieu de recopier une propriété RuntimeRequirements :
REQUIRES_INTERNET = TrueINTERNET_BEFORE_LOAD = FalseNO_INTERNET_FALLBACK = TrueVotre compétence doit réellement implémenter sa réponse hors ligne. Ces indicateurs décrivent ce comportement à OVOS; ils ne créent pas la réponse.
Actualiser loin de la réponse
Section intitulée « Actualiser loin de la réponse »News actualise les adresses audio en arrière-plan. Une personne qui écoute peut alors utiliser une adresse récente sans attendre que chaque flux soit vérifié de nouveau. Le même modèle convient à un petit catalogue public qui tolère d’être un peu vieux.
from thalovant_skillkit.workers import PeriodicWorker
# In your skill's initialize(), after any required super().initialize():self.refresh = PeriodicWorker(self.refresh_catalog, interval=600, name="garden-catalog-refresh")self.refresh.start()
# In shutdown(), before super().shutdown():self.refresh.stop(timeout=1.0)Implémentez refresh_catalog(self, stop_event). Bornez chaque opération réseau, vérifiez
stop_event.is_set() entre les éléments et ne remplacez un instantané complet qu’après
l’avoir validé. Gardez les verrous brefs; ne tenez pas un verrou de cache pendant un téléchargement.
Vérifiez votre résultat : une première actualisation s’exécute immédiatement. La suivante commence 600
secondes après la fin de cette passe. Appeler start() de nouveau ne crée pas de deuxième
tâche de fond. Les exceptions terminent la passe en cours et sont journalisées; la passe suivante attend l’intervalle
normal. stop() interrompt l’attente inactive et indique si le fil d’exécution s’est terminé.
Il ne peut pas tuer un rappel qui ignore l’annulation ou qui bloque indéfiniment.
Utilisez le planificateur d’OVOS pour la minuterie ou l’alarme d’une personne. Cette tâche de fond sert au travail d’actualisation facultatif, pas aux échéances exactes. La compétence décide combien de temps les données restent utilisables et quoi dire si elles sont trop vieilles. Ne mettez pas de détails privés dans les messages d’exception.
Sauvegarder l’état sans recopier le code du serveur de stockage
Section intitulée « Sauvegarder l’état sans recopier le code du serveur de stockage »Utilisez JsonStateStore quand votre compétence a besoin d’un stockage partagé facultatif. Sans
serveur de stockage configuré, un nouveau magasin renvoie None depuis load(), pour que votre compétence puisse
utiliser sa solution de repli locale.
from thalovant_skillkit.storage import JsonStateStore
store = JsonStateStore("household", {}, env_prefix="GARDEN", default_key="watering")records = store.load()Installez redis pour Redis/Sentinel et psycopg pour PostgreSQL si vous les utilisez.
Les réglages ont priorité sur l’environnement préfixé de la compétence, qui a lui-même priorité sur les variables
d’environnement génériques du serveur de stockage. Vérifiez le résultat de chaque sauvegarde : configurer un stockage partagé
ne garantit pas à lui seul qu’un enregistrement a été sauvegardé. Votre compétence conserve la récupération locale, valide les enregistrements et
vérifie à qui ils appartiennent. N’utilisez pas cet outil pour contourner l’API de stockage propre à un service.
Consultez le contrat de stockage pour les clés de configuration, les réglages, les valeurs de retour et le comportement en cas d’échec de connexion.
Garder une partie par joueur
Section intitulée « Garder une partie par joueur »Deux personnes peuvent jouer au quiz en même temps, et un pointage partagé mélangerait leurs parties. SessionStateStore garde une entrée par identifiant de session : la compétence lit l’identifiant dans le message et stocke la partie de ce joueur sous cet identifiant.
from thalovant_skillkit.sessions import SessionStateStore
# Once per skill instance: at most 128 players, forgotten after 90 quiet seconds.self.games = SessionStateStore(max_entries=128, default_ttl=90)
# In a turn:with self.games.lock: game = self.games.get(session_id) ... self.games[session_id] = updated_gameÉcrire une entrée relance son compte à rebours; la lire ne le relance pas. Quand le magasin est plein, l’entrée la plus ancienne s’en va. L’expiration est vérifiée quand le magasin est utilisé : rien ne s’exécute en arrière-plan. Le magasin appartient à une seule instance de la compétence; une deuxième instance ne voit pas ses parties. Tenez le verrou le temps de la décision, pas pendant un appel réseau : le quiz réserve le tour du joueur, libère le verrou pendant qu’il récupère les questions, puis vérifie qu’aucun Stop n’est arrivé entre-temps.
Vérifiez votre résultat : lancez deux parties avec des identifiants de session différents et répondez dans l’une; le pointage de l’autre ne change pas. Avancez une horloge au-delà du TTL : la partie disparaît. Lisez le game.py du tutoriel pour la version complète.
Savoir quand actualiser un cache
Section intitulée « Savoir quand actualiser un cache »| Données en cache | Quand elles changent | Comment les actualiser |
|---|---|---|
| Noms des fichiers sonores fournis | Vous installez ou modifiez les ressources du paquet | Un redémarrage du processus, ou bundled_files.cache_clear() entre deux tours |
| Texte de langue et alias combinés | Vous installez ou modifiez des traductions | Une nouvelle instance de ressources, ou self.locale_resources.clear_cache() entre deux tours; une partie déjà en cours garde les questions avec lesquelles elle a commencé |
| Historique de sélection des dialogues et des sons | Les choix ou leur clé explicite changent | Kit remplace une réserve modifiée; l’éviction n’oublie que l’historique |
| Délai de récupération après un échec de service | Son intervalle expire | Kit autorise automatiquement une nouvelle requête |
| Réponses de service ou état personnel | Dépend de l’application | Aucun cache générique des réponses n’est ajouté; définissez au besoin une politique de fraîcheur circonscrite |
combined_lines(lang, folder, filename) rassemble les alias ou motifs acceptés dans
l’ordre de préférence des langues, y compris la langue par défaut configurée. Son cache de résultats
combinés contient au plus 512 entrées. Les dialogues parlés devraient utiliser la recherche de dialogue habituelle
pour répondre dans une seule langue. Vider le cache ne coordonne pas les modifications de fichiers
simultanées : rechargez entre deux tours. Utilisez des lectures de répertoire ordinaires pour les fichiers
que les personnes téléversent en direct, pas le cache des fichiers fournis.
matches_literal_intent garde aussi en cache les phrases normalisées de chaque
ressource et ensemble de langues compatibles. Réutilisez self.locale_resources
entre les tours et passez un nom de ressource fixe, comme pocket_quiz.start.
Votre compétence n’a pas besoin d’un deuxième cache. Après une modification des
ressources, clear_cache() vide aussi ces correspondances et la recherche des
fichiers jumeaux vocaux.
Mesurer la partie que vous avez modifiée
Section intitulée « Mesurer la partie que vous avez modifiée »Mesurez la même action avant et après votre modification, avec les mêmes versions de Python et d’OVOS. Réchauffez d’abord le cache, répétez l’action et comparez plusieurs exécutions sans que d’autres tests se disputent le processeur. Mesurez aussi un processus neuf : une requête rapide à chaud peut quand même avoir un démarrage lent.
Séparez le temps de sélection local de la latence du service, de la synthèse vocale et du haut-parleur. Mettre une liste de sons en cache accélère la sélection; cela ne fait pas démarrer plus tôt un appareil audio lent. Testez à la fois les accès au cache et son invalidation, et utilisez un banc d’essai sans réseau pour le travail local. La référence SkillKit et les vérifications audio couvrent les contrats des outils et les vérifications supplémentaires pour de vrais tours parlés.