Skip to content

refactor: route OVOSSkill resource loading through ovos-spec-tools (back-compat)#413

Closed
JarbasAl wants to merge 34 commits into
devfrom
feat/drop-resource-files-usage
Closed

refactor: route OVOSSkill resource loading through ovos-spec-tools (back-compat)#413
JarbasAl wants to merge 34 commits into
devfrom
feat/drop-resource-files-usage

Conversation

@JarbasAl
Copy link
Copy Markdown
Member

@JarbasAl JarbasAl commented May 23, 2026

Stacked on #412 (feat/deprecate-resource-files) → #411. Part of the OVOS migration onto ovos-spec-tools (OpenVoiceOS/architecture#7).

OVOSSkill and the rest of ovos_workshop now route resource access through ovos_spec_tools.LocaleResources directly. The legacy ovos_workshop.resource_files.SkillResources is no longer used internally but stays shipped and stays available on every OVOSSkill through a deprecation mixin — every skill in the ecosystem keeps working.

Back-compat surface preserved

All legacy entry points stay on OVOSSkill via the new _LegacyResourcesMixin. Each emits a single DeprecationWarning pointing at the spec-tools-backed replacement and routes through the same cached SkillResources instance so behaviour is unchanged.

Legacy member Behaviour Migration
self.resources Returns a real SkillResources (lazily built once per skill). self.resources.load_dialog, render_dialog, load_vocab, … all keep working. High-level methods (speak_dialog, voc_match, …) or construct ovos_spec_tools.LocaleResources directly.
self.dialog_renderer Returns a real MustacheDialogRenderer from the cached SkillResources. self.dialog_renderer.render(name, data) works unchanged. OVOSSkill.render_dialog (which delegates to ovos_spec_tools.render).
self.find_resource(name, dirname=None, lang=None) Delegates to ovos_workshop.resource_files.find_resource. ovos_spec_tools.LocaleResources.find.
self.load_dialog_files(root=None) No-op shim. Dialogs are now loaded lazily by LocaleResources; pre-loading is unnecessary. Drop the call.
self.voc_match_cache Getter returns self._voc_cache (the live cache used by voc_match / voc_list). Setter still accepts a dict and warns. Use voc_match / voc_list directly — they cache internally.
runtime_requirements / network_requirements Both kept as classproperty shims returning RuntimeRequirements(). network_requirements continues to log the legacy rename warning. Let the framework infer requirements from the intent surface.

The mixin is a single import away from removal — when the deprecation window closes, dropping _LegacyResourcesMixin from OVOSSkill's bases (and deleting _legacy_resources.py) is the only change needed.

Other resource roles the legacy SkillResources exposes (.list, .value, .word, .template, .qml, skill-side .json) keep working through the preserved self.resources handle — workshop internals no longer consume them, but skills that do are unaffected.

What changed internally (no public-surface impact)

  • .dialog rendering → LocaleResources.load_dialog + ovos_spec_tools.render (with vocabularies() for <voc> references). Drives render_dialog, speak_dialog, get_response, the skill.error flow.
  • .vocLocaleResources.vocabularies() (eager Adapt-keyword registration) and load_vocabulary (for voc_list / voc_match / remove_voc).
  • .intent / .entity → file-path resolved via a private OVOSSkill._locate_lang_file helper (closest_lang + os.walk); the path is then passed to intent_service.register_padatious_intent / register_padatious_entity.
  • .rx regex files → OVOSSkill.load_regex_files is kept and self-contained (does not touch resource_files); registration goes through adapt as before.
  • Language directory resolution in OVOSAbstractApplication.get_language_dir uses closest_lang + os.listdir directly.
  • Workshop's internal word_connectors.json / euphony.json are read with closest_lang resolution — no CoreResources.

grep -rn 'resource_files\|SkillResources\|MustacheDialogRenderer\|ResourceFile\|RegexExtractor' ovos_workshop/ matches only inside resource_files.py and the legacy mixin.

One private removal

UniversalSkill._load_lang — a dead override that constructed a SkillResources; never part of the public API (underscore-prefixed). The base-class load_lang (public, on every OVOSSkill) now returns a LocaleResources instead of a SkillResources, but the caller pattern is the same and the methods callers use (load_dialog, load_vocab, etc.) exist on both.

Tests

  • test/unittests/skills/test_legacy_resources.py (new, 358 lines) — exercises every shim on the mixin, verifies the deprecation warnings fire, confirms each call still returns a working object.
  • test/unittests/skills/test_base.py, test_ovos.py — import LocaleResources instead of SkillResources for the new-path assertions.
  • test_resource_files* — untouched (the module is still shipped and tested).
  • test/end2end/test_regex_skill.py, test/end2end/test_universal_skill.py — new ovoscope end-to-end coverage for the .rx and UniversalSkill paths.
  • Full suite: 479 passed (ignoring the pre-existing ovos_yes_no_solver-related collection errors, unrelated to this PR).

Migration path for skill authors

Skills migrate incrementally: each DeprecationWarning points at the spec-tools-backed replacement. No skill needs to change in this release cycle. The mixin and the underlying resource_files module both have a one-major-release deprecation window.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Deprecations

    • Legacy resource discovery and legacy resource APIs now emit deprecation warnings; compatibility shims remain.
  • Improvements

    • More robust language normalization and closer-match fallback for dialog, vocabulary, intent and regex resources.
    • Dialog rendering updated with clearer missing-resource fallback and stabilized slot/vocabulary expansion.
    • Resource lookup decoupled from query language to support internal-language workflows.
  • Tests

    • Expanded unit and end-to-end coverage for resource loading, regex intents, dialog rendering, translation flows, and legacy shims.
  • Chores

    • Updated runtime constraints to include a newer utilities package and a spec-tools package.

Review Change Stack

JarbasAl and others added 5 commits May 22, 2026 18:24
…ec-tools

Wave 2 of the OVOS migration (OpenVoiceOS/architecture#7). Replace
reimplemented language-matching and template-expansion logic with the
conformant reference implementation from ovos-spec-tools.

- resource_files.py: locate_lang_directories now uses
  ovos_spec_tools.lang_distance instead of langcodes.tag_distance;
  vocabulary/intent loading uses ovos_spec_tools.expand instead of
  ovos_utils.bracket_expansion.expand_template. expand is single-spaced
  per OVOS-INTENT-1, fixing double spaces left when an optional segment
  is dropped. sorted() preserves the previous deterministic ordering.
- converse.py: _get_closest_lang uses ovos_spec_tools.closest_lang,
  which standardizes both tags and applies the spec's distance-below-10
  threshold internally.
- Add ovos-spec-tools>=0.0.1a2 to dependencies.

All migrated symbols were internal-only; no public API changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 2 of the OVOS migration (OpenVoiceOS/architecture#7). Replace the
reimplemented MustacheDialogRenderer-based dialog rendering with the
conformant reference renderer from ovos-spec-tools.

- resource_files.py: DialogFile.render / IntentFile.render now load the
  raw phrase list and render it with ovos_spec_tools.render, which
  expands (a|b)/[x] variants and fills {name} slots per OVOS-DIALOG-1.
  SkillResources.render_dialog renders directly without routing through
  a MustacheDialogRenderer. SkillResources keeps handling the legacy
  resource types (qml, json, list, value, regex, word, template) that
  ovos-spec-tools does not cover.
- ovos.py: OVOSSkill gains render_dialog, which renders via
  self.resources.render_dialog; speak_dialog and the get_response /
  ask on-fail flows call render_dialog instead of dialog_renderer.render.
- The SkillResources.dialog_renderer and OVOSSkill.dialog_renderer
  properties are kept as deprecated compat shims (warnings.warn +
  @deprecated) that lazily build a MustacheDialogRenderer for legacy
  callers; removal version computed from VERSION_MAJOR.

A missing .dialog file still falls back to the dialog name with dots
replaced by spaces (eg "record.not.found" -> "record not found"), the
documented contract of the previous renderer. Unfilled slots now raise
ovos_spec_tools.UnfilledSlot instead of being silently dropped.

BREAKING CHANGE: dialog rendering is now performed by
ovos_spec_tools.render. The dialog_renderer properties on SkillResources
and OVOSSkill are deprecated compat shims. A dialog phrase with a
{name} slot that has no value now raises UnfilledSlot instead of
producing a partially-rendered string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DialogFile.render / IntentFile.render now raise FileNotFoundError when
the resource file is missing or empty, instead of silently returning
the resource name with dots replaced by spaces.

OVOSSkill.render_dialog stays polymorphic: speak_dialog / get_response
accept either a dialog key or a literal utterance, so a name with no
matching .dialog file is caught and returned verbatim as the utterance.
The silent dialog-name humanization is gone — a genuinely missing file
is now an error at the layer that owns the file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark the entire resource_files module as deprecated. It predates the OVOS
formal specifications and is considered overengineered. Everything stays
fully functional for downstream consumers; nothing is removed.

All public names (ResourceType, ResourceFile and subclasses, SkillResources,
CoreResources, UserResources, RegexExtractor, locate_base_directories,
locate_lang_directories, find_resource) now carry the @deprecated decorator
plus an inner warnings.warn and a .. deprecated:: docstring notice.

Messages point downstream to the ovos-spec-tools replacements where they
exist (LocaleResources, render/DialogRenderer, expand, closest_lang/
standardize_lang); legacy types with no spec replacement (.qml, .json,
.list, .value, .rx, .word, .template) are flagged as not part of the OVOS
formal specifications.

To avoid the framework spamming deprecation warnings at itself (these
classes are built per-skill, per-language on hot paths), a _caller_is_internal
stack guard suppresses both the log notice and the DeprecationWarning when
the caller is ovos_workshop's own code; the warnings remain visible to
downstream callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gh ovos-spec-tools

OVOSSkill and the rest of ovos_workshop no longer import or call
``ovos_workshop.resource_files``. The 6 OVOS-spec resource roles
(.intent / .dialog / .entity / .voc / .blacklist / .prompt) are now
resolved through ``ovos_spec_tools.LocaleResources``; dialogs are
rendered via ``ovos_spec_tools.render``; language directories are
resolved via ``ovos_spec_tools.closest_lang``.

The legacy resource types are removed from OVOSSkill: ``.rx`` / regex,
``.list``, ``.value``, ``.word``, ``.template``, ``.qml``, and skill-side
``.json``. The corresponding OVOSSkill methods (``load_regex_file*``,
``load_list_file``, ``load_named_value_file``, ``load_word_file``,
``load_template_file``, ``locate_qml_file``, ``load_json_file``,
``load_skill_regex``, ``find_resource``) and the file-based
``load_regex_files`` code path are gone. The string-based
``register_regex`` (adapt regex from a literal pattern) stays.

``OVOSSkill.resources`` now returns ``ovos_spec_tools.LocaleResources``.
``load_lang`` keeps a backwards-compatible signature but the ``lang``
argument is ignored (LocaleResources picks the language per-call).
``dialog_renderer`` is no longer backed by ``MustacheDialogRenderer`` —
the deprecated property returns ``None`` and warns.

``ovos_workshop`` ships small JSON config files
(``word_connectors.json`` / ``euphony.json``) under its own
``locale/`` directory; ``ovos_workshop.skills.util.join_word_list``
now reads them directly with ``closest_lang`` resolution, not through
``CoreResources``.

Test changes:
- ``test_base.py``/``test_ovos.py``: assertion on the type of
  ``OVOSSkill.resources`` updated from ``SkillResources`` to
  ``LocaleResources``.
- ``test_base.py``: removed TODO stubs for the dropped methods
  (``find_resource``, ``load_regex_files``).

Stacks on #412 / #411; part of OpenVoiceOS/architecture#7.

BREAKING CHANGE: legacy resource-type support (``.rx``, ``.list``,
``.value``, ``.word``, ``.template``, ``.qml``, ``.json``) is removed
from ``OVOSSkill``. Skills that registered file-based regex via
``load_regex_files`` / ``load_skill_regex`` or that called
``OVOSSkill.find_resource`` / the dropped ``load_*`` helpers must
migrate to ``ovos_spec_tools.LocaleResources``. ``OVOSSkill.resources``
now returns ``ovos_spec_tools.LocaleResources`` instead of
``ovos_workshop.resource_files.SkillResources``. The 6 OVOS-spec
resource roles continue to work unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Warning

Review limit reached

@JarbasAl, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 21 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51d1b7a8-ee39-41a6-93c7-7e0e04c47eca

📥 Commits

Reviewing files that changed from the base of the PR and between 9b26d79 and 1d78222.

📒 Files selected for processing (2)
  • ovos_workshop/app.py
  • pyproject.toml
📝 Walkthrough

Walkthrough

This PR migrates ovos-workshop's resource handling from legacy SkillResources to ovos_spec_tools.LocaleResources, adds a legacy-deprecation shim, updates language-matching utilities, and expands unit and end-to-end tests for regex loading and UniversalSkill language-decoupling.

Changes

Locale Resources Migration

Layer / File(s) Summary
Deprecation infrastructure and resource_files refactor
ovos_workshop/resource_files.py
Module-level deprecation and _deprecated_public added; DialogFile/IntentFile/VocabularyFile and ResourceFile wrappers updated to delegate rendering/expansion to ovos_spec_tools (render/expand/lang_distance).
Legacy backward compatibility mixin
ovos_workshop/skills/_legacy_resources.py
New _LegacyResourcesMixin exposes deprecated properties/methods (resources, dialog_renderer, find_resource, voc_match_cache, runtime_requirements, network_requirements) that route to cached legacy SkillResources and emit DeprecationWarning for external callers.
OVOSSkill refactor to LocaleResources
ovos_workshop/skills/ovos.py
OVOSSkill now uses LocaleResources (cached per-root), adds render_dialog() with missing-key fallback, rewrites load_lang/load_data_files to use LocaleResources, updates voc/regex registration and voc utilities to use LocaleResources and spec-tools helpers, and removes legacy back-compat accessors.
UniversalSkill language decoupling
ovos_workshop/skills/auto_translatable.py
Introduce _resource_lang pinned to internal_language, gate __tags__ translation, and clarify outbound speak() translation arguments.
ConversationalSkill language and intent handling
ovos_workshop/skills/converse.py
Switch to ovos_spec_tools.closest_lang/standardize_lang, use _locate_lang_file() for intent discovery, and normalize converse request language.
Skill utilities and application updates
ovos_workshop/skills/util.py, ovos_workshop/app.py
Add _load_workshop_json() for workshop JSON locales; update _get_word() and _load_euphony_rules() to use it; update OVOSAbstractApplication.get_language_dir() to use closest_lang() and standardize_lang().
Dependency & ignore
pyproject.toml, .gitignore
Update ovos-utils constraint and add ovos-spec-tools>=0.5.1a1 to project dependencies; ignore test/unittests/skills/temp_config/.
Unit tests: legacy shims & regex loading
test/unittests/skills/test_legacy_resources.py, test/unittests/skills/test_regex_loading.py, test/unittests/skills/test_base.py, test/unittests/skills/test_ovos.py
Add tests validating _locale_resources behavior, legacy shim warnings and continued functionality, legacy file format handling, user-override precedence, and comprehensive .rx loading behavior (group-name prefixing, registration, logging, conditional deprecation warning). Update existing tests to assert LocaleResources usage and remove obsolete tests.
End-to-end regex test
test/end2end/test_regex_skill.py, test/end2end/locale/en-US/play.rx
Add integration test that verifies Adapt .rx regex capture groups are passed into intent handlers end-to-end.
End-to-end UniversalSkill tests
test/end2end/test_universal_skill.py, test/end2end/universal_locale/locale/*
Add E2E tests that pin internal resource language, verify speak_dialog/voc_match use internal-language resources for foreign-session queries, and exercise a Portuguese→internal→Portuguese round-trip translation flow. Include locale fixture files.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through resource trees,
Swapping old leaves for spec-based breeze,
LocaleResources bloom in place,
Legacy shims keep gentle grace,
Dialogs sing in every language — hop! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the main change: refactoring OVOSSkill resource loading to use ovos-spec-tools while maintaining backwards compatibility via a mixin.
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.

✏️ 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 feat/drop-resource-files-usage

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.

JarbasAl and others added 3 commits May 23, 2026 02:23
Restore partial back-compat for downstream skills that still call
`self.resources.load_*` / `render_dialog` etc.: `self.resources` is now
a deprecated property that returns a legacy SkillResources, with a
DeprecationWarning pointing callers at the high-level skill methods
(speak_dialog, register_intent_file, voc_match, ...) or at
ovos_spec_tools.LocaleResources for low-level access.

Workshop's own internal code goes through a private property,
`OVOSSkill._locale_resources`, which returns the LocaleResources built
by load_lang() — no functional change to the rewire, just a separate
public-vs-internal handle. The SkillResources construction inside the
deprecated property is silenced for workshop-internal callers by the
_caller_is_internal guard from #412.

Test assertions for `self.skill.resources` updated to check
`self.skill._locale_resources` (the internal handle) for LocaleResources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restore the two OVOSSkill methods the rewire had removed, as
deprecated-but-functional shims:

- find_resource: delegates to ovos_workshop.resource_files.find_resource
  (itself deprecated, silenced for workshop-internal callers via the
  _caller_is_internal guard).
- load_regex_files: constructs a SkillResources directly (legacy path)
  and registers the .rx files with the adapt intent service, exactly as
  before. .rx is not part of the OVOS formal specifications, but skills
  shipping it still work through this shim.

Both emit a DeprecationWarning and log via @deprecated. UniversalSkill
._load_lang was an empty dead override and stays dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dedicated regression suite around .rx regex resource loading so
the deprecation timeline for legacy adapt-style regex intents stays
under CI control even though the rest of resource_files was dropped.

- unittests/skills/test_regex_loading.py: pins OVOSSkill.load_regex_files
  behaviour — silent on no-.rx trees, registers patterns with the
  alphanumeric skill-id prefix, emits a DeprecationWarning recommending
  padatious-style intents only when a file is actually loaded, logs the
  loaded path, raises on malformed patterns.

- end2end/test_regex_skill.py: ovoscope-driven E2E test — a skill ships a
  .rx file, an adapt IntentBuilder requires the regex's named group,
  inject_message of `recognizer_loop:utterance` causes the intent
  handler to fire and emit `speak`. This trips CI the day regex
  support breaks or is silently removed.

Also fixes load_regex_files locale discovery: walks locale/ subdirs and
matches each subdir name against native_langs via closest_lang, so an
en-US/ tree is picked up by a skill that declares only `en` as native
(previous code joined native_langs to locale/, missing region-tagged
subdirs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JarbasAl and others added 17 commits May 23, 2026 12:46
The hand-rolled locale-subdir walk in load_regex_files
(standardize_lang on each subdir name + closest_lang against
native_langs) is exactly what iter_locale_dirs encapsulates. Drop the
duplicated logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- pyproject: pin ovos-spec-tools>=0.1.0a1 (iter_locale_dirs / lang_matches).
- app.py, converse.py, ovos.py: replace every standardize_lang_tag with
  ovos_spec_tools.standardize_lang and drop the ovos_utils.lang import.
- skills/util.py + skills/ovos.py: collapse the duplicated locale-dir
  walk (list locale/, closest_lang, match best back to dir) into
  iter_locale_dirs + closest_lang from ovos-spec-tools.

resource_files.py keeps its own walk — the whole module is already
deprecated and scheduled for removal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier commits in this PR appended deprecation shims after their
neighbours, swapping find_resource and load_regex_files and sliding
load_lang past both. Restore the original layout — load_lang group,
load_regex_files, find_resource, then the new _locate_lang_file helper
— so the PR diff against origin/dev reads as additions/edits, not as
moves. Pure code-move commit; symmetric 87/87 line swap.
Bump the pin to >=0.2.0a1 and delegate _locate_lang_file to
LocaleResources.find. The override-precedence + closest-language walk
now lives in one place; workshop just keeps the str/Path adaptation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ources at __init__

dialog_renderer was reduced to returning None, breaking callers that
still do self.dialog_renderer.render(name, data). Restore the legacy
MustacheDialogRenderer (lazily built and reused via the same
_skill_resources_compat cache that backs self.resources) — the
deprecation message is unchanged, the return shape is restored.

_locale_resources was a property recomputing LocaleResources(self.res_dir)
on every access. The skill's res_dir is fixed at __init__, so bind the
instance there once and drop the property.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
self.resources, self.dialog_renderer and self.find_resource are kept
for one release while skills migrate to the spec-tools-backed path.
Move all three — and the _skill_resources_compat cache they share —
into ovos_workshop.skills._legacy_resources, which OVOSSkill inherits.

Ends the deprecation cycle in one removal: drop the mixin from
OVOSSkill's bases and delete the file. Nothing on OVOSSkill itself
references the legacy SkillResources path any more.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ixin

ovos-core deprecated runtime_requirements (network_requirements has
been deprecated for years). Move both classproperties into
_LegacyResourcesMixin so the whole 'pre-spec-tools' surface of
OVOSSkill — resources, dialog_renderer, find_resource,
runtime_requirements, network_requirements — disappears as one
removal: drop the mixin from the bases and delete the file.

Subclass overrides keep working: skill_launcher accesses the property
via hasattr/MRO so it does not care whether it lives on OVOSSkill
itself or on a base.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dialogs are loaded lazily by LocaleResources at render time, so the
old load_dialog_files entry point has nothing to do. Move the
backwards-compat no-op into the mixin alongside the other deprecated
surface — it disappears with the rest when the mixin is dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- pyproject: bump pin to >=0.3.0a1 (vocabulary_keywords, utterance_contains,
  strip_samples).
- load_vocab_files collapses from 17 lines to a 4-line loop over
  LocaleResources.vocabulary_keywords — also fixes a latent regression
  where each expanded sample was registered as its own (entity, [])
  pair, losing the OVOS-INTENT-2 §4.3 canonical/alias distinction.
- voc_match delegates the match logic to utterance_contains; the
  workshop method keeps the ensure_ascii single-flag back-compat
  (forwards to both strip_diacritics and strip_punct).
- remove_voc delegates to strip_samples — whole-word, longest-first.

Drops the local ovos_utils.text_utils.remove_accents_and_punct import
and the inline regex match/strip code now living in spec-tools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump pin to >=0.4.0a1 and route voc_list through
LocaleResources.voc_list — which returns [] for a missing resource —
so voc_match no longer needs the FileNotFoundError/log-warning
plumbing and drops to a single utterance_contains call. remove_voc
keeps its strip_samples one-liner.

The per-skill _voc_cache stays — caching is an instance concern
(some skills are short-lived, some long), and the spec-tools loader
has no notion of it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…imports

voc_match_cache is a back-compat accessor for the per-skill _voc_cache
dict; its setter already self-identified as deprecated. Move both
property and setter into the mixin so they disappear with the rest of
the legacy surface, and replace the stringly-typed log.warning with a
real DeprecationWarning.

Also drop now-unused imports from ovos.py: classproperty (only consumer
was runtime_requirements, which moved with the previous commit) and
isdir (only consumer was the hand-rolled locale-dir walk, now folded
into iter_locale_dirs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UniversalSkill authors .dialog / .voc / .intent / .entity files in
internal_language; intent handlers translate utterances into that
language and speak() translates back to the query lang. Pre-spec-tools
this worked through SkillResources(lang=internal_language); the
removal of _load_lang left every resource lookup defaulting to the
query lang instead — dialogs unresolved, voc_match silently False.

Introduce a single extension point on OVOSSkill — the _resource_lang
property, defaulting to self.lang. Every resource lookup on the skill
(render_dialog, voc_list, voc_match, remove_voc, the adapt
keyword/regex registrar) now consumes this property instead of going
direct to self.lang. UniversalSkill overrides _resource_lang to
return self.internal_language; the lang-routing fix is one property,
the entire resource surface follows.

Add an ovoscope tripwire (test/end2end/test_universal_skill.py)
covering the three legs of the contract:

* _resource_lang returns internal_language on a UniversalSkill;
* speak_dialog renders the en-US .dialog when the message's query
  lang is es-ES;
* voc_match matches the en-US affirmative.voc when the query lang
  is fr-FR.

The test stubs translate_utterance / translate_message on the skill
class because the ovoscope env has no translator plugin and we want
to pin the internal-language phrasing anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two long-latent bugs surface as soon as a UniversalSkill round-trip is
actually exercised in CI:

* UniversalSkill.speak called
  translate_utterance(text, sauce_lang, out_lang) — target / source
  positional args swapped. The result translated FROM the user's
  language TO internal, the wrong direction for an outbound reply, so
  English-only skills (Wolfram-style, animal facts, weather API)
  silently leaked English text to non-English users. Pass the args by
  name so the swap can not recur.

* translate_message indexed message.data['__tags__'] unconditionally;
  __tags__ is set by the adapt pipeline and absent under padacioso /
  padatious / direct event handlers, so any UniversalSkill receiving a
  non-adapt intent raised KeyError before the handler ran. Guard with
  a key-presence check.

Expand the ovoscope tripwire (test/end2end/test_universal_skill.py)
into a full Wolfram-style round-trip: the test skill ships an English
.intent file, exposes a fact.intent event for an English-only fake
API, and is invoked with a Portuguese slot capture. The assertion
chain pins (a) the .intent file was located via the en-US fixture
tree, (b) the handler saw the slot folded into internal_language by
translate_message before the callback fired, (c) the English API
reply came back in Portuguese via UniversalSkill.speak. translate_keys
on the test skill is extended to include the 'animal' slot so the
fold-in step covers the captured entity, not just the raw utterance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the synthetic intent-event injection with a real
recognizer_loop:utterance addressed to an English-only skill in
Portuguese. The skill ships pt-PT/fact.intent alongside en-US/, the
minicroft is configured with native_langs covering both, padacioso
matches the pt-PT template, the universal_intent_handler wrapper
folds the captured 'gatos' slot into internal_language ('cats') and
the handler runs purely in English (hits the English-only fake API,
returns an English fact). UniversalSkill.speak translates the reply
back to Portuguese for the user — the English internals stay hidden,
which is the point of the skill class.

Three failure modes the test covers:
* the pt-PT .intent does not register → no match, no speak;
* the inbound translation step is skipped → handler sees 'gatos',
  API misses, user gets the fallback;
* the outbound translation step is skipped or arg-swapped → user
  gets the English fact verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gion

PR OpenVoiceOS/ovos-bus-client#213 (merged into 2.0.0a4) migrated
Session.deserialize off ovos_utils.lang.standardize_lang_tag's
macro=True default, so a session lang of 'pt-PT' is no longer folded
to 'pt' on the way through the bus. Bump the workshop minimum so the
correct behaviour is guaranteed.

Keep the translation stub in the UniversalSkill ovoscope test
resilient to either bus-client release: fold lang tags to their
primary subtag at lookup time. The note in _FAKE_TRANSLATIONS now
points at the bus-client version that fixed the strip, not at a
fixture-side workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JarbasAl JarbasAl changed the base branch from feat/deprecate-resource-files to dev May 23, 2026 16:53
A fresh review of the PR found a subtle regression: __init__ was
constructing the bound LocaleResources from skill_locale alone
(LocaleResources(self.res_dir)), passing res_dir where the
constructor expects the skill's locale/ directory. load_lang() built
the proper three-source chain — user-data, skill, workshop core —
but every call site reading self._locale_resources (render_dialog,
voc_match, _locate_lang_file) went through the truncated instance.

Concrete impact:

* self.voc_match('cancel') would have failed to find workshop's own
  locale/en-US/cancel.voc — every skill relying on the core vocab
  (cancel.voc, skill.error.dialog, game_pause.dialog, …) silently
  broke;
* user resource overrides under <xdg>/resources/<skill_id>/ never
  resolved.

Build the bound instance with the same three sources load_lang() uses
and cache it under res_dir so load_lang(self.res_dir) returns the
same object — single source of truth, no divergence between the two
paths. Drop the now-redundant self._lang_resources = {} init.

Relocate the universal_locale fixture from flat <lang>/ to
locale/<lang>/ so it follows the OVOS-INTENT-2 §2.1 layout that
LocaleResources documents. The pre-review code path accidentally
worked with the flat layout because of the same skill_locale=res_dir
bug; with the bug fixed, the fixture must follow the spec.

Refresh the _LegacyResourcesMixin docstring — the file already grew
beyond the three properties the docstring claimed; it now lists every
surface in the mixin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2026

The code stars have aligned. Here's the update. ✨

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

📋 Repo Health

A detailed health report for the project. 📝

✅ All required files present.

Latest Version: 8.2.0a1

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

🔍 Lint

Checking the alignment of your contribution. 📏

ruff: issues found — see job log

🔨 Build Tests

I tried building your changes, and here's what happened! 🔨

Python Build Install Tests
3.10 ⚠️
3.11 ⚠️
3.12 ⚠️
3.13 ⚠️
3.14 ⚠️

❌ 3.10: Install OK, tests failed
❌ 3.11: Install OK, tests failed
❌ 3.12: Install OK, tests failed
❌ 3.13: Install OK, tests failed
❌ 3.14: Install OK, tests failed
Check job logs for details.

🔒 Security (pip-audit)

Ensuring our digital fortress remains impenetrable. 🏰

✅ No known vulnerabilities found (68 packages scanned).

⚖️ License Check

Scanning for any potential trademark infringements. ™️

✅ No license violations found (49 packages).

License distribution: 12× MIT License, 9× 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 — 49 packages
Package Version License URL
audioop-lts 0.2.2 PSF-2.0 link
build 1.5.0 MIT link
certifi 2026.5.20 Mozilla Public License 2.0 (MPL 2.0) link
charset-normalizer 3.4.7 MIT link
click 8.4.1 BSD-3-Clause link
combo_lock 0.3.1 Apache-2.0 link
filelock 3.29.0 MIT link
idna 3.16 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.2.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.0a2 Apache-2.0 link
ovos-spec-tools 0.6.0a1 Apache Software License link
ovos-utils 0.11.1a1 Apache-2.0 link
ovos-workshop 8.2.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.2 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.5.9 Apache-2.0 AND CNRI-Python link
requests 2.34.2 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.7.0 | MIT | link |
| watchdog | 6.0.0 | Apache Software License | link |
| websocket-client | 1.9.0 | Apache Software License | link |
| zipp | 4.1.0 | MIT | link |

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


Processing... Done! Have a productive day! ☕

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
ovos_workshop/skills/ovos.py (1)

612-612: 💤 Low value

Regex named-group prefixing may produce invalid group names.

The prefixing logic on line 634 produces (?P<skillid_(?P<name> when splitting on (?P< and joining with the unique prefix. This looks correct for transforming (?P<name>...) into (?P<skillid_name>...). However, the unique_prefix on line 612 is "(?P<" + self.alphanumeric_skill_id (no trailing underscore), so a pattern (?P<foo>...) becomes (?P<skillid_foo>...) which concatenates the skill ID directly with the group name.

If alphanumeric_skill_id ends with a digit and the group name starts with a digit, this could produce an invalid Python regex group name (group names must start with a letter or underscore). Consider adding an underscore separator.

Proposed fix
-        unique_prefix = "(?P<" + self.alphanumeric_skill_id
+        unique_prefix = "(?P<" + self.alphanumeric_skill_id + "_"

Also applies to: 634-634

🤖 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/ovos.py` at line 612, The current unique_prefix
construction uses "(?P<" + self.alphanumeric_skill_id without a separator which
can create invalid regex group names when concatenated; update the unique_prefix
used when splitting/joining on "(?P<" (referenced as unique_prefix and the
split/join logic around self.alphanumeric_skill_id) to insert a single
underscore between the skill id and the original group name (e.g. ensure
unique_prefix becomes "(?P<" + self.alphanumeric_skill_id + "_" but avoid double
underscores by only adding the underscore if self.alphanumeric_skill_id does not
already end with one).
test/end2end/test_universal_skill.py (1)

83-93: ⚡ Quick win

Prefer passing internal_language via kwargs before super().__init__ for safer arg binding.

This avoids brittle binding when positional args are present and keeps the constructor robust to upstream call-shape changes.

Proposed refactor
     def __init__(self, *args, **kwargs):
         # resources_dir is honoured at __init__ time (before _startup
         # runs initialize); pass it through so register_intent_file
         # finds fact.intent in the fixture tree.
         kwargs.setdefault("resources_dir", _FIXTURE_DIR)
         # The handler reads a captured ``animal`` slot; tell
         # translate_message to fold that key into internal_language too,
         # alongside the default ``utterance`` / ``utterances`` keys.
         kwargs.setdefault("translate_keys",
                           ["animal", "utterance", "utterances"])
-        super().__init__(internal_language="en-US", *args, **kwargs)
+        kwargs["internal_language"] = "en-US"
+        super().__init__(*args, **kwargs)
🤖 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 `@test/end2end/test_universal_skill.py` around lines 83 - 93, The constructor
currently passes internal_language as a positional/keyword arg to
super().__init__ which can be brittle; instead set kwargs["internal_language"] =
"en-US" (or kwargs.setdefault("internal_language", "en-US")) before calling
super().__init__, preserve the existing kwargs.setdefault("resources_dir",
_FIXTURE_DIR") and kwargs.setdefault("translate_keys", ["animal", "utterance",
"utterances"]) logic, then call super().__init__(*args, **kwargs) so binding is
stable; update the __init__ method accordingly (refer to __init__,
resources_dir, translate_keys, and the super().__init__ call).
🤖 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 `@pyproject.toml`:
- Line 16: The dependency constraint for "ovos_bus_client>=2.0.0a4" is too loose
and may pull incompatible 2.x/3.x releases; update the version specifier in
pyproject.toml for the "ovos_bus_client" entry to a bounded range such as
">=2.0.0a4,<2.0.0" (or mirror ovos-workshop’s constraint like "<2.0.0,>=1.3.8a1"
if you prefer exact alignment) so the package manager cannot install
incompatible major releases.

In `@test/unittests/skills/test_regex_loading.py`:
- Around line 79-81: The assertion's warning filter is checking for "regex" but
elsewhere the test expects a "padatious" deprecation; update the list
comprehension that builds the filtered warnings (the expression using caught and
issubclass(w.category, DeprecationWarning)) to check for "padatious" in
str(w.message).lower() instead of "regex" (to make the filter match the actual
deprecation text being asserted later).
- Around line 38-45: Tests overwrite the XDG_CONFIG_HOME env var but always
remove it in tearDownClass, which can delete a pre-existing value; modify
setUpClass to save the prior os.environ.get("XDG_CONFIG_HOME") (e.g., store on
cls as cls._old_xdg_config_home) before setting cls._tmp_config, and update
tearDownClass to restore the original value: if cls._old_xdg_config_home is None
remove the env var, otherwise set os.environ["XDG_CONFIG_HOME"] back to
cls._old_xdg_config_home; reference the setUpClass/tearDownClass methods and
cls._tmp_config/cls._old_xdg_config_home when making the change.

---

Nitpick comments:
In `@ovos_workshop/skills/ovos.py`:
- Line 612: The current unique_prefix construction uses "(?P<" +
self.alphanumeric_skill_id without a separator which can create invalid regex
group names when concatenated; update the unique_prefix used when
splitting/joining on "(?P<" (referenced as unique_prefix and the split/join
logic around self.alphanumeric_skill_id) to insert a single underscore between
the skill id and the original group name (e.g. ensure unique_prefix becomes
"(?P<" + self.alphanumeric_skill_id + "_" but avoid double underscores by only
adding the underscore if self.alphanumeric_skill_id does not already end with
one).

In `@test/end2end/test_universal_skill.py`:
- Around line 83-93: The constructor currently passes internal_language as a
positional/keyword arg to super().__init__ which can be brittle; instead set
kwargs["internal_language"] = "en-US" (or kwargs.setdefault("internal_language",
"en-US")) before calling super().__init__, preserve the existing
kwargs.setdefault("resources_dir", _FIXTURE_DIR") and
kwargs.setdefault("translate_keys", ["animal", "utterance", "utterances"])
logic, then call super().__init__(*args, **kwargs) so binding is stable; update
the __init__ method accordingly (refer to __init__, resources_dir,
translate_keys, and the super().__init__ call).
🪄 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: 4476971c-a5a7-496b-9fd7-55262cac9467

📥 Commits

Reviewing files that changed from the base of the PR and between 779e54b and bd053e8.

📒 Files selected for processing (20)
  • .gitignore
  • ovos_workshop/app.py
  • ovos_workshop/resource_files.py
  • ovos_workshop/skills/_legacy_resources.py
  • ovos_workshop/skills/auto_translatable.py
  • ovos_workshop/skills/converse.py
  • ovos_workshop/skills/ovos.py
  • ovos_workshop/skills/util.py
  • pyproject.toml
  • test/end2end/__init__.py
  • test/end2end/locale/en-US/play.rx
  • test/end2end/test_regex_skill.py
  • test/end2end/test_universal_skill.py
  • test/end2end/universal_locale/locale/en-US/affirmative.voc
  • test/end2end/universal_locale/locale/en-US/echo.dialog
  • test/end2end/universal_locale/locale/en-US/fact.intent
  • test/end2end/universal_locale/locale/pt-PT/fact.intent
  • test/unittests/skills/test_base.py
  • test/unittests/skills/test_ovos.py
  • test/unittests/skills/test_regex_loading.py

Comment thread pyproject.toml Outdated
Comment thread test/unittests/skills/test_regex_loading.py
Comment thread test/unittests/skills/test_regex_loading.py Outdated
JarbasAl and others added 2 commits May 23, 2026 19:07
…k as a property

Two follow-ups to the review fixes:

1. _locale_resources is now a property delegating to load_lang(self.res_dir)
   rather than an attribute bound at __init__. test_base mutates res_dir
   on a constructed skill (the legitimate pattern for fixture-driven tests
   and live-reload skills), and the init-time binding silently swallowed
   those mutations. load_lang caches per root_directory, so the property
   stays O(1) on the hot path; res_dir changes are picked up on the
   next access.

2. test/unittests/skills/test_legacy_resources.py — 23 new tests pinning
   three layers of the contract this PR establishes:

   * OVOSSkill resource plumbing — _locale_resources is a LocaleResources
     instance, picks up res_dir mutation, threads the workshop-core
     locale source (catches the bug the earlier review uncovered);
     render_dialog renders from .dialog files and returns its argument
     verbatim on miss; _resource_lang defaults to self.lang and
     subclass overrides propagate to voc_list lookups.

   * _LegacyResourcesMixin shims — self.resources / self.dialog_renderer
     / self.find_resource / self.voc_match_cache setter / self.load_dialog_files
     all keep working, emit DeprecationWarning, and share one cached
     SkillResources so they do not drift.

   * Legacy file formats in the wild — .qml, .json, .list, .value,
     .word, .template still load through the deprecated self.resources
     accessor; .qml resolves through self.find_resource. No OVOS-INTENT-2
     replacement exists for these so back-compat through the mixin is
     the contract.

   Plus one §2.1 precedence test: a user file under
   <xdg-data>/resources/<skill_id>/<lang>/cancel.voc overrides workshop's
   bundled core cancel.voc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…omments

pyproject.toml: revert ovos_bus_client to >=1.3.8a1,<2.0.0 — ovos-plugin-manager
2.4.x requires <2.0.0 so the >=2.0.0a4 lower-bound made the dep graph
unresolvable, breaking all CI install steps across every Python version.

test/end2end/test_universal_skill.py:
- update comment to not claim the pin requires a specific bus-client version
- move internal_language into kwargs before super().__init__ call for robust
  positional arg binding (CodeRabbit actionable)

test/unittests/skills/test_regex_loading.py:
- preserve pre-existing XDG_CONFIG_HOME in setUpClass/tearDownClass so a real
  value already in the environment is restored rather than deleted (CodeRabbit
  actionable)
- use "padatious" instead of "regex" in the no-warning filter to match the
  actual deprecation message text, consistent with the positive assertion below
  (CodeRabbit actionable)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JarbasAl JarbasAl changed the title feat!: drop ovos_workshop.resource_files usage from OVOSSkill refactor: route OVOSSkill resource loading through ovos-spec-tools (back-compat) May 25, 2026
JarbasAl and others added 5 commits May 25, 2026 18:58
…k requirements shims

Floor bumps:
- ovos-utils: 0.7.0 -> 0.11.1a1 (region-preserving standardize_lang_tag
  fix #377; without it OVOSAbstractApplication.get_language_dir and the
  10+ standardize_lang_tag(macro=True) call sites in ovos.py silently
  strip the region from en-US et al, which breaks per-region locale
  lookups and final-session equality checks downstream).
- ovos-spec-tools: 0.4.0a1 -> 0.5.1a1 (the release that ships
  LocaleResources.find/load_blacklist/load_vocabulary semantics this PR
  now relies on, plus the empty-msg_type construction fix). Adds the
  matching <1.0.0 upper bound to align with house style.

Tests:
- TestRuntimeRequirementsShim — three cases covering the classproperty
  shim that was added to _LegacyResourcesMixin without dedicated
  coverage: default returns a RuntimeRequirements, subclass override
  is honoured (the LAN/cache/offline pattern that historic skills
  use), network_requirements legacy alias delegates correctly.

26 passed in the legacy-resources suite (was 23).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t-file registration

Two new ovoscope tests covering the highest-risk silent-failure
paths the spec-tools migration introduces:

- test/end2end/test_speak_dialog.py — drives the
  speak_dialog -> render_dialog -> LocaleResources.load_dialog ->
  ovos_spec_tools.render bus chain. Fixture greet.dialog uses both a
  {name} slot AND a <salutation> voc reference, so a clean render
  proves slot substitution AND voc-reference resolution AND
  vocabularies() loading all work end-to-end. If any link broke the
  speak message would carry the raw template (or the dialog
  filename); the assert pins both substitutions.

- test/end2end/test_padatious_intent_file.py — drives
  register_intent_file -> _locate_lang_file (the new private path
  resolver) -> padatious registration -> bus match. _locate_lang_file
  is brand new in this PR; a silent miss returns None and padatious
  registers nothing — the intent never fires and the test catches it.
  Pipeline pinned to padatious so the failure mode is unambiguous;
  skipped via ovoscope.is_pipeline_available when padatious isn't
  installed in the test env.

Local: speak_dialog test passes; padatious test skips (no padatious
in the workshop venv). CI installs padatious (require_padatious=true
on the ovoscope workflow) so both will run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tch coverage

Padacioso is always available (pure Python, no swig/fann), so the
intent-file e2e test no longer needs a skip-guard or
`require_padatious` on the workflow.

- test_padacioso_intent_file.py — renamed from
  test_padatious_intent_file.py; same skill + utterance + assertion,
  pipeline pinned to padacioso. Always runs.
- test_padacioso_entity_file.py (new) — exercises
  register_entity_file end-to-end. Skill registers
  `drink.entity` (coffee/tea/juice) + `order.intent` with a
  {drink} slot; injects "i want a coffee" and asserts the
  handler's echo carries `coffee` in the speak — proves the
  `.entity` resource resolved through `_locate_lang_file` and
  padacioso constrained the slot fill correctly. If the entity
  registration is silently dropped, the bare {drink} slot either
  fails to match or captures the wrong span.
- test_voc_match.py (new) — exercises
  `OVOSSkill.voc_match` end-to-end against
  `locale/en-US/yes.voc`. Two assertions: in-vocabulary phrase
  returns True, out-of-vocabulary returns False. Pins the rewired
  `voc_match -> voc_list -> LocaleResources.load_vocabulary ->
  _voc_cache` chain — the workhorse keyword helper used by
  stop/cancel/confirm flows across the ecosystem.

Dropped a get_response sketch — `get_response` activates listening
and requires more session state than the test rig drives cleanly;
the underlying render_dialog chain is already covered end-to-end
by test_speak_dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…le role

One fixture skill that ships every legacy resource file type the
SkillResources surface still understands, and one initialize() that
calls every deprecated entry point on _LegacyResourcesMixin. If any
one shim regresses, the omnibus skill fails to construct or its
initialize() raises — failure mode is unambiguous and the bisect
target is one file.

Covers, in one skill:

- Class-level shims: runtime_requirements override (offline-skill
  pattern), network_requirements alias.
- Property shims: resources (legacy SkillResources handle),
  dialog_renderer (legacy MustacheDialogRenderer),
  voc_match_cache (getter + external-mutation setter — distinct
  call paths, distinct deprecation warnings).
- Method shims: load_dialog_files no-op, find_resource.
- Legacy resource roles via self.resources.load_*_file:
  .list, .value, .template, .word, .json, .voc (vocabulary).
- UI resource via find_resource: .qml outside the locale tree.

13 assertions in one TestCase: one per surface, plus one that
re-runs the omnibus in a recorded-warnings context and pins each
expected DeprecationWarning substring (4 of them — resources,
dialog_renderer, find_resource, voc_match_cache setter).

Note on the fixture pattern: passing bus + skill_id to OVOSSkill
triggers _startup which calls initialize, so resources_dir MUST be
a constructor kwarg (post-init res_dir mutation is too late and
SkillResources caches against the wrong path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t slot fill

.entity files in OVOS are training-data hints for the intent engine,
not strict slot constraints — an open {slot} captures any token even
without a sibling .entity. The previous test asserted on the slot
fill (`'i want a coffee'` -> handler echoing 'coffee'), which would
pass even if register_entity_file did nothing.

Rewrite to assert on the registration plumbing instead — the part
that actually exercises _locate_lang_file for .entity:

- Skill registers drink.entity on a bus trigger (so the test can
  subscribe BEFORE the registration fires).
- Test subscribes to padatious:register_entity, fires the trigger.
- Asserts file_name ends with drink.entity (proves
  _locate_lang_file resolved the .entity file to a real path).
- Asserts samples == [coffee, juice, tea] (proves the OVOS-side
  entity reader opened the resolved path and parsed it).

Drops the now-unused order.intent fixture and tightens imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
test/unittests/skills/test_legacy_skill_omnibus.py (1)

246-252: ⚡ Quick win

Include load_dialog_files in deprecation-warning assertions to match the omnibus claim.

initialize() explicitly exercises load_dialog_files(), but this warning test does not assert its deprecation signal, leaving a coverage hole in the “every legacy entry point” tripwire.

Suggested patch
         for needle in ("self.resources is deprecated",
+                       "load_dialog_files",
                        "dialog_renderer is deprecated",
                        "find_resource is deprecated",
                        "voc_match_cache external mutation is deprecated"):
🤖 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 `@test/unittests/skills/test_legacy_skill_omnibus.py` around lines 246 - 252,
The test's deprecation assertion loop omits the warning for load_dialog_files
even though initialize() calls it; update the assertions in
test_legacy_skill_omnibus.py to include "load_dialog_files is deprecated" in the
tuple of needles being checked so the test verifies that load_dialog_files()
emits a DeprecationWarning (reference the initialize() call and the
load_dialog_files symbol to locate where the messages list is checked).
🤖 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 `@test/end2end/test_padacioso_entity_file.py`:
- Around line 68-70: The test currently sets the completion gate unconditionally
when any "padatious:register_entity" message arrives; modify the _on_register
callback to inspect msg.data and only call registered.set() and update
registration when the payload matches the expected entity attributes (e.g.,
expected entity name/type/intents) to avoid racing with unrelated bus traffic;
apply the same conditional gating change to the other handler referenced around
lines 82-87 so both handlers validate msg.data before marking the test complete.

In `@test/end2end/test_padacioso_intent_file.py`:
- Around line 65-67: The on_speak handler currently sets speak_event for any
speak message, causing races; update the on_speak function (the callback named
on_speak that appends to seen_speaks and calls speak_event.set()) to first
extract the utterance via msg.data.get("utterance", "") and only call
speak_event.set() when that utterance matches the expected text for the test
(e.g., equals or contains the expected_utterance variable); still append the
utterance to seen_speaks for logging, but guard speak_event.set() behind the
match. Apply the same change to the second speaker handler in the block that
mirrors lines 83-90.

In `@test/end2end/test_speak_dialog.py`:
- Around line 64-67: The on_speak handler currently sets speak_event on the
first speak message and appends any utterance to seen_speaks; change it to only
append and set speak_event when the payload matches the expected utterance(s)
(e.g., inspect msg.data.get("utterance") and compare to the expected greeting),
so unrelated speak traffic won't trigger the test; update the on_speak function
and the corresponding second handler (the block around lines 79-91) to perform
this exact-match (or allowed-set) check before calling speak_event.set() and
adding to seen_speaks.

In `@test/unittests/skills/test_legacy_skill_omnibus.py`:
- Around line 170-173: tearDownClass currently calls cls.fixture.cleanup()
without stopping the class-scoped skill started in setUpClass; call the skill's
shutdown/stop method (the instance of _LegacyOmnibusSkill created in setUpClass)
before deleting fixture files so bus handlers and internal state are torn down.
Locate the class-level skill instance created in setUpClass (referenced as
_LegacyOmnibusSkill or whatever class attribute holds the skill) and invoke its
shutdown/stop/close method (e.g., skill.shutdown() or skill.stop()) prior to
calling cls.fixture.cleanup() in tearDownClass.

---

Nitpick comments:
In `@test/unittests/skills/test_legacy_skill_omnibus.py`:
- Around line 246-252: The test's deprecation assertion loop omits the warning
for load_dialog_files even though initialize() calls it; update the assertions
in test_legacy_skill_omnibus.py to include "load_dialog_files is deprecated" in
the tuple of needles being checked so the test verifies that load_dialog_files()
emits a DeprecationWarning (reference the initialize() call and the
load_dialog_files symbol to locate where the messages list is checked).
🪄 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: 1c47810a-7a4e-406e-bc1b-6d73222b4801

📥 Commits

Reviewing files that changed from the base of the PR and between 257c781 and 9b26d79.

📒 Files selected for processing (10)
  • test/end2end/locale/en-US/drink.entity
  • test/end2end/locale/en-US/greet.dialog
  • test/end2end/locale/en-US/salutation.voc
  • test/end2end/locale/en-US/wave.intent
  • test/end2end/locale/en-US/yes.voc
  • test/end2end/test_padacioso_entity_file.py
  • test/end2end/test_padacioso_intent_file.py
  • test/end2end/test_speak_dialog.py
  • test/end2end/test_voc_match.py
  • test/unittests/skills/test_legacy_skill_omnibus.py
✅ Files skipped from review due to trivial changes (5)
  • test/end2end/locale/en-US/salutation.voc
  • test/end2end/locale/en-US/wave.intent
  • test/end2end/locale/en-US/greet.dialog
  • test/end2end/locale/en-US/yes.voc
  • test/end2end/locale/en-US/drink.entity

Comment on lines +68 to +70
def _on_register(msg):
registration.update(msg.data)
registered.set()
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate completion on the expected entity registration payload.

Line 68-Line 70 currently marks the test complete on the first padatious:register_entity message. That can race with unrelated bus traffic and make this test flaky.

✅ Suggested fix
         def _on_register(msg):
-            registration.update(msg.data)
-            registered.set()
+            data = msg.data or {}
+            file_name = data.get("file_name", "")
+            if file_name.endswith("drink.entity"):
+                registration.update(data)
+                registered.set()

Also applies to: 82-87

🤖 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 `@test/end2end/test_padacioso_entity_file.py` around lines 68 - 70, The test
currently sets the completion gate unconditionally when any
"padatious:register_entity" message arrives; modify the _on_register callback to
inspect msg.data and only call registered.set() and update registration when the
payload matches the expected entity attributes (e.g., expected entity
name/type/intents) to avoid racing with unrelated bus traffic; apply the same
conditional gating change to the other handler referenced around lines 82-87 so
both handlers validate msg.data before marking the test complete.

Comment on lines +65 to +67
def on_speak(msg):
seen_speaks.append(msg.data.get("utterance", ""))
speak_event.set()
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Filter speak events to the expected utterance before setting the wait event.

Line 65-Line 67 currently sets speak_event on any speak message. This can cause false negatives if unrelated speech arrives first.

✅ Suggested fix
         def on_speak(msg):
-            seen_speaks.append(msg.data.get("utterance", ""))
-            speak_event.set()
+            utterance = msg.data.get("utterance", "")
+            seen_speaks.append(utterance)
+            if "waving back" in utterance:
+                speak_event.set()

Also applies to: 83-90

🤖 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 `@test/end2end/test_padacioso_intent_file.py` around lines 65 - 67, The
on_speak handler currently sets speak_event for any speak message, causing
races; update the on_speak function (the callback named on_speak that appends to
seen_speaks and calls speak_event.set()) to first extract the utterance via
msg.data.get("utterance", "") and only call speak_event.set() when that
utterance matches the expected text for the test (e.g., equals or contains the
expected_utterance variable); still append the utterance to seen_speaks for
logging, but guard speak_event.set() behind the match. Apply the same change to
the second speaker handler in the block that mirrors lines 83-90.

Comment on lines +64 to +67
def on_speak(msg):
seen_speaks.append(msg.data.get("utterance", ""))
speak_event.set()

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wait for the expected speak payload, not just any speak event.

Line 64-Line 67 sets the event on the first speak message. If unrelated speak traffic appears, this test can fail before the greeting utterance is emitted.

✅ Suggested fix
         def on_speak(msg):
-            seen_speaks.append(msg.data.get("utterance", ""))
-            speak_event.set()
+            utterance = msg.data.get("utterance", "")
+            seen_speaks.append(utterance)
+            if "Alice" in utterance and "welcome aboard" in utterance:
+                speak_event.set()

Also applies to: 79-91

🤖 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 `@test/end2end/test_speak_dialog.py` around lines 64 - 67, The on_speak handler
currently sets speak_event on the first speak message and appends any utterance
to seen_speaks; change it to only append and set speak_event when the payload
matches the expected utterance(s) (e.g., inspect msg.data.get("utterance") and
compare to the expected greeting), so unrelated speak traffic won't trigger the
test; update the on_speak function and the corresponding second handler (the
block around lines 79-91) to perform this exact-match (or allowed-set) check
before calling speak_event.set() and adding to seen_speaks.

Comment on lines +170 to +173
@classmethod
def tearDownClass(cls):
cls.fixture.cleanup()

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Shut down the class-scoped skill in tearDownClass before deleting fixture files.

setUpClass fully starts _LegacyOmnibusSkill; only deleting fixture.root can leave bus handlers/internal state alive past this test class. Call shutdown first for cleaner test isolation.

Suggested patch
     `@classmethod`
     def tearDownClass(cls):
-        cls.fixture.cleanup()
+        try:
+            if hasattr(cls, "skill"):
+                cls.skill.default_shutdown()
+        finally:
+            cls.fixture.cleanup()
🤖 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 `@test/unittests/skills/test_legacy_skill_omnibus.py` around lines 170 - 173,
tearDownClass currently calls cls.fixture.cleanup() without stopping the
class-scoped skill started in setUpClass; call the skill's shutdown/stop method
(the instance of _LegacyOmnibusSkill created in setUpClass) before deleting
fixture files so bus handlers and internal state are torn down. Locate the
class-level skill instance created in setUpClass (referenced as
_LegacyOmnibusSkill or whatever class attribute holds the skill) and invoke its
shutdown/stop/close method (e.g., skill.shutdown() or skill.stop()) prior to
calling cls.fixture.cleanup() in tearDownClass.

The hand-rolled exact-match short-circuit + closest_lang fallback
collapses to one call now that the helper is public
(ovos-spec-tools 0.6.0a1).

- pyproject: bump ovos-spec-tools floor 0.5.1a1 -> 0.6.0a1.
- app.py: replace the 30+ line implementation with a delegating
  one-liner; drop the local os.listdir / isdir walk and the
  closest_lang/standardize_lang case-fallback dance. Smart-fallback
  policy now lives in one place (OVOS-INTENT-2 §2.2) so workshop
  and any other consumer get the same behaviour.

Signature, return type and case-handling all preserved — the helper
covers exact-case match, case mismatch, bare-language requests, and
minor regional fallback through the same predicate the hand-rolled
version was approximating.

190 passed in the unit + e2e subset that exercises get_language_dir
and its callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant