Intent skill
Answers phrasings you list in an .intent file. Use ThalovantSkill and
OVOS’s @intent_handler. Best when you know what people will say.
A skill is the part of your assistant that answers one kind of question. This page takes you from an empty folder to a working skill, and explains the few decisions you have to make along the way.
thalovant-skillkit carries the plumbing every skill needs, so what you write is the answer and nothing else.
Confirm:
thalovant-skill-<something>.pip install thalovant-skillkitOne command writes a complete skill that already passes its own checks and tests:
thalovant-skillkit new garden-wateringcd thalovant-skill-garden-wateringpip install -e ".[test]"thalovant-skillkit check && pytestYou get this:
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/ the same files, in French├── 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└── .github/workflows/test.yml runs check and pytest on every pushlocale/ is found for you. You never write a path to it.
Say what your skill answers.
A .voc file is a list of words or phrases, one per line. locale/en-US/vocab/GardenWateringKeyword.voc:
garden wateringwater the gardensprinklersSay how it replies.
A .dialog file is a list of possible replies, one per line. One is chosen at random, so your assistant does not repeat itself. locale/en-US/dialog/garden.watering.dialog:
The garden was watered this morning.The sprinklers ran at six.Write the answer.
thalovant_skill_garden_watering/__init__.py:
from thalovant_skillkit.skill import ThalovantFallbackSkill
class GardenWateringSkill(ThalovantFallbackSkill): FALLBACK_PRIORITY = 98
def can_answer(self, message) -> bool: return self.mentions(self.utterance(message), "GardenWateringKeyword")
def reply(self, utterance, lang, context): return self.dialog("garden.watering", lang)reply is the one method most skills need. What it returns is spoken on the hub and shown in the showroom preview, so the two can never disagree. can_answer runs for everything anyone says, so keep it cheap and narrow.
Intent skill
Answers phrasings you list in an .intent file. Use ThalovantSkill and
OVOS’s @intent_handler. Best when you know what people will say.
Fallback skill
Answers what no intent claimed. Use ThalovantFallbackSkill with
can_answer and handle_fallback. Best for open questions.
A skill can be both: give it intents and a fallback for what they miss.
This is the one number worth thinking about. The hub asks fallback skills in order and stops at the first one that says yes, so a low number means “ask me before the others”.
| Priority | Meaning |
|---|---|
| 91–95 | I answer one specific thing |
| 96–98 | I answer a topic |
| 99–100 | I will answer almost anything |
An operator can move your skill with a fallback_priority setting, but only within 91–100 — a number outside that band is refused rather than quietly clamped.
self.utterance(msg) |
what was said |
self.lang_of(msg) |
the language of this utterance |
self.location_of(msg) |
where the house is |
self.mentions(text, "Voc") |
does the text mention this vocabulary |
self.mentioned_term(text, "Voc") |
which term it mentioned |
self.dialog("name", lang) |
a line from locale/<lang>/dialog/name.dialog |
self.setting("key", default) |
one skill setting |
self.locale_resources |
the whole locale/ tree, if you need it directly |
Everything on the OVOS skill class still works: self.speak, self.speak_dialog, self.voc_match, self.settings, and the intent decorators.
Copy the folder and translate the files:
locale/├── en-US/vocab/NewsKeyword.voc└── fr-FR/vocab/NewsKeyword.vocNothing in your code changes. self.lang_of(message) gives you the language of each utterance, and self.dialog and self.mentions use it. A language with no folder falls back to en-US, so a half-finished translation answers in English rather than going silent.
The scaffold wrote four tests. Keep them and add yours:
from thalovant_skillkit.testing import MONTREAL, message
from thalovant_skill_garden_watering import GardenWateringSkill
def test_it_hears_its_keyword(): assert GardenWateringSkill().can_answer(message("water the garden"))
def test_it_hears_it_in_french(): assert GardenWateringSkill().can_answer(message("arrose le jardin", lang="fr-FR"))
def test_it_ignores_what_is_not_its_business(): assert not GardenWateringSkill().can_answer(message("set a timer for ten minutes"))
def test_it_has_something_to_say(): assert GardenWateringSkill().preview_reply("water the garden", "en-US")message() builds what a satellite really sends, including the language. Pass location=MONTREAL when your skill reports a time, a date or the weather — without a location the assistant answers from its default, which is Kansas.
thalovant-skillkit checkThis is what your CI runs. It confirms every locale in supported.json carries every file en-US has with the same placeholders, that JSON parses and regexes compile, that your entry point names a class that exists, that locale/ will be in the installed package, and that your priority is in band. Each problem is one line saying which file and what is wrong.
The scaffold already declared the entry point and the package data in 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/*/*/*"]The first line is how a hub finds your skill. The second is how your locale/ tree reaches the installed package — leave it out and every reply becomes a dialog file’s name. thalovant-skillkit check fails on both mistakes.
can_answer returns true for what your skill handles and false for the rest;| What you see | What it usually is |
|---|---|
| The reply is the dialog file’s name | The .dialog file is missing, or package_data left locale/ out of the install |
| Answers in English when asked in French | No fr-FR folder, so it fell back — add the translation |
| Another skill answers instead | Something broader sits lower in the ladder; raise your priority or narrow its claim |
| Your skill answers things it should not | can_answer is too broad — check what mentions is matching |
| Nothing answers at all | The entry point name does not match the class, or the package is not installed on the hub |