Skip to content

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.

what would you say a tomato is
what is a tomato
describe a tomato
what defines a tomato

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:

red
reddish
green
greenish
yellow
yellowish
ripe
unripe
pale

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:

Count to {number}.
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 :0 unknown-token below are Padatious extensions, not part of the OVOS-INTENT-1 Sentence Template Grammar, so they're not portable to other pipeline plugins. Since ovos-padatious-pipeline-plugin 2.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:

Remind me to {task} at {date:when}.
Set the lights to {color:shade}.

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:

New York City

#### Georgia Street
San Francisco

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:

:0 :0 City

#### :0 Street
:0 :0

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:

Order some {food}.
Order some {food} from {place}.
Grab some {food}.
Grab some {food} from {place}.

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:

(Order | Grab) some {food}
(Order | Grab) some {food} from {place}

or even on a single-line:

(Order | Grab) some {food} (from {place} | )

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:

(Look (at | for) | Find) {object}.

Which would expand to:

Look at {object}
Look for {object}
Find {object}

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:

@intent_handler('what.is.a.tomato.intent')

This decorator must be imported before it is used:

from ovos_workshop.decorators import intent_handler

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