Skip to content

Latest commit

 

History

History
357 lines (264 loc) · 11.2 KB

File metadata and controls

357 lines (264 loc) · 11.2 KB

Skill Interaction Methods

OVOSSkill provides two high-level methods for collecting structured user input: ask_yesno for binary yes/no questions and ask_selection for multiple-choice prompts. Both are backed by pluggable agent engines that can be swapped per-skill or system-wide.


ask_yesno

OVOSSkill.ask_yesno(prompt: str, data: Optional[dict] = None) -> Optional[str]

Speaks prompt, waits for the user's response, and classifies it as "yes", "no", or the raw response string if neither matched.

Source: OVOSSkill.ask_yesnoovos_workshop/skills/ovos.py

Parameters

Parameter Type Description
prompt str Dialog ID (looked up in locale/) or a literal string to speak.
data dict | None Template variables for Mustache rendering of the dialog string.

Return values

Value Meaning
"yes" User confirmed (e.g. "yeah", "sure", "of course")
"no" User declined (e.g. "nope", "nah", "definitely not")
str User spoke something that could not be classified — raw transcript returned
None No response received (timeout or user said nothing)

Basic usage

class MySkill(OVOSSkill):
    def handle_delete_intent(self, message):
        if self.ask_yesno("confirm_delete") == "yes":
            self._do_delete()
            self.speak_dialog("deleted")
        else:
            self.speak_dialog("cancelled")

locale/en-us/confirm_delete.dialog:

Are you sure you want to delete this?
Do you really want to delete it?

With template data

answer = self.ask_yesno("confirm_action", data={"action": "restart the server"})

locale/en-us/confirm_action.dialog:

Are you sure you want to {{action}}?

Handling all return values

answer = self.ask_yesno("do_you_want_music")
if answer == "yes":
    self.play_music()
elif answer == "no":
    self.speak_dialog("okay_nevermind")
elif answer is None:
    self.speak_dialog("no_response")
else:
    # answer is the raw transcript — user said something unexpected
    self.speak_dialog("did_not_understand")

How it works internally

  1. get_response(dialog=prompt, data=data) — speaks the prompt, records user reply.
  2. _get_yesno_engine() — loads the configured YesNoEngine plugin (if any).
  3. If a plugin is loaded: engine.yes_or_no(question=prompt, response=resp, lang=self.lang)True, False, or None.
  4. If no plugin: YesNoSolver().match_yes_or_no(resp, lang=self.lang) (built-in fallback, always available).
  5. True"yes", False"no", None/unmatched → raw response.

ask_selection

OVOSSkill.ask_selection(
    options: List[str],
    dialog: str = '',
    data: Optional[dict] = None,
    min_conf: float = 0.65,
    numeric: bool = False,
    num_retries: int = -1,
) -> Optional[str]

Speaks the options list to the user, optionally follows with a dialog prompt, then resolves the user's spoken response to one of the options.

Source: OVOSSkill.ask_selectionovos_workshop/skills/ovos.py

Parameters

Parameter Type Default Description
options List[str] The predefined options to offer.
dialog str '' Dialog ID or literal string spoken after the options list.
data dict | None None Template variables for the dialog string.
min_conf float 0.65 Minimum fuzzy-match confidence for the default plugin. Passed to the OptionMatcherEngine config if no plugin-level config overrides it.
numeric bool False If True, speaks each option prefixed with its number ("one, pizza; two, pasta; …"). If False, speaks them as a joined list ("pizza, pasta, or salad?").
num_retries int -1 How many times to re-prompt on no response. -1 means use the system default.

Return values

Value Meaning
str One of the strings from options, exactly as provided.
None No match, no response, or plugin failure.

Special cases handled before user interaction:

  • Empty optionsNone immediately.
  • Single-element options → returns that element immediately (no prompt).

Basic usage

class MySkill(OVOSSkill):
    def handle_transport_intent(self, message):
        modes = ["bus", "train", "bicycle"]
        choice = self.ask_selection(modes, dialog="which_transport")
        if choice:
            self.speak_dialog("you_chose", {"mode": choice})

locale/en-us/which_transport.dialog:

Which would you prefer?
How would you like to travel?

The skill speaks: "bus, train, or bicycle? Which would you prefer?"

The user can say:

  • "train" — fuzzy-matched directly
  • "the second one" / "number two" / "two" — position matched
  • "the last one" — last-word matched
  • "option 3" — numeric matched (requires ovos-number-parser)

Numeric menu

choice = self.ask_selection(options, numeric=True)

Speaks each option as: "one, bus; two, train; three, bicycle". Useful when options are long or ambiguous. The user can then say "two" or "the second one".

Handling None

choice = self.ask_selection(options, dialog="which_one", num_retries=1)
if choice is None:
    self.speak_dialog("could_not_understand")
    return

How it works internally

  1. Validates options (raises ValueError if not a list; returns immediately for 0 or 1 items).
  2. Speaks options (as list or numbered menu based on numeric).
  3. get_response(dialog=dialog, data=data, num_retries=num_retries) — speaks optional follow-up dialog, records reply.
  4. _get_selection_engine() — loads the configured OptionMatcherEngine plugin.
  5. engine.match_option(utterance=resp, options=options, lang=self.lang) — resolves response to a slot.
  6. If the engine raises or returns None, ask_selection returns None.

Plugin system

Both methods are backed by pluggable agent engines discovered and loaded via ovos-plugin-manager.

ask_yesno — YesNoEngine

Plugin type: YesNoEngine (opm.agents.yesno) Config key: ask_yesno_plugin Built-in fallback: ovos-solver-yes-no-plugin (always available, no config needed)

YesNoEngine plugins implement:

def yes_or_no(self, question: str, response: str, lang: Optional[str] = None) -> Optional[bool]:
    ...  # True = yes, False = no, None = unclear

The question argument gives the plugin context about what was asked, enabling smarter inference (e.g. an LLM-backed plugin could use it to resolve ambiguous answers).

Available plugins:

Plugin Description
ovos-solver-yes-no-plugin Rule-based multilingual yes/no classifier (default fallback)

ask_selection — OptionMatcherEngine

Plugin type: OptionMatcherEngine (opm.agents.option_matcher) Config key: ask_selection_plugin Default plugin: ovos-option-matcher-fuzzy-plugin (installed as a dependency of ovos-workshop)

OptionMatcherEngine plugins implement:

def match_option(self, utterance: str, options: List[str], lang: Optional[str] = None) -> Optional[str]:
    ...  # returns one of options, or None

Available plugins:

Plugin Description
ovos-option-matcher-fuzzy-plugin Fuzzy + ordinal/cardinal vocab + numeric fallback. Default.

Configuration

Global defaults — mycroft.conf

{
  "skills": {
    "ask_yesno_plugin": "ovos-solver-yes-no-plugin",
    "ask_selection_plugin": "ovos-option-matcher-fuzzy-plugin"
  }
}

ask_yesno_plugin defaults to None (built-in YesNoSolver used). ask_selection_plugin defaults to ovos-option-matcher-fuzzy-plugin.

Per-skill override — settings.json

Place in the skill's settings.json to override for that skill only:

{
  "ask_yesno_plugin": "my-llm-yesno-plugin",
  "ask_selection_plugin": "my-embedding-option-matcher"
}

Plugin config

Pass configuration to the plugin via the same settings block:

{
  "ask_selection_plugin": "ovos-option-matcher-fuzzy-plugin",
  "ask_selection_plugin_config": {
    "min_conf": 0.80
  }
}

Priority order

settings.json  >  mycroft.conf skills block  >  built-in default

Plugins are loaded lazily on first use and cached per plugin name for the lifetime of the skill instance.


Writing a custom YesNoEngine plugin

# my_yesno/__init__.py
from typing import Optional
from ovos_plugin_manager.templates.agents import YesNoEngine

class MyYesNoPlugin(YesNoEngine):
    def yes_or_no(self, question: str, response: str,
                  lang: Optional[str] = None) -> Optional[bool]:
        r = response.lower()
        if "yes" in r or "sure" in r:
            return True
        if "no" in r or "never" in r:
            return False
        return None  # unclear
# pyproject.toml
[project.entry-points."opm.agents.yesno"]
my-yesno-plugin = "my_yesno:MyYesNoPlugin"

Activate for a skill:

{ "ask_yesno_plugin": "my-yesno-plugin" }

Writing a custom OptionMatcherEngine plugin

# my_matcher/__init__.py
from typing import List, Optional
from ovos_plugin_manager.templates.agents import OptionMatcherEngine

class MyOptionMatcher(OptionMatcherEngine):
    def match_option(self, utterance: str, options: List[str],
                     lang: Optional[str] = None) -> Optional[str]:
        # example: embedding similarity via a local model
        scores = self._embed_and_score(utterance, options)
        best_idx = max(range(len(scores)), key=lambda i: scores[i])
        if scores[best_idx] >= self.config.get("min_conf", 0.6):
            return options[best_idx]
        return None
# pyproject.toml
[project.entry-points."opm.agents.option_matcher"]
my-option-matcher-plugin = "my_matcher:MyOptionMatcher"

Activate system-wide:

{ "skills": { "ask_selection_plugin": "my-option-matcher-plugin" } }

Failure behaviour

Scenario ask_yesno result ask_selection result
User says nothing (timeout) None None
User response unclassifiable raw transcript string None
Plugin fails to load falls back to YesNoSolver None
Plugin raises at runtime falls back to YesNoSolver None
No plugin configured YesNoSolver used default fuzzy plugin used

ask_selection is intentionally strict: any failure returns None rather than guessing. Always handle the None case in your skill.


See also