|
65 | 65 |
|
66 | 66 | _PIPELINE_RE = re.compile(r'-(high|medium|low)$') |
67 | 67 |
|
| 68 | +# OVOS-PIPELINE-1 §7.3 reserved intent_names. A Match produced by one of the |
| 69 | +# reserving pipeline-plugin roles below is a reserved-name dispatch: §7.1 |
| 70 | +# requires the ``session.active_handlers`` push to be SUPPRESSED for it, because |
| 71 | +# a reserved name represents a continuation/termination of an already-active |
| 72 | +# skill's participation, not a fresh activation. Keyed off the producing |
| 73 | +# pipeline_id (the role that holds the namespace lease), with the confidence |
| 74 | +# suffix (``-high``/``-medium``/``-low``) stripped before lookup. |
| 75 | +# |
| 76 | +# converse -> ovos-converse-pipeline-plugin (CONVERSE-1 §4/§5: converse, response) |
| 77 | +# stop -> ovos-stop-pipeline-plugin (STOP-1 §4: stop) |
| 78 | +# fallback -> ovos-fallback-pipeline-plugin (FALLBACK-1 §6.3: fallback) |
| 79 | +# common_query -> ovos-common-query-pipeline-plugin (COMMON-QUERY-1 §3: common_query) |
| 80 | +_RESERVED_NAME_PIPELINES = { |
| 81 | + "ovos-converse-pipeline-plugin", |
| 82 | + "ovos-stop-pipeline-plugin", |
| 83 | + "ovos-fallback-pipeline-plugin", |
| 84 | + "ovos-common-query-pipeline-plugin", |
| 85 | +} |
| 86 | + |
| 87 | + |
| 88 | +def _produces_reserved_name(pipeline_id: Optional[str]) -> bool: |
| 89 | + """OVOS-PIPELINE-1 §7.3: True when ``pipeline_id`` is a reserved-name role |
| 90 | + whose dispatches must NOT stamp ``session.active_handlers`` (§7.1).""" |
| 91 | + if not pipeline_id: |
| 92 | + return False |
| 93 | + return _PIPELINE_RE.sub("", pipeline_id) in _RESERVED_NAME_PIPELINES |
| 94 | + |
68 | 95 |
|
69 | 96 | def on_started(): |
70 | 97 | LOG.info('IntentService is starting up.') |
@@ -293,6 +320,35 @@ def _emit_utterance_handled(self, dispatch_msg: Message): |
293 | 320 | utterance.""" |
294 | 321 | self.bus.emit(dispatch_msg.forward(SpecMessage.UTTERANCE_HANDLED, {})) |
295 | 322 |
|
| 323 | + @staticmethod |
| 324 | + def _missing_required_slots(match: IntentHandlerMatch) -> List[str]: |
| 325 | + """OVOS-PIPELINE-1 §6.2 orchestrator backstop for ``required_slots``. |
| 326 | +
|
| 327 | + After a plugin returns a Match, the orchestrator verifies the match's |
| 328 | + slot map contains every slot the matched intent declares as required |
| 329 | + (OVOS-INTENT-3 §5.3, OVOS-INTENT-4 §6.1). If any is absent, the |
| 330 | + orchestrator treats the match as if the plugin had declined and |
| 331 | + continues iteration — no bus event is emitted; the only observable |
| 332 | + effect is a non-match (§6.2). The primary obligation to enforce |
| 333 | + ``required_slots`` still lies with the engine during ``match()``; this |
| 334 | + is a second line of defense against engine bugs. |
| 335 | +
|
| 336 | + The plugin surfaces the constraint in ``match.match_data``: the required |
| 337 | + slot names under ``__required_slots__`` and the captured slot map as the |
| 338 | + remaining keys (OVOS-INTENT-3 §7; ``Match.slots`` in PIPELINE-1 §4.3). |
| 339 | + When a plugin does not surface it (the common case today) this returns |
| 340 | + an empty list and the backstop is a no-op, leaving engine-side |
| 341 | + enforcement authoritative — fully backward compatible. |
| 342 | +
|
| 343 | + Returns: |
| 344 | + List[str]: required slot names absent from the match's slot map. |
| 345 | + """ |
| 346 | + match_data = match.match_data or {} |
| 347 | + required_slots = match_data.get("__required_slots__") |
| 348 | + if not required_slots: |
| 349 | + return [] |
| 350 | + return [slot for slot in required_slots if not match_data.get(slot)] |
| 351 | + |
296 | 352 | def _dispatch_match(self, match: IntentHandlerMatch, message: Message, lang: str, |
297 | 353 | pipeline_id: str = None) -> None: |
298 | 354 | """Orchestrate the OVOS-PIPELINE-1 §6.1 post-match steps, then dispatch. |
@@ -349,7 +405,15 @@ def _dispatch_match(self, match: IntentHandlerMatch, message: Message, lang: str |
349 | 405 |
|
350 | 406 | was_deactivated = match.skill_id in self._deactivations[sess.session_id] |
351 | 407 | if not was_deactivated: |
352 | | - sess.activate_skill(match.skill_id) |
| 408 | + # OVOS-PIPELINE-1 §7.1 pushes the skill onto the session's |
| 409 | + # active-handler recency list. §7.3 SUPPRESSES that push for |
| 410 | + # reserved intent_name dispatches (converse/response/stop/ |
| 411 | + # fallback/common_query): a reserved name is a continuation |
| 412 | + # or termination of an already-active skill's participation, |
| 413 | + # not a fresh activation. `activate_skill` is a back-compat |
| 414 | + # shim over `add_active_handler` (§7.1) in current bus-client. |
| 415 | + if not _produces_reserved_name(pipeline_id): |
| 416 | + sess.activate_skill(match.skill_id) |
353 | 417 | # emit event for skills callback -> self.handle_activate |
354 | 418 | self.bus.emit(reply.forward(f"{match.skill_id}.activate")) |
355 | 419 |
|
@@ -520,6 +584,14 @@ def handle_utterance(self, message: Message): |
520 | 584 | LOG.debug( |
521 | 585 | f"ignoring match, intent '{match.match_type}' blacklisted by Session '{sess.session_id}'") |
522 | 586 | continue |
| 587 | + # OVOS-PIPELINE-1 §6.2: if the matched intent is missing |
| 588 | + # any required slot, treat it as if the plugin had |
| 589 | + # declined and continue iteration; no bus event is emitted. |
| 590 | + missing = self._missing_required_slots(match) |
| 591 | + if missing: |
| 592 | + LOG.debug(f"ignoring match '{match.match_type}': " |
| 593 | + f"missing required slots {missing} (§6.2)") |
| 594 | + continue |
523 | 595 | try: |
524 | 596 | self._dispatch_match(match, message, intent_lang, |
525 | 597 | pipeline_id=pipeline) |
|
0 commit comments