Padatious Intents (Example-Based Intents)¶
In a nutshell
Example-based intents (also called Padatious or template intents) match a whole phrase
against a set of sample sentences you provide in .intent files. They are generally more
accurate than keyword intents, and entities inside {curly braces} are extracted for you
automatically. The trade-off is that you must cover the breadth of ways a user might
phrase a request. See Intent Design for how the two intent styles compare,
and Padatious Pipeline for how the plugin matches these intents
at runtime. New terms are explained in the Glossary.
Example-based parsers have several key benefits over other intent parsing technologies.
-
Intents are easy to create.
-
You can easily extract entities and use these in skills. For example, "Find the nearest gas station" becomes
{ "place":"gas station"}. -
Disambiguation between intents is easier.
-
It is harder to create a bad intent that throws the pipeline plugin off.
NOTE: Padatious does not handle numbers well. Internally it sees all digits as "#". If you need to match digits, use Adapt (keyword intents) instead.
Creating Intents¶
Most example-based pipeline plugins use a series of example sentences to train a machine learning model to identify an intent. Regex can also run behind the scenes, for example to extract entities.
The examples are stored in a skill's locale/<lang>/ directory, in files ending in .intent.
For example, if you were to create a tomato skill to respond to questions about a tomato,
you would create the file
locale/en-us/what.is.a.tomato.intent
This file would contain examples of questions asking what a tomato is.
These sample phrases do not require punctuation like a question mark. We can also leave out contractions such as "what's", since OVOS automatically expands this to "what is" before parsing the utterance.
As a rule of thumb, aim for several examples per intent covering the different ways a user might phrase the request. Too few examples gives the model little to generalize from.
The above example lets us map many phrases to a single intent. Often, though, we need to
extract specific data from an utterance. This might be a date, location, category, or some
other entity.
Defining entities¶
Let's now find out OVOS's opinion on different types of tomatoes. To do this we will create a
new intent file: locale/en-us/do.you.like.intent
with examples of questions about mycroft's opinion about tomatoes:
are you fond of tomatoes
do you like tomatoes
what are your thoughts on tomatoes
are you fond of {type} tomatoes
do you like {type} tomatoes
what are your thoughts on {type} tomatoes
Note the {type} in the above examples. These are wild cards where matching content is
forwarded to the skill's intent handler.
WARNING: digits are not allowed in the entity name inside the
{}. Do NOT use{room1}, use{room_one}.
Specific Entities¶
In the above example, {type} matches anything. This makes the intent flexible, but it will
also match if we say something like "Do you like eating tomatoes?" It would think the type of
tomato is "eating", which doesn't make much sense. Instead, we can specify what type of
things the {type} of tomato should be. We do this by defining the type entity file here:
locale/en-us/type.entity
which might contain something like:
Every .entity file shipped under the skill's locale resources auto-registers when the
skill's resources load for a language (since ovos-workshop 9.5.0a1; opt out with
"skills": {"auto_register_entity_files": false}). An explicit call is only needed for
entity files outside the locale resources, and still works:
from ovos_workshop.skills import OVOSSkill
from ovos_workshop.decorators import intent_handler
class TomatoSkill(OVOSSkill):
def initialize(self):
self.register_entity_file('type.entity')
Now, we can say things like "do you like greenish tomatoes?" and it will tag type as
"greenish". If we say "do you like eating tomatoes?" instead, "eating" is not in our
type.entity file, so the match scores lower, not zero. See the warning below for what
that means in practice.
An .entity file is a scoring hint, not a closed vocabulary
Per OVOS-INTENT-1 §5.4,
an .entity file is a set of training hints, not an allow-list. Padatious trains its
neural matcher on those example values and uses them to score a candidate slot fill:
a value in the file is treated as full confidence, and a value outside the file still
matches, only at a lower, floored confidence. An engine is required to keep out-of-list
candidates matchable; it must never treat the file as exhaustive. If what you actually
want is a small, fixed, closed set of surface words — a strict "must be exactly one of
these" — write them as inline alternation instead of a slot: (red | green | yellow)
rather than {type}. Reserve {slot} for values you intend to read out of
message.data in the handler; if the handler never consumes the captured value, it
should probably not be a slot at all.
An inline alternation group over 64 branches is dropped from padaos entirely for that
line: the engine logs a warning naming the intent and branch count, and that line falls
back to neural-only matching. Past 64 options, use a slot with an .entity file instead.
Out-of-list matches land in the medium stage
In ovos-padatious-pipeline-plugin (2.0.3a1+), an out-of-list value is floored at
ENTITY_HINT_FLOOR = 0.8 rather than collapsing the candidate. A listed value scores
exactly 1.0 through a deterministic exact-match fast path (2.0.5a1+), the same as if
no entity were attached at all. That floor lands in the plugin's medium-confidence band
(conf_med = 0.8, conf_high = 0.95 by default), below conf_high. The floor applies to
the per-slot entity contribution, not the final confidence, so the exact final score
varies with the rest of the match, but in practice an out-of-list value drags the match
below conf_high. The stock mycroft.conf pipeline includes
ovos-padatious-pipeline-plugin-medium (since ovos-config 2.3.9a1) precisely so these
matches still fire. On a deployment that trims the pipeline to the high stage only,
out-of-list slot values silently produce no match at all.
Number matching¶
The portable way to match a number is a plain {slot}, parsed in the handler with
ovos-number-parser's extract_number() —
it handles both digit and spelled-out forms, since which one you get depends on the ASR:
from ovos_number_parser import extract_number
def handle_count_intent(self, message):
number = message.data.get("number")
try:
n = int(number)
except ValueError:
n = extract_number(number, lang=self.lang)
extract_number("five", lang="en-us") returns 5, same as extract_number("3", lang="en-us")
returns 3 — both "count to five" and "count to 3" reach the handler as the matched
{number} slot text, and the fallback resolves either. No .entity file is needed for a
free-form numeric slot like this.
Engine-specific, deprecated: an inline
#digit token (matching only bare digit runs) and the:0unknown-token below are Padatious extensions, not part of the OVOS-INTENT-1 Sentence Template Grammar, so they're not portable to other pipeline plugins. Sinceovos-padatious-pipeline-plugin2.0.8a2, an unescaped inline#in a template line logs a one-time deprecation warning (#still matches digits exactly as before — nothing breaks this cycle). Prefer the{slot}pattern above for new skills. A non-normative spec note on this is pending OpenVoiceOS/architecture#166, not yet merged.
Typed slots¶
A slot can carry a type prefix, {type:name} instead of the plain
{name} from the examples above. The prefix asks the pipeline to compute
a normalized value for the slot ahead of matching, in addition to the
surface text {name} always returns:
Four types are recognized: number, duration, date, and color.
Each normalizes to a fixed shape: number and duration to a plain
number (duration in seconds), date to an RFC 3339 timestamp resolved
in the session's timezone, and color to {"hex": "#rrggbb", "name":
...}. A prefix outside this list, or on a loader that does not support
typed slots at all, degrades silently to the plain {name} form — a
typed placeholder never breaks a template that reads it as untyped, and
message.data["when"] always holds the surface text either way.
Read the normalized value from the handler with self.typed_slot:
def handle_reminder_intent(self, message):
when = self.typed_slot(message, "when") # a date, already resolved
task = message.data["task"] # the surface text, as always
if when is None:
# the utterance had no date the parser could resolve
...
typed_slot(message, name) looks up the slot's declared type from the
intent's own registration and returns the matching entry's normalized
value, or None if nothing covers it. typed_slots(message, slot_type)
returns every entry of one type the parser found in the utterance, not
just the ones a slot captured, useful for a handler that wants to scan
the whole utterance rather than a single named slot. Both read from
message.data["typed_slots"], the map the pipeline attaches ahead of
matching; a handler never has to reach into that map directly.
A typed slot does not make the slot required. An utterance that matches
the template without producing a {date:when} still fires the handler,
with typed_slot returning None for when. Forcing a match to fail
unless a given slot bound something is a separate, orchestrator-level
required_slots check tied to an intent's registration; there is no
skill-facing API yet to declare it directly, so a handler that truly
cannot proceed without a value still validates for None itself, as in
the example above.
Entities with unknown tokens¶
Let's say you want to create an intent to match places:
Directions to {place}.
Navigate me to {place}.
Open maps to {place}.
Show me how to get to {place}.
How do I get to {place}?
This alone will work, but it will still get a high confidence with a phrase like "How do I get
to the boss in my game?" We can try creating a .entity file with things like:
The problem is that now anything that is not specifically a mix of New York City, San
Francisco, or something on Georgia Street won't match. Instead, we can specify an unknown word
with :0. This would be written as:
Now, while this will still match quite a lot, it will match things like "Directions to Baldwin City" more than "How do I get to the boss in my game?"
NOTE: Currently, the number of :0 words is not fully taken into account, so the above might
match quite liberally. This will change in the future.
Parentheses Expansion¶
Sometimes you might find yourself writing many variations of the same thing. For example, to write a skill that orders food, you might write the following intent:
Rather than writing out all combinations of possibilities, you can embed them into one or more
lines by writing each possible option inside parentheses with | between each part. For
example, that same intent above could be written as:
or even on a single-line:
The empty-branch trick
(from {place} | )makes a segment optional. The portable OVOS-INTENT-1 equivalent is the square-bracket optional[from {place}].[x]is defined as exactly equivalent to(x|). Prefer the bracket form when you want spec-conformant templates.
Nested parentheses are supported to create even more complex combinations, such as the following:
Which would expand to:
There is no performance benefit to using parentheses expansion. When used appropriately, this syntax can be much clearer to read. However, break more complex structures down into multiple lines to aid readability and reduce false utterances in the model. Overuse can even cause the model training to time out, making the skill unusable.
Using it in a Skill¶
The intent_handler() decorator can create an examples-based intent handler by passing in the
filename of the .intent file as a string.
You may also see the @intent_file_handler decorator used in skills. This is deprecated. You
can now replace any instance of this with the simpler @intent_handler decorator.
From our first example above, we created a file locale/en-us/what.is.a.tomato.intent. To
register an intent using this file we can use the following decorator, shown on its own.
Place it above a handler method inside your skill class:
This decorator must be imported before it is used:
Learn more about decorators in Python.
Now we can create our Tomato Skill:
from ovos_workshop.skills import OVOSSkill
from ovos_workshop.decorators import intent_handler
class TomatoSkill(OVOSSkill):
def initialize(self):
self.register_entity_file('type.entity')
@intent_handler('what.is.a.tomato.intent')
def handle_what_is(self, message):
self.speak_dialog('tomato.description')
@intent_handler('do.you.like.intent')
def handle_do_you_like(self, message):
tomato_type = message.data.get('type')
if tomato_type is not None:
self.speak_dialog('like.tomato.type',
{'type': tomato_type})
else:
self.speak_dialog('like.tomato.generic')
See Your First Skill for a complete, minimal example of a template-intent skill from scratch.
Common Problems¶
See I am unable to match against the utterance string in the Adapt intents page. The same lowercase-normalization note applies to template-based intent handlers.
My intent is defined correctly but never matches¶
If the .intent/.entity files look right and the utterance still doesn't match anything, check
whether the intent has actually been disabled rather than badly written. A skill can turn
individual intents off with disable_intent(), and whitelist/blacklist controls can also gate
off a skill or its converse participation entirely. See Intent Layers for
per-skill intent enable/disable state and Permissions & Activation Control for
the coarser skill-level gates.
Read next: Context · Intent Layers · Asking the User Related: Intent Design · Adapt Intents (Keyword Intents) · Padatious Pipeline · Test Your Skill