É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.
Python et un terminal
Section intitulée « Python et un terminal »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 :
python3 -m venv .venvsource .venv/bin/activatepython -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.
Partir d’une compétence fonctionnelle
Section intitulée « Partir d’une compétence fonctionnelle »Une seule commande écrit une compétence complète qui réussit déjà ses propres vérifications et tests :
thalovant-skillkit new garden-wateringcd thalovant-skill-garden-wateringpython -m pip install --pre -e ".[test]"thalovant-skillkit checkpython -m pytestVoici 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, declaredLe 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.
Personnalisez-le
Section intitulée « Personnalisez-le »Les chemins qui commencent par locale/ ci-dessous sont relatifs à thalovant_skill_garden_watering/, le répertoire du paquet importable.
-
Indiquez à quoi votre compétence répond.
Un fichier
.vocest une liste de mots ou d’expressions, un par ligne.locale/en-US/vocab/GardenWateringKeyword.voc:garden wateringwater the gardensprinklers -
Indiquez comment il répond.
Un fichier
.dialogest 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. -
Écrivez la réponse.
thalovant_skill_garden_watering/__init__.py:from thalovant_skillkit.skill import ThalovantFallbackSkillclass GardenWateringSkill(ThalovantFallbackSkill):FALLBACK_PRIORITY = 98QUESTIONS_ONLY = Truedef 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.
Choisir la bonne classe de base
Section intitulée « Choisir la bonne classe de base »| Votre interaction | Classe 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édent | ThalovantConversationalSkill 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 OCP | ThalovantCommonPlaySkill 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.
Laisser SkillKit gérer le travail répétitif
Section intitulée « Laisser SkillKit gérer le travail répétitif »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.
Choisir une priorité de repli
Section intitulée « Choisir une priorité de repli »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–95 | Je réponds à une chose précise |
| 96–98 | Je réponds sur un sujet |
| 99–100 | Je 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.
Ce que vous pouvez appeler
Section intitulée « Ce que vous pouvez appeler »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_resources | toute 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.
Écrire des intentions lisibles par OVOS
Section intitulée « Écrire des intentions lisibles par OVOS »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 quizLes 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.
Lire les autres transcriptions
Section intitulée « Lire les autres transcriptions »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.
Ajouter des pauses et guider la prononciation
Section intitulée « Ajouter des pauses et guider la prononciation »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.
Ajouter une autre langue
Section intitulée « Ajouter une autre langue »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.vocPour 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.
Prendre en charge les variantes régionales
Section intitulée « Prendre en charge les variantes régionales »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.
Générer des ressources régionales complètes
Section intitulée « Générer des ressources régionales complètes »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 :
thalovant-skillkit locales --writethalovant-skillkit check --no-fleetpython -m pytest -qRé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 commune | Cibles régionales courantes |
|---|---|
en-US | en-CA, en-GB, en-AU, en-NZ |
fr-FR | fr-CA, fr-BE, fr-CH |
es-ES | es-MX, es-AR, es-CO, es-US |
de-DE | de-AT, de-CH |
pt-PT | pt-AO, pt-MZ |
nl-NL, sv-SE | nl-BE, sv-FI, respectivement |
zh-CN | zh-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.
python -m pip install --upgrade "thalovant-languages>=0.3.1"thalovant-languages checkLa 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.
Rattacher les relances à la bonne conversation
Section intitulée « Rattacher les relances à la bonne conversation »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_offrom thalovant_skillkit.sessions import SessionStateStorefrom 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 TrueIl 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 :
- Le gestionnaire initial appelle
remember_topic, prononce son invite et active la compétence par OVOS quand il a besoin de relances. can_conversevérifie qu’il existe un état actif appartenant à la session entrante et une réponse pertinente, localisée, portant sur l’énoncé entier.converserevérifie ces conditions avant de faire quoi que ce soit, car un arrêt ou une expiration a pu survenir depuis la sélection. RenvoyezFalsepour 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. Appelezset()quand un tour accepté doit la renouveler. default_ttl=None, ouset(..., 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.lockautour 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.
Varier les questions et les sons
Section intitulée « Varier les questions et les sons »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 Randomfrom thalovant_skillkit.selection import ShuffleBag
sounds = ShuffleBag(["duck.ogg", "bear.ogg", "robot.ogg"], rng=Random(7))first = sounds.draw()second = sounds.draw()assert first != secondVé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.
Le tester
Section intitulée « Le tester »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.
Dire une date ou une heure des deux façons
Section intitulée « Dire une date ou une heure des deux façons »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.
Interroger le service de connaissances
Section intitulée « Interroger le service de connaissances »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_replyfrom 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 uuid4from ovos_bus_client.message import Messagefrom 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.
Solliciter le vrai bus OVOS
Section intitulée « Solliciter le vrai bus OVOS »Choisissez la limite de test qui correspond au comportement que vous devez démontrer :
| Test | Démontre | Ne fournit pas |
|---|---|---|
Appels directs et testing.FakeBus | Logique de réponse, sélection de la langue, émissions enregistrées | Répartition, acheminement ni planificateur actif |
testing_ovos.skill_harness | Initialisation de la compétence, répartition par le bus en amont, événements planifiés natifs | Un pipeline d’énoncés |
managed_minicroft et capture_turn | L’acheminement réel dans le pipeline et les résultats sur le bus d’une compétence installée | Reconnaissance 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 :
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 messagefrom 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.
Tester un énoncé avec MiniCroft
Section intitulée « Tester un énoncé avec MiniCroft »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 Messagefrom ovos_bus_client.session import Sessionfrom ovoscope import FALLBACK_PIPELINEfrom 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_idcapture_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 :
python -m pytest test/end2end --timeout=300 --timeout-method=threadVérifier le son et l’acheminement de Stop
Section intitulée « Vérifier le son et l’acheminement de Stop »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.
Le vérifier
Section intitulée « Le vérifier »thalovant-skillkit checkLa 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.
Vérifier ce que vous avez construit
Section intitulée « Vérifier ce que vous avez construit »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 :
python -m pip install buildpython -m buildthalovant-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.gzLe 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 :
| Option | Ce qu’elle vérifie |
|---|---|
--package src/your_package | Sélectionne explicitement un répertoire de paquet importable; répétez-la pour plusieurs paquets. |
--runtime path | Exige un fichier ou un répertoire source supplémentaire dans les deux archives. |
--source scripts | Exige les sources des générateurs dans l’archive source. |
--wheel-exclude scripts | Garde 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.
Vérifier le plugin installé
Section intitulée « Vérifier le plugin installé »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 :
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_pointsfrom 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.")PYcd -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.
Préparer une version
Section intitulée « Préparer une version »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.
Terminé quand
Section intitulée « Terminé quand »can_answerrenvoie 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 le symptôme signifie habituellement
Section intitulée « Ce que le symptôme signifie habituellement »| Ce que vous voyez | Ce que c’est habituellement |
|---|---|
| La réponse est le nom du fichier de dialogue | Le fichier .dialog est absent, ou package_data a exclu locale/ de l’installation |
| Répond en anglais à une question en français | Passez 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 place | Examinez 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 pas | can_answer est trop large : vérifiez ce que mentions reconnaît |
| Rien ne répond | Le 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 encore | Les 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 autre | Limitez 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étence | Installez le projet dans cet interpréteur de test et vérifiez le nom exact de son point d’entrée opm.skill. |
| Une capture expire | Lisez 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 hub | Reconstruisez, exécutez check-artifacts et importez la wheel depuis l’extérieur de votre copie de travail. |