Aller au contenu
Console
Try one

    Écrire une compétence

    Démarrez une compétence Thalovant avec SkillKit, puis trouvez de l'aide sur les langues, les conversations, les tests, l'empaquetage et l'installation.

    Une compétence est un petit programme qui confie une tâche à votre assistant. Elle peut raconter une blague, répondre à une question ou animer un quiz.

    Vous écrivez votre première compétence? Suivez Créer un quiz éducatif. Vous y construirez une compétence fonctionnelle en trois courtes leçons, chacune avec un résultat à vérifier. Ce que SkillKit vous offre liste tous les outils sur une seule page.

    Cette page est votre manuel. Démarrez un autre projet ci-dessous, ou servez-vous de la table des matières pour trouver de l’aide sur une fonctionnalité précise.

    thalovant-skillkit fournit la recherche de ressources, des outils pour les messages, des vérifications, ainsi que des outils facultatifs pour l’état des conversations et les tests d’intégration. OVOS reste responsable de l’acheminement des intentions, de la parole, de l’activation des sessions et des événements planifiés.

    Consultez la référence de l’API SkillKit pour les signatures complètes des outils et leurs valeurs de retour.

    Utilisez Python 3.11 ou une version plus récente, et un terminal. Vous pouvez construire et tester en local sans hub; utilisez un hub pour la vérification orale finale.

    Créez un environnement virtuel dans votre répertoire de développement. Ces commandes shell visent Linux et macOS :

    Fenêtre de terminal
    python3 -m venv .venv
    source .venv/bin/activate
    python -m pip install --pre "thalovant-skillkit==0.24.2"

    SkillKit prend en charge Python 3.10 et les versions plus récentes; le projet qu’il génère pour vous déclare Python 3.11 comme minimum. Sous Windows, activez l’environnement avec .venv\Scripts\Activate.ps1 dans PowerShell.

    Ces exemples utilisent SkillKit 0.24.2. L’option --pre autorise les préversions OVOS utilisées par ses outils de test.

    Tester une ancienne version d’OVOS

    Si vous testez délibérément Workshop 8, installez ovos-workshop==8.0.0 et setuptools<81 ensemble dans un environnement distinct : son ancien gestionnaire de plugins importe pkg_resources, que les versions récentes de setuptools ont retiré. La version actuelle d’OVOS n’a pas besoin de cette contrainte héritée.

    Une seule commande écrit une compétence complète qui réussit déjà ses propres vérifications et tests :

    Fenêtre de terminal
    thalovant-skillkit new garden-watering
    cd thalovant-skill-garden-watering
    python -m pip install --pre -e ".[test]"
    thalovant-skillkit check
    python -m pytest

    Voici les fichiers avec lesquels vous travaillerez :

    thalovant-skill-garden-watering/
    ├── thalovant_skill_garden_watering/
    │ ├── __init__.py the skill
    │ ├── version.py
    │ └── locale/
    │ ├── supported.json which languages you ship
    │ ├── en-US/
    │ │ ├── vocab/GardenWateringKeyword.voc
    │ │ ├── dialog/garden.watering.dialog
    │ │ └── skill.json
    │ └── fr-FR/ starter resources to translate
    ├── test/
    │ ├── test_skill.py hears its keyword, ignores the rest
    │ └── test_contract.py locales complete, packaging sound
    └── pyproject.toml entry point and package data, declared

    Le résultat est une compétence de repli accompagné de quatre tests de comportement et d’un test de contrat sur les sources. Exécutez les commandes suivantes depuis ce répertoire de projet généré. La classe de base trouve locale/ à côté du module de votre compétence.

    Vérifiez votre résultat : pytest signale cinq tests réussis. Vous avez créé un programme et vérifié ses premières réponses. Il n’est pas encore relié à un haut-parleur.

    Les chemins qui commencent par locale/ ci-dessous sont relatifs à thalovant_skill_garden_watering/, le répertoire du paquet importable.

    1. Indiquez à quoi votre compétence répond.

      Un fichier .voc est une liste de mots ou d’expressions, un par ligne. locale/en-US/vocab/GardenWateringKeyword.voc :

      garden watering
      water the garden
      sprinklers
    2. Indiquez comment il répond.

      Un fichier .dialog est une liste de réponses possibles, une par ligne. La sélection aléatoire peut répéter une ligne. locale/en-US/dialog/garden.watering.dialog :

      This garden skill is a demo; it has no watering schedule yet.
      I can hear garden questions, but I have no watering data yet.
    3. Écrivez la réponse.

      thalovant_skill_garden_watering/__init__.py :

      from thalovant_skillkit.skill import ThalovantFallbackSkill
      class GardenWateringSkill(ThalovantFallbackSkill):
      FALLBACK_PRIORITY = 98
      QUESTIONS_ONLY = True
      def can_answer(self, message) -> bool:
      text, lang = self.utterance(message), self.lang_of(message)
      return self.claims(text, lang) and self.mentions(text, "GardenWateringKeyword", lang)
      def reply(self, utterance, lang, context):
      return self.dialog("garden.watering", lang)

    Cet exemple répond à des questions sur un sujet et prononce un texte de démonstration; il ne commande pas d’arroseurs et n’interroge aucun service. Remplacez la démonstration par votre propre logique de réponse quand vous serez prêt.

    La classe de base de repli prononce un résultat reply non vide et renvoie True; un résultat vide renvoie False pour que le traitement de repli puisse continuer. preview_reply appelle la même logique de réponse sans émettre de parole. Des appels distincts peuvent choisir des lignes de dialogue différentes. Gardez reply exempt d’effets de bord sur le bus pour qu’il fonctionne dans les deux contextes.

    QUESTIONS_ONLY = True et claims() empêchent les conversations ordinaires de la pièce de déclencher cette compétence de questions. Pour une commande comme « water the garden », utilisez plutôt une intention définie.

    can_answer est consulté pendant la sélection du repli : gardez-le peu coûteux et étroit. Si vous redéfinissez initialize, appelez super().initialize() pour conserver l’enregistrement du repli.

    Votre interactionClasse de base et implémentation
    Une commande définie, comme « check the garden »ThalovantSkill, un @intent_handler d’OVOS et une ressource .intent. Le gestionnaire prononce explicitement la réponse ou exécute l’action demandée.
    Un sujet que les pipelines précédents n’ont pas traitéThalovantFallbackSkill avec can_answer et reply, comme ci-dessus. Redéfinissez handle_fallback seulement quand renvoyer un texte parlé ne suffit pas.
    Une relance qui dépend d’un tour précédentThalovantConversationalSkill avec un gestionnaire initial, can_converse et converse. L’activation et l’acheminement restent du ressort d’OVOS.
    Recherche et lecture de médias par OCPThalovantCommonPlaySkill quand la classe de base facultative de compétence common-play d’OVOS est installée. Sans cette dépendance, elle n’est pas disponible (None).

    Les gestionnaires d’intentions n’appellent pas reply automatiquement. Par exemple, une compétence à intentions appelle explicitement self.speak_dialog("garden.watering") dans son gestionnaire décoré. Une compétence de repli peut aussi enregistrer des intentions pour ses commandes précises.

    Une compétence qui répond sur un sujet et pose aussi des questions de relance peut utiliser ThalovantConversationalFallbackSkill. Pour des médias avec des tours de relance, utilisez ThalovantConversationalCommonPlaySkill. SkillKit compose pour vous les classes natives d’OVOS, y compris leur enregistrement conversationnel.

    Consultez Garder votre compétence réactive quand vous ajoutez un service, une bibliothèque de sons, une actualisation en arrière-plan ou un état sauvegardé facultatif. Ce guide indique quel outil choisir, comment vérifier le résultat et quand les caches s’actualisent. News utilise les outils partagés de réseau et de tâches de fond; Fart utilise l’inventaire de sons partagé et les outils de dialogues variés; Alarm, Timer, Reminder et Stopwatch partagent la persistance. Votre compétence garde la partie intéressante : ce qu’elle fait et ce qu’elle dit.

    Dans la plage de repli basse, les numéros plus petits sont essayés en premier. Utilisez cette convention pour les compétences Thalovant :

    PrioritéSignification
    91–95Je réponds à une chose précise
    96–98Je réponds sur un sujet
    99–100Je répondrai à presque tout

    Un opérateur peut remplacer fallback_priority par un entier de 91 à 100. Les réglages invalides ou hors plage utilisent la valeur par défaut de la classe; ils ne déclenchent pas d’erreur de configuration. La vérification des sources signale une valeur par défaut de classe hors plage.

    self.utterance(msg)ce qui a été dit
    self.lang_of(msg)la langue de cet énoncé
    self.location_of(msg)le dictionnaire de localisation du message, ou None
    self.mentions(text, "Voc", lang)si le texte mentionne ce vocabulaire
    self.mentioned_term(text, "Voc", lang)le terme correspondant, ou une chaîne vide
    self.dialog("name", lang)une ligne de locale/<lang>/dialog/name.dialog
    self.speak_to(msg, text)le dire à la pièce qui a demandé, dans sa langue
    self.setting("key", default)un réglage de la compétence
    self.locale_resourcestoute l’arborescence locale/, si vous en avez besoin directement

    Tout ce que propose la classe de compétence d’OVOS fonctionne toujours : self.speak, self.speak_dialog, self.voc_match, self.settings et les décorateurs d’intentions. Utilisez self.speak_to quand la réponse doit atteindre le message que vous avez reçu : converse(), un crochet d’arrêt, ou un hub qui répond à deux pièces dans deux langues. self.speak trouve un message en parcourant la pile d’appels et parle dans la langue propre à la compétence. Préférez self.speak_to(message, text) pour répondre aux commandes reçues : une demande multilingue garde ainsi sa propre langue.

    Un fichier .intent décrit une commande, une phrase par ligne. Commencez par des phrases complètes, comme démarre pocket quiz. Ajoutez des gabarits seulement s’ils rendent le fichier plus facile à lire :

    [démarre|lance] pocket quiz
    (joue à|démarre) pocket quiz

    Les parenthèses imposent un choix. Les crochets rendent toute la partie facultative : la première ligne accepte donc aussi pocket quiz. Placez les choix entre crochets ou parenthèses; démarre|lance pocket quiz est invalide. <QuizName> insère les entrées de vocab/QuizName.voc, tandis que {topic} est un emplacement dont le moteur d’intentions fournit la valeur à votre méthode. Gardez les mêmes noms d’emplacements dans chaque traduction. thalovant-skillkit check développe les gabarits selon les règles d’OVOS et signale les lignes mal formées.

    locale_resources.matches_literal_intent(text, name, lang) est plus précis : il compare des lignes littérales complètes, sans tenir compte de la casse, des accents ni de la ponctuation. Il ne développe pas les gabarits et n’extrait pas les emplacements. Utilisez-le pour des commandes fixes dans une conversation; laissez le moteur d’intentions traiter les gabarits. Le quiz garde des phrases littérales pour que le replay et la commande vocale reconnaissent les mêmes mots.

    Un fichier .voc contient des mots ou des motifs OVOS, pas des enregistrements arbitraires. Si votre code lit des lignes comme blue|bleu marine|bleu ciel, placez-les dans locale/fr-FR/tables/colors.table et chargez-les avec self.locale_resources.combined_lines(lang, "tables", "colors.table"). Ainsi, le lecteur de vocabulaire OVOS ne tente pas d’interpréter vos tables.

    La reconnaissance vocale peut proposer plusieurs possibilités. utterance(message) retourne la première; utterances(message) les retourne toutes, sans doublons :

    from thalovant_skillkit.message import utterances
    for candidate in utterances(message):
    if self.locale_resources.matches_literal_intent(candidate, "pocket_quiz.start", lang):
    return self._say(message, "start")

    Ici, lang est la langue du message reçu et _say est la méthode de réponse du quiz. Acceptez la première commande valide, puis arrêtez la recherche. Une autre transcription ne doit jamais produire une deuxième réponse ni modifier le score une deuxième fois.

    Gardez les mots dans un fichier .dialog ordinaire. À côté, ajoutez un fichier .ssml facultatif avec le même nom, les mêmes mots, les mêmes espaces réservés et le même nombre de lignes non vides. SSML permet de donner de petites instructions à la voix, comme « fais une pause ici ».

    locale/fr-FR/dialog/ready.dialog :

    Prêt, {name}? On joue.

    locale/fr-FR/dialog/ready.ssml :

    Prêt, {name}? <break time="250ms"/> On joue.

    Faites passer les deux formes par SkillKit :

    text = self.dialog("ready", self.lang_of(message), {"name": "Sam"})
    self.speak_to(message, text)

    Résultat attendu : le texte affiché reste Prêt, Sam? On joue. Une voix qui prend en charge SSML peut faire une pause d’un quart de seconde après la question. Les autres voix reçoivent toujours les mots ordinaires. La réponse garde la langue et la session de la personne qui a parlé.

    L’extension est .ssml, pas .ssml.dialog. Traduisez chaque fichier jumeau avec son dialogue; SkillKit n’emprunte pas le jumeau d’une autre langue régionale. Gardez les lignes littérales, sauf les espaces réservés : des (choix|variés) ou des [mots facultatifs] font utiliser à speak_dialog le lecteur OVOS ordinaire, sans jumeau. self.dialog ne développe pas ces motifs. Lancez thalovant-skillkit check pour repérer les mots ou espaces réservés qui ne correspondent pas.

    Pour composer une phrase en Python, utilisez les outils plutôt que d’assembler du XML :

    from thalovant_skillkit import ssml
    text = ssml.say("Votre code est", ssml.digits("204"), ssml.pause("250ms"), "Prêt?")
    self.speak_to(message, text)

    Les autres outils incluent spell, telephone, foreign, emphasis et sub. Ils retournent un objet Speech, une chaîne avec du texte ordinaire et du balisage facultatif. ssml.say et + conservent les deux formes; les chaînes formatées et le formatage ordinaire perdent le balisage. Passez les textes d’une personne ou d’un service comme valeurs, jamais comme gabarit SSML : SkillKit protège ces valeurs. SkillResources.dialog lit seulement le texte ordinaire; utilisez self.dialog sur une compétence pour obtenir aussi son jumeau vocal.

    Traduisez les ressources françaises du squelette, y compris son mot-clé initial. Pour une langue supplémentaire, copiez un dossier de langue complet, traduisez ses ressources et ajoutez l’étiquette de langue à locale/supported.json :

    locale/
    ├── en-US/vocab/GardenWateringKeyword.voc
    └── fr-FR/vocab/GardenWateringKeyword.voc

    Pour les tests en français ci-dessous, ajoutez arroses le jardin dans fr-FR/vocab/GardenWateringKeyword.voc. Mettez Cette compétence de jardin est une démo sans données d’arrosage. dans fr-FR/dialog/garden.watering.dialog. Le mot-clé anglais que le squelette place dans le dossier français est un espace réservé à remplacer.

    Passez self.lang_of(message) explicitement aux outils de ressources, comme le fait l’exemple. Sans cet argument, ils utilisent la langue de la compétence, qui peut différer de celle de la personne qui parle.

    Vous n’avez pas besoin de copier les fichiers anglais dans en-CA, en-GB et en-AU. SkillKit peut utiliser la traduction en-US existante pour ces locuteurs. Il en va de même pour les autres langues : fr-BE peut utiliser fr-FR, et pt-AO peut utiliser pt-PT.

    Voyez un dossier régional comme un petit ensemble de remplacements. SkillKit cherche d’abord une correspondance régionale exacte, puis une étiquette parente, puis la traduction de référence de la langue et les traductions régionales compatibles. Ce n’est qu’ensuite qu’il essaie la langue par défaut configurée, normalement en-US. Un dialogue existant fournit ses propres lignes; un fichier absent ou vide poursuit le long de cette chaîne. La correspondance du vocabulaire inclut les termes régionaux compatibles avant d’essayer la langue par défaut.

    Avec les fichiers français de l’étape précédente, ajoutez ce test à test/test_skill.py, qui importe déjà GardenWateringSkill :

    from thalovant_skillkit.testing import message
    def test_canadian_french_uses_french_resources():
    skill = GardenWateringSkill()
    request = message("quand est-ce que tu arroses le jardin", lang="fr-CA")
    lang = skill.lang_of(request)
    assert lang == "fr-CA"
    assert skill.locale_resources.lang(lang) == "fr-FR"
    assert skill.dialog("garden.watering", lang) == (
    "Cette compétence de jardin est une démo sans données d’arrosage."
    )

    Résultat attendu : la demande en français canadien lit la réponse française, tandis que sa langue reste fr-CA. Continuez de passer cette langue d’origine à la parole et aux services; le nom du dossier de ressources ne choisit pas l’accent, les unités, la localisation ni le fuseau horaire de la personne.

    Si le français canadien a besoin d’une formulation différente, ajoutez seulement locale/fr-CA/dialog/garden.watering.dialog. Ce fichier l’emporte désormais pour fr-CA; les autres ressources françaises manquantes viennent toujours de fr-FR. Listez les traductions complètes dans locale/supported.json, qui pilote les vérifications d’exhaustivité. Les remplacements régionaux partiels peuvent rester hors de la liste; la recherche à l’exécution utilise les répertoires fournis.

    Le repli réutilise une traduction; il n’en crée pas. Un système d’écriture différent exige des ressources adaptées : le chinois simplifié seul n’assure pas la prise en charge du chinois traditionnel. La disponibilité de la reconnaissance et des voix dépend aussi des plugins ASR et TTS du hub. Testez les sessions régionales dans OVOS en plus de ces tests de ressources.

    Utilisez SkillKit 0.15.0 ou une version ultérieure quand une traduction régionale doit aussi fonctionner avec les intentions natives d’OVOS ou avec du code qui lit les fichiers directement. Gardez les mots communs dans le dossier de la langue d’origine et n’écrivez que les différences dans locale/regional.json, à l’intérieur du paquet de votre compétence :

    {
    "version": 1,
    "locales": {
    "fr-CA": {
    "source": "fr-FR",
    "overrides": {
    "dialog/garden.watering.dialog": "Cette démo peut parler de la fin de semaine.\n"
    }
    },
    "en-CA": {"source": "en-US", "overrides": {}}
    }
    }

    Chaque clé de remplacement est un chemin de fichier relatif à son dossier de langue. Sa valeur est le texte complet du fichier, y compris les \n entre les lignes. Laissez intacts les espaces réservés comme {score}, les clés JSON, les clés des tables personnalisées placées avant | et les noms de groupes d’expressions régulières. Cette commande copie le texte commun; elle ne le traduit pas pour vous.

    Depuis le dépôt de la compétence, exécutez :

    Fenêtre de terminal
    thalovant-skillkit locales --write
    thalovant-skillkit check --no-fleet
    python -m pytest -q

    Résultat attendu : fr-CA contient la réponse canadienne ainsi que toutes les autres ressources françaises. en-CA contient les ressources anglaises communes. Les deux étiquettes figurent dans supported.json, et le vérificateur indique que la compétence respecte ses contrats. Le test de repli précédent sélectionne maintenant fr-CA et attend la nouvelle réponse canadienne; mettez à jour ces deux assertions après avoir généré cet exemple.

    Validez (commit) le manifeste, les dossiers générés et le supported.json mis à jour. Les modifications futures se font dans la langue source ou dans le manifeste; exécutez ensuite locales --write de nouveau. Ne modifiez pas les fichiers générés directement : un check ordinaire signale les fichiers périmés ou modifiés à la main. thalovant-skillkit locales effectue la même vérification de fraîcheur sans rien écrire. Si un fichier source est supprimé, l’outil nomme les fichiers générés obsolètes pour que vous les supprimiez explicitement; il ne les supprime jamais en silence.

    Les sources doivent être des langues de base existantes dans la même langue. L’enchaînement d’une région générée à une autre n’est pas pris en charge. Un remplacement régional doit nommer un fichier source existant et en préserver les espaces réservés. Pour ajouter une nouvelle ressource, placez-la d’abord dans la langue de base. La validation complète relève toujours de check et de vos tests. La génération a lieu pendant le développement, sans appel réseau ni travail supplémentaire quand quelqu’un parle. Les fichiers générés sont des données de paquet ordinaires et exigent les mêmes vérifications de wheel que les autres traductions.

    Commencez par les variantes régionales courantes des langues que votre compétence prend déjà en charge :

    Traduction communeCibles régionales courantes
    en-USen-CA, en-GB, en-AU, en-NZ
    fr-FRfr-CA, fr-BE, fr-CH
    es-ESes-MX, es-AR, es-CO, es-US
    de-DEde-AT, de-CH
    pt-PTpt-AO, pt-MZ
    nl-NL, sv-SEnl-BE, sv-FI, respectivement
    zh-CNzh-SG; zh-TW exige une formulation en chinois traditionnel

    Conservez les traductions régionales existantes là où elles conviennent déjà. Une formulation identique peut être héritée en toute honnêteté; des dossiers distincts ne signifient pas des révisions distinctes par des locuteurs natifs. L’orthographe régionale de l’anglais, les expressions du français canadien, l’orthographe de l’allemand suisse et la grammaire de l’espagnol argentin exigent une révision en contexte. Pour le chinois, la configuration Taïwan d’OpenCC peut aider à préparer les caractères traditionnels et les termes taïwanais courants. Vérifiez aussi les phrases parlées et les littéraux des expressions régulières; la conversion ne crée pas de traduction cantonaise. Demandez à des locuteurs qui maîtrisent la langue de réviser les parcours importants avant d’affirmer une qualité linguistique.

    Les règles linguistiques communes proviennent de thalovant-languages. Utilisez la version 0.3.1 ou ultérieure pour les recherches de règles en chinois traditionnel (zh-TW, zh-HK, zh-MO, zh-Hant) et la détection des questions dans un texte chinois sans espaces :

    from thalovant_languages import asks
    assert asks("什麼時候出發", "zh-TW") # “When do we leave?” — no question mark needed.

    SkillKit gère les dialogues traduits de votre compétence; thalovant-languages fournit des listes de mots et des règles de grammaire communes. Vous n’avez pas besoin de copier ces règles dans chaque dossier régional. La conversion d’écriture chinoise a lieu quand les responsables construisent les données : les personnes qui utilisent la compétence n’ont donc pas besoin d’OpenCC à l’exécution.

    Fenêtre de terminal
    python -m pip install --upgrade "thalovant-languages>=0.3.1"
    thalovant-languages check

    La vérification ne devrait signaler aucune erreur de données. Les versions arrivent d’abord sur PyPI; piwheels peut suivre plus tard.

    Une compétence de repli qui publie des exemples .intent concrets peut accepter ces phrases complètes avant de vérifier les mots-clés :

    def can_answer(self, message):
    text, lang = self.utterance(message), self.lang_of(message)
    return self.locale_resources.matches_literal_intent(
    text, "garden.watering", lang
    ) or (self.claims(text, lang) and self.mentions(text, "GardenWateringKeyword", lang))

    Utilisez le nom d’une ressource d’intention qui existe réellement dans votre compétence. Cet outil normalise la casse, les accents, la ponctuation et les espaces, lit les ressources régionales compatibles et ignore les lignes qui contiennent des variables (slots) ou des motifs de rechange. Il ne remplace pas le moteur d’intentions. Testez les phrases d’exemple de chaque langue déclarée et vérifiez que les commandes sans rapport sont toujours laissées aux autres compétences.

    Utilisez ThalovantConversationalSkill quand une réponse courte comme « again » dépend d’un tour précédent. Créez un SessionStateStore dans l’instance de la compétence pour conserver ce contexte :

    from thalovant_skillkit import session_of
    from thalovant_skillkit.sessions import SessionStateStore
    from thalovant_skillkit.skill import ThalovantConversationalSkill
    class GardenConversationSkill(ThalovantConversationalSkill):
    def __init__(self, *args, **kwargs):
    self.topics: SessionStateStore[str] = SessionStateStore(
    max_entries=128, default_ttl=90
    )
    super().__init__(*args, **kwargs)
    def remember_topic(self, message, topic):
    session_id = session_of(message).get("session_id")
    if isinstance(session_id, str) and session_id:
    self.topics.set(session_id, topic, ttl=90)
    def can_stop(self, message):
    session_id = session_of(message).get("session_id")
    return (
    isinstance(session_id, str)
    and bool(session_id)
    and session_id in self.topics
    )
    def stop_session(self, session):
    self.topics.remove(session.session_id)
    # OVOS falls through to global stop() if this returns False.
    return True
    def stop(self):
    self.topics.clear()
    return True

    Il s’agit d’une ébauche de gestion d’état, pas d’un remplacement complet de la compétence de repli. Ajoutez le gestionnaire d’intention initial et les deux méthodes de conversation avant d’utiliser la classe de base conversationnelle :

    1. Le gestionnaire initial appelle remember_topic, prononce son invite et active la compétence par OVOS quand il a besoin de relances.
    2. can_converse vérifie qu’il existe un état actif appartenant à la session entrante et une réponse pertinente, localisée, portant sur l’énoncé entier.
    3. converse revérifie ces conditions avant de faire quoi que ce soit, car un arrêt ou une expiration a pu survenir depuis la sélection. Renvoyez False pour les tours sans rapport ou expirés. Ne renouvelez le TTL qu’après un tour accepté.

    La fenêtre d’activation d’OVOS et le TTL de ce magasin sont distincts. Un statut actif ne prouve pas qu’un sujet est toujours en cours, et stocker un sujet n’active pas la compétence.

    Le magasin suit ces règles :

    • Les lectures par get() ne renouvellent pas l’expiration. Appelez set() quand un tour accepté doit la renouveler.
    • default_ttl=None, ou set(..., ttl=None), conserve une entrée jusqu’à sa suppression ou son éviction faute de capacité. Un magasin plein évince l’écriture la plus ancienne.
    • L’expiration utilise une horloge monotone et survient quand on accède au magasin. Elle ne démarre aucune minuterie et ne conserve rien d’un redémarrage à l’autre.
    • remove(session_id) efface une conversation. clear() efface l’instance. remove(session_id, expected=state) ne supprime que ce même objet stocké, de sorte que le nettoyage ne peut pas supprimer un objet de remplacement.
    • Tenez store.lock autour des lectures et des mises à jour composées de valeurs modifiables. Gardez les E/S bloquantes et les attentes de lecture audio hors de ce verrou.

    Utilisez l’identifiant de session OVOS déjà résolu à partir du message entrant. L’absence d’identifiant devrait suivre une politique explicite de la compétence; l’exemple refuse de s’en souvenir. Conservez la session OVOS renvoyée lorsque vous envoyez le tour suivant, comme décrit dans Sessions et contexte.

    OVOS reste responsable de stop_session, stop, schedule_event et cancel_scheduled_event. L’arrêt de session de l’exemple réussit même si son sujet a déjà expiré, ce qui empêche OVOS de basculer vers un arrêt global qui effacerait les autres haut-parleurs. Son can_stop ne revendique qu’un sujet actif pour la session qui fait la demande.

    Effacer la mémoire n’arrête pas la lecture et n’annule pas une minuterie. Annulez séparément le travail planifié et la sortie de cette session, si votre compétence en possède. Utilisez le planificateur d’OVOS pour le travail qui doit s’exécuter à une échéance; l’expiration paresseuse du magasin n’exécute aucun rappel.

    Un choix aléatoire peut tomber deux fois sur le même son. ShuffleBag fonctionne comme un tirage de cartes dans un paquet mélangé : on utilise chaque choix, puis on mélange de nouveau.

    from random import Random
    from thalovant_skillkit.selection import ShuffleBag
    sounds = ShuffleBag(["duck.ogg", "bear.ogg", "robot.ogg"], rng=Random(7))
    first = sounds.draw()
    second = sounds.draw()
    assert first != second

    Vérifiez votre résultat : les deux premiers noms de fichiers diffèrent. Cet outil ne fait que choisir un nom de fichier; utilisez play_audio() d’OVOS pour faire jouer le fichier fourni. Créez le sac dans l’instance de votre compétence pour qu’il se souvienne des choix précédents d’une demande à l’autre. Une graine aléatoire fixe rend un test reproductible; omettez rng en utilisation normale.

    Si un autre haut-parleur joue entre deux tours, utilisez sounds.draw(avoid=last_heard) avec le dernier nom de fichier du haut-parleur qui fait la demande. Le sac évite ce son chaque fois qu’un autre choix existe. Un sac d’un seul son renvoie toujours ce son. Si le son évité est le seul qui reste dans un sac plus grand, le tirage commence un nouveau cycle. Conservez le dernier choix de chaque haut-parleur dans votre état de session.

    Mettez à jour les tests de comportement du squelette en même temps que votre nouveau vocabulaire et vos nouvelles réponses; conservez son test de contrat sur les sources, qui est distinct :

    from thalovant_skillkit.testing import message
    from thalovant_skill_garden_watering import GardenWateringSkill
    def test_it_hears_its_keyword():
    assert GardenWateringSkill().can_answer(message("when should I water the garden"))
    def test_it_hears_it_in_french():
    assert GardenWateringSkill().can_answer(message("quand est-ce que tu arroses le jardin", lang="fr-FR"))
    def test_it_ignores_what_is_not_its_business():
    skill = GardenWateringSkill()
    assert not skill.can_answer(message("set a timer for ten minutes"))
    assert not skill.can_answer(message("the sprinklers are noisy today"))
    def test_it_answers_in_french():
    assert GardenWateringSkill().preview_reply("quand est-ce que tu arroses le jardin", "fr-FR") == (
    "Cette compétence de jardin est une démo sans données d’arrosage."
    )

    message() construit un message de test avec le texte de l’énoncé, la langue et un contexte facultatif. Pour les réponses qui dépendent de la localisation, importez MONTREAL depuis le même module et passez location=MONTREAL. Testez aussi une localisation absente : location_of renvoie None et votre compétence décide comment répondre.

    Un rappel lu à voix haute devrait dire « tomorrow at nine forty a.m. »; le même rappel dans une application devrait afficher « May 24, 9:40 AM ». Les outils moments de SkillKit fournissent les deux, et speak_with_written envoie les deux : un haut-parleur fait entendre l’une et un écran affiche l’autre :

    from thalovant_skillkit.moments import (
    date_text, duration_text, speak_with_written, time_text, uses_24_hour_clock,
    )
    def handle_reminder(self, message, due, now):
    lang = self.lang_of(message)
    clock = uses_24_hour_clock(lang)
    spoken = f"Reminder set for {date_text(due, lang, now)} at {time_text(due, lang, clock)}."
    written = (f"Reminder set for {date_text(due, lang, now, written=True)} "
    f"at {time_text(due, lang, clock, written=True)}.")
    speak_with_written(self, spoken, message, lang, written=written)

    date_text conserve « today » et « tomorrow » quand la date est aussi proche. duration_text(seconds, lang) dit « five minutes » et écrit « 5:00 ». La forme écrite voyage à côté de la forme parlée, et un appareil sans écran l’ignore. Pour une réponse construite à partir de plusieurs morceaux, WrittenForms.say(spoken, written) enregistre chaque paire au fil de la composition, et WrittenForms.render(reply) produit la phrase écrite en une seule passe.

    Learning Lounge, Safety Guide et Source Scout répondent à partir du service de connaissances Thalovant. Ils passent tous par un seul client partagé, pas par trois copies :

    from thalovant_skillkit.knowledge import POLICY_BLOCKED, POLICY_UNAVAILABLE, knowledge_reply
    from thalovant_skillkit.service import request_headers
    reply = knowledge_reply(
    service_url=self.setting("knowledge_url", ""),
    prompt=question, lang=lang, mode="answer", max_sources=3,
    augmentation="auto", age_policy_setting=self.setting("age_policy"),
    headers=request_headers("my-skill", "1.0.0", "my-skill/1.0.0"), timeout=4.0,
    )
    if reply.policy in (POLICY_BLOCKED, POLICY_UNAVAILABLE):
    # The hub's age policy spoke, or could not be evaluated: say so, do not
    # substitute a packaged answer.
    ...
    elif reply.answer:
    self.speak_to(message, reply.answer)

    reply.provenance indique si le texte cite une source ou s’il a été rédigé par un modèle de langage à partir des sources qu’il cite, afin qu’une compétence puisse le préciser à la personne qui écoute. Le client fait une seule requête et ne réessaie pas; validez les réglages que vous lui passez.

    Rendre les réponses du bus partagé à leur demandeur

    Section intitulée « Rendre les réponses du bus partagé à leur demandeur »

    Si deux pièces envoient des requêtes au même service, attendre seulement son sujet de réponse peut donner la première réponse aux deux pièces. Utilisez l’outil d’attente corrélée de SkillKit avec un nouvel identifiant que le fournisseur renvoie en écho :

    from uuid import uuid4
    from ovos_bus_client.message import Message
    from thalovant_skillkit.bus import wait_for_response
    def ask_provider(bus, context):
    request_id = uuid4().hex
    request = Message("example.query", {"id": request_id}, context)
    return wait_for_response(
    bus, request, "example.answer",
    matches=lambda reply: reply.data.get("id") == request_id,
    timeout=3.0,
    )

    Passez le contexte du message d’origine, y compris sa session. Vérifiez que le fournisseur renvoie réellement l’identifiant en écho; cet outil ne peut pas modifier le protocole d’un service distant. Validez les données renvoyées et prononcez une réponse d’indisponibilité en cas d’expiration du délai ou d’échec du transport. L’écouteur est retiré à chaque sortie. L’échéance finie inclut l’envoi, mais ne peut pas interrompre un transport qui bloque dans emit. Testez des requêtes simultanées, des réponses sans rapport ou tardives, et le nettoyage de l’écouteur après l’expiration du délai.

    Choisissez la limite de test qui correspond au comportement que vous devez démontrer :

    TestDémontreNe fournit pas
    Appels directs et testing.FakeBusLogique de réponse, sélection de la langue, émissions enregistréesRépartition, acheminement ni planificateur actif
    testing_ovos.skill_harnessInitialisation de la compétence, répartition par le bus en amont, événements planifiés natifsUn pipeline d’énoncés
    managed_minicroft et capture_turnL’acheminement réel dans le pipeline et les résultats sur le bus d’une compétence installéeReconnaissance par microphone, qualité TTS ni acoustique d’un vrai haut-parleur

    Pour les exemples d’intégration, installez la pile facultative dans votre environnement de développement :

    Fenêtre de terminal
    python -m pip install --pre "thalovant-skillkit[testing]==0.24.2"

    Cet extra inclut OvoScope et les dépendances de préversion OVOS prises en charge. Installez-le dans l’environnement de développement où vous exécutez les exemples d’intégration.

    skill_harness construit la compétence avec le FakeBus de répartition en amont, un fichier de réglages temporaire et le service de planification natif. Il fait le ménage en cas de réussite comme d’échec :

    from thalovant_skillkit.testing import message
    from thalovant_skillkit.testing_ovos import skill_harness
    from thalovant_skill_garden_watering import GardenWateringSkill
    def test_the_skill_emits_speech():
    with skill_harness(
    GardenWateringSkill,
    skill_id="thalovant-skill-garden-watering.thalovant",
    scheduler=False,
    ) as harness:
    spoken = []
    harness.bus.on("ovos.utterance.speak", spoken.append)
    source = message("when should I water the garden", lang="en-US")
    assert harness.skill.can_answer(source)
    assert harness.skill.handle_fallback(source)
    assert len(spoken) == 1
    assert spoken[0].data["utterance"] in {
    "This garden skill is a demo; it has no watering schedule yet.",
    "I can hear garden questions, but I have no watering data yet.",
    }

    Le planificateur est activé par défaut, avec son fil de minuterie arrêté pour que les tests puissent piloter sa méthode publique tick(). Utilisez scheduler_autostart=True pour tester une vraie livraison différée. L’outil ne fournit pas de pipeline d’intentions; utilisez MiniCroft pour l’acheminement des énoncés.

    Isolez les chemins XDG avant que pytest ne collecte les modules qui importent votre compétence. Par exemple, dans test/end2end/conftest.py :

    from contextlib import ExitStack
    from thalovant_skillkit.testing_ovos import isolated_xdg
    def pytest_configure(config):
    cleanup = ExitStack()
    config.add_cleanup(cleanup.close)
    cleanup.enter_context(isolated_xdg())

    Chaque processus de travail obtient ses propres répertoires temporaires, et l’environnement d’origine est restauré ensuite. Le module d’outils diffère ses importations d’OVOS jusqu’à ce qu’elles soient nécessaires. Le plugin pytest d’OVOScope a peut-être déjà chargé OVOS; l’isolation de configuration propre à MiniCroft reste activée séparément.

    Un test de bout en bout pour la compétence de repli peut utiliser le pipeline de repli en amont :

    from ovos_bus_client.message import Message
    from ovos_bus_client.session import Session
    from ovoscope import FALLBACK_PIPELINE
    from thalovant_skillkit.testing_ovos import (
    capture_turn,
    deferred_capture_gc,
    managed_minicroft,
    )
    def test_garden_question_routes_and_speaks():
    skill_id = "thalovant-skill-garden-watering.thalovant"
    with deferred_capture_gc(), managed_minicroft(
    [skill_id], default_pipeline=FALLBACK_PIPELINE,
    ) as croft:
    session = Session("garden-test")
    session.lang = "fr-FR"
    source = Message(
    "recognizer_loop:utterance",
    {"utterances": ["quand est-ce que tu arroses le jardin"], "lang": "fr-FR"},
    {"session": session.serialize()},
    )
    turn = capture_turn(croft, source, timeout=30)
    assert any(
    message.data.get("skill_id") == skill_id
    for message in turn.of_type("ovos.intent.matched")
    )
    assert turn.spoken == [
    "Cette compétence de jardin est une démo sans données d’arrosage."
    ]
    assert turn.session.session_id == session.session_id

    capture_turn échoue à l’expiration du délai et détache toujours ses écouteurs de capture. Son résultat contient messages, spoken, of_type(topic) et la dernière session du haut-parleur d’origine; reportez cette session dans un message de relance. La mise à jour en arrière-plan d’un autre haut-parleur ne peut pas la remplacer. Ajoutez des assertions pour la réponse, l’audio ou le résultat de service attendus : une capture terminée ne prouve pas à elle seule que la compétence a bien répondu.

    deferred_capture_gc circonscrit un contournement connu d’un interblocage de finaliseur entre OVOScope 1.8.5a1 et pyee 12.1.1. Il préserve le réglage précédent du ramasse-miettes et ne touche pas aux autres versions. Utilisez-le autour de la prise en charge de MiniCroft quand vous testez cette combinaison.

    Couvrez une demande sans rapport, une conversation arrêtée ou expirée, et deux haut-parleurs qui utilisent la compétence indépendamment. Exécutez explicitement le répertoire de bout en bout si votre configuration pytest habituelle ne sélectionne que les tests unitaires :

    Fenêtre de terminal
    python -m pytest test/end2end --timeout=300 --timeout-method=thread

    Une capture peut contenir des messages de plusieurs haut-parleurs. Utilisez for_session() avant de vérifier la parole, le son ou le Stop d’un haut-parleur. Cette méthode ne garde que les messages qui portent explicitement cet identifiant de session; les messages qui n’en ont pas sont écartés.

    Par exemple, un test de quiz peut vérifier les octets WAV réels mis en file pour un haut-parleur distant nommé. expected_file est le chemin du son de réussite fourni avec le quiz :

    def assert_quiz_sound(turn, session_id, expected_file):
    mine = turn.for_session(session_id)
    assert len(mine.audio) == 1
    sound = mine.audio[0]
    assert sound.extension == "wav"
    assert sound.binary == expected_file.read_bytes()

    Appelez ceci après avoir capturé le tour de bonne réponse du quiz. Vérifiez votre résultat : le test échoue si ce haut-parleur n’a pas reçu le son attendu, si le mauvais fichier a été mis en file ou si l’extension était incorrecte. Vérifiez aussi les autres sessions quand vous devez exclure des copies en trop. La lecture locale peut plutôt porter un sound.uri, avec sound.binary et sound.extension tous deux à None. L’outil n’ouvre jamais d’URI et ne fait jamais jouer d’audio. Il vérifie les requêtes mises en file, pas ce qui est sorti du haut-parleur.

    Pour un Stop capturé depuis la pièce A, vérifiez turn.for_session("room-a").audio_stops et assurez-vous que turn.for_session("room-b").audio_stops est vide. Envoyez ensuite un autre tour depuis la pièce B et vérifiez sa réponse. Cette dernière étape confirme que l’arrêt de A a préservé la partie de B, en plus de vérifier la destination du message Stop.

    spoken, audio et audio_stops privilégient les noms de sujets actuels d’OVOS plutôt que leurs miroirs hérités. Utilisez of_type(topic) quand vous devez inspecter un sujet précis. Consultez la référence des tests audio pour les règles complètes sur les données et le filtrage.

    Fenêtre de terminal
    thalovant-skillkit check

    La vérification signale les ressources de langue manquantes, le JSON ou les expressions régulières mal formés dans les ressources, les priorités de classe invalides, les problèmes de point d’entrée et les déclarations d’empaquetage manquantes.

    Les vérifications de langue comparent les langues prises en charge avec l’inventaire des fichiers en-US. Les vérifications des espaces réservés comparent les ensembles fichier par fichier. Les ressources françaises, y compris les régions générées à partir de fr-FR, conservent l’exemption existante de la comparaison avec l’anglais. Les remplacements régionaux doivent quand même préserver les espaces réservés de leur propre fichier source. Ces vérifications ne contrôlent pas chaque ligne traduite. Testez les réponses produites pour chaque langue que vous livrez, y compris le français. Un dialogue manquant renvoie le nom de sa ressource, et des valeurs de mise en forme manquantes peuvent laisser du texte de gabarit dans la réponse au lieu de lever une erreur.

    Pour les fichiers .intent, SkillKit vérifie aussi si une autre compétence pourrait répondre aux mêmes phrases. Lisez les résultats : un avertissement sur une vérification de phrases ignorée signifie que cette partie n’a pas été vérifiée. Essayez des exemples plus précis si vos phrases chevauchent celles d’une autre compétence.

    Construisez la wheel et l’archive source, puis comparez leur contenu à votre copie de travail. Remplacez les noms de fichiers d’exemple par ceux produits pour votre version :

    Fenêtre de terminal
    python -m pip install build
    python -m build
    thalovant-skillkit check-artifacts . \
    --wheel dist/thalovant_skill_garden_watering-0.1.0-py3-none-any.whl \
    --sdist dist/thalovant_skill_garden_watering-0.1.0.tar.gz

    Le vérificateur lit les archives sans les extraire ni importer la compétence. Il découvre le premier paquet thalovant_skill_* de premier niveau et vérifie ses fichiers octet par octet. Pour une structure src/, fournissez --package src/your_package; la découverte automatique ne descend pas dans src/.

    Les ressources manquantes ou modifiées, les membres d’archive dangereux ou en double et les métadonnées mal formées font échouer la vérification. Quand les deux archives sont fournies, leurs noms de paquet et leurs versions doivent concorder. Cette vérification ne compare pas leur version déclarée avec celle de votre copie de travail; vérifiez ces métadonnées de version séparément.

    Déclarez des règles d’inventaire supplémentaires quand votre compétence comporte des ressources d’exécution ou des générateurs hors ligne :

    OptionCe qu’elle vérifie
    --package src/your_packageSélectionne explicitement un répertoire de paquet importable; répétez-la pour plusieurs paquets.
    --runtime pathExige un fichier ou un répertoire source supplémentaire dans les deux archives.
    --source scriptsExige les sources des générateurs dans l’archive source.
    --wheel-exclude scriptsGarde les sources de ces générateurs hors de la wheel d’exécution.
    --wheel-count 'your_package/sounds/*.ogg=24'Exige un nombre exact de fichiers correspondants; --sdist-count vérifie l’archive source.

    Ces options décrivent les fichiers que votre projet livre réellement. Le décodage audio, l’intensité sonore et les vérifications d’écoute restent la responsabilité de la compétence audio.

    Le squelette a déjà déclaré le point d’entrée et les données du paquet dans pyproject.toml :

    [project.entry-points."opm.skill"]
    "thalovant-skill-garden-watering.thalovant" = "thalovant_skill_garden_watering:GardenWateringSkill"
    [tool.setuptools.package-data]
    thalovant_skill_garden_watering = ["locale/*.json", "locale/*/*", "locale/*/*/*"]

    Le point d’entrée permet à OVOS de trouver votre classe. Les données du paquet incluent les ressources dont elle a besoin après l’installation. La vérification des sources inspecte ces déclarations; seule la vérification du paquet construit établit que les fichiers sont bien arrivés.

    Depuis le répertoire du projet, installez la wheel dans un environnement distinct. Utilisez un chemin absolu vers la wheel et sortez de votre copie de travail avant l’importation, pour que Python ne puisse pas charger votre arborescence source par accident :

    Fenêtre de terminal
    skill_wheel="$(pwd)/dist/thalovant_skill_garden_watering-0.1.0-py3-none-any.whl"
    skill_smoke_dir="$(mktemp -d)"
    python -m venv "$skill_smoke_dir/venv"
    "$skill_smoke_dir/venv/bin/python" -m pip install --pre "$skill_wheel"
    cd "$skill_smoke_dir"
    "$skill_smoke_dir/venv/bin/python" - <<'PY'
    from importlib.metadata import entry_points
    from importlib.resources import files
    plugin, = entry_points(
    group="opm.skill", name="thalovant-skill-garden-watering.thalovant"
    )
    skill_class = plugin.load()
    assert skill_class.__name__ == "GardenWateringSkill"
    dialog = files("thalovant_skill_garden_watering").joinpath(
    "locale/fr-FR/dialog/garden.watering.dialog"
    )
    assert "démo" in dialog.read_text(encoding="utf-8")
    print("Installed plugin and French resources verified.")
    PY
    cd -

    Charger le point d’entrée prouve que la classe installée peut être importée. Cela ne démarre pas de hub; utilisez le test MiniCroft pour l’acheminement et un vrai haut-parleur pour la vérification d’écoute finale.

    Exécutez les vérifications des sources, les tests de comportement et les vérifications du paquet construit décrits ci-dessus après votre dernière modification de code ou de ressources. Testez les langues que vous prenez en charge, les relances, les signaux sonores et Stop.

    Pour entendre votre compétence sur un haut-parleur, suivez Ajouter votre propre compétence et Ajouter une compétence à un hub. Posez les mêmes questions que dans vos tests, puis essayez une demande sans rapport. Si votre compétence poursuit une conversation, vérifiez que chaque haut-parleur conserve son propre état.

    • can_answer renvoie vrai pour ce que votre compétence prend en charge et faux pour le reste;
    • vos tests réussissent, y compris un pour une langue que vous prenez en charge et un pour une question que vous ne traitez pas;
    • la wheel et l’archive source construites réussissent check-artifacts;
    • le hub répond quand vous posez la question à voix haute.
    Ce que vous voyezCe que c’est habituellement
    La réponse est le nom du fichier de dialogueLe fichier .dialog est absent, ou package_data a exclu locale/ de l’installation
    Répond en anglais à une question en françaisPassez explicitement la langue du message; vérifiez la langue sélectionnée et son fichier de dialogue traduit.
    Une autre compétence répond à la placeExaminez d’abord l’acheminement et can_answer. Dans la plage de repli, un numéro de priorité plus petit s’exécute plus tôt.
    Votre compétence répond à ce qui ne le concerne pascan_answer est trop large : vérifiez ce que mentions reconnaît
    Rien ne répondLe nom du point d’entrée ne correspond pas à la classe, ou le paquet n’est pas installé sur le hub
    Une réponse contient {name}Une valeur de gabarit est absente; testez les réponses produites avec les données que fournit chaque gestionnaire.
    Une conversation expirée répond encoreLes deux méthodes de conversation doivent vérifier l’état actuel de la session; l’activation par OVOS ne suffit pas.
    Arrêter un haut-parleur en efface un autreLimitez l’état et le travail planifié à la session; un résultat faux de stop_session bascule vers le stop global.
    MiniCroft ne trouve pas la compétenceInstallez le projet dans cet interpréteur de test et vérifiez le nom exact de son point d’entrée opm.skill.
    Une capture expireLisez les sujets des messages capturés et vérifiez le pipeline, l’état de préparation et les signaux de fin avant d’augmenter le délai.
    Les vérifications des sources réussissent, mais il manque des ressources sur le hubReconstruisez, exécutez check-artifacts et importez la wheel depuis l’extérieur de votre copie de travail.