Skip to content

feat: yesno/selection agent plugins#390

Merged
JarbasAl merged 22 commits into
devfrom
yesno_select
Apr 9, 2026
Merged

feat: yesno/selection agent plugins#390
JarbasAl merged 22 commits into
devfrom
yesno_select

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Mar 18, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Configurable plugin system for skill interactions, enabling per-skill or global customization of yes/no resolution and option matching.
  • Documentation

    • Added a comprehensive guide for the new interaction APIs, plugin configuration, examples, and failure-behavior guidance.
  • Tests

    • Added unit tests for engine loading, overrides, caching, and ask_yesno/ask_selection behaviors.
  • Chores

    • Updated runtime dependencies to support plugin-based engines and cleaned up legacy requirements.

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces internal yes/no and option-matching logic with configurable plugin engines for OVOSSkill, adds engine-loading/caching, updates ask_yesno/ask_selection to delegate to plugins with fallbacks, adds docs, updates dependencies, and introduces unit tests covering engine resolution and behaviors.

Changes

Cohort / File(s) Summary
Documentation
docs/skill-interaction.md
Add new documentation covering ask_yesno and ask_selection APIs, engine interfaces, plugin config keys, failure semantics, examples, and behavior comparison.
Core Implementation
ovos_workshop/skills/ovos.py
Introduce _get_yesno_engine() and _get_selection_engine() to load/cache engines (settings → config_core → defaults) via plugin manager; update ask_yesno() and ask_selection() to delegate to engine methods with logging and fallbacks to heuristic/default engines.
Tests
test/unittests/test_skill_interaction.py
New unit tests for engine loading precedence, caching, defaults, plugin failures, and ask_yesno/ask_selection edge cases (no response, empty/single options, invalid inputs).
Project config
pyproject.toml, requirements/requirements.txt
Dependency changes: swap yes/no plugin package, add option-matcher fuzzy plugin and plugin-manager; remove entries from legacy requirements file.

Sequence Diagram(s)

sequenceDiagram
    participant Skill as OVOSSkill
    participant Cache as Engine Cache
    participant PluginMgr as Plugin Manager
    participant Engine as YesNo Engine
    participant Solver as Heuristic Solver

    rect rgba(200,150,100,0.5)
    Note over Skill,Solver: ask_yesno() flow
    Skill->>Cache: check cached yesno engine
    Cache-->>Skill: cache miss / hit
    alt cache miss
        Skill->>PluginMgr: load yesno plugin (settings → config_core → default)
        PluginMgr-->>Engine: return engine instance / raise
        Engine-->>Cache: store engine
    end
    Skill->>Engine: yes_or_no(question=prompt, response=resp, lang=lang)
    Engine-->>Skill: True / False / None
    alt engine returned True/False
        Skill-->>Skill: map True->"yes", False->"no"
    else engine returned None or unavailable
        Skill->>Solver: match_yes_or_no(response=resp, lang=lang)
        Solver-->>Skill: match / raw resp / None
    end
    end
Loading
sequenceDiagram
    participant Skill as OVOSSkill
    participant Cache as Engine Cache
    participant PluginMgr as Plugin Manager
    participant Engine as OptionMatcher Engine

    rect rgba(100,150,200,0.5)
    Note over Skill,Engine: ask_selection() flow
    Skill->>Cache: check cached selection engine
    Cache-->>Skill: cache miss / hit
    alt cache miss
        Skill->>PluginMgr: load option-matcher plugin (settings → config_core → default)
        PluginMgr-->>Engine: return engine instance / raise
        Engine-->>Cache: store engine
    end
    Skill->>Engine: match_option(utterance=resp, options=options, lang=lang)
    Engine-->>Skill: matched option / None
    alt engine returns option
        Skill-->>Skill: return matched option
    else engine error or None
        Skill-->>Skill: log and return None
    end
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nudged a plugin, gave it a try,
Engines wake and answers fly,
Cached and ready, fallbacks near,
Tests hop in — no panic, dear,
The rabbit cheers: choices clear.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: yesno/selection agent plugins' accurately summarizes the main change: introducing plugin-based engines for yes/no and selection handling in OVOSSkill.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yesno_select

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Just the facts, ma'am. Here's your report. 👮‍♂️

I've aggregated the results of the automated checks for this PR below.

📋 Repo Health

I've performed a holistic audit of your project's soul. 🧘

✅ All required files present.

Latest Version: 8.1.0a1

ovos_workshop/version.py — Version file
README.md — README
LICENSE — License file
pyproject.toml — pyproject.toml
CHANGELOG.md — Changelog
⚠️ requirements.txt — Requirements
ovos_workshop/version.py has valid version block markers

🔍 Lint

Checking the boxes and crossing the T's. 🖋️

ruff: issues found — see job log

🔨 Build Tests

Ensuring the gears are properly lubricated. 💧

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

❌ 3.10: unknown
❌ 3.11: unknown
❌ 3.12: unknown
❌ 3.13: unknown
❌ 3.14: unknown
Check job logs for details.

🔒 Security (pip-audit)

Checking for any insecure data transmissions. 📡

✅ No known vulnerabilities found (67 packages scanned).

⚖️ License Check

Checking for any potential legal hurdles. 🚧

✅ No license violations found (48 packages).

License distribution: 12× MIT License, 8× Apache Software License, 7× MIT, 6× Apache-2.0, 2× BSD-3-Clause, 2× ISC License (ISCL), 2× PSF-2.0, 2× Python Software Foundation License, +7 more

Full breakdown — 48 packages
Package Version License URL
audioop-lts 0.2.2 PSF-2.0 link
build 1.4.2 MIT link
certifi 2026.2.25 Mozilla Public License 2.0 (MPL 2.0) link
charset-normalizer 3.4.7 MIT link
click 8.3.2 BSD-3-Clause link
combo_lock 0.3.1 Apache-2.0 link
filelock 3.25.2 MIT link
idna 3.11 BSD-3-Clause link
importlib_metadata 9.0.0 Apache-2.0 link
json-database 0.10.1 MIT link
kthread 0.2.3 MIT License link
langcodes 3.5.1 MIT License link
markdown-it-py 4.0.0 MIT License link
mdurl 0.1.2 MIT License link
memory-tempfile 2.2.3 MIT License link
ovos-config 2.1.1 Apache-2.0 link
ovos-number-parser 0.5.1 Apache Software License link
ovos-option-matcher-fuzzy-plugin 0.0.1 Apache Software License link
ovos-plugin-manager 2.4.0a1 Apache-2.0 link
ovos-utils 0.8.5 Apache-2.0 link
ovos-workshop 8.1.0a1 Apache-2.0 link
ovos-yes-no-plugin 0.3.0 Apache Software License link
ovos_bus_client 1.5.0 Apache Software License link
packaging 26.0 Apache-2.0 OR BSD-2-Clause link
padacioso 1.0.0 apache-2.0 link
pexpect 4.9.0 ISC License (ISCL) link
ptyprocess 0.7.0 ISC License (ISCL) link
pyee 12.1.1 MIT License link
Pygments 2.20.0 BSD-2-Clause link
pyproject_hooks 1.2.0 MIT License link
python-dateutil 2.9.0.post0 Apache Software License; BSD License link
PyYAML 6.0.3 MIT License link
quebra-frases 0.3.7 Apache Software License link
RapidFuzz 3.14.5 MIT link
regex 2026.4.4 Apache-2.0 AND CNRI-Python link
requests 2.33.1 Apache Software License link
rich 13.9.4 MIT License link
rich-click 1.9.7 MIT License

Copyright (c) 2022 Phil Ewels

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
| link |
| simplematch | 1.4 | MIT License | link |
| six | 1.17.0 | MIT License | link |
| standard-aifc | 3.13.0 | Python Software Foundation License | link |
| standard-chunk | 3.13.0 | Python Software Foundation License | link |
| typing_extensions | 4.15.0 | PSF-2.0 | link |
| unicode-rbnf | 2.4.0 | MIT License | |
| urllib3 | 2.6.3 | MIT | link |
| watchdog | 6.0.0 | Apache Software License | link |
| websocket-client | 1.9.0 | Apache Software License | link |
| zipp | 3.23.0 | MIT | link |

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.


Your friendly neighborhood bot 🕷️

@github-actions github-actions Bot added feature and removed feature labels Mar 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ovos_workshop/skills/ovos.py (1)

2007-2057: ⚠️ Potential issue | 🟡 Minor

The min_conf parameter is now unused.

The min_conf parameter (line 2008) is no longer used in the method body since the legacy match_one fallback was removed. The parameter signature should either be deprecated or documented as passed to the engine via config.

Additionally, when the engine raises an exception (lines 2052-2056), the method returns None with no fallback matching. This differs from the documentation in docs/skill-interaction.md line 345 which states ask_yesno falls back to YesNoSolver on plugin failure, but ask_selection does not have equivalent fallback behavior—verify this asymmetry is intentional.

Consider documenting or deprecating min_conf

Either remove the parameter with a deprecation warning:

 def ask_selection(self, options: List[str], dialog: str = '',
                   data: Optional[dict] = None, min_conf: float = 0.65,
                   numeric: bool = False, num_retries: int = -1):
+    # Note: min_conf is deprecated and ignored; configure via plugin settings

Or pass it to the engine:

             try:
-                resp = engine.match_option(utterance=resp, options=options, lang=self.lang)
+                resp = engine.match_option(utterance=resp, options=options, lang=self.lang, min_conf=min_conf)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_workshop/skills/ovos.py` around lines 2007 - 2057, The ask_selection
method currently ignores the min_conf parameter and has no fallback when
engine.match_option fails; update ask_selection to either (A) deprecate/remove
min_conf (emit a DeprecationWarning in ask_selection and update docstring), or
(B) pass min_conf to the selection engine by calling
engine.match_option(utterance=resp, options=options, lang=self.lang,
min_conf=min_conf) so engines can use it; additionally add a safe fallback path
in ask_selection in the except block (e.g., call a legacy matcher or a default
solver similar to YesNoSolver) so that when _get_selection_engine() exists but
engine.match_option raises, the method attempts fallback matching instead of
returning None, and ensure logging still records the exception (LOG.error)
before falling back.
🧹 Nitpick comments (4)
test/unittests/test_skill_interaction.py (2)

78-112: Missing test for selection engine caching.

TestGetYesnoEngine includes test_engine_cached_across_calls (lines 69-75), but TestGetSelectionEngine lacks an equivalent test. Consider adding one for consistency:

Add caching test for selection engine
def test_engine_cached_across_calls(self):
    skill = _make_skill()  # Uses default fuzzy plugin
    mock_cls = MagicMock()
    with patch("ovos_plugin_manager.agents.load_option_matcher_plugin", return_value=mock_cls) as mock_load:
        skill._get_selection_engine()
        skill._get_selection_engine()
    mock_load.assert_called_once()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_skill_interaction.py` around lines 78 - 112, Add a
caching test to TestGetSelectionEngine to mirror TestGetYesnoEngine: create a
new test method (e.g. test_engine_cached_across_calls) that calls
skill._get_selection_engine() twice while patching
ovos_plugin_manager.agents.load_option_matcher_plugin to return a MagicMock and
then assert the plugin load was called only once; reference
TestGetSelectionEngine, _get_selection_engine, and load_option_matcher_plugin
when locating where to add the test.

218-224: Clarify test intent: no fallback when engine is None.

This test (test_no_engine_no_response_returns_none) patches _get_selection_engine to return None, but in production _get_selection_engine will never return None unless the default fuzzy plugin fails to load. Consider adding a test for when the engine exists but returns None from match_option, which is the more common "no match" scenario.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_skill_interaction.py` around lines 218 - 224, The test
test_no_engine_no_response_returns_none incorrectly simulates a missing engine
by patching _get_selection_engine to return None; instead, add or replace a test
that simulates a present engine that yields no match by patching
_get_selection_engine to return a mock engine whose match_option method returns
None, then call ask_selection(options, numeric=True) and assert it returns None;
reference the test name, _get_selection_engine, match_option, and ask_selection
so the change targets the selection flow where the engine exists but finds no
match.
docs/skill-interaction.md (1)

46-49: Add language specifiers to fenced code blocks.

Several code blocks for dialog files lack language specifiers. While these are plain text, adding a specifier improves rendering and satisfies linters.

Add language specifiers
-```
+```text
 Are you sure you want to delete this?

Apply similarly to lines 58, 138, and 266.

Also applies to: 58-60, 138-141, 266-268

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/skill-interaction.md` around lines 46 - 49, Several fenced code blocks
containing dialog snippets (e.g., the blocks with text "Are you sure you want to
delete this?", "Do you really want to delete it?", and the other plain-text
dialog blocks around the same sections) lack language specifiers; update each
triple-backtick fence to include a language tag (use "text") so the blocks
become ```text ... ```; apply this change to the code fences containing the
shown lines and the similar blocks at the other locations referenced in the
comment.
ovos_workshop/skills/ovos.py (1)

1964-1982: Default plugin always triggers load attempt.

Unlike _get_yesno_engine, this method defaults to "ovos-option-matcher-fuzzy-plugin" when no config is set (line 1972). This means every call will attempt to load a plugin even if the user hasn't configured one. If the default plugin isn't installed, this will log an error on every ask_selection call.

Consider whether this is intentional—currently pyproject.toml adds the fuzzy plugin as a dependency, so it should always be available. However, if someone removes it, the error will be noisy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_workshop/skills/ovos.py` around lines 1964 - 1982, The method
_get_selection_engine currently defaults plugin_name to
"ovos-option-matcher-fuzzy-plugin" causing a load attempt on every call; change
it to mirror _get_yesno_engine by not defaulting to a plugin: derive plugin_name
only from self.settings and self.config_core (no hardcoded default), compute
cache_key the same, and only call load_option_matcher_plugin(plugin_name) and
instantiate when plugin_name is truthy; if no plugin_name set, set the cache
attribute to None so future calls are cached and avoid repeated error logs from
load_option_matcher_plugin.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/skill-interaction.md`:
- Around line 104-114: The docs claim the min_conf parameter is applied but the
implementation no longer uses it directly; update the documentation for the
options parameter table to state that min_conf is currently unused in the
high-level API (or is forwarded only to the OptionMatcherEngine configuration if
applicable) or mark min_conf as deprecated. Specifically, change the `min_conf`
row to indicate its current behavior (e.g., "currently unused in the high-level
selection flow; only used when configuring OptionMatcherEngine" or "deprecated")
and mention the OptionMatcherEngine symbol so readers can find where to
set/override this value.
- Around line 340-349: The doc claims ask_yesno falls back to YesNoSolver if the
plugin raises, but the code calls engine.yes_or_no() directly; wrap the
engine.yes_or_no() call inside a try/except in the ask_yesno implementation so
any exception from the engine is caught, log the exception, and then invoke the
fallback YesNoSolver path (same return behavior as when plugin fails to load) to
match the documentation; ensure you reference engine.yes_or_no(), ask_yesno, and
YesNoSolver when making the change so the fallback is triggered on runtime
errors.

In `@test/unittests/test_skill_interaction.py`:
- Line 1: Update the copyright header string from "2024" to "2026" in the file
(the top-line copyright comment), ensuring the header now reads "Copyright 2026
Mycroft AI Inc." so the year is current; keep formatting identical aside from
the year change.

---

Outside diff comments:
In `@ovos_workshop/skills/ovos.py`:
- Around line 2007-2057: The ask_selection method currently ignores the min_conf
parameter and has no fallback when engine.match_option fails; update
ask_selection to either (A) deprecate/remove min_conf (emit a DeprecationWarning
in ask_selection and update docstring), or (B) pass min_conf to the selection
engine by calling engine.match_option(utterance=resp, options=options,
lang=self.lang, min_conf=min_conf) so engines can use it; additionally add a
safe fallback path in ask_selection in the except block (e.g., call a legacy
matcher or a default solver similar to YesNoSolver) so that when
_get_selection_engine() exists but engine.match_option raises, the method
attempts fallback matching instead of returning None, and ensure logging still
records the exception (LOG.error) before falling back.

---

Nitpick comments:
In `@docs/skill-interaction.md`:
- Around line 46-49: Several fenced code blocks containing dialog snippets
(e.g., the blocks with text "Are you sure you want to delete this?", "Do you
really want to delete it?", and the other plain-text dialog blocks around the
same sections) lack language specifiers; update each triple-backtick fence to
include a language tag (use "text") so the blocks become ```text ... ```; apply
this change to the code fences containing the shown lines and the similar blocks
at the other locations referenced in the comment.

In `@ovos_workshop/skills/ovos.py`:
- Around line 1964-1982: The method _get_selection_engine currently defaults
plugin_name to "ovos-option-matcher-fuzzy-plugin" causing a load attempt on
every call; change it to mirror _get_yesno_engine by not defaulting to a plugin:
derive plugin_name only from self.settings and self.config_core (no hardcoded
default), compute cache_key the same, and only call
load_option_matcher_plugin(plugin_name) and instantiate when plugin_name is
truthy; if no plugin_name set, set the cache attribute to None so future calls
are cached and avoid repeated error logs from load_option_matcher_plugin.

In `@test/unittests/test_skill_interaction.py`:
- Around line 78-112: Add a caching test to TestGetSelectionEngine to mirror
TestGetYesnoEngine: create a new test method (e.g.
test_engine_cached_across_calls) that calls skill._get_selection_engine() twice
while patching ovos_plugin_manager.agents.load_option_matcher_plugin to return a
MagicMock and then assert the plugin load was called only once; reference
TestGetSelectionEngine, _get_selection_engine, and load_option_matcher_plugin
when locating where to add the test.
- Around line 218-224: The test test_no_engine_no_response_returns_none
incorrectly simulates a missing engine by patching _get_selection_engine to
return None; instead, add or replace a test that simulates a present engine that
yields no match by patching _get_selection_engine to return a mock engine whose
match_option method returns None, then call ask_selection(options, numeric=True)
and assert it returns None; reference the test name, _get_selection_engine,
match_option, and ask_selection so the change targets the selection flow where
the engine exists but finds no match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbbfb193-fd76-43c4-860b-7d60806f6338

📥 Commits

Reviewing files that changed from the base of the PR and between ea76fe2 and 4d7b8df.

📒 Files selected for processing (4)
  • docs/skill-interaction.md
  • ovos_workshop/skills/ovos.py
  • pyproject.toml
  • test/unittests/test_skill_interaction.py

Comment thread docs/skill-interaction.md
Comment thread docs/skill-interaction.md
Comment thread test/unittests/test_skill_interaction.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
ovos_workshop/skills/ovos.py (2)

1983-1987: ⚠️ Potential issue | 🟠 Major

Don’t let a plugin exception take down ask_yesno().

Line 1985 calls external plugin code without a guard. A single runtime failure now bubbles out of ask_yesno() instead of using the same YesNoSolver fallback you already keep for load failures.

Proposed fix
         resp = self.get_response(dialog=prompt, data=data)
         engine = self._get_yesno_engine()
         if engine is not None:
-            answer = engine.yes_or_no(question=prompt, response=resp, lang=self.lang) if resp else None
+            try:
+                answer = engine.yes_or_no(question=prompt, response=resp, lang=self.lang) if resp else None
+            except Exception as e:
+                self.log.exception(f"YesNo plugin failed: {e}")
+                answer = YesNoSolver().match_yes_or_no(resp, lang=self.lang) if resp else resp
         else:
             answer = YesNoSolver().match_yes_or_no(resp, lang=self.lang) if resp else resp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_workshop/skills/ovos.py` around lines 1983 - 1987, When calling the
external plugin in ask_yesno(), wrap the call to
self._get_yesno_engine().yes_or_no(...) in a try/except so a plugin exception
doesn’t propagate; if an exception occurs (or the engine returns unexpectedly),
log or ignore the error and fall back to using
YesNoSolver().match_yes_or_no(resp, lang=self.lang) (preserving the existing
resp falsy behavior where resp -> None or resp as before). Ensure you reference
the existing symbols: ask_yesno, _get_yesno_engine, engine.yes_or_no, and
YesNoSolver.match_yes_or_no.

1995-1997: ⚠️ Potential issue | 🟠 Major

min_conf is a dead parameter in the new flow.

Lines 1996-1997 still expose min_conf, but the plugin-backed implementation never reads it. Existing callers lose per-call control over selection strictness without any warning. Either thread it back into the matcher path or start deprecating the argument now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_workshop/skills/ovos.py` around lines 1995 - 1997, The ask_selection
method currently exposes the min_conf parameter but the plugin-backed selection
path never uses it; either forward min_conf into the matcher/plugin call or
formally deprecate it. Fix option A: update ask_selection to pass min_conf
through to the underlying selection logic (e.g., forward min_conf into whatever
method is invoked for matching/selection such as the plugin call or
matcher.match invoked by the plugin-backed implementation) so per-call
strictness is honored. Fix option B: if you intend to remove it, modify
ask_selection to emit a DeprecationWarning when min_conf is passed (or when
non-default is used), document that it will be removed, and ensure callers
retain behavior by using the global/configured matcher settings. Ensure the
change references ask_selection and the plugin/matcher call sites so the
parameter is actually consumed or deprecated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/skill-interaction.md`:
- Around line 45-49: The Markdown contains unlabeled fenced code blocks that
show dialog examples (e.g., the block containing "Are you sure you want to
delete this?" / "Do you really want to delete it?"), causing markdownlint
warnings; update each unlabeled triple-backtick fence for those dialog examples
to use a language tag `text` (change ``` to ```text) for all similar plain-text
fenced blocks (including the other examples showing short dialog lines) so the
page passes linting.

In `@ovos_workshop/skills/ovos.py`:
- Around line 2037-2045: The code currently returns the raw transcript in resp
when _get_selection_engine() returns None; change this so the method returns
None instead in that path to match ask_selection()'s contract. Locate the block
using _get_selection_engine() and engine.match_option (around the resp handling)
and either set resp = None or early-return None when engine is None, keeping the
existing exception handling for engine.match_option intact.

---

Duplicate comments:
In `@ovos_workshop/skills/ovos.py`:
- Around line 1983-1987: When calling the external plugin in ask_yesno(), wrap
the call to self._get_yesno_engine().yes_or_no(...) in a try/except so a plugin
exception doesn’t propagate; if an exception occurs (or the engine returns
unexpectedly), log or ignore the error and fall back to using
YesNoSolver().match_yes_or_no(resp, lang=self.lang) (preserving the existing
resp falsy behavior where resp -> None or resp as before). Ensure you reference
the existing symbols: ask_yesno, _get_yesno_engine, engine.yes_or_no, and
YesNoSolver.match_yes_or_no.
- Around line 1995-1997: The ask_selection method currently exposes the min_conf
parameter but the plugin-backed selection path never uses it; either forward
min_conf into the matcher/plugin call or formally deprecate it. Fix option A:
update ask_selection to pass min_conf through to the underlying selection logic
(e.g., forward min_conf into whatever method is invoked for matching/selection
such as the plugin call or matcher.match invoked by the plugin-backed
implementation) so per-call strictness is honored. Fix option B: if you intend
to remove it, modify ask_selection to emit a DeprecationWarning when min_conf is
passed (or when non-default is used), document that it will be removed, and
ensure callers retain behavior by using the global/configured matcher settings.
Ensure the change references ask_selection and the plugin/matcher call sites so
the parameter is actually consumed or deprecated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a902e2b6-665e-4509-b678-e7dc90260f69

📥 Commits

Reviewing files that changed from the base of the PR and between 4d7b8df and 9280dc8.

📒 Files selected for processing (4)
  • docs/skill-interaction.md
  • ovos_workshop/skills/ovos.py
  • pyproject.toml
  • test/unittests/test_skill_interaction.py
✅ Files skipped from review due to trivial changes (1)
  • pyproject.toml

Comment thread docs/skill-interaction.md
Comment thread ovos_workshop/skills/ovos.py
@github-actions github-actions Bot added feature and removed feature labels Apr 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ovos_workshop/skills/ovos.py`:
- Around line 1984-1986: The code passes the dialog key `prompt` to the yes/no
engine instead of the rendered question text; to fix, render the prompt with
`self.dialog_renderer.render(prompt, data)` (as `get_response` does internally)
and pass that rendered string into
`_get_yesno_engine().yes_or_no(question=rendered, response=resp,
lang=self.lang)` so the engine receives the actual question context; update
references to `prompt` when calling `yes_or_no`, keeping
`get_response(dialog=prompt, data=data)` and `resp` unchanged.
- Around line 1936-1953: The helper _get_yesno_engine currently returns a bare
YesNoSolver() on plugin load failure but ask_yesno expects an engine with
yes_or_no(question,response,lang); this mismatch causes AttributeError. Fix by
either (A) returning None from _get_yesno_engine on failure and updating
ask_yesno to detect None and call YesNoSolver().match_yes_or_no(resp, self.lang)
as the built-in fallback, or (B) implement a small adapter class (e.g.,
YesNoAdapter) that wraps YesNoSolver and exposes yes_or_no(question, response,
lang) delegating to match_yes_or_no(response, lang), and return an instance of
that adapter from _get_yesno_engine when loading the plugin fails; update
references to _get_yesno_engine/ask_yesno accordingly.
- Around line 1955-1972: The min_conf argument to ask_selection is never
forwarded to the matcher; update ask_selection to pass min_conf through to the
matcher call (e.g. call engine.match_option(..., min_conf=min_conf)) and ensure
plugin instances created in _get_selection_engine (via
load_option_matcher_plugin and fallback FuzzyOptionMatcherPlugin)
accept/configure that value (either by passing a config/min_conf into the plugin
constructor or setting an attribute like engine.min_conf after instantiation) so
the runtime matcher actually uses the provided threshold.

In `@requirements/requirements.txt`:
- Line 6: Add the missing runtime dependency for the fuzzy matcher to
requirements/requirements.txt so importing FuzzyOptionMatcherPlugin in
ovos_workshop/skills/ovos.py (the module-level import of
FuzzyOptionMatcherPlugin from ovos_option_matcher_fuzzy) no longer raises
ModuleNotFoundError; update requirements/requirements.txt to include the package
name used at runtime (ovos-option-matcher-fuzzy-plugin) with an appropriate
version specifier matching pyproject.toml.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6246fbb7-dd85-4b66-82da-a12d613f22a2

📥 Commits

Reviewing files that changed from the base of the PR and between 9280dc8 and d087e63.

📒 Files selected for processing (3)
  • ovos_workshop/skills/ovos.py
  • pyproject.toml
  • requirements/requirements.txt
✅ Files skipped from review due to trivial changes (1)
  • pyproject.toml

Comment thread ovos_workshop/skills/ovos.py Outdated
Comment thread ovos_workshop/skills/ovos.py
Comment thread ovos_workshop/skills/ovos.py
Comment thread requirements/requirements.txt Outdated
@github-actions github-actions Bot added feature and removed feature labels Apr 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
ovos_workshop/skills/ovos.py (2)

1980-1982: ⚠️ Potential issue | 🟠 Major

Render the spoken prompt and guard the yes/no plugin call.

get_response(dialog=prompt, data=data) asks the rendered dialog, but yes_or_no(question=prompt, ...) still sees the unresolved key. Also, any engine exception currently bubbles out and aborts the whole prompt flow. Use the rendered question text here and normalize plugin failures to a raw-response fallback instead of raising.

Proposed fix
         resp = self.get_response(dialog=prompt, data=data)
-        engine = self._get_yesno_engine()
-        answer = engine.yes_or_no(question=prompt, response=resp, lang=self.lang) if resp else None
+        if not resp:
+            return resp
+
+        question = prompt
+        if self.dialog_renderer:
+            question = self.dialog_renderer.render(prompt, data or {})
+
+        engine = self._get_yesno_engine()
+        try:
+            answer = engine.yes_or_no(question=question, response=resp,
+                                      lang=self.lang)
+        except Exception:
+            LOG.exception("YesNo plugin failed")
+            return resp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_workshop/skills/ovos.py` around lines 1980 - 1982, The code calls
yes_or_no with the original prompt and lets plugin exceptions bubble up; change
it to pass the rendered question text (use the returned resp from get_response
as the question argument to engine.yes_or_no) and wrap the plugin call in a
try/except so any exception is caught and normalized to a fallback raw-response
behavior (e.g., set answer to resp or None) instead of raising; update the call
site that uses _get_yesno_engine(), yes_or_no(question=...), resp, and self.lang
accordingly so the rendered text is used and plugin failures return a safe
fallback.

1957-1964: ⚠️ Potential issue | 🟠 Major

min_conf is still ignored by ask_selection().

Line 1991 still exposes min_conf, but the new matcher path builds the engine with defaults and never uses that argument on the call site. Callers now silently lose control over the acceptance threshold and always get whatever default the matcher chooses.

Also applies to: 2033-2035

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_workshop/skills/ovos.py` around lines 1957 - 1964, ask_selection()
currently ignores the min_conf argument because the cached matcher instance
(built from plugin_name via load_option_matcher_plugin and stored under
cache_key) is constructed with defaults and reused; update the code so min_conf
affects matching: either pass min_conf into the matcher on each call to the
engine's match/select method (e.g., engine.match(options, min_conf=...)) or
include min_conf in the cache key so a matcher is constructed with the desired
threshold (e.g., cache_key = f"__selection_engine_{plugin_name}_{min_conf}") and
ensure the matcher is instantiated with that min_conf if its constructor accepts
it; modify the code paths using cache_key, load_option_matcher_plugin, and the
engine invocation in ask_selection() so callers’ min_conf is respected rather
than silently using the plugin defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@ovos_workshop/skills/ovos.py`:
- Around line 1980-1982: The code calls yes_or_no with the original prompt and
lets plugin exceptions bubble up; change it to pass the rendered question text
(use the returned resp from get_response as the question argument to
engine.yes_or_no) and wrap the plugin call in a try/except so any exception is
caught and normalized to a fallback raw-response behavior (e.g., set answer to
resp or None) instead of raising; update the call site that uses
_get_yesno_engine(), yes_or_no(question=...), resp, and self.lang accordingly so
the rendered text is used and plugin failures return a safe fallback.
- Around line 1957-1964: ask_selection() currently ignores the min_conf argument
because the cached matcher instance (built from plugin_name via
load_option_matcher_plugin and stored under cache_key) is constructed with
defaults and reused; update the code so min_conf affects matching: either pass
min_conf into the matcher on each call to the engine's match/select method
(e.g., engine.match(options, min_conf=...)) or include min_conf in the cache key
so a matcher is constructed with the desired threshold (e.g., cache_key =
f"__selection_engine_{plugin_name}_{min_conf}") and ensure the matcher is
instantiated with that min_conf if its constructor accepts it; modify the code
paths using cache_key, load_option_matcher_plugin, and the engine invocation in
ask_selection() so callers’ min_conf is respected rather than silently using the
plugin defaults.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7206124-7afe-447e-9804-1c884b9f9bb7

📥 Commits

Reviewing files that changed from the base of the PR and between d087e63 and c189de5.

📒 Files selected for processing (3)
  • ovos_workshop/skills/ovos.py
  • pyproject.toml
  • requirements/requirements.txt
✅ Files skipped from review due to trivial changes (1)
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • requirements/requirements.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added feature and removed feature labels Apr 8, 2026
JarbasAl and others added 7 commits April 8, 2026 23:27
- patch load_yesno_plugin/load_option_matcher_plugin at ovos_workshop.skills.ovos
  (not ovos_plugin_manager.agents) since they are imported into the module
- _get_yesno_engine falls back to HeuristicYesNoEngine, not None
- _get_selection_engine falls back to FuzzyOptionMatcherPlugin, not None
- replace YesNoSolver references with patch.object on _get_yesno_engine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build_tests already runs the same test path; unit_tests only differed by
adding audio system deps not needed for these tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_base.py: fill in TODO stubs with mock-based tests on real OVOSSkill
  (yes/no/unmatched/timeout for ask_yesno; empty/single/invalid/fuzzy/timeout
  for ask_selection)
- test_ask_e2e.py: ovoscope end-to-end tests — inline AskYesNoSkill and
  AskSelectionSkill loaded via get_minicroft; user response injected via
  {skill_id}.converse.get_response after hearing speak; skipped when
  ovoscope/ovos-core are not installed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- remove pytest.importorskip — e2e tests are mandatory, not optional
- add [project.optional-dependencies] test group to pyproject.toml
  (ovos-core, ovoscope, pytest, pytest-cov, ovos-translate-server-plugin)
- build_tests.yml: pass install_extras=test so CI installs test deps
- keep requirements/test.txt in sync for local pip install -r workflows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ask_selection min_conf parameter was documented but silently ignored.
FuzzyOptionMatcherPlugin reads threshold from self.config["min_conf"],
so set it on the cached engine instance before each match_option call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JarbasAl and others added 11 commits April 9, 2026 00:29
AI-Generated Change:
- Model: claude-sonnet-4-6
- Intent: keep docs accurate after feat: yesno/selection agent plugins
- Impact: updated README.md, docs/index.md, docs/ovos-skill.md, docs/skill-interaction.md
- Changes:
  - README.md: rewritten with install instructions, config table, and docs links
  - docs/skill-interaction.md: fixed fallback class name (HeuristicYesNoEngine not YesNoSolver), corrected ask_yesno_plugin default (ovos-solver-yes-no-plugin not None), added line-number citations, corrected failure behaviour table
  - docs/ovos-skill.md: noted plugin configurability for ask_yesno/ask_selection
  - docs/index.md: added skill-interaction.md to navigation table
- Verified via: manual review against ovos_workshop/skills/ovos.py:1932-2040

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_minicroft blocks up to 60s waiting for READY — too heavy for the
standard build_tests matrix. Move test/unittests/test_ask_e2e.py to
test/ovoscope/ (not scanned by build_tests) and add a dedicated
ovoscope_tests workflow that installs [test] extras and runs that path.
Revert install_extras from build_tests.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ask_selection speaks each option with wait=True, blocking 15s per speak
waiting for recognizer_loop:audio_output_end that never arrives in tests.
Emit that event immediately on every speak message so the skill proceeds
without delay, then inject the user response once speaking settles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tests

Listening on "message" caught the audio_output_end we emitted, causing
infinite recursion. Listen on "speak" specifically instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No need for a separate workflow. Move test_ask_e2e.py back to
test/unittests/ and install [test] extras in build_tests so ovoscope
and ovos-core are available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FakeBus is synchronous — on_speak fires inline during bus.emit(speak),
before the caller sets sess.is_speaking=True. Emitting audio_output_end
in the same stack means wait_while_speaking misses it and hangs 15s.
Use a daemon thread with a 20ms delay so is_speaking is set first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tests fought TTS wait machinery (speak(wait=True) blocks on
recognizer_loop:audio_output_end) without adding coverage beyond what
test_skill_interaction.py already provides via mocked get_response.
Unit tests are sufficient here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No ovoscope e2e tests remain, no need for the heavy dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Patch SessionManager.wait_while_speaking to a no-op so speak(wait=True)
doesn't block on TTS completion. Inject user response via
skill.converse.get_response.enable event instead of timing hacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_minicroft sets SessionManager.bus globally. Without restoring it,
subsequent tests get real wait_while_speaking behaviour instead of the
early-return they expect (bus=None). Also scope wait_while_speaking
patch to setUp/tearDown so it can't leak if tearDownClass throws.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@JarbasAl JarbasAl merged commit a04523f into dev Apr 9, 2026
20 of 23 checks passed
@JarbasAl JarbasAl deleted the yesno_select branch April 9, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant