Aller au contenu
Console
Try one

    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.

    Ce que vous ajoutezUtilisezVotre compétence décide toujours
    De nombreux sons empaquetésbundled_files et ShuffleBagPoolQuel son convient au jeu et quel haut-parleur l’entend
    Plusieurs réponses traduitesself.speak_varied_dialogLes mots eux-mêmes et le moment d’inviter une réponse
    Un service en ligneJsonServiceClient et RequestBudgetLa requête, la validation et la réponse d’indisponibilité
    Un catalogue qui s’actualise de temps en tempsPeriodicWorkerLa fraîcheur, l’annulation et le remplacement sûr du cache
    Un état Redis/PostgreSQL facultatifJsonStateStoreLa validation des enregistrements, leur propriété et la récupération locale
    Une partie ou un brouillon par joueurSessionStateStoreCe qu’un tour modifie, et quand une partie est terminée
    Des alias ou des listes de blocage régionauxSkillResources.combined_linesLes 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.

    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 Path
    from thalovant_skillkit.assets import bundled_files
    from 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.

    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 = True
    INTERNET_BEFORE_LOAD = False
    NO_INTERNET_FALLBACK = True

    Votre 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.

    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.

    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.

    Données en cacheQuand elles changentComment les actualiser
    Noms des fichiers sonores fournisVous installez ou modifiez les ressources du paquetUn redémarrage du processus, ou bundled_files.cache_clear() entre deux tours
    Texte de langue et alias combinésVous installez ou modifiez des traductionsUne 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 sonsLes choix ou leur clé explicite changentKit remplace une réserve modifiée; l’éviction n’oublie que l’historique
    Délai de récupération après un échec de serviceSon intervalle expireKit autorise automatiquement une nouvelle requête
    Réponses de service ou état personnelDépend de l’applicationAucun 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.

    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.