Skip to content

Commit

Permalink
feat/pipeline_plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
JarbasAl committed Sep 20, 2023
1 parent f4cca08 commit 08fa559
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 163 deletions.
112 changes: 55 additions & 57 deletions ovos_core/intent_services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.
#
from collections import namedtuple
from ovos_plugin_manager.templates.pipeline import PipelineComponentPlugin, PipelineMultiConfPlugin, IntentMatch

from ovos_config.config import Configuration
from ovos_config.locale import setup_locale
Expand All @@ -25,8 +26,7 @@
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_utils.log import LOG
from ovos_utils.log import LOG, deprecated
from ovos_utils.messagebus import get_message_lang
from ovos_utils.metrics import Stopwatch

Expand All @@ -35,16 +35,6 @@
except ImportError:
from ovos_core.intent_services.padacioso_service import PadaciosoService as PadatiousService

# 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']
)


class IntentService:
"""Mycroft intent service. parses utterances using a variety of systems.
Expand All @@ -60,8 +50,10 @@ def __init__(self, bus):
# Dictionary for translating a skill id to a name
self.skill_names = {}

self.pipeline_plugins = {}

# TODO - replace with plugins
self.adapt_service = AdaptService()
self.adapt_service = AdaptService(self.bus)
if PadaciosoService is not PadatiousService:
self.padatious_service = PadatiousService(bus, config['padatious'])
else:
Expand All @@ -71,11 +63,10 @@ def __init__(self, bus):
self.fallback = FallbackService(bus)
self.converse = ConverseService(bus)
self.common_qa = CommonQAService(bus)

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)
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)
Expand Down Expand Up @@ -129,6 +120,49 @@ def pipeline(self):
"fallback_low"
])

def load_pipeline_plugins(self):
plugins = {} # TODO

for plug_name, plug in plugins.items():
if issubclass(plug, PipelineMultiConfPlugin):
matchers = [plug.matcher_id,
plug.matcher_id + "_low",
plug.matcher_id + "_medium",
plug.matcher_id + "_high"]
else:
matchers = [plug.matcher_id]

if any(m in self.pipeline for m in matchers):
try:
self.pipeline_plugins[plug_name] = plug(self.bus)
except Exception as e:
LOG.error(f"failed to load plugin {plug_name}:{e}")
continue

@property
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 != matcher_id and not isinstance(plugin, PipelineMultiConfPlugin):
LOG.error("pipeline plugin should subclass PipelineMultiConfPlugin to support match levels")
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

@property
def registered_intents(self):
lang = get_message_lang()
Expand Down Expand Up @@ -347,35 +381,25 @@ 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'))

@deprecated("handle_register_intent moved to AdaptService, overriding this method has no effect, "
"it has been disconnected from the bus event", "0.8.0")
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)
pass

@deprecated("handle_register_intent moved to AdaptService, overriding this method has no effect, "
"it has been disconnected from the bus event", "0.8.0")
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)
pass

def handle_detach_intent(self, message):
"""Remover adapt intent.
Expand Down Expand Up @@ -547,29 +571,3 @@ def handle_entity_manifest(self, message):
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']
53 changes: 46 additions & 7 deletions ovos_core/intent_services/adapt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
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_plugin_manager.templates.pipeline import PipelineComponentPlugin, IntentMatch
from ovos_utils import flatten_list, classproperty
from ovos_utils.intents.intent_service_interface import open_intent_envelope
from ovos_utils.log import LOG
from ovos_utils.messagebus import get_message_lang, get_mycroft_bus


def _entity_skill_id(skill_id):
Expand All @@ -40,22 +42,59 @@ def _entity_skill_id(skill_id):
return skill_id


class AdaptService:
class AdaptService(PipelineComponentPlugin):
"""Intent service wrapping the Adapt intent Parser."""

def __init__(self, config=None):
def __init__(self, bus=None, config=None):
bus = bus or get_mycroft_bus() # backwards compat, bus was optional
core_config = Configuration()
self.config = config or core_config.get("context", {})
config = config or core_config.get("context", {})
super().__init__(bus, config)
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()

@classproperty
def matcher_id(self):
return "adapt"

# plugin api
def match(self, utterances, lang, message):
return self.match_intent(utterances, lang, message)

def register_bus_events(self):
self.bus.on('register_vocab', self.handle_register_vocab)
self.bus.on('register_intent', self.handle_register_intent)

# implementation
def handle_register_vocab(self, message):
"""Register adapt vocabulary.
Args:
message (Message): message containing vocab info
"""
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.register_vocabulary(entity_value, entity_type,
alias_of, regex_str, lang)

def handle_register_intent(self, message):
"""Register adapt intent.
Args:
message (Message): message containing intent info
"""
intent = open_intent_envelope(message)
self.register_intent(intent)

@property
def context_keywords(self):
LOG.warning(
Expand Down Expand Up @@ -180,7 +219,7 @@ def take_best(intent, utt):
sess.context.update_context(ents)

skill_id = best_intent['intent_type'].split(":")[0]
ret = ovos_core.intent_services.IntentMatch(
ret = IntentMatch(
'Adapt', best_intent['intent_type'], best_intent, skill_id,
best_intent['utterance']
)
Expand Down
71 changes: 39 additions & 32 deletions ovos_core/intent_services/commonqa_service.py
Original file line number Diff line number Diff line change
@@ -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 PipelineComponentPlugin, 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
Expand All @@ -15,14 +14,14 @@
EXTENSION_TIME = 10


class CommonQAService:
class CommonQAService(PipelineComponentPlugin):
"""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
Expand All @@ -32,9 +31,41 @@ 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('CommonQuery', None, {}, None, utterance)
break
return match

# implementation
def voc_match(self, utterance, voc_filename, lang, exact=False):
"""Determine if the given utterance contains the vocabulary provided.
Expand Down Expand Up @@ -84,30 +115,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.
Expand Down
Loading

0 comments on commit 08fa559

Please sign in to comment.