From b027d8749ec0484a6e7e3b070fe3a9e814fb311a Mon Sep 17 00:00:00 2001 From: NeonJarbas <59943014+NeonJarbas@users.noreply.github.com> Date: Tue, 19 Mar 2024 19:17:17 +0000 Subject: [PATCH] feat/converse_intents (#12) * feat/converse_intents add support for .intent files in converse method a new decorator can be used to define internal intent handlers * simplify --------- Co-authored-by: jarbasai --- ovos_workshop/decorators/__init__.py | 28 +++++++++++- ovos_workshop/skills/ovos.py | 66 ++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/ovos_workshop/decorators/__init__.py b/ovos_workshop/decorators/__init__.py index bc00d219..e42d8029 100644 --- a/ovos_workshop/decorators/__init__.py +++ b/ovos_workshop/decorators/__init__.py @@ -1,10 +1,13 @@ +from functools import wraps + from ovos_utils.log import log_deprecation + from ovos_workshop.decorators.killable import killable_intent, killable_event from ovos_workshop.decorators.layers import enables_layer, \ disables_layer, layer_intent, removes_layer, resets_layers, replaces_layer from ovos_workshop.decorators.ocp import ocp_play, ocp_pause, ocp_resume, \ ocp_search, ocp_previous, ocp_featured_media -from functools import wraps + # TODO: Deprecate unused import retained for backwards-compat. from ovos_utils import classproperty @@ -25,7 +28,9 @@ def func_wrapper(*args, **kwargs): ret = func(*args, **kwargs) args[0].set_context(context, words) return ret + return func_wrapper + return context_add_decorator @@ -43,7 +48,9 @@ def func_wrapper(*args, **kwargs): ret = func(*args, **kwargs) args[0].remove_context(context) return ret + return func_wrapper + return context_removes_decorator @@ -68,6 +75,7 @@ def intent_file_handler(intent_file: str): """ Deprecated decorator for adding a method as an intent file handler. """ + def real_decorator(func): # Store the intent_file inside the function # This will be used later to call register_intent_file @@ -75,6 +83,7 @@ def real_decorator(func): func.intent_files = [] func.intent_files.append(intent_file) return func + log_deprecation(f"Use `@intent_handler({intent_file})`", "0.1.0") return real_decorator @@ -118,6 +127,22 @@ def converse_handler(func): return func +def conversational_intent(intent_file): + """Decorator for adding a method as an converse intent handler. + NOTE: only padatious intents supported, not adapt + """ + + def real_decorator(func): + # Store the intent_file inside the function + # This will be used later to train intents + if not hasattr(func, 'converse_intents'): + func.converse_intents = [] + func.converse_intents.append(intent_file) + return func + + return real_decorator + + def fallback_handler(priority: int = 50): """ Decorator for adding a fallback intent handler. @@ -125,6 +150,7 @@ def fallback_handler(priority: int = 50): @param priority: Fallback priority (0-100) with lower values having higher priority """ + def real_decorator(func): if not hasattr(func, 'fallback_priority'): func.fallback_priority = priority diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 5240b3b4..8af6bb7b 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -17,9 +17,6 @@ from json_database import JsonStorage from lingua_franca.format import pronounce_number, join_list from lingua_franca.parse import yes_or_no, extract_number -from ovos_config.config import Configuration -from ovos_config.locations import get_xdg_config_save_path - from ovos_backend_client.api import EmailApi, MetricsApi from ovos_bus_client import MessageBusClient from ovos_bus_client.apis.enclosure import EnclosureAPI @@ -28,6 +25,8 @@ from ovos_bus_client.message import Message, dig_for_message from ovos_bus_client.session import SessionManager, Session from ovos_bus_client.util import get_message_lang +from ovos_config.config import Configuration +from ovos_config.locations import get_xdg_config_save_path from ovos_plugin_manager.language import OVOSLangTranslationFactory, OVOSLangDetectionFactory from ovos_utils import camel_case_split, classproperty from ovos_utils.dialog import get_dialog, MustacheDialogRenderer @@ -41,6 +40,8 @@ from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties from ovos_utils.sound import play_audio +from padacioso import IntentContainer + from ovos_workshop.decorators.compat import backwards_compat from ovos_workshop.decorators.killable import AbortEvent, killable_event, \ AbortQuestion @@ -186,6 +187,7 @@ def __init__(self, name: Optional[str] = None, self.intent_service = IntentServiceInterface() self.audio_service = None self.intent_layers = IntentLayers() + self.converse_matchers = {} # Skill Public API self.public_api: Dict[str, dict] = {} @@ -233,6 +235,53 @@ def handle_deactivate(self, message: Message): @param message: `{self.skill_id}.deactivate` Message """ + def register_converse_intent(self, intent_file, handler): + """ converse padacioso intents """ + name = f'{self.skill_id}.converse:{intent_file}' + fuzzy = not self.settings.get("strict_intents", False) + + for lang in self.native_langs: + self.converse_matchers[lang] = IntentContainer(fuzz=fuzzy) + + resources = self.load_lang(self.res_dir, lang) + resource_file = ResourceFile(resources.types.intent, intent_file) + if resource_file.file_path is None: + self.log.error(f'Unable to find "{intent_file}"') + continue + filename = str(resource_file.file_path) + + with open(filename) as f: + samples = [l.strip() for l in f.read().split("\n") + if l and not l.startswith("#")] + + self.converse_matchers[lang].add_intent(name, samples) + + self.add_event(name, handler, 'mycroft.skill.handler') + + def _handle_converse_intents(self, message): + """ called before converse method + this gives active skills a chance to parse their own intents and + consume the utterance, see conversational_intent decorator for usage + """ + if self.lang not in self.converse_matchers: + return False + + best_score = 0 + response = None + + for utt in message.data['utterances']: + match = self.converse_matchers[self.lang].calc_intent(utt) + if match and match["conf"] > best_score: + best_score = match["conf"] + response = message.forward(match["name"], match["entities"]) + + if not response or best_score < self.settings.get("min_intent_conf", 0.5): + return False + + # send intent event + self.bus.emit(response) + return True + def converse(self, message: Optional[Message] = None) -> bool: """ Override to handle an utterance before intent parsing while this skill @@ -880,6 +929,10 @@ def _register_decorated(self): if hasattr(method, 'converse'): self.converse = method + if hasattr(method, 'converse_intents'): + for intent_file in getattr(method, 'converse_intents'): + self.register_converse_intent(intent_file, method) + def _upload_settings(self): """ Upload settings to a remote backend if configured. @@ -1129,6 +1182,13 @@ def _handle_converse_request(self, message: Message): if message.data.get("skill_id") != self.skill_id: return # not for us! + # check if a conversational intent triggered + # these are skill specific intents that may trigger instead of converse + if self._handle_converse_intents(message): + self.bus.emit(message.reply('skill.converse.response', + {"skill_id": self.skill_id, "result": True})) + return + try: # converse can have multiple signatures params = signature(self.converse).parameters