Skip to content

fix: do not silently disable fallback skills that predate can_answer - #515

Merged
JarbasAl merged 1 commit into
devfrom
fix/legacy-fallback-skills-can-answer
Aug 11, 2026
Merged

JarbasAl merged 1 commit into
devfrom
fix/legacy-fallback-skills-can-answer

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Aug 11, 2026

Copy link
Copy Markdown
Member

🤖 Auto-generated by Claude Opus 5 (claude-opus-5) via Claude Code — NOT human-reviewed. Verify before acting.

Found on a live install: tell me a joke and what is the weather returned nothing at all. The utterance reached ovos-core and routed to fallback correctly — then died there.

The bug

can_answer is an opt-in optimization: it lets a skill decline before ovos-core pays for a full fallback request. The base implementation raises NotImplementedError, and _handle_fallback_ack called it unguarded:

self.bus.emit(message.reply("ovos.skills.fallback.pong",
                            data={"skill_id": self.skill_id,
                                  "can_handle": self.can_answer(message)},

So the ping handler throws, no pong is ever emitted, and from ovos-core's side the skill looks unreachable. Every fallback skill that never overrode can_answer is dead. Among the official skills on a stock install:

skill overrides can_answer working
ovos-skill-ddg yes yes
ovos-skill-fallback-unknown yes yes
ovos-skill-wolfie no dead
ovos-skill-wikipedia no dead
ovos-skill-icanhazdadjokes no dead

The only symptom is silence. The traceback (NotImplementedError at fallback.py:98) is logged per-skill per-utterance and is easy to read as noise.

The change

Treat NotImplementedError as "has not opted in, so ask it" — the behaviour those skills were written against. An explicit True or False from a skill that did override can_answer is still respected, so the optimization keeps working for skills that use it.

Verified

4 new tests in test/unittests/skills/test_fallback_skill.py, mutation-checked: the legacy-skill test fails against current dev and passes with the fix. The two control tests (explicit accept, explicit decline) pass either way, which is their job.

Suite: 544 passed, 2 skipped. Two failures are pre-existing and order-dependent — unpatched dev fails test_skill_reload and test_skill_load_blacklisted in full-suite order, so this change makes that strictly better, not worse. test_ask_yesno also fails on unpatched dev in isolation. test_ask_e2e.py needs ovoscope, absent from my venv.

Reproduced against ovos-workshop 9.3.3a1 (current latest prerelease) with ovos-core 2.6.0a1.

Summary by CodeRabbit

  • Bug Fixes

    • Fallback skills that do not explicitly implement capability checks continue to respond correctly.
    • Explicitly accepting or declining fallback requests remains respected.
  • Tests

    • Added coverage for legacy fallback skills and capability responses.

can_answer is an opt-in optimization: it lets a skill decline before
ovos-core pays for a full fallback request. The base implementation raises
NotImplementedError, and _handle_fallback_ack called it unguarded, so the
ping handler threw and no pong was ever emitted. From ovos-core's side the
skill looked unreachable.

Every fallback skill that never overrode can_answer was therefore dead --
wolfie, wikipedia and icanhazdadjokes among the official ones. 'tell me a
joke' and 'what is the weather' returned nothing at all on a live install.

Treat NotImplementedError as 'has not opted in, so ask it', which is the
behaviour those skills were written against. An explicit True or False from
a skill that did override can_answer is still respected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the fix label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The fallback handler now treats an unimplemented can_answer method as affirmative. Regression tests cover legacy skills and explicit False and True responses.

Changes

Fallback compatibility

Layer / File(s) Summary
Capability check and regression coverage
ovos_workshop/skills/fallback.py, test/unittests/skills/test_fallback_skill.py
The handler converts NotImplementedError from can_answer into can_handle=True while preserving explicit results. Tests verify legacy, declining, and accepting fallback skills.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the fix for legacy fallback skills that lack can_answer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/legacy-fallback-skills-can-answer

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.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The bots have spoken! Here's the summary of their findings. 🗣️

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

🔍 Lint

The latest check cycle has concluded. 🔄

ruff: issues found — see job log

🔒 Security (pip-audit)

I've checked for any hardcoded credentials. 🔑

✅ No known vulnerabilities found (74 packages scanned).

⚖️ License Check

Ensuring our legal documentation is accessible. 📖

✅ No license violations found.

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

🔨 Build Tests

Checking if the code is properly tempered. ⚔️

✅ All versions pass

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

Powered by OVOS scripts and a bit of magic. ✨

@github-actions github-actions Bot added fix and removed fix labels Aug 11, 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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ovos_workshop/skills/fallback.py`:
- Around line 114-122: Restrict the NotImplementedError fallback in the fallback
handling around self.can_answer to cases where the bound method resolves to
FallbackSkill.can_answer; allow exceptions from overridden implementations to
retain their normal failure behavior. Add a regression test covering a subclass
override of can_answer that raises NotImplementedError.
🪄 Autofix

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 Plus

Run ID: 8baf9372-c72c-4be8-9858-704e242a7202

📥 Commits

Reviewing files that changed from the base of the PR and between 7aaa4c5 and b9b5935.

📒 Files selected for processing (2)
  • ovos_workshop/skills/fallback.py
  • test/unittests/skills/test_fallback_skill.py

Comment on lines +114 to +122
try:
can_handle = self.can_answer(message)
except NotImplementedError:
# can_answer is an opt-in optimization: it lets a skill decline
# before ovos-core pays for a full fallback request. A skill that
# predates it simply has not opted in, so it must still be asked.
# Treating the base implementation as a refusal silently disables
# every such skill.
can_handle = True

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Only apply the affirmative fallback to the base can_answer implementation.

Lines 114-122 also catch NotImplementedError from an overridden can_answer. If an opted-in skill raises that exception from its own unsupported path, this handler emits can_handle=True and causes an unnecessary fallback request that cannot succeed.

Check whether self.can_answer resolves to FallbackSkill.can_answer before setting can_handle=True. Otherwise, call the override and let its exception retain its normal failure behavior. Add a regression test for an override that raises NotImplementedError.

Proposed fix
-        try:
-            can_handle = self.can_answer(message)
-        except NotImplementedError:
-            # can_answer is an opt-in optimization: it lets a skill decline
-            # before ovos-core pays for a full fallback request. A skill that
-            # predates it simply has not opted in, so it must still be asked.
-            # Treating the base implementation as a refusal silently disables
-            # every such skill.
+        can_answer = self.can_answer
+        if getattr(can_answer, "__func__", None) is FallbackSkill.can_answer:
             can_handle = True
+        else:
+            can_handle = can_answer(message)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
can_handle = self.can_answer(message)
except NotImplementedError:
# can_answer is an opt-in optimization: it lets a skill decline
# before ovos-core pays for a full fallback request. A skill that
# predates it simply has not opted in, so it must still be asked.
# Treating the base implementation as a refusal silently disables
# every such skill.
can_handle = True
can_answer = self.can_answer
if getattr(can_answer, "__func__", None) is FallbackSkill.can_answer:
can_handle = True
else:
can_handle = can_answer(message)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ovos_workshop/skills/fallback.py` around lines 114 - 122, Restrict the
NotImplementedError fallback in the fallback handling around self.can_answer to
cases where the bound method resolves to FallbackSkill.can_answer; allow
exceptions from overridden implementations to retain their normal failure
behavior. Add a regression test covering a subclass override of can_answer that
raises NotImplementedError.

@JarbasAl
JarbasAl merged commit 544f585 into dev Aug 11, 2026
13 checks passed
@JarbasAl
JarbasAl deleted the fix/legacy-fallback-skills-can-answer branch August 11, 2026 14:08
JarbasAl added a commit that referenced this pull request Aug 11, 2026
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