Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 6 additions & 116 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,130 +1,20 @@
# Changelog

## [0.10.1a1](https://github.com/OpenVoiceOS/nebulento/tree/0.10.1a1) (2026-08-11)
## [0.10.3a1](https://github.com/OpenVoiceOS/nebulento/tree/0.10.3a1) (2026-08-13)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.10.0a3...0.10.1a1)
[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.10.2a1...0.10.3a1)

**Merged pull requests:**

- fix: tolerate malformed templates during intent registration [\#43](https://github.com/OpenVoiceOS/nebulento/pull/43) ([JarbasAl](https://github.com/JarbasAl))
- fix: dealias munged workshop entity names before container registration [\#48](https://github.com/OpenVoiceOS/nebulento/pull/48) ([JarbasAl](https://github.com/JarbasAl))

## [0.10.0a3](https://github.com/OpenVoiceOS/nebulento/tree/0.10.0a3) (2026-08-11)
## [0.10.2a1](https://github.com/OpenVoiceOS/nebulento/tree/0.10.2a1) (2026-08-12)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.10.0a2...0.10.0a3)
[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.10.1...0.10.2a1)

**Merged pull requests:**

- docs: clarify that the OVOS pipeline plugin needs the \[ovos\] extra [\#41](https://github.com/OpenVoiceOS/nebulento/pull/41) ([JarbasAl](https://github.com/JarbasAl))

## [0.10.0a2](https://github.com/OpenVoiceOS/nebulento/tree/0.10.0a2) (2026-08-11)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.10.0a1...0.10.0a2)

**Merged pull requests:**

- docs: rewrite README in Simplified Technical English [\#40](https://github.com/OpenVoiceOS/nebulento/pull/40) ([JarbasAl](https://github.com/JarbasAl))

## [0.10.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.10.0a1) (2026-07-03)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.9.0a1...0.10.0a1)

**Merged pull requests:**

- feat: OVOS-CONTEXT-1 §7 context slot fill + OVOS-INTENT-2 §4.3 blacklist [\#38](https://github.com/OpenVoiceOS/nebulento/pull/38) ([JarbasAl](https://github.com/JarbasAl))

## [0.9.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.9.0a1) (2026-07-02)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.8.0a1...0.9.0a1)

**Merged pull requests:**

- feat: enforce OVOS-CONTEXT-1 requires/excludes\_context gating [\#36](https://github.com/OpenVoiceOS/nebulento/pull/36) ([JarbasAl](https://github.com/JarbasAl))

## [0.8.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.8.0a1) (2026-06-27)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.7.0a1...0.8.0a1)

**Merged pull requests:**

- feat: consume OVOS-INTENT-4 template registration \(alongside legacy\) [\#33](https://github.com/OpenVoiceOS/nebulento/pull/33) ([JarbasAl](https://github.com/JarbasAl))

## [0.7.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.7.0a1) (2026-06-25)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.6.0a2...0.7.0a1)

**Merged pull requests:**

- feat: massive-templates benchmark, ovos-spec-tools migration, benchmark cleanup [\#28](https://github.com/OpenVoiceOS/nebulento/pull/28) ([JarbasAl](https://github.com/JarbasAl))

## [0.6.0a2](https://github.com/OpenVoiceOS/nebulento/tree/0.6.0a2) (2026-06-13)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.6.0a1...0.6.0a2)

**Merged pull requests:**

- docs: standardize NGI0 Commons Fund attribution [\#30](https://github.com/OpenVoiceOS/nebulento/pull/30) ([JarbasAl](https://github.com/JarbasAl))

## [0.6.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.6.0a1) (2026-05-23)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.5.0a1...0.6.0a1)

**Merged pull requests:**

- feat: migrate to ovos-spec-tools [\#27](https://github.com/OpenVoiceOS/nebulento/pull/27) ([JarbasAl](https://github.com/JarbasAl))

## [0.5.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.5.0a1) (2026-05-22)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.4.0a1...0.5.0a1)

**Merged pull requests:**

- feat: add HierarchicalNebulentoPipeline for two-stage intent matching [\#25](https://github.com/OpenVoiceOS/nebulento/pull/25) ([JarbasAl](https://github.com/JarbasAl))

## [0.4.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.4.0a1) (2026-05-14)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.3.0a4...0.4.0a1)

**Merged pull requests:**

- feat: ovoscope end-to-end tests for NebulentoPipeline [\#23](https://github.com/OpenVoiceOS/nebulento/pull/23) ([JarbasAl](https://github.com/JarbasAl))

## [0.3.0a4](https://github.com/OpenVoiceOS/nebulento/tree/0.3.0a4) (2026-05-13)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.3.0a3...0.3.0a4)

**Merged pull requests:**

- Feat/ovoscope e2e tests [\#21](https://github.com/OpenVoiceOS/nebulento/pull/21) ([JarbasAl](https://github.com/JarbasAl))

## [0.3.0a3](https://github.com/OpenVoiceOS/nebulento/tree/0.3.0a3) (2026-05-13)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.3.0a2...0.3.0a3)

## [0.3.0a2](https://github.com/OpenVoiceOS/nebulento/tree/0.3.0a2) (2026-05-13)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.3.0a1...0.3.0a2)

**Merged pull requests:**

- Update marocchino/sticky-pull-request-comment action to v3 [\#17](https://github.com/OpenVoiceOS/nebulento/pull/17) ([renovate[bot]](https://github.com/apps/renovate))
- Update actions/setup-python action to v6 [\#16](https://github.com/OpenVoiceOS/nebulento/pull/16) ([renovate[bot]](https://github.com/apps/renovate))
- Update actions/checkout action to v6 [\#15](https://github.com/OpenVoiceOS/nebulento/pull/15) ([renovate[bot]](https://github.com/apps/renovate))

## [0.3.0a1](https://github.com/OpenVoiceOS/nebulento/tree/0.3.0a1) (2026-05-12)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.2.1a1...0.3.0a1)

**Merged pull requests:**

- feat: ovoscope end-to-end tests for NebulentoPipeline [\#14](https://github.com/OpenVoiceOS/nebulento/pull/14) ([JarbasAl](https://github.com/JarbasAl))

## [0.2.1a1](https://github.com/OpenVoiceOS/nebulento/tree/0.2.1a1) (2026-05-12)

[Full Changelog](https://github.com/OpenVoiceOS/nebulento/compare/0.2.0...0.2.1a1)

**Merged pull requests:**

- docs: NLNet credit and pipeline plugin tests [\#10](https://github.com/OpenVoiceOS/nebulento/pull/10) ([JarbasAl](https://github.com/JarbasAl))
- fix: dynamic pyproject version so alpha publishes actually track version.py [\#46](https://github.com/OpenVoiceOS/nebulento/pull/46) ([JarbasAl](https://github.com/JarbasAl))



Expand Down
42 changes: 38 additions & 4 deletions nebulento/opm.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,38 @@

from nebulento import HierarchicalIntentContainer, IntentContainer, MatchStrategy

# ovos-workshop's ``register_entity_file`` builds the entity name as
# ``<skill_id>:<basename>_<md5(entity_file)>``. Nebulento's own slot-tagging
# contract (see IntentContainer.add_entity / match_fuzzy) requires the
# registered entity name to be the *bare* token that appears literally as
# ``{token}`` inside the skill's own intent templates — it never carries a
# skill_id prefix, since templates are written as e.g. ``"drive to {place}"``,
# not ``"drive to {skill_id:place}"``. A munged (skill_id-prefixed and/or
# hash-suffixed) registration name can therefore never satisfy the
# ``"{" + ent + "}" in sample`` check in ``match_fuzzy``, so the entity is
# found in the sentence but never tagged into the returned match_data —
# the same wildcard/orphaned-slot bug class fixed for padatious in
# ovos-padatious-pipeline-plugin#95, just with an additional (pre-existing)
# skill_id-prefix mismatch on top of the hash suffix.
_ENTITY_HASH_SUFFIX = re.compile(r"_[0-9a-f]{32}$")


def _dealias_entity_name(name: Optional[str]) -> Optional[str]:
"""Fold a (possibly skill_id-prefixed, hash-suffixed) entity
registration name onto the bare token nebulento's own container expects.

Collapsing here — at the registration/removal boundary this plugin owns —
repairs every emitter vintage, including deployed ovos-workshop releases
that will keep emitting the munged name.
"""
if not name:
return name
if ":" in name:
name = name.rsplit(":", 1)[1]
if name.endswith(".entity"):
name = name[:-len(".entity")]
return _ENTITY_HASH_SUFFIX.sub("", name)


class NebulentoIntent:
"""
Expand Down Expand Up @@ -254,15 +286,15 @@ def _add_intent(self, container: IntentContainer, name: str,
def _add_entity(self, container: IntentContainer, name: str,
samples: List[str]) -> None:
"""Register entity *name* with *samples* on *container*."""
container.add_entity(name, samples)
container.add_entity(_dealias_entity_name(name), samples)

def _remove_intent(self, container: IntentContainer, name: str) -> None:
"""Remove intent *name* from *container*."""
container.remove_intent(name)

def _remove_entity(self, container: IntentContainer, name: str) -> None:
"""Remove entity *name* from *container*."""
container.remove_entity(name)
container.remove_entity(_dealias_entity_name(name))

def _remove_skill(self, container: IntentContainer, skill_id: str) -> None:
"""Remove anything left for *skill_id* after per-intent detach.
Expand Down Expand Up @@ -600,15 +632,17 @@ def _add_intent(self, container: HierarchicalIntentContainer, name: str,

def _add_entity(self, container: HierarchicalIntentContainer, name: str,
samples: List[str]) -> None:
container.register_domain_entity(self._domain_of(name), name, samples)
container.register_domain_entity(self._domain_of(name),
_dealias_entity_name(name), samples)

def _remove_intent(self, container: HierarchicalIntentContainer,
name: str) -> None:
container.remove_domain_intent(self._domain_of(name), name)

def _remove_entity(self, container: HierarchicalIntentContainer,
name: str) -> None:
container.remove_domain_entity(self._domain_of(name), name)
container.remove_domain_entity(self._domain_of(name),
_dealias_entity_name(name))

def _remove_skill(self, container: HierarchicalIntentContainer,
skill_id: str) -> None:
Expand Down
2 changes: 1 addition & 1 deletion nebulento/version.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# START_VERSION_BLOCK
VERSION_MAJOR = 0
VERSION_MINOR = 10
VERSION_BUILD = 1
VERSION_BUILD = 3
VERSION_ALPHA = 1
# END_VERSION_BLOCK

Expand Down
13 changes: 8 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
name = "nebulento"
version = "0.1.1a1"
dynamic = ["version"]
description = "dead simple fuzzy matching intent parser"
readme = "README.md"
license = { text = "apache-2.0" }
Expand Down Expand Up @@ -56,5 +56,8 @@ Homepage = "https://github.com/OpenJarbas/nebulento"
"ovos-nebulento-pipeline-plugin" = "nebulento.opm:NebulentoPipeline"
"ovos-nebulento-hierarchical-pipeline-plugin" = "nebulento.opm:HierarchicalNebulentoPipeline"

[tool.hatch.build.targets.wheel]
packages = ["nebulento"]
[tool.setuptools.dynamic]
version = {attr = "nebulento.version.__version__"}

[tool.setuptools.packages.find]
where = ["."]
70 changes: 70 additions & 0 deletions test/test_entity_dealias_regression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Regression test for the entity-name munging bug (mirrors
ovos-padatious-pipeline-plugin#95).

ovos-workshop's register_entity_file() emits entity registration messages
with name = ``<skill_id>:<entity>_<md5hex32>``. Nebulento's slot-tagging
logic (IntentContainer.match_fuzzy) only tags a matched entity value into
the result when the literal token ``{<registered_entity_name>}`` appears in
one of the intent's own templates. Since skill authors write bare templates
like ``"drive to {place}"``, a munged registration name can never satisfy
that check, so the entity slot is silently dropped from match_data.
"""
from ovos_bus_client.message import Message
from ovos_utils.fakebus import FakeBus

from nebulento.opm import NebulentoPipeline


def _make_pipeline():
return NebulentoPipeline(bus=FakeBus(), config={})


def test_entity_registered_via_real_workshop_munged_name_is_matched():
p = _make_pipeline()
lang = p.lang
skill_id = "skill-driver.tigregotico"
entity_bare = "place"
# exact shape emitted by ovos_workshop.skills.base.register_entity_file
munged_name = f"{skill_id}:{entity_bare}_1234567890abcdef1234567890abcdef"

p.bus.emit(Message("padatious:register_intent", {
"name": f"{skill_id}:drive.intent",
"samples": ["drive to {place}", "navigate to {place}"],
"lang": lang,
}))
p.bus.emit(Message("padatious:register_entity", {
"name": munged_name,
"samples": ["kitchen", "office", "garden"],
"lang": lang,
}))

match = p.match_low(["drive to kitchen"], lang, Message("recognizer_loop:utterance"))
assert match is not None, "intent failed to match at all"
assert "kitchen" in (match.match_data.get("place") or []), \
f"expected bare 'place' slot to contain 'kitchen', got {match.match_data!r}"
p.shutdown()


def test_detach_entity_via_munged_name_actually_removes_it():
p = _make_pipeline()
lang = p.lang
skill_id = "skill-driver.tigregotico"
munged_name = f"{skill_id}:place_1234567890abcdef1234567890abcdef"

p.bus.emit(Message("padatious:register_intent", {
"name": f"{skill_id}:drive.intent",
"samples": ["drive to {place}"],
"lang": lang,
}))
p.bus.emit(Message("padatious:register_entity", {
"name": munged_name,
"samples": ["kitchen"],
"lang": lang,
}))
container = p.containers[lang]
assert "place" in container.registered_entities

p.bus.emit(Message("detach_entity", {"name": munged_name, "lang": lang}))
assert "place" not in container.registered_entities, \
"remove path did not dealias the munged name, entity was never removed"
p.shutdown()
5 changes: 4 additions & 1 deletion test/test_opm.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,11 @@ def test_handle_detach_entity_via_bus(self):
def test_handle_detach_entity_missing_name_is_noop(self):
"""detach_entity without a name must not raise or clear the container."""
self.pipeline.handle_detach_entity(Message("detach_entity", {}))
# entity names are dealiased to their bare token at the registration
# boundary (see _dealias_entity_name) — "skill:item" is stored as
# "item", matching the ``{item}`` token in the registered template.
self.assertIn(
"skill:item",
"item",
self.pipeline.containers["en-US"].registered_entities,
)

Expand Down