Skip to content

Commit cccd290

Browse files
JarbasAlclaude
andcommitted
feat: gate bus topics by legacy_namespace (PIPELINE-1 §8/§9.6, STOP-1 §4.2)
A skill emits either the legacy mycroft.* topics or the OVOS spec ovos.* topics, chosen by the deployment 'legacy_namespace' config (default True) — never both, so a subscriber never sees duplicate messages. Skills subscribe on BOTH namespaces. Covers: - handler-lifecycle trio: mycroft.skill.handler.* <-> ovos.intent.handler.* (§8) - speak <-> ovos.utterance.speak (§9.6) - stop ping/pong: {skill}.stop.ping/skill.stop.pong <-> ovos.stop.ping/ovos.stop.pong (§4.2) - stop dispatch: also subscribe ovos.stop and {skill}:stop (§4.3/§5.3) Adds dual-namespace unit tests (test/unittests/test_spec_bus_messages.py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 779e54b commit cccd290

2 files changed

Lines changed: 135 additions & 13 deletions

File tree

ovos_workshop/skills/ovos.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,9 +1070,15 @@ def _register_system_event_handlers(self):
10701070
"""
10711071
Register default messagebus event handlers
10721072
"""
1073+
# Stop topics: subscribe to BOTH the legacy and the OVOS-STOP-1 spec
1074+
# namespaces. Only one namespace is ever emitted (chosen by the
1075+
# 'legacy_namespace' config), so a subscriber never sees duplicates.
10731076
self.add_event('mycroft.stop', self._handle_session_stop, speak_errors=False)
1077+
self.add_event('ovos.stop', self._handle_session_stop, speak_errors=False) # STOP-1 §5.3
10741078
self.add_event(f"{self.skill_id}.stop", self._handle_session_stop, speak_errors=False)
1079+
self.add_event(f"{self.skill_id}:stop", self._handle_session_stop, speak_errors=False) # STOP-1 §4.3
10751080
self.add_event(f"{self.skill_id}.stop.ping", self._handle_stop_ack, speak_errors=False)
1081+
self.add_event("ovos.stop.ping", self._handle_stop_ack, speak_errors=False) # STOP-1 §4.2
10761082
self.add_event(f"{self.skill_id}.converse.get_response", self.__handle_get_response, speak_errors=False)
10771083

10781084
self.add_event('mycroft.skill.enable_intent', self.handle_enable_intent, speak_errors=False)
@@ -1142,15 +1148,28 @@ def handle_settings_change(self, message: Message):
11421148
f"remote changes not handled!: {e}")
11431149
self._start_filewatcher()
11441150

1151+
@staticmethod
1152+
def _legacy_namespace() -> bool:
1153+
"""Whether to emit the legacy ``mycroft.*`` bus topics (default) or the
1154+
OVOS spec ``ovos.*`` topics, during the bus-namespace transition.
1155+
1156+
Deployment-wide, controlled by the ``legacy_namespace`` config key
1157+
(default ``True``). Emitters pick exactly one namespace so subscribers —
1158+
which listen on both — never receive duplicate messages.
1159+
"""
1160+
return Configuration().get("legacy_namespace", True)
1161+
11451162
def _handle_stop_ack(self, message: Message):
11461163
"""
1147-
Inform skills service if we want to handle stop. Individual skills
1148-
must implement the method self.can_stop to enable or
1149-
disable stop support.
1150-
@param message: `{self.skill_id}.stop.ping` Message
1164+
Answer a stoppability ping. Individual skills must implement the method
1165+
self.can_stop to enable or disable stop support. Replies on the legacy
1166+
``skill.stop.pong`` or the spec ``ovos.stop.pong`` (OVOS-STOP-1 §4.2)
1167+
depending on the active namespace.
1168+
@param message: a ``{self.skill_id}.stop.ping`` or ``ovos.stop.ping`` Message
11511169
"""
1170+
topic = "skill.stop.pong" if self._legacy_namespace() else "ovos.stop.pong"
11521171
self.bus.emit(message.reply(
1153-
"skill.stop.pong",
1172+
topic,
11541173
data={"skill_id": self.skill_id,
11551174
"can_handle": self.can_stop(message)},
11561175
context={"skill_id": self.skill_id}))
@@ -1423,6 +1442,16 @@ def handle_remove_cross_context(self, message: Message):
14231442
context = message.data.get('context')
14241443
self.remove_context(context)
14251444

1445+
def _intent_handler_data(self, message: Optional[Message],
1446+
skill_data: dict) -> dict:
1447+
"""Build the OVOS-PIPELINE-1 §8.2 handler-lifecycle payload
1448+
(``skill_id`` + ``intent_name``) on top of the legacy ``skill_data``."""
1449+
data = dict(skill_data)
1450+
data["skill_id"] = self.skill_id
1451+
if message is not None and ":" in message.msg_type:
1452+
data["intent_name"] = message.msg_type.split(":", 1)[-1]
1453+
return data
1454+
14261455
def _on_event_start(self, message: Message, handler_info: str,
14271456
skill_data: dict, activation: Optional[bool] = None):
14281457
"""
@@ -1434,9 +1463,15 @@ def _on_event_start(self, message: Message, handler_info: str,
14341463
"""
14351464
if handler_info:
14361465
# Indicate that the skill handler is starting if requested
1437-
msg_type = handler_info + '.start'
14381466
message.context["skill_id"] = self.skill_id
1439-
self.bus.emit(message.forward(msg_type, skill_data))
1467+
# OVOS-PIPELINE-1 §8 handler-lifecycle trio: legacy mycroft.skill.handler.*
1468+
# or spec ovos.intent.handler.* depending on the active namespace.
1469+
if handler_info == "mycroft.skill.handler" and not self._legacy_namespace():
1470+
self.bus.emit(message.forward(
1471+
"ovos.intent.handler.start",
1472+
self._intent_handler_data(message, skill_data)))
1473+
else:
1474+
self.bus.emit(message.forward(handler_info + '.start', skill_data))
14401475

14411476
def _on_event_end(self, message: Message, handler_info: str,
14421477
skill_data: dict, is_intent: bool = False):
@@ -1445,9 +1480,14 @@ def _on_event_end(self, message: Message, handler_info: str,
14451480
completed.
14461481
"""
14471482
if handler_info:
1448-
msg_type = handler_info + '.complete'
14491483
message.context["skill_id"] = self.skill_id
1450-
self.bus.emit(message.forward(msg_type, skill_data))
1484+
# OVOS-PIPELINE-1 §8: legacy or spec namespace.
1485+
if handler_info == "mycroft.skill.handler" and not self._legacy_namespace():
1486+
self.bus.emit(message.forward(
1487+
"ovos.intent.handler.complete",
1488+
self._intent_handler_data(message, skill_data)))
1489+
else:
1490+
self.bus.emit(message.forward(handler_info + '.complete', skill_data))
14511491
if is_intent:
14521492
self.bus.emit(message.forward("ovos.utterance.handled", skill_data))
14531493

@@ -1473,10 +1513,15 @@ def _on_event_error(self, error: str, message: Message, handler_info: str,
14731513
skill_data['exception'] = repr(error)
14741514
if handler_info:
14751515
# Indicate that the skill handler errored
1476-
msg_type = handler_info + '.error'
14771516
message = message or Message("")
14781517
message.context["skill_id"] = self.skill_id
1479-
self.bus.emit(message.forward(msg_type, skill_data))
1518+
# OVOS-PIPELINE-1 §8: legacy or spec namespace.
1519+
if handler_info == "mycroft.skill.handler" and not self._legacy_namespace():
1520+
self.bus.emit(message.forward(
1521+
"ovos.intent.handler.error",
1522+
self._intent_handler_data(message, skill_data)))
1523+
else:
1524+
self.bus.emit(message.forward(handler_info + '.error', skill_data))
14801525

14811526
def _register_adapt_intent(self,
14821527
intent_parser: Union[IntentBuilder, Intent, str],
@@ -1541,8 +1586,11 @@ def speak(self, utterance: str, expect_response: bool = False,
15411586

15421587
# grab message that triggered speech so we can keep context
15431588
message = dig_for_message()
1544-
m = message.forward("speak", data) if message \
1545-
else Message("speak", data)
1589+
# OVOS-PIPELINE-1 §9.6 natural-language response exit point: the legacy
1590+
# 'speak' topic or the spec 'ovos.utterance.speak', per active namespace.
1591+
topic = "speak" if self._legacy_namespace() else "ovos.utterance.speak"
1592+
m = message.forward(topic, data) if message \
1593+
else Message(topic, data)
15461594
m.context["skill_id"] = self.skill_id
15471595

15481596
# update any auto-translation metadata in message.context
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Namespace bus-message tests for OVOSSkill.
2+
3+
Each skill-emitted event goes out in exactly one namespace, chosen by the
4+
``legacy_namespace`` config (default True): the legacy ``mycroft.*`` topics or
5+
the OVOS spec ``ovos.*`` topics. Both modes are covered for the handler trio
6+
(PIPELINE-1 §8), speak (§9.6) and the stop pong (STOP-1 §4.2).
7+
"""
8+
import unittest
9+
10+
from ovos_bus_client.message import Message
11+
from ovos_config.config import Configuration
12+
from ovos_utils.fakebus import FakeBus
13+
14+
from ovos_workshop.skills.ovos import OVOSSkill
15+
16+
17+
class TestBusNamespace(unittest.TestCase):
18+
19+
def setUp(self):
20+
self.bus = FakeBus()
21+
self.skill = OVOSSkill(skill_id="test.skill", bus=self.bus)
22+
self.seen = set()
23+
for topic in ("speak", "ovos.utterance.speak",
24+
"skill.stop.pong", "ovos.stop.pong",
25+
"mycroft.skill.handler.start", "ovos.intent.handler.start"):
26+
self.bus.on(topic, lambda m: self.seen.add(m.msg_type))
27+
28+
def tearDown(self):
29+
Configuration()["legacy_namespace"] = True
30+
31+
# -- speak (PIPELINE-1 §9.6) ------------------------------------------
32+
def test_speak_legacy_namespace(self):
33+
Configuration()["legacy_namespace"] = True
34+
self.skill.speak("hi")
35+
self.assertIn("speak", self.seen)
36+
self.assertNotIn("ovos.utterance.speak", self.seen)
37+
38+
def test_speak_spec_namespace(self):
39+
Configuration()["legacy_namespace"] = False
40+
self.skill.speak("hi")
41+
self.assertIn("ovos.utterance.speak", self.seen)
42+
self.assertNotIn("speak", self.seen)
43+
44+
# -- stop pong (STOP-1 §4.2) ------------------------------------------
45+
def test_stop_pong_legacy_namespace(self):
46+
Configuration()["legacy_namespace"] = True
47+
self.skill._handle_stop_ack(Message("test.skill.stop.ping"))
48+
self.assertIn("skill.stop.pong", self.seen)
49+
self.assertNotIn("ovos.stop.pong", self.seen)
50+
51+
def test_stop_pong_spec_namespace(self):
52+
Configuration()["legacy_namespace"] = False
53+
self.skill._handle_stop_ack(Message("ovos.stop.ping"))
54+
self.assertIn("ovos.stop.pong", self.seen)
55+
self.assertNotIn("skill.stop.pong", self.seen)
56+
57+
# -- handler trio (PIPELINE-1 §8) -------------------------------------
58+
def test_handler_start_legacy_namespace(self):
59+
Configuration()["legacy_namespace"] = True
60+
self.skill._on_event_start(Message("test.skill:greet"),
61+
"mycroft.skill.handler", {"name": "h"})
62+
self.assertIn("mycroft.skill.handler.start", self.seen)
63+
self.assertNotIn("ovos.intent.handler.start", self.seen)
64+
65+
def test_handler_start_spec_namespace(self):
66+
Configuration()["legacy_namespace"] = False
67+
self.skill._on_event_start(Message("test.skill:greet"),
68+
"mycroft.skill.handler", {"name": "h"})
69+
self.assertIn("ovos.intent.handler.start", self.seen)
70+
self.assertNotIn("mycroft.skill.handler.start", self.seen)
71+
72+
73+
if __name__ == "__main__":
74+
unittest.main()

0 commit comments

Comments
 (0)