From 8334a90107b778e5e543c8387d9ac8c7e586f654 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Wed, 20 Sep 2023 02:13:00 +0100 Subject: [PATCH] feat/pipeline_plugins bye padacioso service, you never made it into a stable release api compat for PadatiousService - ensure existing code remains valid syntax api compat for AdaptService - ensure existing code remains valid syntax move to deprecated mycroft module (for import compat only, unused in intent service) deprecated stuff cleaning up keep the junk in isolated classes easy to remove later on no_side_effects new base classes padatious handlers in padatious service adapt deprecation logs adapt handlers in adapt plugin deprecation logs everywhere - part 1 --- .../skills/intent_services/adapt_service.py | 269 +++++++- .../intent_services/padatious_service.py | 205 +++++- ovos_core/intent_services/__init__.py | 590 +++++++++--------- ovos_core/intent_services/adapt_service.py | 289 +-------- ovos_core/intent_services/commonqa_service.py | 77 ++- ovos_core/intent_services/converse_service.py | 37 +- ovos_core/intent_services/fallback_service.py | 53 +- .../intent_services/padacioso_service.py | 248 -------- .../intent_services/padatious_service.py | 286 +-------- .../skills/test_utterance_intents.py | 2 + 10 files changed, 880 insertions(+), 1176 deletions(-) delete mode 100644 ovos_core/intent_services/padacioso_service.py diff --git a/mycroft/skills/intent_services/adapt_service.py b/mycroft/skills/intent_services/adapt_service.py index ebac8cafaa08..9159d632c4bb 100644 --- a/mycroft/skills/intent_services/adapt_service.py +++ b/mycroft/skills/intent_services/adapt_service.py @@ -16,5 +16,270 @@ from adapt.context import ContextManagerFrame from adapt.engine import IntentDeterminationEngine from ovos_utils.intents import AdaptIntent, IntentBuilder, Intent -from ovos_core.intent_services.adapt_service import ContextManager, AdaptService -from ovos_utils.intents import AdaptIntent, IntentBuilder, Intent +from ovos_config.config import Configuration +from ovos_bus_client.message import Message, dig_for_message +from ovos_bus_client.session import IntentContextManager as ContextManager, \ + SessionManager +from ovos_intent_plugin_adapt import AdaptPipelinePlugin +from ovos_utils.log import deprecated, log_deprecation +from ovos_utils.messagebus import get_message_lang, get_mycroft_bus + + +class AdaptService: + """ this class provides methods that were deprecated in 0.0.8 , + it's sole purpose is to log warnings and direct users to use the plugin that replaced this implementation + + Methods here were present in ovos-core 0.0.7 and will be fully removed by ovos-core 0.0.9""" + + @deprecated("AdaptService has been replaced by ovos-intent-plugin-adapt, " + "use the new pipeline plugin mechanism instead of this class", "0.0.9") + def __init__(self, bus=None, config=None): + self.bus = bus or get_mycroft_bus() # backwards compat, bus was optional + core_config = Configuration() + config = config or core_config.get("context", {}) + self._plugin = AdaptPipelinePlugin(self.bus, config) + + def bind(self, plugin): + self._plugin = plugin + self._plugin.bus.on('intent.service.adapt.get', self.handle_get_adapt) + self._plugin.bus.on('intent.service.adapt.manifest.get', self.handle_adapt_manifest) + self._plugin.bus.on('intent.service.adapt.vocab.manifest.get', self.handle_vocab_manifest) + + @property + def plugin(self): + # lazy loaded only if accessed, allow IntentService to pass reference of already loaded plugin + if not self._plugin: + _plugin = AdaptPipelinePlugin(self.bus) + self.bind(_plugin) + return self._plugin + + # deprecated proxies for plugin methods + @deprecated("AdaptService has been replaced by ovos-intent-plugin-adapt, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def register_vocab(self, start_concept, end_concept, + alias_of, regex_str, lang): + """Register Vocabulary. DEPRECATED + + This method should not be used, it has been replaced by + register_vocabulary(). + """ + self.register_vocabulary(start_concept, end_concept, alias_of, + regex_str, lang) + + @deprecated("AdaptService has been replaced by ovos-intent-plugin-adapt, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def register_vocabulary(self, entity_value, entity_type, + alias_of, regex_str, lang): + """Register skill vocabulary as adapt entity. + + This will handle both regex registration and registration of normal + keywords. if the "regex_str" argument is set all other arguments will + be ignored. + + Argument: + entity_value: the natural langauge word + entity_type: the type/tag of an entity instance + alias_of: entity this is an alternative for + """ + m = dig_for_message() or Message("") # to dig skill_id in message.context + m = m.forward("register_vocab", {"entity_type": entity_type, + "entity_value": entity_value, + "regex": regex_str, + "alias_of": alias_of, + "lang": lang or self.plugin}) + self.plugin._handle_adapt_vocab(m) + + @deprecated("AdaptService has been replaced by ovos-intent-plugin-adapt, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def register_intent(self, intent): + """Register new intent with adapt engine. + + Args: + intent (IntentParser): IntentParser to register + """ + m = dig_for_message() or Message("") # to dig skill_id in message.context + m = m.forward("register_intent", {"name": intent.name, + "requires": intent.requires, + "optional": intent.optional, + "at_least_one": intent.at_least_one}) + self.plugin.handle_register_keyword_intent(m) + + @deprecated("AdaptService has been replaced by ovos-intent-plugin-adapt, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def detach_skill(self, skill_id): + """Remove all intents for skill. + + Args: + skill_id (str): skill to process + """ + self.plugin.detach_skill(skill_id) + + @deprecated("AdaptService has been replaced by ovos-intent-plugin-adapt, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def detach_intent(self, intent_name): + """Detach a single intent + + Args: + intent_name (str): Identifier for intent to remove. + """ + m = dig_for_message() or Message("") # to dig skill_id in message.context + skill_id = m.data.get("skill_id") or m.context.get("skill_id") + self.plugin.detach_intent(skill_id=skill_id, intent_name=intent_name) + + # deprecated bus api handlers - moved from IntentService for deprecation + def handle_get_adapt(self, message): + """handler getting the adapt response for an utterance. + + Args: + message (Message): message containing utterance + """ + utterance = message.data["utterance"] + lang = get_message_lang(message) + intent = self.plugin.match_intent([utterance], lang, message) + intent_data = intent.intent_data if intent else None + self.plugin.bus.emit(message.reply("intent.service.adapt.reply", + {"intent": intent_data})) + + def handle_adapt_manifest(self, message): + """Send adapt intent manifest to caller. + + Argument: + message: query message to reply to. + """ + self.plugin.bus.emit(message.reply("intent.service.adapt.manifest", + {"intents": self.registered_intents})) + + def handle_vocab_manifest(self, message): + """Send adapt vocabulary manifest to caller. + + Argument: + message: query message to reply to. + """ + self.plugin.bus.emit(message.reply("intent.service.adapt.vocab.manifest", + {"vocab": self.registered_vocab})) + + # deprecated properties / backwards compat syntax + @property + def registered_vocab(self): + log_deprecation("self.registered_vocab has been deprecated and is unused," + " use the adapt plugin directly instead", "0.0.8") + return [] + + @registered_vocab.setter + def registered_vocab(self, val): + log_deprecation("self.registered_vocab has been deprecated and is unused, " + "use the adapt plugin directly instead", "0.0.8") + + @property + def registered_intents(self): + lang = get_message_lang() + return [parser.__dict__ for parser in self.plugin.engines[lang].intent_parsers] + + @property + def context_keywords(self): + log_deprecation("self.context_keywords has been deprecated and is unused," + " use self.config.get('keywords', []) instead", "0.1.0") + return self.plugin.config.get('keywords', []) + + @context_keywords.setter + def context_keywords(self, val): + log_deprecation("self.context_keywords has been deprecated and is unused, " + "edit mycroft.conf instead, setter will be ignored", "0.1.0") + + @property + def context_max_frames(self): + log_deprecation("self.context_keywords has been deprecated and is unused, " + "use self.config.get('max_frames', 3) instead", "0.1.0") + return self.plugin.config.get('max_frames', 3) + + @context_max_frames.setter + def context_max_frames(self, val): + log_deprecation("self.context_max_frames has been deprecated and is unused, " + "edit mycroft.conf instead, setter will be ignored", "0.1.0") + + @property + def context_timeout(self): + log_deprecation("self.context_timeout has been deprecated and is unused," + " use self.config.get('timeout', 2) instead", "0.1.0") + return self.plugin.config.get('timeout', 2) + + @context_timeout.setter + def context_timeout(self, val): + log_deprecation("self.context_timeout has been deprecated and is unused," + " edit mycroft.conf instead, setter will be ignored", "0.1.0") + + @property + def context_greedy(self): + log_deprecation("self.context_greedy has been deprecated and is unused, " + "use self.config.get('greedy', False) instead", "0.1.0") + return self.plugin.config.get('greedy', False) + + @context_greedy.setter + def context_greedy(self, val): + log_deprecation("self.context_greedy has been deprecated and is unused," + " edit mycroft.conf instead, setter will be ignored", "0.1.0") + + @property + def context_manager(self): + log_deprecation("context_manager has been deprecated, use Session.context instead", "0.1.0") + sess = SessionManager.get() + return sess.context + + @context_manager.setter + def context_manager(self, val): + log_deprecation("context_manager has been deprecated, use Session.context instead", "0.1.0") + + assert isinstance(val, ContextManager) + sess = SessionManager.get() + sess.context = val + + @deprecated("update_context has been deprecated, use Session.context.update_context instead", "0.1.0") + def update_context(self, intent): + """Updates context with keyword from the intent. + + NOTE: This method currently won't handle one_of intent keywords + since it's not using quite the same format as other intent + keywords. This is under investigation in adapt, PR pending. + + Args: + intent: Intent to scan for keywords + """ + sess = SessionManager.get() + ents = [tag['entities'][0] for tag in intent['__tags__'] if 'entities' in tag] + sess.context.update_context(ents) + + +def _entity_skill_id(skill_id): + """Helper converting a skill id to the format used in entities. + + Arguments: + skill_id (str): skill identifier + + Returns: + (str) skill id on the format used by skill entities + """ + skill_id = skill_id[:-1] + skill_id = skill_id.replace('.', '_') + skill_id = skill_id.replace('-', '_') + return skill_id + + +def _is_old_style_keyword_message(message): + """Simple check that the message is not using the updated format. + TODO: Remove in v22.02 + Args: + message (Message): Message object to check + Returns: + (bool) True if this is an old messagem, else False + """ + return ('entity_value' not in message.data and 'start' in message.data) + + +def _update_keyword_message(message): + """Make old style keyword registration message compatible. + Copies old keys in message data to new names. + Args: + message (Message): Message to update + """ + message.data['entity_value'] = message.data['start'] + message.data['entity_type'] = message.data['end'] diff --git a/mycroft/skills/intent_services/padatious_service.py b/mycroft/skills/intent_services/padatious_service.py index b5d1809a4312..43ebb76120b3 100644 --- a/mycroft/skills/intent_services/padatious_service.py +++ b/mycroft/skills/intent_services/padatious_service.py @@ -13,5 +13,208 @@ # limitations under the License. # """Intent service wrapping padatious.""" -from ovos_core.intent_services.padatious_service import PadatiousMatcher, PadatiousService, PadatiousIntent +from threading import Event +from time import time as get_time, sleep +from typing import List, Optional +from padacioso.opm import PadaciosoPipelinePlugin, _unmunge + +from ovos_bus_client.message import Message +from ovos_utils.log import LOG, deprecated +from ovos_utils.messagebus import get_message_lang + +try: + import padatious + from padatious.match_data import MatchData as PadatiousIntent + from ovos_intent_plugin_padatious import PadatiousPipelinePlugin +except: + PadatiousPipelinePlugin = None + + +class PadatiousMatcher: + """Matcher class to avoid redundancy in padatious intent matching.""" + + @deprecated("PadatiousMatcher class is deprecated, use padatious plugin instead", "0.1.0") + def __init__(self, service): + self.service = service + + def match_high(self, utterances, lang=None, message=None): + """Intent matcher for high confidence. + + Args: + utterances (list of tuples): Utterances to parse, originals paired + with optional normalized version. + """ + return self.service.match_high(utterances, lang, message) + + def match_medium(self, utterances, lang=None, message=None): + """Intent matcher for medium confidence. + + Args: + utterances (list of tuples): Utterances to parse, originals paired + with optional normalized version. + """ + return self.service.match_medium(utterances, lang, message) + + def match_low(self, utterances, lang=None, message=None): + """Intent matcher for low confidence. + + Args: + utterances (list of tuples): Utterances to parse, originals paired + with optional normalized version. + """ + return self.service.match_low(utterances, lang, message) + + +class PadatiousService: + """Service class for padatious intent matching.""" + + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "use the new pipeline plugin mechanism instead of this class", "0.0.9") + def __init__(self, bus, config): + self.bus = bus + self._plugin = None + # TODO - wrap everything below into properties with deprecation warnings + self.padatious_config = config + self.conf_high = self.padatious_config.get("conf_high") or 0.95 + self.conf_med = self.padatious_config.get("conf_med") or 0.8 + self.conf_low = self.padatious_config.get("conf_low") or 0.5 + self.containers = {} + self.finished_training_event = Event() + self.finished_initial_train = False + self.train_delay = self.padatious_config.get('train_delay', 4) + self.train_time = get_time() + self.train_delay + self.registered_intents = [] + self.registered_entities = [] + + def bind(self, plugin): + self._plugin = plugin + if not isinstance(self._plugin, PadaciosoPipelinePlugin): + LOG.debug('Using Padatious intent parser.') + else: + LOG.debug('Using Padacioso intent parser.') + + @property + def plugin(self): + # lazy loaded only if accessed, allow IntentService to pass reference of already loaded plugin + if not self._plugin: + if self.padatious_config.get("regex_only") or PadatiousPipelinePlugin is None: + _plugin = PadaciosoPipelinePlugin(self.bus, self.padatious_config) + else: + try: + _plugin = PadatiousPipelinePlugin(self.bus, self.padatious_config) + except: + _plugin = PadaciosoPipelinePlugin(self.bus, self.padatious_config) + self.bind(_plugin) + return self._plugin + + # deprecated proxies for plugin methods + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def train(self, message=None): + """Perform padatious training. + Args: + message (Message): optional triggering message + """ + self.finished_training_event.clear() + padatious_single_thread = self.padatious_config.get('single_thread', True) + if message is None: + single_thread = padatious_single_thread + else: + single_thread = message.data.get('single_thread', + padatious_single_thread) + self.plugin.train(single_thread=single_thread) + + LOG.info('Training complete.') + self.finished_training_event.set() + if not self.finished_initial_train: + self.bus.emit(Message('mycroft.skills.trained')) + self.finished_initial_train = True + + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def wait_and_train(self): + """Wait for minimum time between training and start training.""" + if not self.finished_initial_train: + return + sleep(self.train_delay) + if self.train_time < 0.0: + return + + if self.train_time <= get_time() + 0.01: + self.train_time = -1.0 + self.train() + + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def handle_detach_intent(self, message): + """Messagebus handler for detaching padatious intent. + Args: + message (Message): message triggering action + """ + munged = message.data.get('intent_name') + name, skill_id = _unmunge(munged) # TODO - validate - is original munging same as plugin ? + self.plugin.detach_intent(skill_id=skill_id, intent_name=name) + + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def handle_detach_skill(self, message): + """Messagebus handler for detaching all intents for skill. + Args: + message (Message): message triggering action + """ + skill_id = message.data['skill_id'] + self.plugin.detach_skill(skill_id) + + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def register_intent(self, message): + """Messagebus handler for registering intents. + Args: + message (Message): message triggering action + """ + munged = message.data.get('name') + name, skill_id = _unmunge(munged) # TODO - validate - is original munging same as plugin ? + lang = get_message_lang() + file_name = message.data.get("file_name") + if file_name: + self.plugin.register_intent_from_file(skill_id=skill_id, intent_name=name, + lang=lang, file_name=file_name) + else: + samples = message.data.get("samples") or [] + self.plugin.register_intent(skill_id=skill_id, intent_name=name, + lang=lang, samples=samples) + + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def register_entity(self, message): + """Messagebus handler for registering entities. + Args: + message (Message): message triggering action + """ + munged = message.data.get('name') + name, skill_id = _unmunge(munged) # TODO - validate - is original munging same as plugin ? + lang = get_message_lang() + file_name = message.data.get("file_name") + if file_name: + self.plugin.register_entity_from_file(skill_id=skill_id, entity_name=name, + lang=lang, file_name=file_name) + else: + samples = message.data.get("samples") or [] + self.plugin.register_entity(skill_id=skill_id, entity_name=name, + lang=lang, samples=samples) + + @deprecated("PadatiousService has been replaced by ovos-intent-plugin-padatious, " + "this handler is deprecated and no longer connected to a bus event (it won't be called)", "0.0.9") + def calc_intent(self, utterances: List[str], lang: str = None) -> Optional[PadatiousIntent]: + """ + Get the best intent match for the given list of utterances. Utilizes a + thread pool for overall faster execution. Note that this method is NOT + compatible with Padatious, but is compatible with Padacioso. + @param utterances: list of string utterances to get an intent for + @param lang: language of utterances + @return: + """ + if isinstance(utterances, str): + utterances = [utterances] # backwards compat when arg was a single string + return self.plugin.match(utterances, lang) diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index 5c26cace4486..3ed78b091f59 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from collections import namedtuple - from ovos_config.config import Configuration from ovos_config.locale import setup_locale @@ -23,31 +21,232 @@ from ovos_core.intent_services.commonqa_service import CommonQAService from ovos_core.intent_services.converse_service import ConverseService from ovos_core.intent_services.fallback_service import FallbackService -from ovos_core.intent_services.padacioso_service import PadaciosoService from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService -from ovos_utils.intents.intent_service_interface import open_intent_envelope +from ovos_plugin_manager.templates.pipeline import PipelineStagePlugin from ovos_utils.log import LOG, deprecated, log_deprecation from ovos_utils.messagebus import get_message_lang from ovos_utils.metrics import Stopwatch +from ovos_core.intent_services.padatious_service import PadatiousService, PadatiousMatcher + + +class IntentServiceCompatLayer: + """contains only junk code that logs deprecation warnings to not break downstream api""" + + def __init__(self, bus): + self.bus = bus + self.pipeline_plugins = {} + + @deprecated("skill manifest moved to SkillManager, " + "this handler is not connected to bus events, subclassing it has no effect") + def handle_get_skills(self, message): + """Send registered skills to caller. + + Argument: + message: query message to reply to. + """ + # TODO - move this to SkillManager + self.bus.emit(message.reply("intent.service.skills.reply", + {"skills": self.skill_names # TODO - skill_ids + })) + + # deprecated properties / handlers + # convenience properties around default pipeline components / backwards compat + @property + def active_skills(self): + log_deprecation("self.active_skills is deprecated! use Session instead", "0.0.9") + session = SessionManager.get() + return session.active_skills + + @active_skills.setter + def active_skills(self, val): + log_deprecation("self.active_skills is deprecated! use Session instead", "0.0.9") + session = SessionManager.get() + session.active_skills = [] + for skill_id, ts in val: + session.activate_skill(skill_id) + + @property + def converse(self): + log_deprecation("self.converse has been deprecated, " + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + return self.pipeline_plugins.get("converse") + + @property + def common_qa(self): + log_deprecation("self.common_qa has been deprecated, " + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + return self.pipeline_plugins.get("common_qa") + + @property + def fallback(self): + log_deprecation("self.fallback has been deprecated, " + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + return self.pipeline_plugins.get("fallback") + + @property + def adapt_service(self): + log_deprecation("self.adapt_service has been deprecated, " + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + return self.pipeline_plugins.get("adapt") + + @property + def padacioso_service(self): + log_deprecation("self.padacioso has been deprecated, " + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + return self.pipeline_plugins.get("padacioso") + + @property + def padatious_service(self): + log_deprecation("self.padatious has been deprecated, " + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + return self.pipeline_plugins.get("padatious") or self.padacioso_service + + @property + def skill_names(self): + log_deprecation("self.skill_names has been deprecated and is always an empty dict," + " skill names no longer in use, reference skill_id directly", "0.0.8") + return {} + + @property + def registered_intents(self): + log_deprecation("self.registered_intents has been deprecated, moved to AdaptService," + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + adapt = self.pipeline_plugins.get("adapt") + if adapt: + return adapt.registered_intents + return [] + + @property + def registered_vocab(self): + log_deprecation("self.registered_vocab has been deprecated, moved to AdaptService," + "pipeline plugin object references can be found under self.pipeline_plugins", "0.1.0") + adapt = self.pipeline_plugins.get("adapt") + if adapt: + return adapt.registered_vocab + return [] + + @deprecated("skill names have been replaced across the whole ecosystem with skill_ids, " + "this handler is no longer connected to the messagebus", "0.0.8") + def update_skill_name_dict(self, message): + """Messagebus handler, updates dict of id to skill name conversions.""" + pass + + @deprecated("skill names have been replaced across the whole ecosystem with skill_ids, " + "get_skill_name is no longer necessary", "0.0.8") + def get_skill_name(self, skill_id): + """Get skill name from skill ID. + + Args: + skill_id: a skill id as encoded in Intent handlers. + + Returns: + (str) Skill name or the skill id if the skill wasn't found + """ + return skill_id + + @deprecated("handle_register_intent moved to AdaptService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_register_vocab(self, message): + """Register adapt vocabulary. + + Args: + message (Message): message containing vocab info + """ + pass + + @deprecated("handle_register_intent moved to AdaptService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_register_intent(self, message): + """Register adapt intent. -try: - from ovos_core.intent_services.padatious_service import PadatiousService, PadatiousMatcher -except ImportError: - from ovos_core.intent_services.padacioso_service import PadaciosoService as PadatiousService + Args: + message (Message): message containing intent info + """ + pass + + @deprecated("handle_detach_intent moved to AdaptService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_detach_intent(self, message): + """Remover adapt intent. + + Args: + message (Message): message containing intent info + """ + pass + + @deprecated("handle_detach_skill moved to AdaptService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_detach_skill(self, message): + """Remove all intents registered for a specific skill. + + Args: + message (Message): message containing intent info + """ + pass + + @deprecated("handle_get_adapt moved to AdaptService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_get_adapt(self, message): + """handler getting the adapt response for an utterance. + + Args: + message (Message): message containing utterance + """ + pass -# Intent match response tuple containing -# intent_service: Name of the service that matched the intent -# intent_type: intent name (used to call intent handler over the message bus) -# intent_data: data provided by the intent match -# skill_id: the skill this handler belongs to -IntentMatch = namedtuple('IntentMatch', - ['intent_service', 'intent_type', - 'intent_data', 'skill_id', 'utterance'] - ) + @deprecated("handle_adapt_manifest moved to AdaptService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_adapt_manifest(self, message): + """Send adapt intent manifest to caller. + Argument: + message: query message to reply to. + """ + pass -class IntentService: - """Mycroft intent service. parses utterances using a variety of systems. + @deprecated("handle_vocab_manifest moved to AdaptService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_vocab_manifest(self, message): + """Send adapt vocabulary manifest to caller. + + Argument: + message: query message to reply to. + """ + pass + + @deprecated("handle_get_padatious moved to PadatiousService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_get_padatious(self, message): + """messagebus handler for perfoming padatious parsing. + + Args: + message (Message): message triggering the method + """ + pass + + @deprecated("handle_padatious_manifest moved to PadatiousService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_padatious_manifest(self, message): + """Messagebus handler returning the registered padatious intents. + + Args: + message (Message): message triggering the method + """ + pass + + @deprecated("handle_entity_manifest moved to PadatiousService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") + def handle_entity_manifest(self, message): + """Messagebus handler returning the registered padatious entities. + + Args: + message (Message): message triggering the method + """ + pass + + +class IntentService(IntentServiceCompatLayer): + """OpenVoiceOS intent service. parses utterances using a variety of systems. The intent service also provides the internal API for registering and querying the intent service. @@ -57,52 +256,27 @@ def __init__(self, bus): self.bus = bus config = Configuration() - # Dictionary for translating a skill id to a name - self.skill_names = {} - - # TODO - replace with plugins - self.adapt_service = AdaptService() - if PadaciosoService is not PadatiousService: - self.padatious_service = PadatiousService(bus, config['padatious']) - else: - LOG.error(f'Failed to create padatious handlers, padatious not installed') - self.padatious_service = None - self.padacioso_service = PadaciosoService(bus, config['padatious']) - self.fallback = FallbackService(bus) - self.converse = ConverseService(bus) - self.common_qa = CommonQAService(bus) + self.pipeline_plugins = self.load_pipeline_plugins() self.utterance_plugins = UtteranceTransformersService(bus, config=config) self.metadata_plugins = MetadataTransformersService(bus, config=config) - self.bus.on('register_vocab', self.handle_register_vocab) - self.bus.on('register_intent', self.handle_register_intent) + # Intents API + self.bus.on('intent.service.intent.get', self.handle_get_intent) + self.bus.on('intent.service.skills.get', self.handle_get_skills) + + # Pipeline API self.bus.on('recognizer_loop:utterance', self.handle_utterance) - self.bus.on('detach_intent', self.handle_detach_intent) - self.bus.on('detach_skill', self.handle_detach_skill) + # Context related handlers + self.bus.on('intent.service:add_context', self.handle_add_context) + self.bus.on('intent.service:remove_context', self.handle_remove_context) + self.bus.on('intent.service:clear_context', self.handle_clear_context) + + # backwards compat namespace - TODO deprecate 0.2.0 self.bus.on('add_context', self.handle_add_context) self.bus.on('remove_context', self.handle_remove_context) self.bus.on('clear_context', self.handle_clear_context) - # Converse method - self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict) - - # Intents API - self.registered_vocab = [] - self.bus.on('intent.service.intent.get', self.handle_get_intent) - self.bus.on('intent.service.skills.get', self.handle_get_skills) - self.bus.on('intent.service.adapt.get', self.handle_get_adapt) - self.bus.on('intent.service.adapt.manifest.get', - self.handle_adapt_manifest) - self.bus.on('intent.service.adapt.vocab.manifest.get', - self.handle_vocab_manifest) - self.bus.on('intent.service.padatious.get', - self.handle_get_padatious) - self.bus.on('intent.service.padatious.manifest.get', - self.handle_padatious_manifest) - self.bus.on('intent.service.padatious.entities.manifest.get', - self.handle_entity_manifest) - @property def pipeline(self): # List of functions to use to match the utterance with intent, listed in priority order. @@ -119,57 +293,46 @@ def pipeline(self): "fallback_low" ]) - @property - def registered_intents(self): - lang = get_message_lang() - return [parser.__dict__ - for parser in self.adapt_service.engines[lang].intent_parsers] - - def update_skill_name_dict(self, message): - """Messagebus handler, updates dict of id to skill name conversions.""" - self.skill_names[message.data['id']] = message.data['name'] - - def get_skill_name(self, skill_id): - """Get skill name from skill ID. - - Args: - skill_id: a skill id as encoded in Intent handlers. - - Returns: - (str) Skill name or the skill id if the skill wasn't found - """ - return self.skill_names.get(skill_id, skill_id) + def load_pipeline_plugins(self): + plugins = {} # TODO in OPM + loaded_plugins = {} + for plug_name, plug in plugins.items(): + matchers = [plug.matcher_id, + plug.matcher_id + "_low", + plug.matcher_id + "_medium", + plug.matcher_id + "_high"] + if any(m in self.pipeline for m in matchers): + try: + # TODO - read plugin config + conf = {} + loaded_plugins[plug_name] = plug(self.bus, conf) + except Exception as e: + LOG.error(f"failed to load plugin {plug_name}:{e}") + continue + return loaded_plugins - # converse handling @property - def active_skills(self): - log_deprecation("self.active_skills is deprecated! use Session instead", "0.0.9") - session = SessionManager.get() - return session.active_skills - - @active_skills.setter - def active_skills(self, val): - log_deprecation("self.active_skills is deprecated! use Session instead", "0.0.9") - session = SessionManager.get() - session.active_skills = [] - for skill_id, ts in val: - session.activate_skill(skill_id) - - @deprecated("handle_activate_skill_request moved to ConverseService, overriding this method has no effect, " - "it has been disconnected from the bus event", "0.0.8") - def handle_activate_skill_request(self, message): - self.converse.handle_activate_skill_request(message) - - @deprecated("handle_deactivate_skill_request moved to ConverseService, overriding this method has no effect, " - "it has been disconnected from the bus event", "0.0.8") - def handle_deactivate_skill_request(self, message): - self.converse.handle_deactivate_skill_request(message) - - @deprecated("reset_converse moved to ConverseService, overriding this method has no effect, " - "it has been disconnected from the bus event", "0.0.8") - def reset_converse(self, message): - self.converse.reset_converse(message) + def pipeline_matchers(self): + matchers = [] + for m in self.pipeline: + matcher_id = m.replace("_high", "").replace("_medium", "").replace("_low", "") + if matcher_id not in self.pipeline_plugins: + LOG.error(f"{matcher_id} not installed, skipping pipeline stage!") + continue + + plugin = self.pipeline_plugins[matcher_id] + + if m.endswith("_high"): + matchers.append(plugin.match_high) + elif m.endswith("_medium"): + matchers.append(plugin.match_medium) + elif m.endswith("_low"): + matchers.append(plugin.match_low) + else: + matchers.append(plugin.match) + return matchers + # service implementation def _handle_transformers(self, message): """ Pipe utterance through transformer plugins to get more metadata. @@ -211,39 +374,31 @@ def disambiguate_lang(message): return default_lang - def get_pipeline(self, skips=None): + def get_pipeline(self, no_side_effects=False): """return a list of matcher functions ordered by priority utterances will be sent to each matcher in order until one can handle the utterance the list can be configured in mycroft.conf under intents.pipeline, in the future plugins will be supported for users to define their own pipeline""" - - # Create matchers - # TODO - from plugins - if self.padatious_service is None: - if any("padatious" in p for p in self.pipeline): - LOG.warning("padatious is not available! using padacioso in it's place") - padatious_matcher = self.padacioso_service - else: - from ovos_core.intent_services.padatious_service import PadatiousMatcher - padatious_matcher = PadatiousMatcher(self.padatious_service) - - matchers = { - "converse": self.converse.converse_with_skills, - "padatious_high": padatious_matcher.match_high, - "padacioso_high": self.padacioso_service.match_high, - "adapt": self.adapt_service.match_intent, - "common_qa": self.common_qa.match, - "fallback_high": self.fallback.high_prio, - "padatious_medium": padatious_matcher.match_medium, - "padacioso_medium": self.padacioso_service.match_medium, - "fallback_medium": self.fallback.medium_prio, - "padatious_low": padatious_matcher.match_low, - "padacioso_low": self.padacioso_service.match_low, - "fallback_low": self.fallback.low_prio - } - skips = skips or [] - pipeline = [k for k in self.pipeline if k not in skips] - return [matchers[k] for k in pipeline] + matchers = [] + for m in self.pipeline: + matcher_id = m.replace("_high", "").replace("_medium", "").replace("_low", "") + if matcher_id not in self.pipeline_plugins: + LOG.error(f"{matcher_id} not installed, skipping pipeline stage!") + continue + + plugin = self.pipeline_plugins[matcher_id] + if no_side_effects and isinstance(plugin, PipelineStagePlugin): + continue + + if m.endswith("_high"): + matchers.append(plugin.match_high) + elif m.endswith("_medium"): + matchers.append(plugin.match_medium) + elif m.endswith("_low"): + matchers.append(plugin.match_low) + else: + matchers.append(plugin.match) + return matchers def handle_utterance(self, message): """Main entrypoint for handling user utterances @@ -301,7 +456,7 @@ def handle_utterance(self, message): message.data["utterance"] = match.utterance if match.skill_id: - self.converse.activate_skill(match.skill_id) + self.converse.activate_skill(match.skill_id) # TODO - use Session message.context["skill_id"] = match.skill_id # If the service didn't report back the skill_id it # takes on the responsibility of making the skill "active" @@ -335,54 +490,6 @@ def send_complete_intent_failure(self, message): self.bus.emit(message.forward('mycroft.audio.play_sound', {"uri": sound})) self.bus.emit(message.forward('complete_intent_failure')) - def handle_register_vocab(self, message): - """Register adapt vocabulary. - - Args: - message (Message): message containing vocab info - """ - # TODO: 22.02 Remove backwards compatibility - if _is_old_style_keyword_message(message): - LOG.warning('Deprecated: Registering keywords with old message. ' - 'This will be removed in v22.02.') - _update_keyword_message(message) - - entity_value = message.data.get('entity_value') - entity_type = message.data.get('entity_type') - regex_str = message.data.get('regex') - alias_of = message.data.get('alias_of') - lang = get_message_lang(message) - self.adapt_service.register_vocabulary(entity_value, entity_type, - alias_of, regex_str, lang) - self.registered_vocab.append(message.data) - - def handle_register_intent(self, message): - """Register adapt intent. - - Args: - message (Message): message containing intent info - """ - intent = open_intent_envelope(message) - self.adapt_service.register_intent(intent) - - def handle_detach_intent(self, message): - """Remover adapt intent. - - Args: - message (Message): message containing intent info - """ - intent_name = message.data.get('intent_name') - self.adapt_service.detach_intent(intent_name) - - def handle_detach_skill(self, message): - """Remove all intents registered for a specific skill. - - Args: - message (Message): message containing intent info - """ - skill_id = message.data.get('skill_id') - self.adapt_service.detach_skill(skill_id) - def handle_add_context(self, message): """Add context @@ -422,7 +529,7 @@ def handle_clear_context(self, message): sess.context.clear_context() def handle_get_intent(self, message): - """Get intent from either adapt or padatious. + """Get intent from pipeline Args: message (Message): message containing utterance @@ -431,10 +538,7 @@ def handle_get_intent(self, message): lang = get_message_lang(message) # Loop through the matching functions until a match is found. - for match_func in self.get_pipeline(skips=["converse", - "fallback_high", - "fallback_medium", - "fallback_low"]): + for match_func in self.get_pipeline(no_side_effects=True): match = match_func([utterance], lang, message) if match: if match.intent_type: @@ -450,115 +554,3 @@ def handle_get_intent(self, message): # signal intent failure self.bus.emit(message.reply("intent.service.intent.reply", {"intent": None})) - - def handle_get_skills(self, message): - """Send registered skills to caller. - - Argument: - message: query message to reply to. - """ - self.bus.emit(message.reply("intent.service.skills.reply", - {"skills": self.skill_names})) - - @deprecated("handle_get_active_skills moved to ConverseService, overriding this method has no effect, " - "it has been disconnected from the bus event", "0.0.8") - def handle_get_active_skills(self, message): - """Send active skills to caller. - - Argument: - message: query message to reply to. - """ - self.converse.handle_get_active_skills(message) - - def handle_get_adapt(self, message): - """handler getting the adapt response for an utterance. - - Args: - message (Message): message containing utterance - """ - utterance = message.data["utterance"] - lang = get_message_lang(message) - intent = self.adapt_service.match_intent([utterance], lang, message) - intent_data = intent.intent_data if intent else None - self.bus.emit(message.reply("intent.service.adapt.reply", - {"intent": intent_data})) - - def handle_adapt_manifest(self, message): - """Send adapt intent manifest to caller. - - Argument: - message: query message to reply to. - """ - self.bus.emit(message.reply("intent.service.adapt.manifest", - {"intents": self.registered_intents})) - - def handle_vocab_manifest(self, message): - """Send adapt vocabulary manifest to caller. - - Argument: - message: query message to reply to. - """ - self.bus.emit(message.reply("intent.service.adapt.vocab.manifest", - {"vocab": self.registered_vocab})) - - def handle_get_padatious(self, message): - """messagebus handler for perfoming padatious parsing. - - Args: - message (Message): message triggering the method - """ - utterance = message.data["utterance"] - norm = message.data.get('norm_utt', utterance) - intent = self.padacioso_service.calc_intent(utterance) - if not intent and norm != utterance: - intent = self.padacioso_service.calc_intent(norm) - if intent: - intent = intent.__dict__ - self.bus.emit(message.reply("intent.service.padatious.reply", - {"intent": intent})) - - def handle_padatious_manifest(self, message): - """Messagebus handler returning the registered padatious intents. - - Args: - message (Message): message triggering the method - """ - self.bus.emit(message.reply( - "intent.service.padatious.manifest", - {"intents": self.padacioso_service.registered_intents})) - - def handle_entity_manifest(self, message): - """Messagebus handler returning the registered padatious entities. - - Args: - message (Message): message triggering the method - """ - self.bus.emit(message.reply( - "intent.service.padatious.entities.manifest", - {"entities": self.padacioso_service.registered_entities})) - - -def _is_old_style_keyword_message(message): - """Simple check that the message is not using the updated format. - - TODO: Remove in v22.02 - - Args: - message (Message): Message object to check - - Returns: - (bool) True if this is an old messagem, else False - """ - return ('entity_value' not in message.data and 'start' in message.data) - - -def _update_keyword_message(message): - """Make old style keyword registration message compatible. - - Copies old keys in message data to new names. - - Args: - message (Message): Message to update - """ - message.data['entity_value'] = message.data['start'] - message.data['entity_type'] = message.data['end'] diff --git a/ovos_core/intent_services/adapt_service.py b/ovos_core/intent_services/adapt_service.py index 87ddefc4eea2..e291b1f2268e 100644 --- a/ovos_core/intent_services/adapt_service.py +++ b/ovos_core/intent_services/adapt_service.py @@ -1,287 +1,2 @@ -# Copyright 2020 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""An intent parsing service using the Adapt parser.""" -from threading import Lock - -from adapt.engine import IntentDeterminationEngine -from ovos_config.config import Configuration - -import ovos_core.intent_services -from ovos_bus_client.session import IntentContextManager as ContextManager, \ - SessionManager -from ovos_utils import flatten_list -from ovos_utils.log import LOG - - -def _entity_skill_id(skill_id): - """Helper converting a skill id to the format used in entities. - - Arguments: - skill_id (str): skill identifier - - Returns: - (str) skill id on the format used by skill entities - """ - skill_id = skill_id[:-1] - skill_id = skill_id.replace('.', '_') - skill_id = skill_id.replace('-', '_') - return skill_id - - -class AdaptService: - """Intent service wrapping the Adapt intent Parser.""" - - def __init__(self, config=None): - core_config = Configuration() - self.config = config or core_config.get("context", {}) - self.lang = core_config.get("lang", "en-us") - langs = core_config.get('secondary_langs') or [] - if self.lang not in langs: - langs.append(self.lang) - - self.engines = {lang: IntentDeterminationEngine() - for lang in langs} - - self.lock = Lock() - - @property - def context_keywords(self): - LOG.warning( - "self.context_keywords has been deprecated and is unused, use self.config.get('keywords', []) instead") - return self.config.get('keywords', []) - - @context_keywords.setter - def context_keywords(self, val): - LOG.warning( - "self.context_keywords has been deprecated and is unused, edit mycroft.conf instead, setter will be ignored") - - @property - def context_max_frames(self): - LOG.warning( - "self.context_keywords has been deprecated and is unused, use self.config.get('max_frames', 3) instead") - return self.config.get('max_frames', 3) - - @context_max_frames.setter - def context_max_frames(self, val): - LOG.warning( - "self.context_max_frames has been deprecated and is unused, edit mycroft.conf instead, setter will be ignored") - - @property - def context_timeout(self): - LOG.warning("self.context_timeout has been deprecated and is unused, use self.config.get('timeout', 2) instead") - return self.config.get('timeout', 2) - - @context_timeout.setter - def context_timeout(self, val): - LOG.warning( - "self.context_timeout has been deprecated and is unused, edit mycroft.conf instead, setter will be ignored") - - @property - def context_greedy(self): - LOG.warning( - "self.context_greedy has been deprecated and is unused, use self.config.get('greedy', False) instead") - return self.config.get('greedy', False) - - @context_greedy.setter - def context_greedy(self, val): - LOG.warning( - "self.context_greedy has been deprecated and is unused, edit mycroft.conf instead, setter will be ignored") - - @property - def context_manager(self): - LOG.warning("context_manager has been deprecated, use Session.context instead") - sess = SessionManager.get() - return sess.context - - @context_manager.setter - def context_manager(self, val): - LOG.warning("context_manager has been deprecated, use Session.context instead") - assert isinstance(val, ContextManager) - sess = SessionManager.get() - sess.context = val - - def update_context(self, intent): - """Updates context with keyword from the intent. - - NOTE: This method currently won't handle one_of intent keywords - since it's not using quite the same format as other intent - keywords. This is under investigation in adapt, PR pending. - - Args: - intent: Intent to scan for keywords - """ - LOG.warning("update_context has been deprecated, use Session.context.update_context instead") - sess = SessionManager.get() - ents = [tag['entities'][0] for tag in intent['__tags__'] if 'entities' in tag] - sess.context.update_context(ents) - - def match_intent(self, utterances, lang=None, message=None): - """Run the Adapt engine to search for an matching intent. - - Args: - utterances (iterable): utterances for consideration in intent - matching. As a practical matter, a single utterance will be - passed in most cases. But there are instances, such as - streaming STT that could pass multiple. Each utterance - is represented as a tuple containing the raw, normalized, and - possibly other variations of the utterance. - - Returns: - Intent structure, or None if no match was found. - """ - # we call flatten in case someone is sending the old style list of tuples - utterances = flatten_list(utterances) - lang = lang or self.lang - if lang not in self.engines: - return None - - best_intent = {} - - def take_best(intent, utt): - nonlocal best_intent - best = best_intent.get('confidence', 0.0) if best_intent else 0.0 - conf = intent.get('confidence', 0.0) - if conf > best: - best_intent = intent - # TODO - Shouldn't Adapt do this? - best_intent['utterance'] = utt - - sess = SessionManager.get(message) - for utt in utterances: - try: - intents = [i for i in self.engines[lang].determine_intent( - utt, 100, - include_tags=True, - context_manager=sess.context)] - if intents: - utt_best = max( - intents, key=lambda x: x.get('confidence', 0.0) - ) - take_best(utt_best, utt) - - except Exception as err: - LOG.exception(err) - - if best_intent: - ents = [tag['entities'][0] for tag in best_intent['__tags__'] if 'entities' in tag] - - sess.context.update_context(ents) - - skill_id = best_intent['intent_type'].split(":")[0] - ret = ovos_core.intent_services.IntentMatch( - 'Adapt', best_intent['intent_type'], best_intent, skill_id, - best_intent['utterance'] - ) - else: - ret = None - return ret - - def register_vocab(self, start_concept, end_concept, - alias_of, regex_str, lang): - """Register Vocabulary. DEPRECATED - - This method should not be used, it has been replaced by - register_vocabulary(). - """ - self.register_vocabulary(start_concept, end_concept, alias_of, - regex_str, lang) - - def register_vocabulary(self, entity_value, entity_type, - alias_of, regex_str, lang): - """Register skill vocabulary as adapt entity. - - This will handle both regex registration and registration of normal - keywords. if the "regex_str" argument is set all other arguments will - be ignored. - - Argument: - entity_value: the natural langauge word - entity_type: the type/tag of an entity instance - alias_of: entity this is an alternative for - """ - if lang in self.engines: - with self.lock: - if regex_str: - self.engines[lang].register_regex_entity(regex_str) - else: - self.engines[lang].register_entity( - entity_value, entity_type, alias_of=alias_of) - - def register_intent(self, intent): - """Register new intent with adapt engine. - - Args: - intent (IntentParser): IntentParser to register - """ - for lang in self.engines: - with self.lock: - self.engines[lang].register_intent_parser(intent) - - def detach_skill(self, skill_id): - """Remove all intents for skill. - - Args: - skill_id (str): skill to process - """ - with self.lock: - for lang in self.engines: - skill_parsers = [ - p.name for p in self.engines[lang].intent_parsers if - p.name.startswith(skill_id) - ] - self.engines[lang].drop_intent_parser(skill_parsers) - self._detach_skill_keywords(skill_id) - self._detach_skill_regexes(skill_id) - - def _detach_skill_keywords(self, skill_id): - """Detach all keywords registered with a particular skill. - - Arguments: - skill_id (str): skill identifier - """ - skill_id = _entity_skill_id(skill_id) - - def match_skill_entities(data): - return data and data[1].startswith(skill_id) - - for lang in self.engines: - self.engines[lang].drop_entity(match_func=match_skill_entities) - - def _detach_skill_regexes(self, skill_id): - """Detach all regexes registered with a particular skill. - - Arguments: - skill_id (str): skill identifier - """ - skill_id = _entity_skill_id(skill_id) - - def match_skill_regexes(regexp): - return any([r.startswith(skill_id) - for r in regexp.groupindex.keys()]) - - for lang in self.engines: - self.engines[lang].drop_regex_entity(match_func=match_skill_regexes) - - def detach_intent(self, intent_name): - """Detatch a single intent - - Args: - intent_name (str): Identifier for intent to remove. - """ - for lang in self.engines: - new_parsers = [ - p for p in self.engines[lang].intent_parsers if p.name != intent_name - ] - self.engines[lang].intent_parsers = new_parsers +# deprecated file, TODO remove in 0.0.8 +from mycroft.skills.intent_services.adapt_service import AdaptService diff --git a/ovos_core/intent_services/commonqa_service.py b/ovos_core/intent_services/commonqa_service.py index a02acadb832a..675ed3928330 100644 --- a/ovos_core/intent_services/commonqa_service.py +++ b/ovos_core/intent_services/commonqa_service.py @@ -1,12 +1,11 @@ import re -from threading import Lock, Event - import time from itertools import chain -from ovos_bus_client.message import Message, dig_for_message +from threading import Lock, Event -import ovos_core.intent_services -from ovos_utils import flatten_list +from ovos_bus_client.message import Message, dig_for_message +from ovos_plugin_manager.templates.pipeline import PipelineStagePlugin, IntentMatch +from ovos_utils import flatten_list, classproperty from ovos_utils.enclosure.api import EnclosureAPI from ovos_utils.log import LOG from ovos_utils.messagebus import get_message_lang @@ -15,14 +14,14 @@ EXTENSION_TIME = 10 -class CommonQAService: +class CommonQAService(PipelineStagePlugin): """Intent Service handling common query skills. All common query skills answer and the best answer is selected This is in contrast to triggering best intent directly. """ - def __init__(self, bus): - self.bus = bus + def __init__(self, bus, config=None): + super().__init__(bus, config) self.skill_id = "common_query.openvoiceos" # fake skill self.query_replies = {} # cache of received replies self.query_extensions = {} # maintains query timeout extensions @@ -32,9 +31,47 @@ def __init__(self, bus): self.answered = False self.enclosure = EnclosureAPI(self.bus, self.skill_id) self._vocabs = {} + + @classproperty + def matcher_id(self): + return "common_qa" + + # plugin api + def register_bus_events(self): self.bus.on('question:query.response', self.handle_query_response) self.bus.on('common_query.question', self.handle_question) + def match(self, utterances, lang, message): + """Send common query request and select best response + + Args: + utterances (list): List of tuples, + utterances and normalized version + lang (str): Language code + message: Message for session context + Returns: + IntentMatch or None + """ + # we call flatten in case someone is sending the old style list of tuples + utterances = flatten_list(utterances) + match = None + for utterance in utterances: + if self.is_question_like(utterance, lang): + message.data["lang"] = lang # only used for speak + message.data["utterance"] = utterance + answered = self.handle_question(message) + if answered: + match = IntentMatch( + intent_service=self.matcher_id, + intent_type="common_query", + intent_data={}, + skill_id=self.skill_id, + utterance=utterance, + confidence=1.0) + break + return match + + # implementation def voc_match(self, utterance, voc_filename, lang, exact=False): """Determine if the given utterance contains the vocabulary provided. @@ -84,30 +121,6 @@ def is_question_like(self, utterance, lang): return False return True - def match(self, utterances, lang, message): - """Send common query request and select best response - - Args: - utterances (list): List of tuples, - utterances and normalized version - lang (str): Language code - message: Message for session context - Returns: - IntentMatch or None - """ - # we call flatten in case someone is sending the old style list of tuples - utterances = flatten_list(utterances) - match = None - for utterance in utterances: - if self.is_question_like(utterance, lang): - message.data["lang"] = lang # only used for speak - message.data["utterance"] = utterance - answered = self.handle_question(message) - if answered: - match = ovos_core.intent_services.IntentMatch('CommonQuery', None, {}, None, utterance) - break - return match - def handle_question(self, message): """ Send the phrase to the CommonQuerySkills and prepare for handling the replies. diff --git a/ovos_core/intent_services/converse_service.py b/ovos_core/intent_services/converse_service.py index 915ddaa58a57..d15f45132ddd 100644 --- a/ovos_core/intent_services/converse_service.py +++ b/ovos_core/intent_services/converse_service.py @@ -3,35 +3,39 @@ from ovos_config.config import Configuration from ovos_config.locale import setup_locale -import ovos_core.intent_services from ovos_bus_client.message import Message from ovos_bus_client.session import SessionManager -from ovos_utils import flatten_list +from ovos_plugin_manager.templates.pipeline import PipelineStagePlugin, IntentMatch +from ovos_utils import flatten_list, classproperty from ovos_utils.log import LOG from ovos_utils.messagebus import get_message_lang from ovos_workshop.permissions import ConverseMode, ConverseActivationMode -class ConverseService: +class ConverseService(PipelineStagePlugin): """Intent Service handling conversational skills.""" - def __init__(self, bus): - self.bus = bus + def __init__(self, bus, config=None): + config = config or Configuration().get("skills", {}).get("converse") or {} + super().__init__(bus, config) self._consecutive_activations = {} + + # plugin api + @classproperty + def matcher_id(self): + return "converse" + + def match(self, utterances: list, lang: str, message: Message): + return self.converse_with_skills(utterances, lang, message) + + def register_bus_events(self): self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse) self.bus.on('intent.service.skills.deactivate', self.handle_deactivate_skill_request) self.bus.on('intent.service.skills.activate', self.handle_activate_skill_request) self.bus.on('active_skill_request', self.handle_activate_skill_request) # TODO backwards compat, deprecate self.bus.on('intent.service.active_skills.get', self.handle_get_active_skills) - @property - def config(self): - """ - Returns: - converse_config (dict): config for converse handling options - """ - return Configuration().get("skills", {}).get("converse") or {} - + # implementation @property def active_skills(self): session = SessionManager.get() @@ -290,7 +294,12 @@ def converse_with_skills(self, utterances, lang, message): # check if any skill wants to handle utterance for skill_id in self._collect_converse_skills(message): if self.converse(utterances, skill_id, lang, message): - return ovos_core.intent_services.IntentMatch('Converse', None, None, skill_id, utterances[0]) + return IntentMatch(intent_service=self.matcher_id, + intent_type=f"converse_{skill_id}", + intent_data={}, + skill_id=skill_id, + confidence=1.0, + utterance=utterances[0]) return None def handle_activate_skill_request(self, message): diff --git a/ovos_core/intent_services/fallback_service.py b/ovos_core/intent_services/fallback_service.py index baac1ded0615..bf52cafcb7f4 100644 --- a/ovos_core/intent_services/fallback_service.py +++ b/ovos_core/intent_services/fallback_service.py @@ -14,29 +14,54 @@ # """Intent service for Mycroft's fallback system.""" import operator +import time from collections import namedtuple -import time from ovos_config import Configuration -import ovos_core.intent_services -from ovos_utils import flatten_list +from ovos_bus_client.message import Message +from ovos_plugin_manager.templates.pipeline import PipelineStagePlugin, IntentMatch +from ovos_utils import flatten_list, classproperty from ovos_utils.log import LOG from ovos_workshop.skills.fallback import FallbackMode FallbackRange = namedtuple('FallbackRange', ['start', 'stop']) -class FallbackService: +class FallbackService(PipelineStagePlugin): """Intent Service handling fallback skills.""" - def __init__(self, bus): - self.bus = bus - self.fallback_config = Configuration()["skills"].get("fallbacks", {}) + def __init__(self, bus, config=None): + config = config or Configuration()["skills"].get("fallbacks", {}) + super().__init__(bus, config) self.registered_fallbacks = {} # skill_id: priority + + # plugin api + @classproperty + def matcher_id(self): + return "fallback" + + def register_bus_events(self): self.bus.on("ovos.skills.fallback.register", self.handle_register_fallback) self.bus.on("ovos.skills.fallback.deregister", self.handle_deregister_fallback) + def match(self, utterances: list, lang: str, message: Message): + return self._fallback_range(utterances, lang, message, FallbackRange(0, 101)) + + def match_high(self, utterances: list, lang: str, message: Message): + return self.high_prio(utterances, lang, message) + + def match_medium(self, utterances: list, lang: str, message: Message): + return self.medium_prio(utterances, lang, message) + + def match_low(self, utterances: list, lang: str, message: Message): + return self.low_prio(utterances, lang, message) + + # implementation + @property + def fallback_config(self): + return self.config + def handle_register_fallback(self, message): skill_id = message.data.get("skill_id") priority = message.data.get("priority") or 101 @@ -165,7 +190,12 @@ def _fallback_range(self, utterances, lang, message, fb_range): for skill_id, prio in sorted_handlers: result = self.attempt_fallback(utterances, skill_id, lang, message) if result: - return ovos_core.intent_services.IntentMatch('Fallback', None, {}, None, utterances[0]) + return IntentMatch(intent_service=self.matcher_id, + intent_type=f"fallback_{skill_id}", + intent_data={}, + skill_id=skill_id, + utterance=utterances[0], + confidence=prio / 100) # old style deprecated fallback skill singleton class LOG.debug("checking for FallbackSkillsV1") @@ -178,7 +208,12 @@ def _fallback_range(self, utterances, lang, message, fb_range): response = self.bus.wait_for_response(msg, timeout=10) if response and response.data['handled']: - return ovos_core.intent_services.IntentMatch('Fallback', None, {}, None, utterances[0]) + return IntentMatch(intent_service=self.matcher_id, + intent_type=f"legacy_fallback_{fb_range.stop}", + intent_data={}, + skill_id="fallbacks.openvoiceos", + utterance=utterances[0], + confidence=(fb_range.stop - fb_range.start) / 100) return None def high_prio(self, utterances, lang, message): diff --git a/ovos_core/intent_services/padacioso_service.py b/ovos_core/intent_services/padacioso_service.py deleted file mode 100644 index a34e2e0d4555..000000000000 --- a/ovos_core/intent_services/padacioso_service.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Intent service wrapping padacioso.""" -import concurrent.futures -from functools import lru_cache -from os.path import isfile -from typing import List, Optional - -from ovos_config.config import Configuration -from ovos_utils import flatten_list -from ovos_utils.log import LOG -from padacioso import IntentContainer as FallbackIntentContainer - -import ovos_core.intent_services -from ovos_bus_client.message import Message - - -class PadaciosoIntent: - """ - A set of data describing how a query fits into an intent - Attributes: - name (str): Name of matched intent - sent (str): The input utterance associated with the intent - conf (float): Confidence (from 0.0 to 1.0) - matches (dict of str -> str): Key is the name of the entity and - value is the extracted part of the sentence - """ - - def __init__(self, name, sent, matches=None, conf=0.0): - self.name = name - self.sent = sent - self.matches = matches or {} - self.conf = conf - - def __getitem__(self, item): - return self.matches.__getitem__(item) - - def __contains__(self, item): - return self.matches.__contains__(item) - - def get(self, key, default=None): - return self.matches.get(key, default) - - def __repr__(self): - return repr(self.__dict__) - - -class PadaciosoService: - """Service class for padacioso intent matching.""" - - def __init__(self, bus, config): - self.padacioso_config = config - self.bus = bus - - core_config = Configuration() - self.lang = core_config.get("lang", "en-us") - langs = core_config.get('secondary_langs') or [] - if self.lang not in langs: - langs.append(self.lang) - - self.conf_high = self.padacioso_config.get("conf_high") or 0.95 - self.conf_med = self.padacioso_config.get("conf_med") or 0.8 - self.conf_low = self.padacioso_config.get("conf_low") or 0.5 - self.workers = self.padacioso_config.get("workers") or 4 - - LOG.debug('Using Padacioso intent parser.') - self.containers = {lang: FallbackIntentContainer( - self.padacioso_config.get("fuzz"), n_workers=self.workers) - for lang in langs} - - self.bus.on('padatious:register_intent', self.register_intent) - self.bus.on('padatious:register_entity', self.register_entity) - self.bus.on('detach_intent', self.handle_detach_intent) - self.bus.on('detach_skill', self.handle_detach_skill) - - self.registered_intents = [] - self.registered_entities = [] - - def _match_level(self, utterances, limit, lang=None): - """Match intent and make sure a certain level of confidence is reached. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - limit (float): required confidence level. - """ - LOG.debug(f'Padacioso Matching confidence > {limit}') - # call flatten in case someone is sending the old style list of tuples - utterances = flatten_list(utterances) - lang = lang or self.lang - padacioso_intent = self.calc_intent(utterances, lang) - if padacioso_intent is not None and padacioso_intent.conf > limit: - skill_id = padacioso_intent.name.split(':')[0] - return ovos_core.intent_services.IntentMatch( - 'Padacioso', padacioso_intent.name, - padacioso_intent.matches, skill_id, padacioso_intent.sent) - - def match_high(self, utterances, lang=None, message=None): - """Intent matcher for high confidence. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - """ - return self._match_level(utterances, self.conf_high, lang) - - def match_medium(self, utterances, lang=None, message=None): - """Intent matcher for medium confidence. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - """ - return self._match_level(utterances, self.conf_med, lang) - - def match_low(self, utterances, lang=None, message=None): - """Intent matcher for low confidence. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - """ - return self._match_level(utterances, self.conf_low, lang) - - def __detach_intent(self, intent_name): - """ Remove an intent if it has been registered. - - Args: - intent_name (str): intent identifier - """ - if intent_name in self.registered_intents: - self.registered_intents.remove(intent_name) - for lang in self.containers: - self.containers[lang].remove_intent(intent_name) - - def handle_detach_intent(self, message): - """Messagebus handler for detaching padacioso intent. - - Args: - message (Message): message triggering action - """ - self.__detach_intent(message.data.get('intent_name')) - - def handle_detach_skill(self, message): - """Messagebus handler for detaching all intents for skill. - - Args: - message (Message): message triggering action - """ - skill_id = message.data['skill_id'] - remove_list = [i for i in self.registered_intents if skill_id in i] - for i in remove_list: - self.__detach_intent(i) - - def _register_object(self, message, object_name, register_func): - """Generic method for registering a padacioso object. - - Args: - message (Message): trigger for action - object_name (str): type of entry to register - register_func (callable): function to call for registration - """ - file_name = message.data.get('file_name') - samples = message.data.get("samples") - name = message.data['name'] - - LOG.debug('Registering Padacioso ' + object_name + ': ' + name) - - if (not file_name or not isfile(file_name)) and not samples: - LOG.error('Could not find file ' + file_name) - return - - if not samples and isfile(file_name): - with open(file_name) as f: - samples = [l.strip() for l in f.readlines()] - - register_func(name, samples) - - def register_intent(self, message): - """Messagebus handler for registering intents. - - Args: - message (Message): message triggering action - """ - lang = message.data.get('lang', self.lang) - lang = lang.lower() - if lang in self.containers: - self.registered_intents.append(message.data['name']) - try: - self._register_object(message, 'intent', - self.containers[lang].add_intent) - except RuntimeError: - name = message.data.get('name', "") - # padacioso fails on reloading a skill, just ignore - if name not in self.containers[lang].intent_samples: - raise - - def register_entity(self, message): - """Messagebus handler for registering entities. - - Args: - message (Message): message triggering action - """ - lang = message.data.get('lang', self.lang) - lang = lang.lower() - if lang in self.containers: - self.registered_entities.append(message.data) - self._register_object(message, 'entity', - self.containers[lang].add_entity) - - def calc_intent(self, utterances: List[str], lang: str = None) -> Optional[PadaciosoIntent]: - """ - Get the best intent match for the given list of utterances. Utilizes a - thread pool for overall faster execution. Note that this method is NOT - compatible with Padacioso, but is compatible with Padacioso. - @param utterances: list of string utterances to get an intent for - @param lang: language of utterances - @return: - """ - if isinstance(utterances, str): - utterances = [utterances] # backwards compat when arg was a single string - lang = lang or self.lang - lang = lang.lower() - if lang in self.containers: - intent_container = self.containers.get(lang) - intents = [_calc_padacioso_intent(utt, intent_container) for utt in utterances] - intents = [i for i in intents if i is not None] - # select best - if intents: - return max(intents, key=lambda k: k.conf) - - -@lru_cache(maxsize=3) # repeat calls under different conf levels wont re-run code -def _calc_padacioso_intent(utt, intent_container) -> \ - Optional[PadaciosoIntent]: - """ - Try to match an utterance to an intent in an intent_container - @param args: tuple of (utterance, IntentContainer) - @return: matched PadaciosoIntent - """ - try: - intent = intent_container.calc_intent(utt) - if "entities" in intent: - intent["matches"] = intent.pop("entities") - intent["sent"] = utt - intent = PadaciosoIntent(**intent) - intent.sent = utt - return intent - except Exception as e: - LOG.error(e) diff --git a/ovos_core/intent_services/padatious_service.py b/ovos_core/intent_services/padatious_service.py index 3c0e83112147..4b4094bc9ce5 100644 --- a/ovos_core/intent_services/padatious_service.py +++ b/ovos_core/intent_services/padatious_service.py @@ -1,285 +1,3 @@ -# Copyright 2020 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Intent service wrapping padatious.""" -import concurrent.futures -from functools import lru_cache -from os import path -from os.path import expanduser, isfile -from threading import Event -from time import time as get_time, sleep -from typing import List, Optional +# deprecated file, TODO remove in 0.0.8 +from mycroft.skills.intent_services.padatious_service import PadatiousService, PadatiousMatcher, PadatiousIntent -import padatious -from padatious.match_data import MatchData as PadatiousIntent -from ovos_config.config import Configuration -from ovos_config.meta import get_xdg_base -from ovos_utils import flatten_list -from ovos_utils.log import LOG -from ovos_utils.xdg_utils import xdg_data_home - -import ovos_core.intent_services -from ovos_bus_client.message import Message - - -class PadatiousMatcher: - """Matcher class to avoid redundancy in padatious intent matching.""" - - def __init__(self, service): - self.service = service - - def _match_level(self, utterances, limit, lang=None): - """Match intent and make sure a certain level of confidence is reached. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - limit (float): required confidence level. - """ - LOG.debug(f'Padatious Matching confidence > {limit}') - # call flatten in case someone is sending the old style list of tuples - utterances = flatten_list(utterances) - lang = lang or self.service.lang - padatious_intent = self.service.calc_intent(utterances, lang) - if padatious_intent is not None and padatious_intent.conf > limit: - skill_id = padatious_intent.name.split(':')[0] - return ovos_core.intent_services.IntentMatch( - 'Padatious', padatious_intent.name, - padatious_intent.matches, skill_id, padatious_intent.sent) - - def match_high(self, utterances, lang=None, message=None): - """Intent matcher for high confidence. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - """ - return self._match_level(utterances, self.service.conf_high, lang) - - def match_medium(self, utterances, lang=None, message=None): - """Intent matcher for medium confidence. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - """ - return self._match_level(utterances, self.service.conf_med, lang) - - def match_low(self, utterances, lang=None, message=None): - """Intent matcher for low confidence. - - Args: - utterances (list of tuples): Utterances to parse, originals paired - with optional normalized version. - """ - return self._match_level(utterances, self.service.conf_low, lang) - - -class PadatiousService: - """Service class for padatious intent matching.""" - - def __init__(self, bus, config): - self.padatious_config = config - self.bus = bus - - core_config = Configuration() - self.lang = core_config.get("lang", "en-us") - langs = core_config.get('secondary_langs') or [] - if self.lang not in langs: - langs.append(self.lang) - - self.conf_high = self.padatious_config.get("conf_high") or 0.95 - self.conf_med = self.padatious_config.get("conf_med") or 0.8 - self.conf_low = self.padatious_config.get("conf_low") or 0.5 - - LOG.debug('Using Padatious intent parser.') - intent_cache = self.padatious_config.get( - 'intent_cache') or f"{xdg_data_home()}/{get_xdg_base()}/intent_cache" - self.containers = { - lang: padatious.IntentContainer(path.join(expanduser(intent_cache), lang)) - for lang in langs} - - self.bus.on('padatious:register_intent', self.register_intent) - self.bus.on('padatious:register_entity', self.register_entity) - self.bus.on('detach_intent', self.handle_detach_intent) - self.bus.on('detach_skill', self.handle_detach_skill) - self.bus.on('mycroft.skills.initialized', self.train) - - self.finished_training_event = Event() - self.finished_initial_train = False - - self.train_delay = self.padatious_config.get('train_delay', 4) - self.train_time = get_time() + self.train_delay - - self.registered_intents = [] - self.registered_entities = [] - - def train(self, message=None): - """Perform padatious training. - - Args: - message (Message): optional triggering message - """ - self.finished_training_event.clear() - padatious_single_thread = self.padatious_config.get('single_thread', True) - if message is None: - single_thread = padatious_single_thread - else: - single_thread = message.data.get('single_thread', - padatious_single_thread) - for lang in self.containers: - self.containers[lang].train(single_thread=single_thread) - - LOG.info('Training complete.') - self.finished_training_event.set() - if not self.finished_initial_train: - self.bus.emit(Message('mycroft.skills.trained')) - self.finished_initial_train = True - - def wait_and_train(self): - """Wait for minimum time between training and start training.""" - if not self.finished_initial_train: - return - sleep(self.train_delay) - if self.train_time < 0.0: - return - - if self.train_time <= get_time() + 0.01: - self.train_time = -1.0 - self.train() - - def __detach_intent(self, intent_name): - """ Remove an intent if it has been registered. - - Args: - intent_name (str): intent identifier - """ - if intent_name in self.registered_intents: - self.registered_intents.remove(intent_name) - for lang in self.containers: - self.containers[lang].remove_intent(intent_name) - - def handle_detach_intent(self, message): - """Messagebus handler for detaching padatious intent. - - Args: - message (Message): message triggering action - """ - self.__detach_intent(message.data.get('intent_name')) - - def handle_detach_skill(self, message): - """Messagebus handler for detaching all intents for skill. - - Args: - message (Message): message triggering action - """ - skill_id = message.data['skill_id'] - remove_list = [i for i in self.registered_intents if skill_id in i] - for i in remove_list: - self.__detach_intent(i) - - def _register_object(self, message, object_name, register_func): - """Generic method for registering a padatious object. - - Args: - message (Message): trigger for action - object_name (str): type of entry to register - register_func (callable): function to call for registration - """ - file_name = message.data.get('file_name') - samples = message.data.get("samples") - name = message.data['name'] - - LOG.debug('Registering Padatious ' + object_name + ': ' + name) - - if (not file_name or not isfile(file_name)) and not samples: - LOG.error('Could not find file ' + file_name) - return - - if not samples and isfile(file_name): - with open(file_name) as f: - samples = [l.strip() for l in f.readlines()] - - register_func(name, samples) - - self.train_time = get_time() + self.train_delay - self.wait_and_train() - - def register_intent(self, message): - """Messagebus handler for registering intents. - - Args: - message (Message): message triggering action - """ - lang = message.data.get('lang', self.lang) - lang = lang.lower() - if lang in self.containers: - self.registered_intents.append(message.data['name']) - try: - self._register_object(message, 'intent', - self.containers[lang].add_intent) - except RuntimeError: - name = message.data.get('name', "") - # padacioso fails on reloading a skill, just ignore - if name not in self.containers[lang].intent_samples: - raise - - def register_entity(self, message): - """Messagebus handler for registering entities. - - Args: - message (Message): message triggering action - """ - lang = message.data.get('lang', self.lang) - lang = lang.lower() - if lang in self.containers: - self.registered_entities.append(message.data) - self._register_object(message, 'entity', - self.containers[lang].add_entity) - - def calc_intent(self, utterances: List[str], lang: str = None) -> Optional[PadatiousIntent]: - """ - Get the best intent match for the given list of utterances. Utilizes a - thread pool for overall faster execution. Note that this method is NOT - compatible with Padatious, but is compatible with Padacioso. - @param utterances: list of string utterances to get an intent for - @param lang: language of utterances - @return: - """ - if isinstance(utterances, str): - utterances = [utterances] # backwards compat when arg was a single string - lang = lang or self.lang - lang = lang.lower() - if lang in self.containers: - intent_container = self.containers.get(lang) - intents = [_calc_padatious_intent(utt, intent_container) for utt in utterances] - intents = [i for i in intents if i is not None] - # select best - if intents: - return max(intents, key=lambda k: k.conf) - - -@lru_cache(maxsize=3) # repeat calls under different conf levels wont re-run code -def _calc_padatious_intent(utt, intent_container) -> Optional[PadatiousIntent]: - """ - Try to match an utterance to an intent in an intent_container - @param args: tuple of (utterance, IntentContainer) - @return: matched PadatiousIntent - """ - try: - intent = intent_container.calc_intent(utt) - intent.sent = utt - return intent - except Exception as e: - LOG.error(e) diff --git a/test/unittests/skills/test_utterance_intents.py b/test/unittests/skills/test_utterance_intents.py index e7bb3cb7d1e3..4e08f9d51799 100644 --- a/test/unittests/skills/test_utterance_intents.py +++ b/test/unittests/skills/test_utterance_intents.py @@ -3,6 +3,8 @@ from ovos_utils.messagebus import FakeBus from ovos_bus_client.message import Message +# TODO - rewrite tests around pipeline config, not padatious deprecated config flag + from ovos_core.intent_services.padacioso_service import FallbackIntentContainer, PadaciosoService