From 883ba862b0b83bee602b6bdfdf75d5ae3a08eb52 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 14 Apr 2023 18:58:55 -0700 Subject: [PATCH 1/4] More dependency troubleshooting Resolve test failure from upstream refactor Troubleshooting ovos-backend-client fix Add import to apply module patch Remove extra exception handling Update patching in unit test Update patching in tests More test failure troubleshooting More test failure troubleshooting More test failure troubleshooting More test failure troubleshooting Mark `decorators` module as deprecated Update imports from `mycroft` to `ovos-core` Remove invalid test case Troubleshoot test failures Replace skill settings patch More test failure troubleshooting Update dependencies to troubleshoot backwards-compat Update core module dependencies Update ovos-core patching Remove dependency patches Update patches and ovos-core version to latest alpha Update ovos_workshop dependency version and add comon_query patching --- neon_core/__init__.py | 4 + neon_core/skills/__init__.py | 18 ++-- neon_core/skills/decorators.py | 10 +- neon_core/skills/intent_service.py | 3 +- neon_core/skills/patched_common_query.py | 3 +- neon_core/skills/patched_plugin_loader.py | 107 ---------------------- neon_core/skills/skill_manager.py | 2 +- test/test_skills_module.py | 18 ++-- 8 files changed, 31 insertions(+), 134 deletions(-) delete mode 100644 neon_core/skills/patched_plugin_loader.py diff --git a/neon_core/__init__.py b/neon_core/__init__.py index bf6d37be3..05109769d 100644 --- a/neon_core/__init__.py +++ b/neon_core/__init__.py @@ -33,3 +33,7 @@ ovos_utils.messagebus.get_handler_name = get_handler_name ovos_utils.messagebus.create_wrapper = create_wrapper ovos_utils.messagebus.EventContainer = EventContainer + + +# TODO: Patching out Backend Client +from ovos_backend_client.backends.offline import OfflineBackend diff --git a/neon_core/skills/__init__.py b/neon_core/skills/__init__.py index d5b94574d..2175c01ba 100644 --- a/neon_core/skills/__init__.py +++ b/neon_core/skills/__init__.py @@ -29,19 +29,13 @@ from neon_core.skills.decorators import intent_handler, intent_file_handler, \ resting_screen_handler, conversational_intent -# TODO: Remove below patches with ovos-core 0.0.8 refactor -import neon_core.skills.patched_plugin_loader -import neon_core.skills.patched_common_query +# TODO: Resolve remote config bug in skill settings +import neon_core.skills.patched_skill_settings -import mycroft.skills -from mycroft.skills import api -from mycroft.skills import skill_manager -from mycroft.skills.intent_services import padatious_service, converse_service -from ovos_bus_client.message import Message -mycroft.skills.api.Message = Message -mycroft.skills.skill_manager.Message = Message -mycroft.skills.intent_services.padatious_service.Message = Message -mycroft.skills.intent_services.converse_service.Message = Message + +# Backwards-compat import +from ovos_workshop.decorators import intent_handler, intent_file_handler, \ + resting_screen_handler __all__ = ['intent_handler', 'intent_file_handler', diff --git a/neon_core/skills/decorators.py b/neon_core/skills/decorators.py index 067bd8f5d..4fd484c97 100644 --- a/neon_core/skills/decorators.py +++ b/neon_core/skills/decorators.py @@ -25,6 +25,11 @@ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from ovos_utils.log import log_deprecation +log_deprecation("This module is deprecated. " + "Import from `ovos_workshop.decorators", "24.2.0") + """Decorators for use with MycroftSkill methods""" import time import threading @@ -34,8 +39,9 @@ from ovos_bus_client import Message from ovos_utils import create_killable_daemon -from mycroft.skills.mycroft_skill.decorators import intent_handler, \ - intent_file_handler, resting_screen_handler, skill_api_method +# Backwards-compat import +from ovos_workshop.decorators import intent_handler, intent_file_handler, \ + resting_screen_handler, skill_api_method class AbortEvent(StopIteration): diff --git a/neon_core/skills/intent_service.py b/neon_core/skills/intent_service.py index 88ba42228..f7535f63f 100644 --- a/neon_core/skills/intent_service.py +++ b/neon_core/skills/intent_service.py @@ -46,8 +46,7 @@ from neon_core.configuration import Configuration from neon_core.language import get_lang_config -from mycroft.skills.intent_service import IntentService -from mycroft.skills.intent_services import ConverseService, IntentMatch +from ovos_core.intent_services import IntentService, ConverseService try: from neon_utterance_translator_plugin import UtteranceTranslator diff --git a/neon_core/skills/patched_common_query.py b/neon_core/skills/patched_common_query.py index 1a1c29538..11c58f95f 100644 --- a/neon_core/skills/patched_common_query.py +++ b/neon_core/skills/patched_common_query.py @@ -38,9 +38,10 @@ from ovos_bus_client.apis.enclosure import EnclosureAPI from ovos_bus_client.util import get_message_lang from ovos_utils.log import LOG +from ovos_utils.messagebus import get_message_lang +from ovos_workshop.resource_files import CoreResources from mycroft.skills.intent_services.base import IntentMatch -from mycroft.skills.skill_data import CoreResources # TODO: Timeout from config # TODO: Port to ovos-core diff --git a/neon_core/skills/patched_plugin_loader.py b/neon_core/skills/patched_plugin_loader.py deleted file mode 100644 index 06637572e..000000000 --- a/neon_core/skills/patched_plugin_loader.py +++ /dev/null @@ -1,107 +0,0 @@ -# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework -# All trademark and other rights reserved by their respective owners -# Copyright 2008-2022 Neongecko.com Inc. -# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, -# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo -# BSD-3 License -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from this -# software without specific prior written permission. -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# TODO: Remove below patches with ovos-core 0.0.8 -import mycroft.skills.skill_loader - -from ovos_bus_client.message import Message -from ovos_utils.log import LOG - -try: - from ovos_workshop.skill_launcher import SkillLoader - from ovos_workshop.skill_launcher import PluginSkillLoader as _Plugin - from ovos_workshop.skill_launcher import get_skill_class, \ - get_create_skill_function - - class PluginSkillLoader(_Plugin): - def _create_skill_instance(self, skill_module=None): - skill_module = skill_module or self.skill_module - - try: - # in skill classes __new__ should fully create the skill object - skill_class = self._skill_class or get_skill_class(skill_module) - LOG.debug(f"loading skill: {skill_class}") - self.instance = skill_class(bus=self.bus, skill_id=self.skill_id) - return self.instance is not None - except Exception as e: - LOG.warning(f"Skill load raised exception: {e}") - - try: - # attempt to use old style create_skill function entrypoint - skill_creator = get_create_skill_function(skill_module) or \ - self.skill_class - except Exception as e: - LOG.exception(f"Failed to load skill creator: {e}") - self.instance = None - return False - - # if the signature supports skill_id and bus pass them - # to fully initialize the skill in 1 go - try: - # skills that do will have bus and skill_id available - # as soon as they call super() - self.instance = skill_creator(bus=self.bus, - skill_id=self.skill_id) - except Exception as e: - # most old skills do not expose bus/skill_id kwargs - LOG.warning(f"Legacy skill: {e}") - self.instance = skill_creator() - - try: - # finish initialization of skill if we didn't manage to inject - # skill_id and bus kwargs. - # these skills only have skill_id and bus available in initialize, - # not in __init__ - try: - if not self.instance.is_fully_initialized: - self.instance._startup(self.bus, self.skill_id) - except AttributeError: - if not self.instance._is_fully_initialized: - self.instance._startup(self.bus, self.skill_id) - except Exception as e: - LOG.exception(f'Skill __init__ failed with {e}') - self.instance = None - - return self.instance is not None - -except ImportError: - LOG.warning(f"Patching PluginSkillLoader") - from mycroft.skills.skill_loader import PluginSkillLoader as _Plugin - from mycroft.skills.skill_loader import SkillLoader - get_skill_class, get_create_skill_function = None, None - - - class PluginSkillLoader(_Plugin): - def load(self, skill_class=None): - skill_class = skill_class or self._skill_class - _Plugin.load(self, skill_class) - - -mycroft.skills.skill_loader.SkillLoader = SkillLoader -mycroft.skills.skill_loader.PluginSkillLoader = PluginSkillLoader -mycroft.skills.skill_loader.Message = Message diff --git a/neon_core/skills/skill_manager.py b/neon_core/skills/skill_manager.py index 2c820673a..7670978ad 100644 --- a/neon_core/skills/skill_manager.py +++ b/neon_core/skills/skill_manager.py @@ -31,7 +31,7 @@ from ovos_utils.xdg_utils import xdg_data_home from ovos_utils.log import LOG -from mycroft.skills.skill_manager import SkillManager +from ovos_core.skill_manager import SkillManager class NeonSkillManager(SkillManager): diff --git a/test/test_skills_module.py b/test/test_skills_module.py index 50a1e5688..1b2fc2174 100644 --- a/test/test_skills_module.py +++ b/test/test_skills_module.py @@ -26,7 +26,6 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import importlib import os import shutil import sys @@ -36,10 +35,9 @@ from copy import deepcopy from os.path import join, dirname, expanduser, isdir from threading import Event -from time import time, sleep +from time import time -from mock import Mock -from mock.mock import patch +from unittest.mock import Mock, patch from ovos_bus_client import Message from ovos_utils.messagebus import FakeBus from ovos_utils.xdg_utils import xdg_data_home @@ -81,7 +79,7 @@ def tearDownClass(cls) -> None: shutil.rmtree(cls.config_dir) # @patch("neon_core.skills.skill_store.SkillsStore.install_default_skills") - @patch("mycroft.skills.skill_manager.SkillManager.run") + @patch("ovos_core.skill_manager.SkillManager.run") def test_neon_skills_service(self, run): from neon_core.skills.service import NeonSkillService from neon_core.skills.skill_manager import NeonSkillManager @@ -111,13 +109,14 @@ def test_neon_skills_service(self, run): started = Event() - def ready_hook(): + def ready_hook(*_, **__): started.set() alive_hook = Mock() started_hook = Mock() error_hook = Mock() stopping_hook = Mock() + run.side_effect = ready_hook service = NeonSkillService(alive_hook, started_hook, ready_hook, error_hook, stopping_hook, config=config, daemonic=True) @@ -126,10 +125,11 @@ def ready_hook(): self.assertIsInstance(Configuration()["location"]["timezone"], dict) self.assertTrue(all(config['skills'][x] == service.config['skills'][x] for x in config['skills'])) + self.assertIsInstance(service.config['location'], dict, service.config) service.bus = FakeBus() service.bus.connected_event = Event() service.start() - started.wait(60) + self.assertTrue(started.wait(30)) self.assertTrue(service.config['skills']['auto_update']) # install_default.assert_called_once() @@ -268,7 +268,7 @@ def mod_2_parse(utterances, lang): self.assertTrue(all([p for p in valid_parsers if p in self.intent_service.transformers.loaded_modules])) - @patch("mycroft.skills.intent_service.IntentService.handle_utterance") + @patch("ovos_core.intent_services.IntentService.handle_utterance") def test_handle_utterance(self, patched): test_message_invalid = Message("test", {"utterances": [' ', ' ']}) self.intent_service.handle_utterance(test_message_invalid) @@ -382,7 +382,7 @@ def tearDownClass(cls) -> None: # manager.stop() # @patch("neon_core.skills.skill_store.SkillsStore.install_default_skills") - @patch("mycroft.skills.skill_manager.SkillManager.run") + @patch("ovos_core.skill_manager.SkillManager.run") def test_get_default_skills_dir(self, _): from neon_core.skills.skill_manager import NeonSkillManager manager = NeonSkillManager(FakeBus()) From 4d3b87010485dd772f6cf40806c9b0210d07f220 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 May 2024 14:41:42 -0700 Subject: [PATCH 2/4] Remove unused patches Replace remaining `mycroft` imports with new package paths Cleanup unused imports --- neon_core/__init__.py | 12 - neon_core/launcher.py | 4 +- neon_core/skills/__init__.py | 8 +- neon_core/skills/intent_service.py | 2 +- neon_core/skills/patched_common_query.py | 290 ----------------------- neon_core/util/skill_utils.py | 1 - 6 files changed, 5 insertions(+), 312 deletions(-) delete mode 100644 neon_core/skills/patched_common_query.py diff --git a/neon_core/__init__.py b/neon_core/__init__.py index 05109769d..718d1b001 100644 --- a/neon_core/__init__.py +++ b/neon_core/__init__.py @@ -25,15 +25,3 @@ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -# TODO: Patching for ovos-core 0.0.7 -import ovos_utils.messagebus -from ovos_utils.events import get_handler_name, create_wrapper, EventContainer -ovos_utils.messagebus.get_handler_name = get_handler_name -ovos_utils.messagebus.create_wrapper = create_wrapper -ovos_utils.messagebus.EventContainer = EventContainer - - -# TODO: Patching out Backend Client -from ovos_backend_client.backends.offline import OfflineBackend diff --git a/neon_core/launcher.py b/neon_core/launcher.py index 726d2ab54..a7d48c92f 100644 --- a/neon_core/launcher.py +++ b/neon_core/launcher.py @@ -26,8 +26,8 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from mycroft.lock import Lock -from mycroft.util import wait_for_exit_signal, reset_sigint_handler +from ovos_utils import wait_for_exit_signal +from ovos_utils.process_utils import reset_sigint_handler, PIDLock as Lock from neon_audio.service import NeonPlaybackService from neon_messagebus.service import NeonBusService diff --git a/neon_core/skills/__init__.py b/neon_core/skills/__init__.py index 2175c01ba..23c3edf30 100644 --- a/neon_core/skills/__init__.py +++ b/neon_core/skills/__init__.py @@ -26,12 +26,8 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from neon_core.skills.decorators import intent_handler, intent_file_handler, \ - resting_screen_handler, conversational_intent - -# TODO: Resolve remote config bug in skill settings -import neon_core.skills.patched_skill_settings - +# TODO: Deprecate `conversational_intent` +from neon_core.skills.decorators import conversational_intent # Backwards-compat import from ovos_workshop.decorators import intent_handler, intent_file_handler, \ diff --git a/neon_core/skills/intent_service.py b/neon_core/skills/intent_service.py index f7535f63f..1c5c35165 100644 --- a/neon_core/skills/intent_service.py +++ b/neon_core/skills/intent_service.py @@ -46,7 +46,7 @@ from neon_core.configuration import Configuration from neon_core.language import get_lang_config -from ovos_core.intent_services import IntentService, ConverseService +from ovos_core.intent_services import IntentService, ConverseService, IntentMatch try: from neon_utterance_translator_plugin import UtteranceTranslator diff --git a/neon_core/skills/patched_common_query.py b/neon_core/skills/patched_common_query.py deleted file mode 100644 index 11c58f95f..000000000 --- a/neon_core/skills/patched_common_query.py +++ /dev/null @@ -1,290 +0,0 @@ -# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework -# All trademark and other rights reserved by their respective owners -# Copyright 2008-2022 Neongecko.com Inc. -# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, -# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo -# BSD-3 License -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from this -# software without specific prior written permission. -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import re -import time -from dataclasses import dataclass -from itertools import chain -from threading import Event -from typing import Dict -from ovos_bus_client.session import SessionManager -from ovos_bus_client.message import Message, dig_for_message -from ovos_utils import flatten_list -from ovos_bus_client.apis.enclosure import EnclosureAPI -from ovos_bus_client.util import get_message_lang -from ovos_utils.log import LOG -from ovos_utils.messagebus import get_message_lang -from ovos_workshop.resource_files import CoreResources - -from mycroft.skills.intent_services.base import IntentMatch - -# TODO: Timeout from config -# TODO: Port to ovos-core -EXTENSION_TIME = 15 -MIN_RESPONSE_WAIT = 3 - - -@dataclass -class Query: - session_id: str - query: str - replies: list = None - extensions: list = None - query_time: float = time.time() - timeout_time: float = time.time() + 1 - responses_gathered: Event = Event() - completed: Event = Event() - answered: bool = False - - -class CommonQuery: - def __init__(self, bus): - self.bus = bus - self.skill_id = "common_query.neongeckocom" # fake skill - self.active_queries: Dict[str, Query] = dict() - self.enclosure = EnclosureAPI(self.bus, self.skill_id) - self._vocabs = {} - self.bus.on('question:query.response', self.handle_query_response) - self.bus.on('common_query.question', self.handle_question) - # TODO: Register available CommonQuery skills - - def voc_match(self, utterance, voc_filename, lang, exact=False): - """Determine if the given utterance contains the vocabulary provided. - - By default the method checks if the utterance contains the given vocab - thereby allowing the user to say things like "yes, please" and still - match against "Yes.voc" containing only "yes". An exact match can be - requested. - - The method checks the "res/text/{lang}" folder of mycroft-core. - The result is cached to avoid hitting the disk each time the method is called. - - Args: - utterance (str): Utterance to be tested - voc_filename (str): Name of vocabulary file (e.g. 'yes' for - 'res/text/en-us/yes.voc') - lang (str): Language code, defaults to self.lang - exact (bool): Whether the vocab must exactly match the utterance - - Returns: - bool: True if the utterance has the given vocabulary it - """ - match = False - - if lang not in self._vocabs: - resources = CoreResources(language=lang) - vocab = resources.load_vocabulary_file(voc_filename) - self._vocabs[lang] = list(chain(*vocab)) - - if utterance: - if exact: - # Check for exact match - match = any(i.strip() == utterance - for i in self._vocabs[lang]) - else: - # Check for matches against complete words - match = any([re.match(r'.*\b' + i + r'\b.*', utterance) - for i in self._vocabs[lang]]) - - return match - - def is_question_like(self, utterance, lang): - # skip utterances with less than 3 words - if len(utterance.split(" ")) < 3: - return False - # skip utterances meant for common play - if self.voc_match(utterance, "common_play", 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 = IntentMatch('CommonQuery', None, {}, None) - break - return match - - def handle_question(self, message): - """ Send the phrase to the CommonQuerySkills and prepare for handling - the replies. - """ - utt = message.data.get('utterance') - sid = SessionManager.get(message).session_id - # TODO: Why are defaults not creating new objects on init? - query = Query(session_id=sid, query=utt, replies=[], extensions=[], - query_time=time.time(), timeout_time=time.time() + 1, - responses_gathered=Event(), completed=Event(), - answered=False) - assert query.responses_gathered.is_set() is False - assert query.completed.is_set() is False - self.active_queries[sid] = query - self.enclosure.mouth_think() - - LOG.info(f'Searching for {utt}') - # Send the query to anyone listening for them - msg = message.reply('question:query', data={'phrase': utt}) - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg) - - query.timeout_time = time.time() + 1 - timeout = False - while not query.responses_gathered.wait(EXTENSION_TIME): - if time.time() > query.timeout_time + 1: - LOG.debug(f"Timeout gathering responses ({query.session_id})") - timeout = True - break - - # forcefully timeout if search is still going - if timeout: - LOG.warning(f"Timed out getting responses for: {query.query}") - self._query_timeout(message) - if not query.completed.wait(10): - raise TimeoutError("Timed out processing responses") - answered = bool(query.answered) - self.active_queries.pop(sid) - LOG.debug(f"answered={answered}|" - f"remaining active_queries={len(self.active_queries)}") - return answered - - def handle_query_response(self, message): - search_phrase = message.data['phrase'] - skill_id = message.data['skill_id'] - searching = message.data.get('searching') - answer = message.data.get('answer') - - query = self.active_queries.get(SessionManager.get(message).session_id) - if not query: - LOG.warning(f"No active query for: {search_phrase}") - # Manage requests for time to complete searches - if searching: - LOG.debug(f"{skill_id} is searching") - # request extending the timeout by EXTENSION_TIME - query.timeout_time = time.time() + EXTENSION_TIME - # TODO: Perhaps block multiple extensions? - if skill_id not in query.extensions: - query.extensions.append(skill_id) - else: - # Search complete, don't wait on this skill any longer - if answer: - LOG.info(f'Answer from {skill_id}') - query.replies.append(message.data) - - # Remove the skill from list of timeout extensions - if skill_id in query.extensions: - LOG.debug(f"Done waiting for {skill_id}") - query.extensions.remove(skill_id) - - time_to_wait = query.query_time + MIN_RESPONSE_WAIT - time.time() - if time_to_wait > 0: - LOG.debug(f"Waiting {time_to_wait}s before checking extensions") - query.responses_gathered.wait(time_to_wait) - # not waiting for any more skills - if not query.extensions: - LOG.debug(f"No more skills to wait for ({query.session_id})") - query.responses_gathered.set() - - def _query_timeout(self, message): - query = self.active_queries.get(SessionManager.get(message).session_id) - LOG.info(f'Check responses with {len(query.replies)} replies') - search_phrase = message.data.get('phrase', "") - if query.extensions: - query.extensions = [] - self.enclosure.mouth_reset() - - # Look at any replies that arrived before the timeout - # Find response(s) with the highest confidence - best = None - ties = [] - for response in query.replies: - if not best or response['conf'] > best['conf']: - best = response - ties = [] - elif response['conf'] == best['conf']: - ties.append(response) - - if best: - if ties: - # TODO: Ask user to pick between ties or do it automagically - pass - - # invoke best match - self.speak(best['answer'], message) - LOG.info('Handling with: ' + str(best['skill_id'])) - cb = best.get('callback_data') or {} - self.bus.emit(message.forward('question:action', - data={'skill_id': best['skill_id'], - 'phrase': search_phrase, - 'callback_data': cb})) - query.answered = True - else: - query.answered = False - query.completed.set() - - def speak(self, utterance, message=None): - """Speak a sentence. - - Args: - utterance (str): sentence mycroft should speak - """ - # registers the skill as being active - self.enclosure.register(self.skill_id) - - message = message or dig_for_message() - lang = get_message_lang(message) - data = {'utterance': utterance, - 'expect_response': False, - 'meta': {"skill": self.skill_id}, - 'lang': lang} - - m = message.forward("speak", data) if message \ - else Message("speak", data) - m.context["skill_id"] = self.skill_id - self.bus.emit(m) - - -import mycroft.skills.intent_services.commonqa_service -mycroft.skills.intent_services.commonqa_service.CommonQAService = CommonQuery -mycroft.skills.intent_service.CommonQAService = CommonQuery diff --git a/neon_core/util/skill_utils.py b/neon_core/util/skill_utils.py index f8b249271..68d77192e 100644 --- a/neon_core/util/skill_utils.py +++ b/neon_core/util/skill_utils.py @@ -26,7 +26,6 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import json import re from copy import copy From 1dd3bf9cfbb4a62f2c7cd97399f6ed2c519424ca Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 May 2024 16:26:19 -0700 Subject: [PATCH 3/4] Deprecate ConverseService patch --- neon_core/skills/intent_service.py | 275 +---------------------------- 1 file changed, 5 insertions(+), 270 deletions(-) diff --git a/neon_core/skills/intent_service.py b/neon_core/skills/intent_service.py index 1c5c35165..04d21f91f 100644 --- a/neon_core/skills/intent_service.py +++ b/neon_core/skills/intent_service.py @@ -28,25 +28,21 @@ import time import wave -from threading import Event -from typing import List - -from ovos_bus_client.util import get_message_lang -from ovos_utils import flatten_list +from typing import List from neon_transformers.text_transformers import UtteranceTransformersService -from ovos_bus_client import Message, MessageBusClient, SessionManager, UtteranceState -from neon_utils.message_utils import get_message_user, dig_for_message +from ovos_bus_client import Message, MessageBusClient +from neon_utils.message_utils import get_message_user from neon_utils.metrics_utils import Stopwatch from neon_utils.user_utils import apply_local_user_profile_updates from neon_utils.configuration_utils import get_neon_user_config from lingua_franca.parse import get_full_lang_code -from ovos_config.locale import set_default_lang, setup_locale +from ovos_config.locale import set_default_lang from ovos_utils.log import LOG from neon_core.configuration import Configuration from neon_core.language import get_lang_config -from ovos_core.intent_services import IntentService, ConverseService, IntentMatch +from ovos_core.intent_services import IntentService try: from neon_utterance_translator_plugin import UtteranceTranslator @@ -66,7 +62,6 @@ class NeonIntentService(IntentService): def __init__(self, bus: MessageBusClient): super().__init__(bus) - self.converse = NeonConverseService(bus) self.config = Configuration() self.language_config = get_lang_config() LOG.debug(f"Languages Adapt={self.adapt_service.engines.keys()}|" @@ -282,263 +277,3 @@ def handle_get_padatious(self, message): intent = intent.__dict__ self.bus.emit(message.reply("intent.service.padatious.reply", {"intent": intent})) - - -# TODO: Overrides below backporting 0.0.8 compat -class NeonConverseService(ConverseService): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - 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) - self.bus.on("skill.converse.get_response.enable", self.handle_get_response_enable) - self.bus.on("skill.converse.get_response.disable", self.handle_get_response_disable) - - @property - def active_skills(self): - session = SessionManager.get() - return session.active_skills - - @active_skills.setter - def active_skills(self, val): - session = SessionManager.get() - session.active_skills = [] - for skill_id, ts in val: - session.activate_skill(skill_id) - - def get_active_skills(self, message=None): - """Active skill ids ordered by converse priority - this represents the order in which converse will be called - - Returns: - active_skills (list): ordered list of skill_ids - """ - session = SessionManager.get(message) - return [skill[0] for skill in session.active_skills] - - def deactivate_skill(self, skill_id, source_skill=None, message=None): - """Remove a skill from being targetable by converse. - - Args: - skill_id (str): skill to remove - source_skill (str): skill requesting the removal - """ - source_skill = source_skill or skill_id - if self._deactivate_allowed(skill_id, source_skill): - session = SessionManager.get(message) - if session.is_active(skill_id): - # update converse session - session.deactivate_skill(skill_id) - - # keep message.context - message = message or Message("") - message.context["session"] = session.serialize() # update session active skills - # send bus event - self.bus.emit( - message.forward("intent.service.skills.deactivated", - data={"skill_id": skill_id})) - if skill_id in self._consecutive_activations: - self._consecutive_activations[skill_id] = 0 - - def activate_skill(self, skill_id, source_skill=None, message=None): - """Add a skill or update the position of an active skill. - - The skill is added to the front of the list, if it's already in the - list it's removed so there is only a single entry of it. - - Args: - skill_id (str): identifier of skill to be added. - source_skill (str): skill requesting the removal - """ - source_skill = source_skill or skill_id - if self._activate_allowed(skill_id, source_skill): - # update converse session - session = SessionManager.get(message) - session.activate_skill(skill_id) - - # keep message.context - message = message or Message("") - message.context["session"] = session.serialize() # update session active skills - message = message.forward("intent.service.skills.activated", - {"skill_id": skill_id}) - # send bus event - self.bus.emit(message) - # update activation counter - self._consecutive_activations[skill_id] += 1 - - def _collect_converse_skills(self, message=None): - """use the messagebus api to determine which skills want to converse - This includes all skills and external applications""" - message = message or dig_for_message() - session = SessionManager.get(message) - - skill_ids = [] - # include all skills in get_response state - want_converse = [skill_id for skill_id, state in session.utterance_states.items() - if state == UtteranceState.RESPONSE] - skill_ids += want_converse # dont wait for these pong answers (optimization) - - active_skills = self.get_active_skills() - - if not active_skills: - return want_converse - - event = Event() - - def handle_ack(msg): - nonlocal event - skill_id = msg.data["skill_id"] - - # validate the converse pong - if all((skill_id not in want_converse, - msg.data.get("can_handle", True), - skill_id in active_skills)): - want_converse.append(skill_id) - - if skill_id not in skill_ids: # track which answer we got - skill_ids.append(skill_id) - - if all(s in skill_ids for s in active_skills): - # all skills answered the ping! - event.set() - - self.bus.on("skill.converse.pong", handle_ack) - - # ask skills if they want to converse - for skill_id in active_skills: - self.bus.emit(message.forward(f"{skill_id}.converse.ping", - {"skill_id": skill_id})) - - # wait for all skills to acknowledge they want to converse - event.wait(timeout=0.5) - - self.bus.remove("skill.converse.pong", handle_ack) - return want_converse - - def _check_converse_timeout(self, message=None): - """ filter active skill list based on timestamps """ - message = message or dig_for_message() - timeouts = self.config.get("skill_timeouts") or {} - def_timeout = self.config.get("timeout", 300) - session = SessionManager.get(message) - session.active_skills = [ - skill for skill in session.active_skills - if time.time() - skill[1] <= timeouts.get(skill[0], def_timeout)] - - def converse(self, utterances, skill_id, lang, message): - """Call skill and ask if they want to process the utterance. - - Args: - utterances (list of tuples): utterances paired with normalized - versions. - skill_id: skill to query. - lang (str): current language - message (Message): message containing interaction info. - - Returns: - handled (bool): True if handled otherwise False. - """ - session = SessionManager.get(message) - session.lang = lang - - state = session.utterance_states.get(skill_id, UtteranceState.INTENT) - if state == UtteranceState.RESPONSE: - converse_msg = message.reply(f"{skill_id}.converse.get_response", - {"utterances": utterances, - "lang": lang}) - self.bus.emit(converse_msg) - return True - - if self._converse_allowed(skill_id): - converse_msg = message.reply(f"{skill_id}.converse.request", - {"utterances": utterances, - "lang": lang}) - result = self.bus.wait_for_response(converse_msg, - 'skill.converse.response') - if result and 'error' in result.data: - error_msg = result.data['error'] - LOG.error(f"{skill_id}: {error_msg}") - return False - elif result is not None: - return result.data.get('result', False) - return False - - def converse_with_skills(self, utterances, lang, message): - """Give active skills a chance at the utterance - - Args: - utterances (list): list of utterances - lang (string): 4 letter ISO language code - message (Message): message to use to generate reply - - Returns: - IntentMatch if handled otherwise None. - """ - # we call flatten in case someone is sending the old style list of tuples - utterances = flatten_list(utterances) - # filter allowed skills - self._check_converse_timeout(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 IntentMatch('Converse', None, None, skill_id) - return None - - def handle_get_response_enable(self, message): - skill_id = message.data["skill_id"] - session = SessionManager.get(message) - session.enable_response_mode(skill_id) - if session.session_id == "default": - SessionManager.sync(message) - - def handle_get_response_disable(self, message): - skill_id = message.data["skill_id"] - session = SessionManager.get(message) - session.disable_response_mode(skill_id) - if session.session_id == "default": - SessionManager.sync(message) - - def handle_activate_skill_request(self, message): - # TODO imperfect solution - only a skill can activate itself - # someone can forge this message and emit it raw, but in OpenVoiceOS all - # skill messages should have skill_id in context, so let's make sure - # this doesnt happen accidentally at very least - skill_id = message.data['skill_id'] - source_skill = message.context.get("skill_id") - self.activate_skill(skill_id, source_skill, message) - sess = SessionManager.get(message) - if sess.session_id == "default": - SessionManager.sync(message) - - def handle_deactivate_skill_request(self, message): - # TODO imperfect solution - only a skill can deactivate itself - # someone can forge this message and emit it raw, but in ovos-core all - # skill message should have skill_id in context, so let's make sure - # this doesnt happen accidentally - skill_id = message.data['skill_id'] - source_skill = message.context.get("skill_id") or skill_id - self.deactivate_skill(skill_id, source_skill, message) - sess = SessionManager.get(message) - if sess.session_id == "default": - SessionManager.sync(message) - - def reset_converse(self, message): - """Let skills know there was a problem with speech recognition""" - lang = get_message_lang(message) - try: - setup_locale(lang) # restore default lang - except Exception as e: - LOG.exception(f"Failed to set lingua_franca default lang to {lang}") - - self.converse_with_skills([], lang, message) - - def handle_get_active_skills(self, message): - """Send active skills to caller. - - Argument: - message: query message to reply to. - """ - self.bus.emit(message.reply("intent.service.active_skills.reply", - {"skills": self.get_active_skills(message)})) From 2eb1e9a3ea888c6b2298d1d2d1a01d62617b2de5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 May 2024 18:53:24 -0700 Subject: [PATCH 4/4] Update neon-utils to resolve CommonQuery response bug --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 18ac6d220..321ce7bfd 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,7 +2,7 @@ ovos-core[mycroft,lgpl]>=0.0.8a95 # padacioso==0.1.3a2 -neon-utils[network,audio]~=1.10,>=1.10.2a2 +neon-utils[network,audio]~=1.10,>=1.10.2a3 # TODO: audio for alpha resolution ovos-utils~=0.0,>=0.0.38 ovos-bus-client~=0.0.8,>=0.0.9a20