From e582182555a0549137c4e86c7cf355d28727edc7 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 22 May 2025 16:32:15 +0100 Subject: [PATCH 01/57] Merge pull request #681 from OpenVoiceOS/feaat/m2v feat: m2v pipeline --- ovos_core/intent_services/__init__.py | 68 +++++++++++++++++++-------- requirements/plugins.txt | 2 + requirements/skills-essential.txt | 2 +- requirements/skills-internet.txt | 6 +-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index b3ee90c9c40..ee538b9a5f7 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -15,6 +15,7 @@ import json import warnings +import time from collections import defaultdict from typing import Tuple, Callable, Union, List @@ -40,10 +41,16 @@ from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService from ovos_persona import PersonaService +# TODO - to be dropped once pluginified +# just a placeholder during alphas until https://github.com/OpenVoiceOS/ovos-core/pull/570 try: from ovos_ollama_intent_pipeline import LLMIntentPipeline except ImportError: LLMIntentPipeline = None +try: + from ovos_m2v_pipeline import Model2VecIntentPipeline +except ImportError: + Model2VecIntentPipeline = None class IntentService: @@ -55,13 +62,13 @@ class IntentService: def __init__(self, bus, config=None): """ - Initializes the IntentService with intent parsing pipelines, transformer services, and messagebus event handlers. + Initializes the IntentService with all intent parsing pipelines, transformer services, and messagebus event handlers. Args: - bus: The messagebus connection for event handling. + bus: The messagebus connection used for event-driven communication. config: Optional configuration dictionary for intent services. - Sets up skill name mapping, loads all supported intent matching pipelines, initializes utterance and metadata transformer services, connects the session manager, and registers all relevant messagebus event handlers for utterance processing, context management, intent queries, and skill tracking. + Sets up skill name mapping, loads all supported intent matching pipelines (including Adapt, Padatious, Padacioso, Fallback, Converse, CommonQA, Stop, OCP, Persona, and optionally LLM and Model2Vec pipelines), initializes utterance and metadata transformer services, connects the session manager, and registers all relevant messagebus event handlers for utterance processing, context management, intent queries, and skill deactivation tracking. """ self.bus = bus self.config = config or Configuration().get("intents", {}) @@ -78,6 +85,7 @@ def __init__(self, bus, config=None): self._stop = None self._ocp = None self._ollama = None + self._m2v = None self._load_pipeline_plugins() self.utterance_plugins = UtteranceTransformersService(bus) @@ -107,9 +115,9 @@ def __init__(self, bus, config=None): def _load_pipeline_plugins(self): # TODO - replace with plugin loader from OPM """ - Initializes and configures all intent matching pipeline plugins used by the service. + Initializes and configures all intent matching pipeline plugins for the service. - Loads and sets up the Adapt, Padatious, Padacioso, Fallback, Converse, CommonQA, Stop, OCP, Persona, and optionally LLM intent pipelines based on the current configuration. Handles conditional loading and disabling of Padatious and Padacioso pipelines, and logs relevant status or errors. + Sets up Adapt, Padatious, Padacioso, Fallback, Converse, CommonQA, Stop, OCP, Persona, and optionally LLM and Model2Vec intent pipelines based on the current configuration. Handles conditional loading and disabling of Padatious and Padacioso pipelines, and logs relevant status or errors. """ self._adapt_service = AdaptPipeline(bus=self.bus, config=self.config.get("adapt", {})) if "padatious" not in self.config: @@ -140,14 +148,24 @@ def _load_pipeline_plugins(self): self._ocp = OCPPipelineMatcher(self.bus, config=self.config.get("OCP", {})) self._persona = PersonaService(self.bus, config=self.config.get("persona", {})) if LLMIntentPipeline is not None: - self._ollama = LLMIntentPipeline(self.bus, config=self.config.get("ovos-ollama-intent-pipeline", {})) + try: + self._ollama = LLMIntentPipeline(self.bus, config=self.config.get("ovos-ollama-intent-pipeline", {})) + except Exception as e: + LOG.error(f"Failed to load LLMIntentPipeline ({e})") + if Model2VecIntentPipeline is not None: + try: + self._m2v = Model2VecIntentPipeline(self.bus, config=self.config.get("ovos-m2v-pipeline", {})) + except Exception as e: + LOG.error(f"Failed to load Model2VecIntentPipeline ({e})") + + LOG.debug(f"Default pipeline: {SessionManager.get().pipeline}") def update_skill_name_dict(self, message): """ - Updates the mapping of skill IDs to skill names based on a messagebus event. + Updates the internal mapping of skill IDs to skill names from a message event. Args: - message: A message containing 'id' and 'name' fields for the skill. + message: A message object containing 'id' and 'name' fields for the skill. """ self.skill_names[message.data['id']] = message.data['name'] @@ -205,19 +223,21 @@ def disambiguate_lang(message): def get_pipeline(self, skips=None, session=None) -> Tuple[str, Callable]: """ - Returns an ordered list of intent matcher functions for the current session pipeline. + Constructs and returns the ordered list of intent matcher functions for the current session. - The pipeline is determined by the session's configuration and may be filtered by - the optional `skips` list. Each matcher is paired with its pipeline key, and the - resulting list reflects the order in which utterances will be processed for intent - matching. If a requested pipeline component is unavailable, it is skipped with a warning. + The pipeline sequence is determined by the session's configuration and may be filtered by + an optional list of pipeline keys to skip. Each entry in the returned list is a tuple of + the pipeline key and its corresponding matcher function, in the order they will be applied + for intent matching. If a requested pipeline component is unavailable, it is skipped and a + warning is logged. Args: skips: Optional list of pipeline keys to exclude from the matcher sequence. session: Optional session object; if not provided, the current session is used. Returns: - A list of (pipeline_key, matcher_function) tuples in the order they will be applied. + A list of (pipeline_key, matcher_function) tuples representing the active intent + matching pipeline for the session. """ session = session or SessionManager.get() @@ -253,6 +273,10 @@ def get_pipeline(self, skips=None, session=None) -> Tuple[str, Callable]: } if self._ollama is not None: matchers["ovos-ollama-intent-pipeline"] = self._ollama.match_low + if self._m2v is not None: + matchers["ovos-m2v-pipeline-high"] = self._m2v.match_high + matchers["ovos-m2v-pipeline-medium"] = self._m2v.match_medium + matchers["ovos-m2v-pipeline-low"] = self._m2v.match_low if self._padacioso_service is not None: matchers.update({ "padacioso_high": self._padacioso_service.match_high, @@ -616,7 +640,7 @@ def handle_get_intent(self, message): utterance = message.data["utterance"] lang = get_message_lang(message) sess = SessionManager.get(message) - + match = None # Loop through the matching functions until a match is found. for pipeline, match_func in self.get_pipeline(skips=["converse", "common_qa", @@ -624,21 +648,25 @@ def handle_get_intent(self, message): "fallback_medium", "fallback_low"], session=sess): + s = time.monotonic() match = match_func([utterance], lang, message) + LOG.debug(f"matching '{pipeline}' took: {time.monotonic() - s} seconds") if match: if match.match_type: - intent_data = match.match_data + intent_data = dict(match.match_data) intent_data["intent_name"] = match.match_type intent_data["intent_service"] = pipeline intent_data["skill_id"] = match.skill_id intent_data["handler"] = match_func.__name__ - self.bus.emit(message.reply("intent.service.intent.reply", - {"intent": intent_data})) + LOG.debug(f"final intent match: {intent_data}") + m = message.reply("intent.service.intent.reply", + {"intent": intent_data, "utterance": utterance}) + self.bus.emit(m) return - + LOG.error(f"bad pipeline match! {match}") # signal intent failure self.bus.emit(message.reply("intent.service.intent.reply", - {"intent": None})) + {"intent": None, "utterance": utterance})) def handle_get_skills(self, message): """Send registered skills to caller. diff --git a/requirements/plugins.txt b/requirements/plugins.txt index 10351857f8d..4e9a237af8d 100644 --- a/requirements/plugins.txt +++ b/requirements/plugins.txt @@ -5,3 +5,5 @@ ovos-translate-server-plugin>=0.0.2, <1.0.0 ovos-utterance-normalizer>=0.2.1, <1.0.0 ovos-number-parser>=0.0.1,<1.0.0 ovos-date-parser>=0.0.3,<1.0.0 +ovos-m2v-pipeline>=0.0.5,<1.0.0 +ovos-ollama-intent-pipeline-plugin>=0.0.1,<1.0.0 \ No newline at end of file diff --git a/requirements/skills-essential.txt b/requirements/skills-essential.txt index 997e979b12f..a2db9274f22 100644 --- a/requirements/skills-essential.txt +++ b/requirements/skills-essential.txt @@ -2,7 +2,7 @@ ovos-skill-fallback-unknown>=0.1.5,<1.0.0 ovos-skill-alerts>=0.1.10,<1.0.0 ovos-skill-personal>=0.1.7,<1.0.0 -ovos-skill-date-time>=0.4.2,<1.0.0 +ovos-skill-date-time>=0.4.2,<2.0.0 ovos-skill-hello-world>=0.1.10,<1.0.0 ovos-skill-spelling>=0.2.5,<1.0.0 ovos-skill-diagnostics>=0.0.2,<1.0.0 diff --git a/requirements/skills-internet.txt b/requirements/skills-internet.txt index 7d6e5553818..ac4ad92cf7e 100644 --- a/requirements/skills-internet.txt +++ b/requirements/skills-internet.txt @@ -1,7 +1,7 @@ # skills that require internet connectivity, should not be installed in offline devices -ovos-skill-weather>=0.1.11,<1.0.0 -ovos-skill-ddg>=0.1.9,<1.0.0 -ovos-skill-wolfie>=0.2.9,<1.0.0 +ovos-skill-weather>=0.1.11,<2.0.0 +skill-ddg>=0.1.9,<1.0.0 +skill-wolfie>=0.2.9,<1.0.0 ovos-skill-wikipedia>=0.5.3,<1.0.0 ovos-skill-wikihow>=0.2.5,<1.0.0 ovos-skill-speedtest>=0.3.2,<1.0.0 From f7816e417b447d937b07c27c0cc1d98f7182c1d7 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 22 May 2025 15:32:46 +0000 Subject: [PATCH 02/57] Increment Version to 1.4.0a1 --- ovos_core/version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index dc55042d439..380cf558145 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 1 -VERSION_MINOR = 3 -VERSION_BUILD = 1 -VERSION_ALPHA = 0 +VERSION_MINOR = 4 +VERSION_BUILD = 0 +VERSION_ALPHA = 1 # END_VERSION_BLOCK # for compat with old imports From 798eca6f638e36a5fc59821bb1dbd8d49dd0ad35 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 22 May 2025 15:33:28 +0000 Subject: [PATCH 03/57] Update Changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fae3217dff1..5c01c57ef09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Changelog -## [1.3.1a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.3.1a1) (2025-05-15) +## [1.4.0a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.4.0a1) (2025-05-22) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.3.0...1.3.1a1) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.3.1...1.4.0a1) **Merged pull requests:** -- fix: update requirements skill package names [\#683](https://github.com/OpenVoiceOS/ovos-core/pull/683) ([JarbasAl](https://github.com/JarbasAl)) +- feat: m2v pipeline [\#681](https://github.com/OpenVoiceOS/ovos-core/pull/681) ([JarbasAl](https://github.com/JarbasAl)) From 166b78fe17ffef7c44a46fb21779238b8b878567 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 27 May 2025 19:13:52 +0100 Subject: [PATCH 04/57] feat: intent transformers (#686) * feat: intent transformers add support for intent transformer plugins * fix: deprecated imports in unittests * fix: bus init * fix: keyword name --- ovos_core/intent_services/__init__.py | 14 +-- ovos_core/transformers.py | 96 ++++++++++++++++++- requirements/plugins.txt | 4 +- requirements/requirements.txt | 2 +- .../ovos_tskill_abort/__init__.py | 8 +- 5 files changed, 111 insertions(+), 13 deletions(-) diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index ee538b9a5f7..4c46650fcfb 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -38,7 +38,7 @@ from ovos_core.intent_services.converse_service import ConverseService from ovos_core.intent_services.fallback_service import FallbackService from ovos_core.intent_services.stop_service import StopService -from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService +from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService, IntentTransformersService from ovos_persona import PersonaService # TODO - to be dropped once pluginified @@ -90,6 +90,7 @@ def __init__(self, bus, config=None): self.utterance_plugins = UtteranceTransformersService(bus) self.metadata_plugins = MetadataTransformersService(bus) + self.intent_plugins = IntentTransformersService(bus) # connection SessionManager to the bus, # this will sync default session across all components @@ -367,12 +368,13 @@ def _emit_match_message(self, match: Union[IntentHandlerMatch, PipelineMatch], m Returns: None """ - reply = None try: - sess = match.updated_session or SessionManager.get(message) - except AttributeError: # old ovos-plugin-manager version - LOG.warning("outdated ovos-plugin-manager detected! please update to version 0.8.0") - sess = SessionManager.get(message) + match = self.intent_plugins.transform(match) + except Exception as e: + LOG.error(f"Error in IntentTransformers: {e}") + + reply = None + sess = match.updated_session or SessionManager.get(message) sess.lang = lang # ensure it is updated # utterance fully handled by pipeline matcher diff --git a/ovos_core/transformers.py b/ovos_core/transformers.py index 9984cc36275..3bd10887265 100644 --- a/ovos_core/transformers.py +++ b/ovos_core/transformers.py @@ -1,8 +1,10 @@ -from typing import Optional, List +from typing import Optional, List, Union from ovos_config import Configuration +from ovos_plugin_manager.intent_transformers import find_intent_transformer_plugins from ovos_plugin_manager.metadata_transformers import find_metadata_transformer_plugins from ovos_plugin_manager.text_transformers import find_utterance_transformer_plugins +from ovos_plugin_manager.templates.pipeline import IntentHandlerMatch, PipelineMatch from ovos_utils.json_helper import merge_dict from ovos_utils.log import LOG @@ -115,6 +117,17 @@ def shutdown(self): pass def transform(self, context: Optional[dict] = None): + """ + Sequentially applies all loaded metadata transformer plugins to the provided context. + + Each plugin's `transform` method is called in order of descending priority, and the resulting data is merged into the context. Sensitive session data is excluded from debug logs. Exceptions raised by plugins are logged as warnings and do not interrupt the transformation process. + + Args: + context: Optional dictionary containing metadata to be transformed. + + Returns: + The updated context dictionary after all transformations. + """ context = context or {} for module in self.plugins: @@ -128,3 +141,84 @@ def transform(self, context: Optional[dict] = None): return context +class IntentTransformersService: + + def __init__(self, bus, config=None): + """ + Initializes the IntentTransformersService with the provided message bus and configuration. + + Loads and prepares intent transformer plugins based on the configuration, making them ready for use. + """ + self.config_core = config or Configuration() + self.loaded_plugins = {} + self.has_loaded = False + self.bus = bus + self.config = self.config_core.get("intent_transformers") or {} + self.load_plugins() + + @staticmethod + def find_plugins(): + """ + Discovers and returns available intent transformer plugins. + + Returns: + An iterable of (plugin_name, plugin_class) pairs for all discovered intent transformer plugins. + """ + return find_intent_transformer_plugins().items() + + def load_plugins(self): + """ + Loads and initializes enabled intent transformer plugins based on the configuration. + + Plugins marked as inactive in the configuration are skipped. Successfully loaded plugins are added to the internal registry, while failures are logged without interrupting the loading process. + """ + for plug_name, plug in self.find_plugins(): + if plug_name in self.config: + # if disabled skip it + if not self.config[plug_name].get("active", True): + continue + try: + self.loaded_plugins[plug_name] = plug() + self.loaded_plugins[plug_name].bind(self.bus) + LOG.info(f"loaded intent transformer plugin: {plug_name}") + except Exception as e: + LOG.error(e) + LOG.exception(f"Failed to load intent transformer plugin: {plug_name}") + + @property + def plugins(self): + """ + Returns the loaded intent transformer plugins sorted by priority. + """ + return sorted(self.loaded_plugins.values(), + key=lambda k: k.priority, reverse=True) + + def shutdown(self): + """ + Shuts down all loaded plugins, suppressing any exceptions raised during shutdown. + """ + for module in self.plugins: + try: + module.shutdown() + except: + pass + + def transform(self, intent: Union[IntentHandlerMatch, PipelineMatch]) -> Union[IntentHandlerMatch, PipelineMatch]: + """ + Sequentially applies all loaded intent transformer plugins to the given intent object. + + Each plugin's `transform` method is called in order of priority. Exceptions raised by individual plugins are logged as warnings, and processing continues with the next plugin. The final, transformed intent object is returned. + + Args: + intent: The intent match object to be transformed. + + Returns: + The transformed intent match object after all plugins have been applied. + """ + for module in self.plugins: + try: + intent = module.transform(intent) + LOG.debug(f"{module.name}: {intent}") + except Exception as e: + LOG.warning(f"{module.name} transform exception: {e}") + return intent diff --git a/requirements/plugins.txt b/requirements/plugins.txt index 4e9a237af8d..816c8fcfd7f 100644 --- a/requirements/plugins.txt +++ b/requirements/plugins.txt @@ -6,4 +6,6 @@ ovos-utterance-normalizer>=0.2.1, <1.0.0 ovos-number-parser>=0.0.1,<1.0.0 ovos-date-parser>=0.0.3,<1.0.0 ovos-m2v-pipeline>=0.0.5,<1.0.0 -ovos-ollama-intent-pipeline-plugin>=0.0.1,<1.0.0 \ No newline at end of file +ovos-ollama-intent-pipeline-plugin>=0.0.1,<1.0.0 +keyword-template-matcher>=0.0.1,<1.0.0 +ahocorasick-ner>=0.0.1,<1.0.0 \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cbe30a2a348..53872a83777 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -11,6 +11,6 @@ ovos-persona>=0.4.4,<1.0.0 ovos-utils[extras]>=0.6.0,<1.0.0 ovos_bus_client>=0.1.4,<2.0.0 -ovos-plugin-manager>=0.8.0,<1.0.0 +ovos-plugin-manager>=0.9.0,<1.0.0 ovos-config>=0.0.13,<2.0.0 ovos-workshop>=3.3.4,<4.0.0 diff --git a/test/integrationtests/ovos_tskill_abort/__init__.py b/test/integrationtests/ovos_tskill_abort/__init__.py index ea1059fef4d..f18c11d81f4 100644 --- a/test/integrationtests/ovos_tskill_abort/__init__.py +++ b/test/integrationtests/ovos_tskill_abort/__init__.py @@ -1,6 +1,6 @@ from ovos_workshop.decorators import killable_intent from ovos_workshop.skills.ovos import OVOSSkill -from ovos_workshop.decorators import intent_file_handler +from ovos_workshop.decorators import intent_handler from time import sleep @@ -24,7 +24,7 @@ def handle_intent_aborted(self): self.my_special_var = "default" @killable_intent(callback=handle_intent_aborted) - @intent_file_handler("test.intent") + @intent_handler("test.intent") def handle_test_abort_intent(self, message): self.stop_called = False self.my_special_var = "changed" @@ -32,7 +32,7 @@ def handle_test_abort_intent(self, message): sleep(1) self.speak("still here") - @intent_file_handler("test2.intent") + @intent_handler("test2.intent") @killable_intent(callback=handle_intent_aborted) def handle_test_get_response_intent(self, message): self.stop_called = False @@ -43,7 +43,7 @@ def handle_test_get_response_intent(self, message): self.speak("question aborted") @killable_intent(msg="my.own.abort.msg", callback=handle_intent_aborted) - @intent_file_handler("test3.intent") + @intent_handler("test3.intent") def handle_test_msg_intent(self, message): self.stop_called = False if self.my_special_var != "default": From 44209cd47ed0a02f8a41d26c1d41823c12321342 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 27 May 2025 18:14:21 +0000 Subject: [PATCH 05/57] Increment Version to 1.5.0a1 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 380cf558145..f636c4d5743 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 1 -VERSION_MINOR = 4 +VERSION_MINOR = 5 VERSION_BUILD = 0 VERSION_ALPHA = 1 # END_VERSION_BLOCK From d2a1276c4635c023331149f8332905b808187d3f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 27 May 2025 18:15:11 +0000 Subject: [PATCH 06/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c01c57ef09..5153fdcb93d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.5.0a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.0a1) (2025-05-27) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.4.0a1...1.5.0a1) + +**Merged pull requests:** + +- feat: intent transformers [\#686](https://github.com/OpenVoiceOS/ovos-core/pull/686) ([JarbasAl](https://github.com/JarbasAl)) + ## [1.4.0a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.4.0a1) (2025-05-22) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.3.1...1.4.0a1) From 1e7787dc62242b6d317767b83148b78b57446598 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:20:12 +0100 Subject: [PATCH 07/57] publish coverage report under gh-pages (#692) * publish coverage report under gh-pages * test requirements --- .github/workflows/gh_pages_coverage.yml | 45 +++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/gh_pages_coverage.yml diff --git a/.github/workflows/gh_pages_coverage.yml b/.github/workflows/gh_pages_coverage.yml new file mode 100644 index 00000000000..a7962377f7b --- /dev/null +++ b/.github/workflows/gh_pages_coverage.yml @@ -0,0 +1,45 @@ +name: Publish Coverage to gh-pages + +on: + push: + branches: + - dev + workflow_dispatch: + +permissions: + contents: write # Required to push to gh-pages + +jobs: + test-and-publish-coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt install python3-dev swig libssl-dev portaudio19-dev libpulse-dev libfann-dev + python -m pip install build wheel uv + + - name: Install core repo + run: | + uv pip install --system -e .[mycroft,plugins,skills-essential,lgpl,test] + + - name: Run tests and collect coverage + run: | + coverage run -m pytest test/ + coverage html + + - name: Deploy coverage report to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./htmlcov + publish_branch: gh-pages diff --git a/setup.py b/setup.py index 61ca37d8759..697c087b602 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,7 @@ def required(requirements_file): long_description_content_type="text/markdown", install_requires=required('requirements/requirements.txt'), extras_require={ + 'test': required('requirements/tests.txt'), 'mycroft': required('requirements/mycroft.txt'), 'lgpl': required('requirements/lgpl.txt'), 'plugins': required('requirements/plugins.txt'), From f292a22cfefe120994bb5cde1e07f5015b694f8d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Jun 2025 18:20:40 +0000 Subject: [PATCH 08/57] Increment Version to 1.5.0a2 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index f636c4d5743..db89a7a8ac2 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 1 VERSION_MINOR = 5 VERSION_BUILD = 0 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK # for compat with old imports From 85bd6a661218b8c34fe861838e47a88a19bc940d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Jun 2025 18:21:23 +0000 Subject: [PATCH 09/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5153fdcb93d..cf17ac024e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.5.0a2](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.0a2) (2025-06-09) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.0a1...1.5.0a2) + +**Merged pull requests:** + +- publish coverage report under gh-pages [\#692](https://github.com/OpenVoiceOS/ovos-core/pull/692) ([JarbasAl](https://github.com/JarbasAl)) + ## [1.5.0a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.0a1) (2025-05-27) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.4.0a1...1.5.0a1) From c22a14ecf54d990193ed7749015854d55c0571e2 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:32:55 +0100 Subject: [PATCH 10/57] refactor: ovoscope (#691) moved to the new end2end test framework https://github.com/TigreGotico/ovoscope drop codecov for self hosted solution --- .coveragerc | 2 + .github/workflows/coverage.yml | 85 +- .github/workflows/end2end_tests.yml | 82 -- .github/workflows/integration_tests.yml | 47 - .github/workflows/unit_tests.yml | 63 +- .gitignore | 18 +- codecov.yml | 11 - requirements/requirements.txt | 2 +- requirements/tests.txt | 4 +- .../metadata_test/__init__.py | 12 - test/end2end/metadata-test-plugin/setup.py | 28 - test/end2end/minicroft.py | 78 -- test/end2end/routing/__init__.py | 0 test/end2end/routing/test_sched.py | 85 -- test/end2end/routing/test_session.py | 183 --- test/end2end/session/__init__.py | 0 test/end2end/session/test_blacklist.py | 462 ------- test/end2end/session/test_complete_failure.py | 250 ---- test/end2end/session/test_converse.py | 577 -------- test/end2end/session/test_fallback.py | 356 ----- test/end2end/session/test_get_response.py | 1026 -------------- test/end2end/session/test_ocp.py | 1176 ----------------- test/end2end/session/test_sched.py | 222 ---- test/end2end/session/test_session.py | 299 ----- test/end2end/session/test_stop.py | 392 ------ test/end2end/session/test_transformers.py | 166 --- test/end2end/skill-converse_test/__init__.py | 111 -- .../locale/en-us/converse_off.intent | 1 - .../locale/en-us/converse_on.intent | 1 - .../locale/en-us/deactivate.intent | 1 - .../locale/en-us/get.dialog | 1 - .../locale/en-us/question.dialog | 1 - .../locale/en-us/test.intent | 1 - .../locale/en-us/test2.intent | 1 - .../locale/en-us/test3.intent | 1 - .../locale/en-us/test_get_response.intent | 1 - .../locale/en-us/test_get_response3.intent | 2 - .../en-us/test_get_response_cascade.intent | 1 - test/end2end/skill-converse_test/setup.py | 40 - test/end2end/skill-fake-fm/__init__.py | 39 - test/end2end/skill-fake-fm/setup.py | 46 - test/end2end/skill-new-stop/__init__.py | 26 - .../locale/en-us/vocab/HelloWorldKeyword.voc | 1 - test/end2end/skill-new-stop/setup.py | 46 - test/end2end/skill-old-stop/__init__.py | 22 - .../locale/en-us/vocab/HelloWorldKeyword.voc | 1 - test/end2end/skill-old-stop/setup.py | 45 - test/end2end/skill-ovos-fakewiki/__init__.py | 14 - test/end2end/skill-ovos-fakewiki/setup.py | 23 - .../skill-ovos-fallback-unknown/__init__.py | 20 - .../locale/en-us/unknown.dialog | 9 - .../skill-ovos-fallback-unknown/setup.py | 42 - .../skill-ovos-hello-world/MANIFEST.in | 7 - .../skill-ovos-hello-world/__init__.py | 10 - .../locale/en-us/dialog/hello.world.dialog | 3 - .../locale/en-us/vocab/HelloWorldKeyword.voc | 2 - test/end2end/skill-ovos-hello-world/setup.py | 45 - test/end2end/skill-ovos-schedule/MANIFEST.in | 7 - test/end2end/skill-ovos-schedule/__init__.py | 14 - .../locale/en-us/dialog/done.dialog | 1 - .../locale/en-us/dialog/trigger.dialog | 1 - .../locale/en-us/vocab/Schedule.voc | 1 - test/end2end/skill-ovos-schedule/setup.py | 45 - .../skill-ovos-slow-fallback/__init__.py | 20 - .../end2end/skill-ovos-slow-fallback/setup.py | 42 - test/end2end/test_helloworld.py | 243 ++++ test/end2end/test_no_skills.py | 48 + test/integrationtests/__init__.py | 0 .../integrationtests/common_query/__init__.py | 0 .../ovos_tskill_fakewiki/__init__.py | 66 - .../locale/en-us/More.voc | 4 - .../locale/en-us/no_answer.dialog | 1 - .../locale/en-us/search_fakewiki.intent | 9 - .../ovos_tskill_fakewiki/setup.py | 23 - .../common_query/test_continuous_dialog.py | 82 -- .../common_query/test_skill.py | 73 - .../ovos_tskill_abort/__init__.py | 60 - .../locale/en-us/question.dialog | 1 - .../locale/en-us/test.intent | 1 - .../locale/en-us/test2.intent | 1 - .../locale/en-us/test3.intent | 1 - .../ovos_tskill_abort/readme.md | 1 - .../ovos_tskill_abort/setup.py | 23 - test/integrationtests/test_workshop.py | 275 ---- 84 files changed, 364 insertions(+), 6869 deletions(-) create mode 100644 .coveragerc delete mode 100644 .github/workflows/end2end_tests.yml delete mode 100644 .github/workflows/integration_tests.yml delete mode 100644 codecov.yml delete mode 100644 test/end2end/metadata-test-plugin/metadata_test/__init__.py delete mode 100644 test/end2end/metadata-test-plugin/setup.py delete mode 100644 test/end2end/minicroft.py delete mode 100644 test/end2end/routing/__init__.py delete mode 100644 test/end2end/routing/test_sched.py delete mode 100644 test/end2end/routing/test_session.py delete mode 100644 test/end2end/session/__init__.py delete mode 100644 test/end2end/session/test_blacklist.py delete mode 100644 test/end2end/session/test_complete_failure.py delete mode 100644 test/end2end/session/test_converse.py delete mode 100644 test/end2end/session/test_fallback.py delete mode 100644 test/end2end/session/test_get_response.py delete mode 100644 test/end2end/session/test_ocp.py delete mode 100644 test/end2end/session/test_sched.py delete mode 100644 test/end2end/session/test_session.py delete mode 100644 test/end2end/session/test_stop.py delete mode 100644 test/end2end/session/test_transformers.py delete mode 100644 test/end2end/skill-converse_test/__init__.py delete mode 100644 test/end2end/skill-converse_test/locale/en-us/converse_off.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/converse_on.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/deactivate.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/get.dialog delete mode 100644 test/end2end/skill-converse_test/locale/en-us/question.dialog delete mode 100644 test/end2end/skill-converse_test/locale/en-us/test.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/test2.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/test3.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/test_get_response.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/test_get_response3.intent delete mode 100644 test/end2end/skill-converse_test/locale/en-us/test_get_response_cascade.intent delete mode 100644 test/end2end/skill-converse_test/setup.py delete mode 100644 test/end2end/skill-fake-fm/__init__.py delete mode 100755 test/end2end/skill-fake-fm/setup.py delete mode 100644 test/end2end/skill-new-stop/__init__.py delete mode 100644 test/end2end/skill-new-stop/locale/en-us/vocab/HelloWorldKeyword.voc delete mode 100755 test/end2end/skill-new-stop/setup.py delete mode 100644 test/end2end/skill-old-stop/__init__.py delete mode 100644 test/end2end/skill-old-stop/locale/en-us/vocab/HelloWorldKeyword.voc delete mode 100755 test/end2end/skill-old-stop/setup.py delete mode 100644 test/end2end/skill-ovos-fakewiki/__init__.py delete mode 100755 test/end2end/skill-ovos-fakewiki/setup.py delete mode 100644 test/end2end/skill-ovos-fallback-unknown/__init__.py delete mode 100755 test/end2end/skill-ovos-fallback-unknown/locale/en-us/unknown.dialog delete mode 100755 test/end2end/skill-ovos-fallback-unknown/setup.py delete mode 100644 test/end2end/skill-ovos-hello-world/MANIFEST.in delete mode 100644 test/end2end/skill-ovos-hello-world/__init__.py delete mode 100644 test/end2end/skill-ovos-hello-world/locale/en-us/dialog/hello.world.dialog delete mode 100644 test/end2end/skill-ovos-hello-world/locale/en-us/vocab/HelloWorldKeyword.voc delete mode 100755 test/end2end/skill-ovos-hello-world/setup.py delete mode 100644 test/end2end/skill-ovos-schedule/MANIFEST.in delete mode 100644 test/end2end/skill-ovos-schedule/__init__.py delete mode 100644 test/end2end/skill-ovos-schedule/locale/en-us/dialog/done.dialog delete mode 100644 test/end2end/skill-ovos-schedule/locale/en-us/dialog/trigger.dialog delete mode 100644 test/end2end/skill-ovos-schedule/locale/en-us/vocab/Schedule.voc delete mode 100755 test/end2end/skill-ovos-schedule/setup.py delete mode 100644 test/end2end/skill-ovos-slow-fallback/__init__.py delete mode 100755 test/end2end/skill-ovos-slow-fallback/setup.py create mode 100644 test/end2end/test_helloworld.py create mode 100644 test/end2end/test_no_skills.py delete mode 100644 test/integrationtests/__init__.py delete mode 100644 test/integrationtests/common_query/__init__.py delete mode 100644 test/integrationtests/common_query/ovos_tskill_fakewiki/__init__.py delete mode 100644 test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/More.voc delete mode 100644 test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/no_answer.dialog delete mode 100644 test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/search_fakewiki.intent delete mode 100755 test/integrationtests/common_query/ovos_tskill_fakewiki/setup.py delete mode 100644 test/integrationtests/common_query/test_continuous_dialog.py delete mode 100644 test/integrationtests/common_query/test_skill.py delete mode 100644 test/integrationtests/ovos_tskill_abort/__init__.py delete mode 100644 test/integrationtests/ovos_tskill_abort/locale/en-us/question.dialog delete mode 100644 test/integrationtests/ovos_tskill_abort/locale/en-us/test.intent delete mode 100644 test/integrationtests/ovos_tskill_abort/locale/en-us/test2.intent delete mode 100644 test/integrationtests/ovos_tskill_abort/locale/en-us/test3.intent delete mode 100644 test/integrationtests/ovos_tskill_abort/readme.md delete mode 100755 test/integrationtests/ovos_tskill_abort/setup.py delete mode 100644 test/integrationtests/test_workshop.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000000..d4fa8be10b1 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +relative_files = true \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9ea6860ba43..62d26ac381a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,59 +1,36 @@ -name: Run CodeCov +# .github/workflows/coverage.yml +name: Post coverage comment + on: - push: - branches: - - dev - workflow_dispatch: + workflow_run: + workflows: ["Run Tests"] + types: + - completed jobs: - run: + test: + name: Run tests & display coverage runs-on: ubuntu-latest - env: - PYTHON: "3.11" + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + permissions: + # Gives the action the necessary permissions for publishing new + # comments in pull requests. + pull-requests: write + # Gives the action the necessary permissions for editing existing + # comments (to avoid publishing multiple comments in the same PR) + contents: write + # Gives the action the necessary permissions for looking up the + # workflow that launched this workflow, and download the related + # artifact that contains the comment to be published + actions: read steps: - - uses: actions/checkout@master - - name: Setup Python - uses: actions/setup-python@master - with: - python-version: "3.11" - - name: Install System Dependencies - run: | - sudo apt-get update - sudo apt install python3-dev libssl-dev libfann-dev portaudio19-dev libpulse-dev - python -m pip install build wheel - - name: Install test dependencies - run: | - pip install -r requirements/tests.txt - pip install ./test/integrationtests/common_query/ovos_tskill_fakewiki - pip install ./test/end2end/skill-ovos-hello-world - pip install ./test/end2end/skill-ovos-fallback-unknown - pip install ./test/end2end/skill-ovos-slow-fallback - pip install ./test/end2end/skill-converse_test - pip install ./test/end2end/skill-ovos-schedule - pip install ./test/end2end/skill-new-stop - pip install ./test/end2end/skill-old-stop - pip install ./test/end2end/skill-fake-fm - pip install ./test/end2end/skill-ovos-fakewiki - pip install ./test/end2end/metadata-test-plugin - - name: Install core repo - run: | - pip install -e .[mycroft,deprecated,plugins] - - name: Generate coverage report - run: | - pytest --cov=ovos_core --cov-report xml test/unittests - pytest --cov-append --cov=ovos_core --cov-report xml test/end2end - - name: Generate coverage report with padatious - run: | - sudo apt install libfann-dev - pip install .[lgpl] - pytest --cov-append --cov=ovos_core --cov-report xml test/unittests/skills - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - directory: ./coverage/reports/ - fail_ci_if_error: true - files: ./coverage.xml,!./cache - flags: unittests - name: codecov-umbrella - verbose: true + # DO NOT run actions/checkout here, for security reasons + # For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + - name: Post comment + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }} + # Update those if you changed the default values: + # COMMENT_ARTIFACT_NAME: python-coverage-comment-action + # COMMENT_FILENAME: python-coverage-comment-action.txt \ No newline at end of file diff --git a/.github/workflows/end2end_tests.yml b/.github/workflows/end2end_tests.yml deleted file mode 100644 index fe76a3ac756..00000000000 --- a/.github/workflows/end2end_tests.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Run End2End tests -on: - pull_request: - branches: - - dev - paths-ignore: - - 'ovos_core/version.py' - - 'examples/**' - - '.github/**' - - '.gitignore' - - 'LICENSE' - - 'CHANGELOG.md' - - 'MANIFEST.in' - - 'readme.md' - - 'scripts/**' - push: - branches: - - master - paths-ignore: - - 'ovos_core/version.py' - - 'requirements/**' - - 'examples/**' - - '.github/**' - - '.gitignore' - - 'LICENSE' - - 'CHANGELOG.md' - - 'MANIFEST.in' - - 'readme.md' - - 'scripts/**' - workflow_dispatch: - -jobs: - end2end_tests: - strategy: - max-parallel: 3 - matrix: - python-version: ["3.11"] - runs-on: ubuntu-latest - timeout-minutes: 35 - steps: - - uses: actions/checkout@v4 - - name: Set up python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install System Dependencies - run: | - sudo apt-get update - sudo apt install python3-dev swig libssl-dev - python -m pip install build wheel uv - - name: Install test dependencies - run: | - uv pip install --system -r requirements/tests.txt - uv pip install --system ./test/integrationtests/common_query/ovos_tskill_fakewiki - uv pip install --system ./test/end2end/skill-ovos-hello-world - uv pip install --system ./test/end2end/skill-ovos-fallback-unknown - uv pip install --system ./test/end2end/skill-ovos-slow-fallback - uv pip install --system ./test/end2end/skill-converse_test - uv pip install --system ./test/end2end/skill-ovos-schedule - uv pip install --system ./test/end2end/skill-new-stop - uv pip install --system ./test/end2end/skill-old-stop - uv pip install --system ./test/end2end/skill-fake-fm - uv pip install --system ./test/end2end/skill-ovos-fakewiki - uv pip install --system ./test/end2end/metadata-test-plugin - - name: Install core repo - run: | - uv pip install --system -e .[plugins] - - name: Run end2end tests - run: | - pytest --cov-append --cov=ovos_core --cov-report xml test/end2end - - name: Upload coverage - env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - directory: ./coverage/reports/ - fail_ci_if_error: true - files: ./coverage.xml,!./cache - flags: end2end - name: codecov-end2end - verbose: true diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml deleted file mode 100644 index 15bcdc8f521..00000000000 --- a/.github/workflows/integration_tests.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Run Integration Tests -on: - pull_request: - branches: - - dev - - master - paths-ignore: - - 'ovos_core/version.py' - - 'examples/**' - - '.github/**' - - '.gitignore' - - 'LICENSE' - - 'CHANGELOG.md' - - 'MANIFEST.in' - - 'readme.md' - - 'scripts/**' - workflow_dispatch: - -jobs: - integration_tests: - strategy: - max-parallel: 3 - matrix: - python-version: ["3.11"] - runs-on: ubuntu-latest - timeout-minutes: 35 - steps: - - uses: actions/checkout@v4 - - name: Set up python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install System Dependencies - run: | - sudo apt-get update - sudo apt install python3-dev swig - python -m pip install build wheel uv - - name: Install test dependencies - run: | - uv pip install --system -r requirements/tests.txt - uv pip install --system ./test/integrationtests/common_query/ovos_tskill_fakewiki - - name: Install core repo - run: | - uv pip install --system -e .[plugins] - - name: Run integration tests - run: | - pytest test/integrationtests diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 5e3c2374ccb..3eae6909a8c 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -1,31 +1,29 @@ -name: Run UnitTests +name: Run Tests on: pull_request: branches: - dev paths-ignore: - 'ovos_core/version.py' - - 'examples/**' - '.github/**' - '.gitignore' - 'LICENSE' - 'CHANGELOG.md' - 'MANIFEST.in' - - 'readme.md' + - 'README.md' - 'scripts/**' push: branches: - - master + - dev paths-ignore: - 'ovos_core/version.py' - 'requirements/**' - - 'examples/**' - '.github/**' - '.gitignore' - 'LICENSE' - 'CHANGELOG.md' - 'MANIFEST.in' - - 'readme.md' + - 'README.md' - 'scripts/**' workflow_dispatch: @@ -34,8 +32,16 @@ jobs: strategy: max-parallel: 3 matrix: - python-version: ["3.11"] + python-version: ["3.11", "3.12"] runs-on: ubuntu-latest + permissions: + # Gives the action the necessary permissions for publishing new + # comments in pull requests. + pull-requests: write + # Gives the action the necessary permissions for pushing data to the + # python-coverage-comment-action branch, and for editing existing + # comments (to avoid publishing multiple comments in the same PR) + contents: write timeout-minutes: 35 steps: - uses: actions/checkout@v4 @@ -46,33 +52,26 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev swig libssl-dev portaudio19-dev libpulse-dev + sudo apt install python3-dev swig libssl-dev portaudio19-dev libpulse-dev libfann-dev python -m pip install build wheel uv - - name: Install test dependencies - run: | - uv pip install --system -r requirements/tests.txt - name: Install core repo run: | - uv pip install --system -e .[mycroft,plugins] - - name: Run unittests - run: | - pytest --cov=ovos_core --cov-report xml test/unittests - - name: Install padatious - run: | - sudo apt install libfann-dev - uv pip install --system .[lgpl] - - name: Run unittests with padatious + uv pip install --system -e .[mycroft,plugins,skills-essential,lgpl,test] + - name: Run tests run: | - pytest --cov-append --cov=ovos_core --cov-report xml test/unittests - - name: Upload coverage - env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - uses: codecov/codecov-action@v3 + pytest --cov=ovos_core --cov-report xml test/ + + - name: Coverage comment + id: coverage_comment + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ github.token }} + + - name: Store Pull Request comment to be posted + uses: actions/upload-artifact@v4 + if: steps.coverage_comment.outputs.COMMENT_FILE_WRITTEN == 'true' with: - token: ${{ secrets.CODECOV_TOKEN }} - directory: ./coverage/reports/ - fail_ci_if_error: true - files: ./coverage.xml,!./cache - flags: unittests - name: codecov-unittests - verbose: true + # If you use a different name, update COMMENT_ARTIFACT_NAME accordingly + name: python-coverage-comment-action + # If you use a different name, update COMMENT_FILENAME accordingly + path: python-coverage-comment-action.txt \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4d7bd682c9c..abdde43c26a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,22 +5,11 @@ dev.env *.pyc *.swp *~ -mimic -/skills -pocketsphinx-python *.egg-info/ build dist tornado.web tornado.ioloop -mycroft/__version__.py -scripts/logs/* -logs/* -.coverage -/htmlcov -test/audio_accuracy/data -scripts/*.screen -doc/_build/ .installed .mypy_cache .vscode @@ -28,8 +17,11 @@ doc/_build/ .venv/ # Created by unit tests -test/unittests/skills/test_skill/settings.json -test_conf.json +coverage.xml +pytest.ini +.coverage +.testmondata* .pytest_cache/ +/htmlcov /.gtm/ !/.ruff_cache/ diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index a41de4b6a74..00000000000 --- a/codecov.yml +++ /dev/null @@ -1,11 +0,0 @@ -# -# Copyright (c) 2018 Intel Corporation -# -# SPDX-License-Identifier: Apache-2.0 -# - -coverage: - status: - patch: - default: - enabled: no diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 53872a83777..c3d6fb7ffda 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,7 +6,7 @@ combo-lock>=0.2.2, <0.4 padacioso>=1.0.0, <2.0.0 ovos-adapt-parser>=1.0.5, <2.0.0 ovos_ocp_pipeline_plugin>=1.0.10, <2.0.0 -ovos-common-query-pipeline-plugin>=1.0.5, <2.0.0 +ovos-common-query-pipeline-plugin>=1.0.5,<2.0.0 ovos-persona>=0.4.4,<1.0.0 ovos-utils[extras]>=0.6.0,<1.0.0 diff --git a/requirements/tests.txt b/requirements/tests.txt index 24ec8fa3ef0..098263f32ea 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -2,5 +2,7 @@ coveralls>=1.8.2 flake8>=3.7.9 pytest>=5.2.4 pytest-cov>=2.8.1 +pytest-testmon>=2.1.3 +pytest-randomly>=3.16.0 cov-core>=1.15.0 -ovos-backend-client>=0.1.0,<2.0.0 \ No newline at end of file +ovoscope>=0.3.0,<1.0.0 \ No newline at end of file diff --git a/test/end2end/metadata-test-plugin/metadata_test/__init__.py b/test/end2end/metadata-test-plugin/metadata_test/__init__.py deleted file mode 100644 index 7b2e0f82941..00000000000 --- a/test/end2end/metadata-test-plugin/metadata_test/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Optional - -from ovos_plugin_manager.templates.transformers import MetadataTransformer - - -class MetadataPlugin(MetadataTransformer): - - def __init__(self, name="ovos-metadata-test-plugin", priority=15): - super().__init__(name, priority) - - def transform(self, context: Optional[dict] = None) -> dict: - return {"metadata": "test"} diff --git a/test/end2end/metadata-test-plugin/setup.py b/test/end2end/metadata-test-plugin/setup.py deleted file mode 100644 index 05f24d1d800..00000000000 --- a/test/end2end/metadata-test-plugin/setup.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 - -from setuptools import setup - -META_ENTRY_POINT = 'ovos-metadata-test-plugin=metadata_test:MetadataPlugin' - -setup( - name="ovos-metadata-test-plugin", - description='OpenVoiceOS metadata test Plugin', - version="0.0.1", - author_email='jarbasai@mailfence.com', - license='apache-2.0', - packages=["metadata_test"], - include_package_data=True, - zip_safe=True, - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Topic :: Text Processing :: Linguistic', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - ], - entry_points={ - 'neon.plugin.metadata': META_ENTRY_POINT - } -) diff --git a/test/end2end/minicroft.py b/test/end2end/minicroft.py deleted file mode 100644 index 23eacd74a8f..00000000000 --- a/test/end2end/minicroft.py +++ /dev/null @@ -1,78 +0,0 @@ -from time import sleep - -from ovos_bus_client.session import SessionManager, Session -from ovos_bus_client.util.scheduler import EventScheduler -from ovos_core.intent_services import IntentService -from ovos_core.skill_manager import SkillManager -from ovos_plugin_manager.skills import find_skill_plugins -from ovos_utils.log import LOG -from ovos_utils.fakebus import FakeBus -from ovos_utils.process_utils import ProcessState -from ovos_workshop.skills.fallback import FallbackSkill - -LOG.set_level("DEBUG") - - -class MiniCroft(SkillManager): - def __init__(self, skill_ids, *args, **kwargs): - bus = FakeBus() - super().__init__(bus, *args, **kwargs) - self.skill_ids = skill_ids - self.intent_service = self._register_intent_services() - self.scheduler = EventScheduler(bus, schedule_file="/tmp/schetest.json") - - def load_metadata_transformers(self, cfg): - self.intent_service.metadata_plugins.config = cfg - self.intent_service.metadata_plugins.load_plugins() - - def _register_intent_services(self): - """Start up the all intent services and connect them as needed. - - Args: - bus: messagebus client to register the services on - """ - service = IntentService(self.bus) - # Register handler to trigger fallback system - self.bus.on( - 'mycroft.skills.fallback', - FallbackSkill.make_intent_failure_handler(self.bus) - ) - return service - - def load_plugin_skills(self): - LOG.info("loading skill plugins") - plugins = find_skill_plugins() - for skill_id, plug in plugins.items(): - LOG.debug(skill_id) - if skill_id not in self.skill_ids: - continue - if skill_id not in self.plugin_skills: - self._load_plugin_skill(skill_id, plug) - - def run(self): - """Load skills and update periodically from disk and internet.""" - self.status.set_alive() - - self.load_plugin_skills() - - self.status.set_ready() - - LOG.info("Skills all loaded!") - - def stop(self): - super().stop() - self.scheduler.shutdown() - SessionManager.bus = None - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - - -def get_minicroft(skill_id): - if isinstance(skill_id, str): - skill_id = [skill_id] - assert isinstance(skill_id, list) - croft1 = MiniCroft(skill_id) - croft1.start() - while croft1.status.state != ProcessState.READY: - sleep(0.2) - return croft1 diff --git a/test/end2end/routing/__init__.py b/test/end2end/routing/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/end2end/routing/test_sched.py b/test/end2end/routing/test_sched.py deleted file mode 100644 index 639b8ed40d9..00000000000 --- a/test/end2end/routing/test_sched.py +++ /dev/null @@ -1,85 +0,0 @@ -import time -from time import sleep -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestSched(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-schedule.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def test_no_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.pipeline = ["adapt_high"] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), m.msg_type, m.context.get("source"), m.context.get("destination")) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["schedule event"]}, - {"source": "A", "destination": "B"}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.activate", # skill callback - f"{self.skill_id}:ScheduleIntent", # intent trigger - "mycroft.skill.handler.start", # intent code start - "speak", - "mycroft.scheduler.schedule_event", - - "mycroft.skill.handler.complete", # intent code end - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default", # session update (end of utterance default sync) - - # skill event triggering after 3 seconds - "skill-ovos-schedule.openvoiceos:my_event", - "speak" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that source and destination are swapped after intent trigger - self.assertEqual(messages[2].msg_type, f"{self.skill_id}:ScheduleIntent") - for m in messages: - # messages FOR ovos-core - if m.msg_type in ["recognizer_loop:utterance", - "ovos.session.update_default"]: - self.assertEqual(messages[0].context["source"], "A") - self.assertEqual(messages[0].context["destination"], "B") - # messages FROM ovos-core - else: - self.assertEqual(m.context["source"], "B") - self.assertEqual(m.context["destination"], "A") - - def tearDown(self) -> None: - self.core.stop() diff --git a/test/end2end/routing/test_session.py b/test/end2end/routing/test_session.py deleted file mode 100644 index f1cd842e3d3..00000000000 --- a/test/end2end/routing/test_session.py +++ /dev/null @@ -1,183 +0,0 @@ -import time -from time import sleep -from unittest import TestCase -from ovos_utils.ocp import PlayerState, MediaState -from ocp_pipeline.opm import OCPPlayerProxy - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestRouting(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-hello-world.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_no_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.pipeline = ["adapt_high"] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these - messages.append(m) - print(len(messages), m.msg_type, m.context.get("source"), m.context.get("destination")) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}, - {"source": "A", "destination": "B"}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.activate", - f"{self.skill_id}:HelloWorldIntent", - "mycroft.skill.handler.start", - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - - # verify that source and destination are swapped after intent trigger - self.assertEqual(messages[2].msg_type, f"{self.skill_id}:HelloWorldIntent") - for m in messages: - if m.msg_type in ["recognizer_loop:utterance", "ovos.session.update_default"]: - self.assertEqual(messages[0].context["source"], "A") - self.assertEqual(messages[0].context["destination"], "B") - else: - self.assertEqual(m.context["source"], "B") - self.assertEqual(m.context["destination"], "A") - - -class TestOCPRouting(TestCase): - - def setUp(self): - self.skill_id = "skill-fake-fm.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_no_session(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["gui.status.request", - "ovos.common_play.status", - "ovos.skills.settings_changed"]: - return # skip these - messages.append(m) - print(len(messages), m.msg_type, m.context.get("source"), m.context.get("destination")) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["play some radio station"]}, - {"session": sess.serialize(), # explicit - "source": "A", "destination": "B"}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.activate", - "ocp:play", - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", # media type radio - # skill searching (radio) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - # good results because of radio media type - "ovos.common_play.reset", - "add_context", # NowPlaying context - "ovos.common_play.play", # OCP api, - "ovos.common_play.search.populate", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that source and destination are swapped after utterance - for m in messages: - if m.msg_type in ["recognizer_loop:utterance"]: - self.assertEqual(m.context["source"], "A") - self.assertEqual(m.context["destination"], "B") - elif m.msg_type in ["ovos.common_play.play", - "ovos.common_play.reset", - "ovos.common_play.query"]: - # OCP messages that should make it to the client - self.assertEqual(m.context["source"], "B") - self.assertEqual(m.context["destination"], "A") - elif m.msg_type.startswith("ovos.common_play"): - # internal search messages, should not leak to external clients - self.assertEqual(messages[0].context["source"], "A") - self.assertEqual(messages[0].context["destination"], "B") - else: - self.assertEqual(m.context["source"], "B") - self.assertEqual(m.context["destination"], "A") diff --git a/test/end2end/session/__init__.py b/test/end2end/session/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/end2end/session/test_blacklist.py b/test/end2end/session/test_blacklist.py deleted file mode 100644 index 9a57192fca3..00000000000 --- a/test/end2end/session/test_blacklist.py +++ /dev/null @@ -1,462 +0,0 @@ -import time -from time import sleep -from unittest import TestCase, skip - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ovos_utils.ocp import PlayerState, MediaState -from ocp_pipeline.opm import OCPPlayerProxy -from ..minicroft import get_minicroft - - -class TestSessions(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-hello-world.openvoiceos" - self.core = get_minicroft([self.skill_id]) - - def tearDown(self) -> None: - self.core.stop() - - def test_blacklist(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = ["adapt_high"] - SessionManager.default_session.blacklisted_skills = [] - SessionManager.default_session.blacklisted_intents = [] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - ######################################## - # empty blacklist - sess = Session("123") - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - f"{self.skill_id}.activate", - f"{self.skill_id}:HelloWorldIntent", - "mycroft.skill.handler.start", - # skill code executing - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # sanity check correct intent triggered - self.assertEqual(messages[-3].data["meta"]["dialog"], "hello.world") - - ######################################## - # skill in blacklist - messages = [] - sess.blacklisted_skills = [self.skill_id] - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm complete intent failure - expected_messages = [ - "recognizer_loop:utterance", - # complete intent failure - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.utterance.handled" - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ######################################## - # intent in blacklist - messages = [] - sess.blacklisted_skills = [] - sess.blacklisted_intents = [f"{self.skill_id}:HelloWorldIntent"] - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm complete intent failure - expected_messages = [ - "recognizer_loop:utterance", - # complete intent failure - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.utterance.handled" - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - -class TestOCP(TestCase): - - def setUp(self): - self.skill_id = "skill-fake-fm.openvoiceos" - self.core = get_minicroft([self.skill_id]) - - def tearDown(self) -> None: - self.core.stop() - - def test_ocp(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["play Fake FM"]}, # auto derived from skill class name in this case - {"session": sess.serialize(), - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - f"ovos.common_play.query.{self.skill_id}", # explicitly search skill - # skill searching (explicit) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - # good results - "ovos.common_play.reset", - "add_context", # NowPlaying context - "ovos.common_play.play", # OCP api - "ovos.common_play.search.populate", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ######################################## - # skill in blacklist - generic search - messages = [] - sess.blacklisted_skills = [self.skill_id] - - utt = Message("recognizer_loop:utterance", - {"utterances": ["play some radio station"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm complete intent failure - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - f"ovos.common_play.query", - # skill searching - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - 'ovos.common_play.reset', - # playback failure - would play if not blacklisted - "speak", # "dialog":"cant.play" - "ovos.utterance.handled" - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ######################################## - # skill in blacklist - search by name - messages = [] - sess.blacklisted_skills = [self.skill_id] - - utt = Message("recognizer_loop:utterance", - {"utterances": ["play Fake FM"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm complete intent failure - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - f"ovos.common_play.query", # NOT explicitly searching skill, unlike first test - # skill searching (generic) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - 'ovos.common_play.reset', - # playback failure - "speak", # "dialog":"cant.play" - "ovos.utterance.handled" - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - -class TestFallback(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-fallback-unknown.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_fallback(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "fallback_high" - ] - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", - "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("123") - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # FallbackV2 - "ovos.skills.fallback.ping", - "ovos.skills.fallback.pong", - # skill executing - f"ovos.skills.fallback.{self.skill_id}.request", - f"ovos.skills.fallback.{self.skill_id}.start", - "speak", - f"ovos.skills.fallback.{self.skill_id}.response", - f"{self.skill_id}.activate", - "ovos.utterance.handled" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - messages = [] - sess.blacklisted_skills = [self.skill_id] - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm intent failure - expected_messages = [ - "recognizer_loop:utterance", - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.utterance.handled" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - -class TestCommonQuery(TestCase): - - def setUp(self): - self.skill_id = "ovos-skill-fakewiki.openvoiceos" - self.core = get_minicroft(self.skill_id) - # self.core.intent_service.common_qa.common_query_skills = [self.skill_id] - - def tearDown(self) -> None: - self.core.stop() - - @skip("TODO - reenable later, default reranker is discarding the common_query match in latest versions due to low confidence") - def test_common_qa(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = ["common_qa"] - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", - "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("123", - blacklisted_skills=[], - pipeline=["common_qa"]) - utt = Message("recognizer_loop:utterance", - {"utterances": ["what is the speed of light"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "enclosure.mouth.think", - "question:query", - "question:query.response", # searching - "question:query.response", # response - "enclosure.mouth.reset", - f"{self.skill_id}.activate", - "question:action", # similar to an intent triggering - "mycroft.skill.handler.start", - "speak", # answer - "speak", # callback - "mycroft.skill.handler.complete", - "ovos.utterance.handled" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - messages = [] - sess.blacklisted_skills = [self.skill_id] - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm intent failure - expected_messages = [ - "recognizer_loop:utterance", - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.utterance.handled" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) diff --git a/test/end2end/session/test_complete_failure.py b/test/end2end/session/test_complete_failure.py deleted file mode 100644 index 10da8f11d44..00000000000 --- a/test/end2end/session/test_complete_failure.py +++ /dev/null @@ -1,250 +0,0 @@ -import time -from time import sleep -from unittest import TestCase, skip - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestSessions(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-hello-world.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_complete_failure(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.active_skills = [(self.skill_id, time.time())] - SessionManager.default_session.pipeline = [ - "stop_high", - "converse", - "padatious_high", - "adapt_high", - "fallback_high", - "stop_medium", - "adapt_medium", - "padatious_medium", - "adapt_low", - "common_qa", - "fallback_medium", - "fallback_low" - ] - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # Converse - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - - # complete intent failure - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that contexts are kept around - for m in messages: - self.assertEqual(m.context["session"]["session_id"], "default") - - # verify ping/pong answer from hello world skill - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.converse.ping") - self.assertEqual(messages[2].msg_type, "skill.converse.pong") - self.assertEqual(messages[2].data["skill_id"], self.skill_id) - self.assertEqual(messages[2].context["skill_id"], self.skill_id) - self.assertFalse(messages[2].data["can_handle"]) - - # complete intent failure - self.assertEqual(messages[3].msg_type, "mycroft.audio.play_sound") - self.assertEqual(messages[3].data["uri"], "snd/error.mp3") - self.assertEqual(messages[4].msg_type, "complete_intent_failure") - self.assertEqual(messages[5].msg_type, "ovos.utterance.handled") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - - @skip("TODO works if run standalone, otherwise has side effects in other tests") - def test_complete_failure_lang_detect(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.active_skills = [(self.skill_id, time.time())] - SessionManager.default_session.pipeline = [ - "stop_high", - "converse", - "padatious_high", - "adapt_high", - "fallback_high", - "stop_medium", - "adapt_medium", - "padatious_medium", - "adapt_low", - "common_qa", - "fallback_medium", - "fallback_low" - ] - - stt_lang_detect = "pt-pt" - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - SessionManager.default_session.valid_languages = ["en-US", stt_lang_detect, "fr-fr"] - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}, - {"session": SessionManager.default_session.serialize(), - "stt_lang": stt_lang_detect, # lang detect plugin - "detected_lang": "not-valid" # text lang detect - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.session.update_default", # language changed - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - "mycroft.skills.fallback", - "mycroft.skill.handler.start", - "mycroft.skill.handler.complete", - "mycroft.skills.fallback.response", - "mycroft.skills.fallback", - "mycroft.skill.handler.start", - "mycroft.skill.handler.complete", - "mycroft.skills.fallback.response", - "mycroft.skills.fallback", - "mycroft.skill.handler.start", - "mycroft.skill.handler.complete", - "mycroft.skills.fallback.response", - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - mtypes = [m.msg_type for m in messages] - for m in expected_messages: - self.assertTrue(m in mtypes) - - # verify that contexts are kept around - for m in messages: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["stt_lang"], stt_lang_detect) - self.assertEqual(m.context["detected_lang"], "not-valid") - - # verify session lang updated with pt-pt from lang disambiguation step - self.assertEqual(messages[1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[1].data["session_data"]["session_id"], "default") - self.assertEqual(messages[1].data["session_data"]["lang"], stt_lang_detect) - - # verify ping/pong answer from hello world skill - self.assertEqual(messages[2].msg_type, f"{self.skill_id}.converse.ping") - self.assertEqual(messages[3].msg_type, "skill.converse.pong") - self.assertEqual(messages[3].data["skill_id"], self.skill_id) - self.assertEqual(messages[3].context["skill_id"], self.skill_id) - self.assertFalse(messages[3].data["can_handle"]) - - # verify fallback is triggered with pt-pt from lang disambiguation step - self.assertEqual(messages[4].msg_type, "mycroft.skills.fallback") - self.assertEqual(messages[4].data["lang"], stt_lang_detect) - - # high prio fallback - self.assertEqual(messages[4].data["fallback_range"], [0, 5]) - self.assertEqual(messages[5].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[5].data["handler"], "fallback") - self.assertEqual(messages[6].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[6].data["handler"], "fallback") - self.assertEqual(messages[7].msg_type, "mycroft.skills.fallback.response") - self.assertFalse(messages[7].data["handled"]) - - # medium prio fallback - self.assertEqual(messages[8].msg_type, "mycroft.skills.fallback") - self.assertEqual(messages[8].data["lang"], stt_lang_detect) - self.assertEqual(messages[8].data["fallback_range"], [5, 90]) - self.assertEqual(messages[9].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[9].data["handler"], "fallback") - self.assertEqual(messages[10].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[10].data["handler"], "fallback") - self.assertEqual(messages[11].msg_type, "mycroft.skills.fallback.response") - self.assertFalse(messages[11].data["handled"]) - - # low prio fallback - self.assertEqual(messages[12].msg_type, "mycroft.skills.fallback") - self.assertEqual(messages[12].data["lang"], stt_lang_detect) - self.assertEqual(messages[12].data["fallback_range"], [90, 101]) - self.assertEqual(messages[13].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[13].data["handler"], "fallback") - self.assertEqual(messages[14].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[14].data["handler"], "fallback") - self.assertEqual(messages[15].msg_type, "mycroft.skills.fallback.response") - self.assertFalse(messages[15].data["handled"]) - - # complete intent failure - self.assertEqual(messages[16].msg_type, "mycroft.audio.play_sound") - self.assertEqual(messages[16].data["uri"], "snd/error.mp3") - self.assertEqual(messages[17].msg_type, "complete_intent_failure") - - # verify default session is now updated - self.assertEqual(messages[18].msg_type, "ovos.session.update_default") - self.assertEqual(messages[18].data["session_data"]["session_id"], "default") - self.assertEqual(messages[18].data["session_data"]["lang"], "pt-pt") - self.assertEqual(SessionManager.default_session.lang, "pt-pt") - - SessionManager.default_session.lang = "en-US" diff --git a/test/end2end/session/test_converse.py b/test/end2end/session/test_converse.py deleted file mode 100644 index 4520d73d67b..00000000000 --- a/test/end2end/session/test_converse.py +++ /dev/null @@ -1,577 +0,0 @@ -from unittest import TestCase - -import time -from time import sleep - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestSessions(TestCase): - - def setUp(self): - self.skill_id = "ovos-tskill-abort.openvoiceos" - self.other_skill_id = "skill-ovos-hello-world.openvoiceos" - self.core = get_minicroft([self.skill_id, self.other_skill_id]) - - def tearDown(self) -> None: - self.core.stop() - - def test_no_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "padatious_high", - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - ###################################### - # STEP 1 - # triggers intent from converse test skill to make it active - # no converse ping pong as no skill is active - # verify active skills list after triggering skill (test) - utt = Message("recognizer_loop:utterance", - {"utterances": ["no"]}) # converse returns False - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.activate", - # skill selected - f"{self.skill_id}:converse_off.intent", - # skill triggering - "mycroft.skill.handler.start", - # intent code executing - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["lang"], "en-US") - - # verify skill is activated - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.activate") - # verify intent triggers - self.assertEqual(messages[2].msg_type, f"{self.skill_id}:converse_off.intent") - # verify skill_id is present in every message.context - for m in messages[1:]: - if m.msg_type == "ovos.session.update_default": - continue - self.assertEqual(m.context["skill_id"], self.skill_id) - - # verify intent execution - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "TestAbortSkill.handle_converse_off") - self.assertEqual(messages[-3].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[-3].data["name"], "TestAbortSkill.handle_converse_off") - self.assertEqual(messages[-2].msg_type, "ovos.utterance.handled") - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - # test that active skills list has been updated - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.skill_id) - - messages = [] - - ###################################### - # STEP 2 - # converse test skill is now active - # test hello world skill triggers, converse test skill says it does not want to converse - # verify active skills list (hello, test) - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.converse.ping", # default session injected - "skill.converse.pong", - f"{self.skill_id}.converse.request", - "skill.converse.response", # does not want to converse - # skill selected - f"{self.other_skill_id}.activate", - f"{self.other_skill_id}:HelloWorldIntent", - "mycroft.skill.handler.start", - # skill executing - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated - "ovos.session.update_default" - ] - - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["lang"], "en-US") - - # verify that "lang" is injected by converse.ping - # (missing in utterance message) and kept in all messages - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.converse.ping") - - # verify "pong" answer from converse test skill - self.assertEqual(messages[2].msg_type, "skill.converse.pong") - # assert it reports converse method has been implemented by skill - self.assertTrue(messages[2].data["can_handle"]) - - # verify answer from skill that it does not want to converse - self.assertEqual(messages[3].msg_type, f"{self.skill_id}.converse.request") - self.assertEqual(messages[4].msg_type, "skill.converse.response") - self.assertEqual(messages[4].data["skill_id"], self.skill_id) - self.assertFalse(messages[4].data["result"]) # does not want to converse - - # verify skill is activated - self.assertEqual(messages[5].msg_type, f"{self.other_skill_id}.activate") - # verify intent triggers - self.assertEqual(messages[6].msg_type, f"{self.other_skill_id}:HelloWorldIntent") - # verify skill_id is present in every message.context - for m in messages[5:]: - if m.msg_type == "ovos.session.update_default": - continue - self.assertEqual(m.context["skill_id"], self.other_skill_id) - - self.assertEqual(messages[7].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[7].data["name"], "HelloWorldSkill.handle_hello_world_intent") - - # verify intent execution - self.assertEqual(messages[-3].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[-3].data["name"], "HelloWorldSkill.handle_hello_world_intent") - - # verify default session is now updated - self.assertEqual(messages[-2].msg_type, "ovos.utterance.handled") - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - # test that active skills list has been updated - self.assertEqual(sess.active_skills[0][0], self.other_skill_id) - self.assertEqual(sess.active_skills[1][0], self.skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.other_skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][1][0], self.skill_id) - - messages = [] - - ###################################### - # STEP 3 - # both skills are now active - # trigger skill intent that makes it return True in next converse - # verify active skills list gets swapped (test, hello) - utt = Message("recognizer_loop:utterance", - {"utterances": ["yes"]}) # converse returns True - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.converse.ping", # default session injected - "skill.converse.pong", - f"{self.other_skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.converse.request", - "skill.converse.response", # does not want to converse - f"{self.skill_id}.activate", - f"{self.skill_id}:converse_on.intent", - # skill executing - "mycroft.skill.handler.start", - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - mtypes = [m.msg_type for m in messages] - for m in expected_messages: - self.assertTrue(m in mtypes) - - # verify that "session" and "lang" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["lang"], "en-US") - - # converse - self.assertEqual(messages[1].msg_type, f"{self.other_skill_id}.converse.ping") - self.assertEqual(messages[2].msg_type, "skill.converse.pong") - self.assertEqual(messages[2].data["skill_id"], messages[2].context["skill_id"]) - self.assertFalse(messages[2].data["can_handle"]) - self.assertEqual(messages[3].msg_type, f"{self.skill_id}.converse.ping") - self.assertEqual(messages[4].msg_type, "skill.converse.pong") - self.assertEqual(messages[4].data["skill_id"], messages[4].context["skill_id"]) - self.assertTrue(messages[4].data["can_handle"]) - - # verify answer from skill that it does not want to converse - self.assertEqual(messages[5].msg_type, f"{self.skill_id}.converse.request") - self.assertEqual(messages[6].msg_type, "skill.converse.response") - self.assertEqual(messages[6].data["skill_id"], self.skill_id) - self.assertFalse(messages[6].data["result"]) # do not want to converse - - # verify intent triggers - self.assertEqual(messages[8].msg_type, f"{self.skill_id}:converse_on.intent") - # verify skill_id is now present in every message.context - for m in messages[8:]: - if m.msg_type == "ovos.session.update_default": - continue - self.assertEqual(m.context["skill_id"], self.skill_id) - - # verify intent execution - self.assertEqual(messages[9].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[9].data["name"], "TestAbortSkill.handle_converse_on") - - self.assertEqual(messages[-3].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[-3].data["name"], "TestAbortSkill.handle_converse_on") - - # verify default session is now updated - self.assertEqual(messages[-2].msg_type, "ovos.utterance.handled") - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - # test that active skills list has been updated - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertEqual(sess.active_skills[1][0], self.other_skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][1][0], self.other_skill_id) - - messages = [] - - ###################################### - # STEP 4 - # test converse capture, hello world utterance wont reach hello world skill - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.converse.ping", # default session injected - "skill.converse.pong", - f"{self.other_skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.converse.request", - "skill.converse.response", # CONVERSED - f"{self.skill_id}.activate", - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - mtypes = [m.msg_type for m in messages] - for m in expected_messages: - self.assertTrue(m in mtypes) - - # verify that "session" and "lang" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["lang"], "en-US") - - # converse - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.converse.ping") - self.assertEqual(messages[2].msg_type, "skill.converse.pong") - self.assertEqual(messages[2].data["skill_id"], messages[2].context["skill_id"]) - self.assertTrue(messages[2].data["can_handle"]) - self.assertEqual(messages[3].msg_type, f"{self.other_skill_id}.converse.ping") - self.assertEqual(messages[4].msg_type, "skill.converse.pong") - self.assertEqual(messages[4].data["skill_id"], messages[4].context["skill_id"]) - self.assertFalse(messages[4].data["can_handle"]) - - # verify answer from skill that it does not want to converse - self.assertEqual(messages[5].msg_type, f"{self.skill_id}.converse.request") - - # verify skill conversed - self.assertEqual(messages[-4].msg_type, "skill.converse.response") - self.assertEqual(messages[-4].data["skill_id"], self.skill_id) - self.assertTrue(messages[-4].data["result"]) # CONVERSED - - # verify default session is now updated - self.assertEqual(messages[-2].msg_type, "ovos.utterance.handled") - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - # test that active skills list has been updated - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertEqual(sess.active_skills[1][0], self.other_skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][1][0], self.other_skill_id) - - messages = [] - - ###################################### - # STEP 5 - xternal deactivate - utt = Message("test_deactivate") - self.core.bus.emit(utt) - - self.assertEqual(SessionManager.default_session.active_skills[0][0], self.other_skill_id) - # confirm all expected messages are sent - expected_messages = [ - "test_deactivate", - "intent.service.skills.deactivate", - "intent.service.skills.deactivated", - "ovos-tskill-abort.openvoiceos.deactivate", - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify skill is no longer in active skills - self.assertEqual(SessionManager.default_session.active_skills[0][0], self.other_skill_id) - self.assertEqual(len(SessionManager.default_session.active_skills), 1) - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - # test that active skills list has been updated - self.assertEqual(len(sess.active_skills), 1) - self.assertEqual(sess.active_skills[0][0], self.other_skill_id) - self.assertEqual(len(messages[-1].data["session_data"]["active_skills"]), 1) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.other_skill_id) - - messages = [] - - ###################################### - # STEP 6 - external activate - self.assertEqual(SessionManager.default_session.active_skills[0][0], self.other_skill_id) - utt = Message("test_activate") - self.core.bus.emit(utt) - self.assertEqual(SessionManager.default_session.active_skills[0][0], self.skill_id) - # confirm all expected messages are sent - expected_messages = [ - "test_activate", - "intent.service.skills.activate", - "intent.service.skills.activated", - f"{self.skill_id}.activate", - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify skill is again in active skills - self.assertEqual(SessionManager.default_session.active_skills[0][0], self.skill_id) - self.assertEqual(len(SessionManager.default_session.active_skills), 2) - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - # test that active skills list has been updated - self.assertEqual(len(sess.active_skills), 2) - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertEqual(len(messages[-1].data["session_data"]["active_skills"]), 2) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.skill_id) - - ###################################### - # STEP 7 - deactivate inside intent handler - # should not send activate message - # session should not contain skill as active - SessionManager.default_session = Session(session_id="default") # reset state - messages = [] - utt = Message("recognizer_loop:utterance", - {"utterances": ["deactivate skill"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.activate", - "ovos-tskill-abort.openvoiceos:deactivate.intent", - # skill selected - "mycroft.skill.handler.start", - # intent code - "intent.service.skills.deactivate", - "intent.service.skills.deactivated", - f"{self.skill_id}.deactivate", - "ovos.session.update_default", - "speak", # "deactivated" - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ###################################### - # STEP 8 - deactivate inside converse handler - # should not send activate message - # session should not contain skill as active - # NOTE: if converse returns True, skill activated again! - sess = Session(session_id="default") - sess.activate_skill(self.skill_id) - utt = Message("converse_deactivate") - self.core.bus.emit(utt) # set internal test skill flag - messages = [] - - utt = Message("recognizer_loop:utterance", - {"utterances": ["deactivate converse"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - expected_messages = [ - "recognizer_loop:utterance", # converse gets it - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.converse.request", - # converse code - "intent.service.skills.deactivate", - "intent.service.skills.deactivated", - f"{self.skill_id}.deactivate", - "ovos.session.update_default", - # needs ovos-workshop PR - "skill.converse.response", # conversed! - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated - "ovos.session.update_default" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - -class TestTimeOut(TestCase): - - def setUp(self): - self.skill_id = "ovos-skill-slow-fallback.openvoiceos" - self.core = get_minicroft([self.skill_id]) - - def tearDown(self) -> None: - self.core.stop() - - def test_kill(self): - messages = [] - sess = Session("123", pipeline=["converse"]) - sess.activate_skill(self.skill_id) - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hang forever in converse"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.converse.ping", # default session injected - "skill.converse.pong", - f"{self.skill_id}.converse.request", - - # skill hangs forever here and never gets to emit a response - - "ovos.skills.converse.force_timeout", # killed by core - "skill.converse.response", - f"{self.skill_id}.converse.killed", - - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.utterance.handled" # handle_utterance returned (intent service) - - ] - - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) diff --git a/test/end2end/session/test_fallback.py b/test/end2end/session/test_fallback.py deleted file mode 100644 index 7b4e33b533d..00000000000 --- a/test/end2end/session/test_fallback.py +++ /dev/null @@ -1,356 +0,0 @@ -import time -from time import sleep -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestFallback(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-fallback-unknown.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_fallback(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "fallback_high", - "fallback_medium", - "fallback_low" - ] - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}, - {"session": SessionManager.default_session.serialize(), # explicit default sess - "x": "xx"}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # FallbackV2 - "ovos.skills.fallback.ping", - "ovos.skills.fallback.pong", - # skill executing - f"ovos.skills.fallback.{self.skill_id}.request", - f"ovos.skills.fallback.{self.skill_id}.start", - "speak", - f"ovos.skills.fallback.{self.skill_id}.response", - # activated only after skill returns True - f"{self.skill_id}.activate", - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that contexts are kept around - for m in messages: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["x"], "xx") - # verify active skills is empty until "intent.service.skills.activated" - for m in messages[:7]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["session"]["active_skills"], []) - - # verify fallback ping/pong answer from skill - self.assertEqual(messages[1].msg_type, "ovos.skills.fallback.ping") - self.assertEqual(messages[2].msg_type, "ovos.skills.fallback.pong") - self.assertEqual(messages[2].data["skill_id"], self.skill_id) - self.assertEqual(messages[2].context["skill_id"], self.skill_id) - self.assertTrue(messages[2].data["can_handle"]) - - # verify skill executes - self.assertEqual(messages[3].msg_type, f"ovos.skills.fallback.{self.skill_id}.request") - self.assertEqual(messages[3].data["skill_id"], self.skill_id) - self.assertEqual(messages[4].msg_type, f"ovos.skills.fallback.{self.skill_id}.start") - self.assertEqual(messages[5].msg_type, "speak") - self.assertEqual(messages[5].data["meta"]["dialog"], "unknown") - self.assertEqual(messages[5].data["meta"]["skill"], self.skill_id) - - # end of fallback - self.assertEqual(messages[6].msg_type, f"ovos.skills.fallback.{self.skill_id}.response") - self.assertTrue(messages[6].data["result"]) - self.assertEqual(messages[6].data["fallback_handler"], "UnknownSkill.handle_fallback") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - - # test second message with no session resumes default active skills - messages = [] - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}) - self.core.bus.emit(utt) - # converse ping/pong due being active - expected_messages.extend([f"{self.skill_id}.converse.ping", "skill.converse.pong"]) - - wait_for_n_messages(len(expected_messages)) - self.assertEqual(len(expected_messages), len(messages)) - - # verify that contexts are kept around - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["session"]["active_skills"][0][0], self.skill_id) - - def test_fallback_with_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "fallback_high", - "fallback_medium", - "fallback_low" - ] - messages = [] - - sess = Session(pipeline=[ - "fallback_high", - "fallback_medium", - "fallback_low" - ]) - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}, - {"session": sess.serialize(), # explicit sess - "x": "xx"}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # FallbackV2 - "ovos.skills.fallback.ping", - "ovos.skills.fallback.pong", - # skill executing - TODO "mycroft.skill.handler.start" + "mycroft.skill.handler.complete" should be added - f"ovos.skills.fallback.{self.skill_id}.request", - f"ovos.skills.fallback.{self.skill_id}.start", - - "speak", - f"ovos.skills.fallback.{self.skill_id}.response", - f"{self.skill_id}.activate", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that contexts are kept around - for m in messages: - self.assertEqual(m.context["session"]["session_id"], sess.session_id) - self.assertEqual(m.context["x"], "xx") - - # verify fallback ping/pong answer from skill - self.assertEqual(messages[1].msg_type, "ovos.skills.fallback.ping") - self.assertEqual(messages[2].msg_type, "ovos.skills.fallback.pong") - self.assertEqual(messages[2].data["skill_id"], self.skill_id) - self.assertEqual(messages[2].context["skill_id"], self.skill_id) - self.assertTrue(messages[2].data["can_handle"]) - - # verify skill executes - self.assertEqual(messages[3].msg_type, f"ovos.skills.fallback.{self.skill_id}.request") - self.assertEqual(messages[3].data["skill_id"], self.skill_id) - self.assertEqual(messages[4].msg_type, f"ovos.skills.fallback.{self.skill_id}.start") - self.assertEqual(messages[5].msg_type, "speak") - self.assertEqual(messages[5].data["meta"]["dialog"], "unknown") - self.assertEqual(messages[5].data["meta"]["skill"], self.skill_id) - self.assertEqual(messages[6].msg_type, f"ovos.skills.fallback.{self.skill_id}.response") - self.assertTrue(messages[6].data["result"]) - self.assertEqual(messages[6].data["fallback_handler"], "UnknownSkill.handle_fallback") - - # test that active skills list has been updated - for m in messages[10:]: - self.assertEqual(m.context["session"]["active_skills"][0][0], self.skill_id) - - def test_deactivate_in_fallback(self): - messages = [] - - sess = Session("123") - sess.activate_skill(self.skill_id) # skill is active - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("fallback_deactivate") - self.core.bus.emit(utt) # set internal test skill flag - messages = [] - - utt = Message("recognizer_loop:utterance", - {"utterances": ["deactivate fallback"]}, - {"session":sess.serialize()}) - self.core.bus.emit(utt) - - expected_messages = [ - "recognizer_loop:utterance", - # skill is active, so we get converse events - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - # FallbackV2 - "ovos.skills.fallback.ping", - "ovos.skills.fallback.pong", - # skill executing - f"ovos.skills.fallback.{self.skill_id}.request", - f"ovos.skills.fallback.{self.skill_id}.start", - - "speak", - # deactivate skill in fallback handler - "intent.service.skills.deactivate", - "intent.service.skills.deactivated", - f"{self.skill_id}.deactivate", - # activate events suppressed - f"ovos.skills.fallback.{self.skill_id}.response", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - -class TestFallbackTimeout(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-fallback-unknown.openvoiceos" - self.skill_id2 = "ovos-skill-slow-fallback.openvoiceos" - self.core = get_minicroft([self.skill_id, self.skill_id2]) - - def tearDown(self) -> None: - self.core.stop() - - def test_fallback(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "fallback_medium", - "fallback_low" - ] - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["invalid"]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # Fallback High - "ovos.skills.fallback.ping", - "ovos.skills.fallback.pong", - "ovos.skills.fallback.pong", - - # slow skill executing - f"ovos.skills.fallback.{self.skill_id2}.request", - f"ovos.skills.fallback.{self.skill_id2}.start", - "ovos.skills.fallback.force_timeout", # timeout from core - f"ovos.skills.fallback.{self.skill_id2}.response", - f"ovos.skills.fallback.{self.skill_id2}.killed", # killable_event decorator response - - # Fallback Medium - "ovos.skills.fallback.ping", - "ovos.skills.fallback.pong", - "ovos.skills.fallback.pong", - - # skill executing - f"ovos.skills.fallback.{self.skill_id}.request", - f"ovos.skills.fallback.{self.skill_id}.start", - "speak", - f"ovos.skills.fallback.{self.skill_id}.response", - - # activated only after skill return True - f"{self.skill_id}.activate", - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) diff --git a/test/end2end/session/test_get_response.py b/test/end2end/session/test_get_response.py deleted file mode 100644 index b2320d41a16..00000000000 --- a/test/end2end/session/test_get_response.py +++ /dev/null @@ -1,1026 +0,0 @@ -import time -from time import sleep -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestSessions(TestCase): - - def setUp(self): - self.skill_id = "ovos-tskill-abort.openvoiceos" - self.other_skill_id = "skill-ovos-hello-world.openvoiceos" - self.core = get_minicroft([self.skill_id, self.other_skill_id]) - - def tearDown(self) -> None: - self.core.stop() - - def test_no_response(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "padatious_high", - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - def on_speak(msg): - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_start")) - sleep(1) # simulate TTS playback - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_end")) - - self.core.bus.on("message", new_msg) - self.core.bus.on("speak", on_speak) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get response"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - - "speak", # 'mycroft.mic.listen' if no dialog passed to get_response - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - - f"{self.skill_id}.get_response.waiting", - - # "recognizer_loop:utterance" would be here if user answered - "skill.converse.get_response.disable", # end of get_response - "ovos.session.update_default", # sync get_response status - # intent code post self.get_response - # from speak inside intent - "speak", # speak "ERROR" inside intent - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - "mycroft.skill.handler.complete", # original intent finished executing - "ovos.utterance.handled", - # session updated at end of intent pipeline - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["lang"], "en-US") - - # verify skill_id is now present in every message.context - for m in messages[1:]: - if m.msg_type == "ovos.session.update_default": - continue - self.assertEqual(m.context["skill_id"], self.skill_id) - - # verify intent execution - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "TestAbortSkill.handle_test_get_response") - - # question dialog - self.assertEqual(messages[6].msg_type, "speak") - self.assertEqual(messages[6].data["lang"], "en-US") - self.assertTrue(messages[6].data["expect_response"]) # listen after dialog - self.assertEqual(messages[6].data["meta"]["skill"], self.skill_id) - - # post self.get_response intent code - self.assertEqual(messages[12].msg_type, "speak") - self.assertEqual(messages[12].data["lang"], "en-US") - self.assertFalse(messages[12].data["expect_response"]) - self.assertEqual(messages[12].data["utterance"], "ERROR") - self.assertEqual(messages[12].data["meta"]["skill"], self.skill_id) - - self.assertEqual(messages[15].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[15].data["name"], "TestAbortSkill.handle_test_get_response") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - def test_with_response(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "padatious_high", - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - def answer_get_response(msg): - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_start")) - sleep(1) # simulate TTS playback - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_end")) # end wait=True in self.speak - if msg.data["utterance"] == "give me an answer": - sleep(0.5) - utt = Message("recognizer_loop:utterance", - {"utterances": ["ok"]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - - self.core.bus.on("speak", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get response"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - - "speak", # 'mycroft.mic.listen' if no dialog passed to get_response - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - - "recognizer_loop:utterance", # answer to get_response from user, - # converse check - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - # get response handling - f"{self.skill_id}.converse.get_response", # returning user utterance to running intent self.get_response - "ovos.session.update_default", # sync skill activated by converse - f"{self.skill_id}.get_response.waiting", - "skill.converse.get_response.disable", # end of get_response - "ovos.session.update_default", # sync get_response status - # intent code post self.get_response - - "speak", # speak "ok" inside intent - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - "mycroft.skill.handler.complete", # original intent finished executing - "ovos.utterance.handled", - # session updated at end of intent pipeline - "ovos.session.update_default" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - - # verify intent execution - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "TestAbortSkill.handle_test_get_response") - - # question dialog - self.assertEqual(messages[6].msg_type, "speak") - self.assertEqual(messages[6].data["utterance"], "give me an answer", ) - self.assertEqual(messages[6].data["lang"], "en-US") - self.assertTrue(messages[6].data["expect_response"]) # listen after dialog - self.assertEqual(messages[6].data["meta"]["skill"], self.skill_id) - - # captured utterance sent to get_response handler that is waiting - self.assertEqual(messages[12].msg_type, f"{self.skill_id}.converse.get_response") - self.assertEqual(messages[12].data["utterances"], ["ok"]) - - # post self.get_response intent code - self.assertEqual(messages[17].msg_type, "speak") - self.assertEqual(messages[17].data["lang"], "en-US") - self.assertFalse(messages[17].data["expect_response"]) - self.assertEqual(messages[17].data["utterance"], "ok") - self.assertEqual(messages[17].data["meta"]["skill"], self.skill_id) - - self.assertEqual(messages[20].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[20].data["name"], "TestAbortSkill.handle_test_get_response") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - def test_cancel_response(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "padatious_high", - "adapt_high"] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - def answer_get_response(msg): - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_start")) - sleep(1) # simulate TTS playback - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_end")) # end wait=True in self.speak - if msg.data["utterance"] == "give me an answer": - sleep(0.5) - utt = Message("recognizer_loop:utterance", - {"utterances": ["cancel"]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - - self.core.bus.on("speak", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get response"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - - "speak", # 'mycroft.mic.listen' if no dialog passed to get_response - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - - "recognizer_loop:utterance", # answer to get_response from user, - # converse check - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - # get response handling - f"{self.skill_id}.converse.get_response", # returning user utterance to running intent self.get_response - "ovos.session.update_default", # sync skill activated by converse - - f"{self.skill_id}.get_response.waiting", - "skill.converse.get_response.disable", # end of get_response - "ovos.session.update_default", # sync get_response status - # intent code post self.get_response - - "speak", # speak "ERROR" inside intent - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - "mycroft.skill.handler.complete", # original intent finished executing - - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated at end of intent pipeline - "ovos.session.update_default" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - - # verify intent execution - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "TestAbortSkill.handle_test_get_response") - - # question dialog - self.assertEqual(messages[6].msg_type, "speak") - self.assertEqual(messages[6].data["utterance"], "give me an answer", ) - self.assertEqual(messages[6].data["lang"], "en-US") - self.assertTrue(messages[6].data["expect_response"]) # listen after dialog - self.assertEqual(messages[6].data["meta"]["skill"], self.skill_id) - - # captured utterance sent to get_response handler that is waiting - self.assertEqual(messages[12].msg_type, f"{self.skill_id}.converse.get_response") - self.assertEqual(messages[12].data["utterances"], ["cancel"]) # was canceled by user, returned None - - # post self.get_response intent code - self.assertEqual(messages[17].msg_type, "speak") - self.assertEqual(messages[17].data["lang"], "en-US") - self.assertFalse(messages[17].data["expect_response"]) - self.assertEqual(messages[17].data["utterance"], "ERROR") - self.assertEqual(messages[17].data["meta"]["skill"], self.skill_id) - - # vrify handler name - self.assertEqual(messages[20].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[20].data["name"], "TestAbortSkill.handle_test_get_response") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - def test_with_reprompt(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "padatious_high", - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - counter = 0 - - def answer_get_response(msg): - nonlocal counter - counter += 1 - if counter == 3: # answer on 3rd prompt only - sleep(0.5) - utt = Message("recognizer_loop:utterance", - {"utterances": ["ok"]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - - self.core.bus.on("mycroft.mic.listen", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["3 prompts"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response3.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - "mycroft.mic.listen", # no dialog in self.get_response - - f"{self.skill_id}.get_response.waiting", - - "mycroft.mic.listen", - "mycroft.mic.listen", - - "recognizer_loop:utterance", # answer to get_response from user, - # converse check - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - # get response handling - f"{self.skill_id}.converse.get_response", # returning user utterance to running intent self.get_response - "ovos.session.update_default", # sync skill activated by converse - "skill.converse.get_response.disable", # end of get_response - "ovos.session.update_default", # sync get_response status - # intent code post self.get_response - - "speak", # speak "ok" inside intent - "mycroft.skill.handler.complete", # original intent finished executing - - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated at end of intent pipeline - "ovos.session.update_default" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - - # verify intent execution - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "TestAbortSkill.handle_test_get_response3") - - # captured utterance sent to get_response handler that is waiting - self.assertEqual(messages[13].msg_type, f"{self.skill_id}.converse.get_response") - self.assertEqual(messages[13].data["utterances"], ["ok"]) - - # post self.get_response intent code - self.assertEqual(messages[17].msg_type, "speak") - self.assertEqual(messages[17].data["lang"], "en-US") - self.assertFalse(messages[17].data["expect_response"]) - self.assertEqual(messages[17].data["utterance"], "ok") - self.assertEqual(messages[17].data["meta"]["skill"], self.skill_id) - - self.assertEqual(messages[18].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[18].data["name"], "TestAbortSkill.handle_test_get_response3") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - def test_nested(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "padatious_high", - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - items = ["A", "B", "C"] - - def answer_get_response(msg): - nonlocal items - sleep(0.5) - if not len(items): - utt = Message("recognizer_loop:utterance", - {"utterances": ["cancel"]}, - {"session": SessionManager.default_session.serialize()}) - else: - utt = Message("recognizer_loop:utterance", - {"utterances": [items[0]]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - items = items[1:] - - self.core.bus.on("mycroft.mic.listen", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get items"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - - f"{self.skill_id}.activate", - - f"{self.skill_id}:test_get_response_cascade.intent", - "mycroft.skill.handler.start", - - # intent code before self.get_response - - "speak", # "give me items" - - # first get_response - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - "mycroft.mic.listen", # no dialog in self.get_response - "recognizer_loop:utterance", # A - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.converse.get_response", # A - "ovos.session.update_default", - f"{self.skill_id}.get_response.waiting", - "skill.converse.get_response.disable", - "ovos.session.update_default", # sync get_response status - - # second get_response - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - "mycroft.mic.listen", # no dialog in self.get_response - "recognizer_loop:utterance", # B - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.converse.get_response", # B - "ovos.session.update_default", # sync skill trigger - f"{self.skill_id}.get_response.waiting", - "skill.converse.get_response.disable", - "ovos.session.update_default", # sync get_response status - - # 3rd get_response - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - "mycroft.mic.listen", # no dialog in self.get_response - "recognizer_loop:utterance", # C - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.converse.get_response", # C - "ovos.session.update_default", # sync skill trigger - f"{self.skill_id}.get_response.waiting", - "skill.converse.get_response.disable", - "ovos.session.update_default", # sync get_response status - - # cancel get_response - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - "mycroft.mic.listen", # no dialog in self.get_response - "recognizer_loop:utterance", # cancel - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.converse.get_response", # cancel - "ovos.session.update_default", # sync skill trigger - f"{self.skill_id}.get_response.waiting", - "skill.converse.get_response.disable", - "ovos.session.update_default", # sync get_response status - - "skill_items", # skill emitted message [A, B, C] - - "mycroft.skill.handler.complete", # original intent finished executing - - "ovos.utterance.handled", # handle_utterance returned (intent service) - # session updated at end of intent pipeline - "ovos.session.update_default" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - - # verify intent triggers - self.assertEqual(messages[2].msg_type, f"{self.skill_id}:test_get_response_cascade.intent") - - # verify intent execution - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "TestAbortSkill.handle_test_get_response_cascade") - - # post self.get_response intent code - self.assertEqual(messages[4].msg_type, "speak") - self.assertEqual(messages[4].data["lang"], "en-US") - self.assertFalse(messages[4].data["expect_response"]) - self.assertEqual(messages[4].data["utterance"], "give me items") - self.assertEqual(messages[4].data["meta"]["skill"], self.skill_id) - - responses = ["A", "B", "C", "cancel"] - for response in responses: - i = 4 + responses.index(response) * 11 - print(i, response) - # enable get_response for this session - self.assertEqual(messages[i + 1].msg_type, "skill.converse.get_response.enable") - self.assertEqual(messages[i + 2].msg_type, "ovos.session.update_default") - - # 3 sound prompts (no dialog in this test) - self.assertEqual(messages[i + 3].msg_type, "mycroft.mic.listen") - - # check utterance goes through converse cycle - self.assertEqual(messages[i + 4].msg_type, "recognizer_loop:utterance") - self.assertEqual(messages[i + 5].msg_type, f"{self.skill_id}.converse.ping") - self.assertEqual(messages[i + 6].msg_type, "skill.converse.pong") - - # captured utterance sent to get_response handler that is waiting - self.assertEqual(messages[i + 7].msg_type, f"{self.skill_id}.converse.get_response") - self.assertEqual(messages[i + 7].data["utterances"], [response]) - - # converse pipeline activates the skill last_used timestamp - self.assertEqual(messages[i + 8].msg_type, "ovos.session.update_default") - - # disable get_response for this session - self.assertEqual(messages[i + 9].msg_type, f"{self.skill_id}.get_response.waiting") - self.assertEqual(messages[i + 10].msg_type, "skill.converse.get_response.disable") - self.assertEqual(messages[i + 11].msg_type, "ovos.session.update_default") - - # intent return - self.assertEqual(messages[-4].msg_type, "skill_items") - self.assertEqual(messages[-4].data, {"items": ["A", "B", "C"]}) - - # report handler complete - self.assertEqual(messages[-3].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[-3].data["name"], "TestAbortSkill.handle_test_get_response_cascade") - - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - - def test_kill_response(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "padatious_high", - "adapt_high"] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - def answer_get_response(msg): - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_start")) - sleep(1) # simulate TTS playback - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_end")) # end wait=True in self.speak - - def abort_response(msg): - # abort ongoing get_response - GLOBAL, no skill_id targeted - self.core.bus.emit(msg.forward("mycroft.skills.abort_question")) - - self.core.bus.on(f"{self.skill_id}.get_response.waiting", abort_response) - self.core.bus.on("speak", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get response"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - "ovos.session.update_default", # sync get_response status - - "speak", # 'mycroft.mic.listen' if no dialog passed to get_response - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - - f"{self.skill_id}.get_response.waiting", - "mycroft.skills.abort_question", # kill get_response from core - f"{self.skill_id}.get_response.killed", # ack from workshop that get_response was killed - - "skill.converse.get_response.disable", # end of get_response - "ovos.session.update_default", # sync get_response status - # intent code post self.get_response - - "speak", # speak "ERROR" inside intent - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - "mycroft.skill.handler.complete", # original intent finished executing - "ovos.utterance.handled", - # session updated at end of intent pipeline - "ovos.session.update_default" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_kill_response_with_session_and_id(self): - - messages = [] - sess = Session("123") - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", - "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - def answer_get_response(msg): - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_start")) - sleep(1) # simulate TTS playback - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_end")) # end wait=True in self.speak - - def abort_response(msg): - # abort ongoing get_response - self.core.bus.emit(msg.forward("mycroft.skills.abort_question", - {"skill_id": self.skill_id})) - - self.core.bus.on(f"{self.skill_id}.get_response.waiting", abort_response) - self.core.bus.on("speak", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get response"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - - - "speak", # 'mycroft.mic.listen' if no dialog passed to get_response - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - - f"{self.skill_id}.get_response.waiting", - "mycroft.skills.abort_question", # kill get_response from core - f"{self.skill_id}.get_response.killed", # ack from workshop that get_response was killed - - "skill.converse.get_response.disable", # end of get_response - - # intent code post self.get_response - - "speak", # speak "ERROR" inside intent - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - "mycroft.skill.handler.complete", # original intent finished executing - "ovos.utterance.handled" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_kill_response_with_skill_mismatch(self): - - messages = [] - sess = Session("123") - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", - "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - def answer_get_response(msg): - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_start")) - sleep(1) # simulate TTS playback - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_end")) # end wait=True in self.speak - - def abort_response(msg): - # abort ongoing get_response for WRONG skill_id - self.core.bus.emit(msg.forward("mycroft.skills.abort_question", - {"skill_id": "OTHER"})) - - self.core.bus.on(f"{self.skill_id}.get_response.waiting", abort_response) - self.core.bus.on("speak", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get response"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - - - "speak", # 'mycroft.mic.listen' if no dialog passed to get_response - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - - f"{self.skill_id}.get_response.waiting", - "mycroft.skills.abort_question", # kill get_response from core - - # f"{self.skill_id}.get_response.killed", # ignored due to skill_id mismatch - - "skill.converse.get_response.disable", # end of get_response - - # intent code post self.get_response - - "speak", # speak "ERROR" inside intent - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - "mycroft.skill.handler.complete", # original intent finished executing - "ovos.utterance.handled" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_kill_response_with_session_mismatch(self): - - messages = [] - sess = Session("123") - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", - "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - def answer_get_response(msg): - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_start")) - sleep(1) # simulate TTS playback - self.core.bus.emit(msg.forward("recognizer_loop:audio_output_end")) # end wait=True in self.speak - - def abort_response(msg): - # abort ongoing get_response for WRONG session - msg.context["session"] = Session("456").serialize() - self.core.bus.emit(msg.forward("mycroft.skills.abort_question", - {"skill_id": self.skill_id})) - - self.core.bus.on(f"{self.skill_id}.get_response.waiting", abort_response) - self.core.bus.on("speak", answer_get_response) - - # trigger get_response - utt = Message("recognizer_loop:utterance", - {"utterances": ["test get response"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # trigger intent to start the test - f"{self.skill_id}.activate", - f"{self.skill_id}:test_get_response.intent", - "mycroft.skill.handler.start", - # intent code - "skill.converse.get_response.enable", # start of get_response - - - "speak", # 'mycroft.mic.listen' if no dialog passed to get_response - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - - f"{self.skill_id}.get_response.waiting", - "mycroft.skills.abort_question", # kill get_response from core - - # f"{self.skill_id}.get_response.killed", # ignored due to session mismatch - - "skill.converse.get_response.disable", # end of get_response - - # intent code post self.get_response - - "speak", # speak "ERROR" inside intent - "recognizer_loop:audio_output_start", - "recognizer_loop:audio_output_end", - "mycroft.skill.handler.complete", # original intent finished executing - "ovos.utterance.handled" - - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py deleted file mode 100644 index 0ec4ecb6cbd..00000000000 --- a/test/end2end/session/test_ocp.py +++ /dev/null @@ -1,1176 +0,0 @@ -import time -from time import sleep -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import Session -from ovos_utils.ocp import PlayerState, MediaState -from ocp_pipeline.opm import OCPPlayerProxy -from ovos_plugin_manager.ocp import available_extractors -from ..minicroft import get_minicroft - - -class TestOCPPipeline(TestCase): - - def setUp(self): - self.skill_id = "skill-fake-fm.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_no_match(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["play unknown thing"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", - # skill searching (generic) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - # no good results - "ovos.common_play.reset", - "speak", # error, - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_player_info(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", pipeline=["ocp_high"]) - if sess.session_id in self.core.intent_service._ocp.ocp_sessions: - self.core.intent_service._ocp.ocp_sessions.pop(sess.session_id) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["play something"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.SEI.get", # request player info - "ovos.common_play.SEI.get", # (we didnt get player answer, so we try again) - # no response - "ovos.common_play.activate", - "ocp:play", - - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.SEI.get", # request info again, cause player didnt answer before - "ovos.common_play.search.end", - "ovos.common_play.reset", - - "speak", # nothing to play - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - self.assertFalse(self.core.intent_service._ocp.ocp_sessions[sess.session_id].ocp_available) - self.assertEqual(self.core.intent_service._ocp.ocp_sessions[sess.session_id].available_extractors, - available_extractors()) # stream extractors handled in core before returning result - - # now test with OCP response - messages = [] - - def on_get(m): - # response that OCP would emit if available - self.core.bus.emit(m.response(data={"SEI": ["test"]})) - - self.core.bus.on("ovos.common_play.SEI.get", on_get) - - if sess.session_id in self.core.intent_service._ocp.ocp_sessions: - self.core.intent_service._ocp.ocp_sessions.pop(sess.session_id) - - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.SEI.get", # request player info - "ovos.common_play.SEI.get.response", # OCP response - "ovos.common_play.activate", - "ocp:play", - - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - "ovos.common_play.reset", - - "speak", # nothing to play - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - self.assertTrue(self.core.intent_service._ocp.ocp_sessions[sess.session_id].ocp_available) - self.assertEqual(self.core.intent_service._ocp.ocp_sessions[sess.session_id].available_extractors, - ["test"]) - - # test OCP player state sync - self.assertEqual(self.core.intent_service._ocp.ocp_sessions[sess.session_id].player_state, - PlayerState.STOPPED) - messages = [] - utt = Message("ovos.common_play.status.response", - {"player_state": PlayerState.PLAYING.value}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - self.assertEqual(self.core.intent_service._ocp.ocp_sessions[sess.session_id].player_state, - PlayerState.PLAYING) - - def test_radio_media_match(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["play some radio station"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", # media type radio - # skill searching (radio) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - # good results because of radio media type - "ovos.common_play.reset", - "add_context", # NowPlaying context - "ovos.common_play.play", # OCP api, - "ovos.common_play.search.populate", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - play = messages[-3] - self.assertEqual(play.data["media"]["uri"], "https://fake_4.mp3") - - def test_unk_media_match(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["play the alien movie"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # stop any ongoing previous search - "ovos.common_play.query", # generic media type, no movie skills available - # skill searching (generic) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - # no good results - "ovos.common_play.reset", - - "speak", # error - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_skill_name_match(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["play Fake FM"]}, # auto derived from skill class name in this case - {"session": sess.serialize(), - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - f"ovos.common_play.query.{self.skill_id}", # explicitly search skill - # skill searching (explicit) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - # good results - "ovos.common_play.reset", - "add_context", # NowPlaying context - "ovos.common_play.play", # OCP api - "ovos.common_play.search.populate", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_legacy_match(self): - self.assertIsNotNone(self.core.intent_service._ocp) - self.core.intent_service._ocp.config = {"legacy": True} - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=False, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["play some radio station"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:play", - - "speak", - "ovos.common_play.search.start", - "enclosure.mouth.think", - "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", # media type radio - # skill searching (radio) - "ovos.common_play.skill.search_start", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.query.response", - "ovos.common_play.skill.search_end", - "ovos.common_play.search.end", - # good results because of radio media type - "ovos.common_play.reset", - "add_context", # NowPlaying context - 'mycroft.audio.service.play', # LEGACY api - "mycroft.audio.service.queue", - "mycroft.audio.service.queue", - "mycroft.audio.service.queue", - "mycroft.audio.service.queue", - "ovos.common_play.search.populate", - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ocp = self.core.intent_service._ocp.ocp_sessions[sess.session_id] - self.assertEqual(ocp.player_state, PlayerState.PLAYING) - self.assertEqual(ocp.media_state, MediaState.LOADING_MEDIA) - - def test_legacy_pause(self): - self.assertIsNotNone(self.core.intent_service._ocp) - self.core.intent_service._ocp.config = {"legacy": True} - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=False, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["pause"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:pause", - 'mycroft.audio.service.pause', # LEGACY api - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ocp = self.core.intent_service._ocp.ocp_sessions[sess.session_id] - self.assertEqual(ocp.player_state, PlayerState.PAUSED) - - def test_legacy_resume(self): - self.assertIsNotNone(self.core.intent_service._ocp) - self.core.intent_service._ocp.config = {"legacy": True} - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=False, - player_state=PlayerState.PAUSED, media_state=MediaState.LOADED_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["resume"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:resume", - 'mycroft.audio.service.resume', # LEGACY api - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ocp = self.core.intent_service._ocp.ocp_sessions[sess.session_id] - self.assertEqual(ocp.player_state, PlayerState.PLAYING) - - def test_legacy_stop(self): - self.assertIsNotNone(self.core.intent_service._ocp) - self.core.intent_service._ocp.config = {"legacy": True} - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=False, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["stop"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:media_stop", - 'mycroft.audio.service.stop', # LEGACY api - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - ocp = self.core.intent_service._ocp.ocp_sessions[sess.session_id] - self.assertEqual(ocp.player_state, PlayerState.STOPPED) - - def test_legacy_next(self): - self.assertIsNotNone(self.core.intent_service._ocp) - self.core.intent_service._ocp.config = {"legacy": True} - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=False, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["next"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:next", - 'mycroft.audio.service.next', # LEGACY api - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_legacy_prev(self): - self.assertIsNotNone(self.core.intent_service._ocp) - self.core.intent_service._ocp.config = {"legacy": True} - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=False, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["previous"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:prev", - 'mycroft.audio.service.prev', # LEGACY api - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_pause(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["pause"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:pause", - 'ovos.common_play.pause', - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_resume(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.PAUSED, media_state=MediaState.LOADED_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["resume"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:resume", - 'ovos.common_play.resume', - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_stop(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["stop"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:media_stop", - 'ovos.common_play.stop', - "ovos.common_play.stop.response", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_next(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["next"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:next", - 'ovos.common_play.next', - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_prev(self): - self.assertIsNotNone(self.core.intent_service._ocp) - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.PLAYING, media_state=MediaState.LOADED_MEDIA) - utt = Message("recognizer_loop:utterance", - {"utterances": ["previous"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "ovos.common_play.activate", - "ocp:prev", - 'ovos.common_play.previous', - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_status_matches_not_playing(self): - self.assertIsNotNone(self.core.intent_service._ocp) - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "ocp_high" - ]) - - self.core.intent_service._ocp.ocp_sessions[sess.session_id] = OCPPlayerProxy( - session_id=sess.session_id, available_extractors=[], ocp_available=True, - player_state=PlayerState.STOPPED, media_state=MediaState.NO_MEDIA) - - # wont match unless PlayerState.Playing - for t in ["pause", "resume", "stop", "next", "previous"]: - messages = [] - - utt = Message("recognizer_loop:utterance", - {"utterances": [t]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.status", - "mycroft.audio.play_sound", - "complete_intent_failure", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - def test_legacy_cps(self): - self.assertIsNotNone(self.core.intent_service._ocp) - - self.core.intent_service._ocp.config = {"legacy_cps": True} - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request", "register_vocab",]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "ocp_legacy" - ]) - utt = Message("recognizer_loop:utterance", - {"utterances": ["play rammstein"]}, - {"session": sess.serialize(), # explicit - }) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "ovos.common_play.activate", - "ocp:legacy_cps", - # legacy cps api - "play:query", - "mycroft.audio.play_sound", # error - no results - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - diff --git a/test/end2end/session/test_sched.py b/test/end2end/session/test_sched.py deleted file mode 100644 index a9378e3b9e7..00000000000 --- a/test/end2end/session/test_sched.py +++ /dev/null @@ -1,222 +0,0 @@ -import time -from time import sleep -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestSessions(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-schedule.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def test_no_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["schedule event"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - f"{self.skill_id}.activate", - f"{self.skill_id}:ScheduleIntent", - "mycroft.skill.handler.start", - "speak", - "mycroft.scheduler.schedule_event", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default", - # event triggering after 3 seconds - "skill-ovos-schedule.openvoiceos:my_event", - "speak" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" and "lang" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["lang"], "en-US") - - # verify skill_id is now present in every message.context - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.activate") - for m in messages[1:]: - if m.msg_type == "ovos.session.update_default": - continue - self.assertEqual(m.context["skill_id"], self.skill_id) - - # verify intent triggers - self.assertEqual(messages[2].msg_type, f"{self.skill_id}:ScheduleIntent") - self.assertEqual(messages[2].data["intent_type"], f"{self.skill_id}:ScheduleIntent") - - # verify intent execution - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "ScheduleSkill.handle_sched_intent") - - self.assertEqual(messages[4].msg_type, "speak") - self.assertEqual(messages[4].data["lang"], "en-US") - self.assertFalse(messages[4].data["expect_response"]) - self.assertEqual(messages[4].data["meta"]["dialog"], "done") - self.assertEqual(messages[4].data["meta"]["skill"], self.skill_id) - self.assertEqual(messages[5].msg_type, "mycroft.scheduler.schedule_event") - self.assertEqual(messages[6].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[6].data["name"], "ScheduleSkill.handle_sched_intent") - - self.assertEqual(messages[7].msg_type, "ovos.utterance.handled") - # verify default session is now updated - self.assertEqual(messages[8].msg_type, "ovos.session.update_default") - self.assertEqual(messages[8].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[8].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - # test that active skills list has been updated - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertEqual(messages[8].data["session_data"]["active_skills"][0][0], self.skill_id) - - # ensure context in triggered event is the same from message that triggered the intent - intent_context = messages[-3].context # when skill added to active list (last context change) - intent_context["skill_id"] = 'skill-ovos-schedule.openvoiceos' # for tests below, skill_id is injected - - self.assertEqual(messages[-2].msg_type, "skill-ovos-schedule.openvoiceos:my_event") - self.assertEqual(messages[-2].context, intent_context) - self.assertEqual(messages[-1].msg_type, "speak") - self.assertEqual(messages[-1].data["lang"], "en-US") - self.assertFalse(messages[-1].data["expect_response"]) - self.assertEqual(messages[-1].data["meta"]["dialog"], "trigger") - self.assertEqual(messages[-1].data["meta"]["skill"], self.skill_id) - self.assertEqual(messages[-1].context, intent_context) - - def test_explicit_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session(pipeline=[ - "adapt_high" - ]) - utt = Message("recognizer_loop:utterance", - {"utterances": ["schedule event"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - f"{self.skill_id}.activate", - f"{self.skill_id}:ScheduleIntent", - "mycroft.skill.handler.start", - "speak", - "mycroft.scheduler.schedule_event", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - # event triggering after 3 seconds - "skill-ovos-schedule.openvoiceos:my_event", - "speak" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" is the same in all message - # (missing in utterance message) and kept in all messages - for m in messages: - self.assertEqual(m.context["session"]["session_id"], sess.session_id) - - # verify skill is activated - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.activate") - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.activate") - # verify skill_id is now present in every message.context - for m in messages[1:]: - self.assertEqual(m.context["skill_id"], self.skill_id) - # verify intent triggers - self.assertEqual(messages[2].msg_type, f"{self.skill_id}:ScheduleIntent") - self.assertEqual(messages[2].data["intent_type"], f"{self.skill_id}:ScheduleIntent") - - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "ScheduleSkill.handle_sched_intent") - - # verify intent execution - self.assertEqual(messages[4].msg_type, "speak") - self.assertEqual(messages[4].data["lang"], "en-US") - self.assertFalse(messages[4].data["expect_response"]) - self.assertEqual(messages[4].data["meta"]["dialog"], "done") - self.assertEqual(messages[4].data["meta"]["skill"], self.skill_id) - self.assertEqual(messages[5].msg_type, "mycroft.scheduler.schedule_event") - self.assertEqual(messages[6].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[6].data["name"], "ScheduleSkill.handle_sched_intent") - - # ensure context in triggered event is the same from message that triggered the intent - intent_context = messages[2].context - self.assertEqual(messages[-2].msg_type, "skill-ovos-schedule.openvoiceos:my_event") - self.assertEqual(messages[-2].context, intent_context) - self.assertEqual(messages[-1].msg_type, "speak") - self.assertEqual(messages[-1].data["lang"], "en-US") - self.assertFalse(messages[-1].data["expect_response"]) - self.assertEqual(messages[-1].data["meta"]["dialog"], "trigger") - self.assertEqual(messages[-1].data["meta"]["skill"], self.skill_id) - self.assertEqual(messages[-1].context, intent_context) - - def tearDown(self) -> None: - self.core.stop() diff --git a/test/end2end/session/test_session.py b/test/end2end/session/test_session.py deleted file mode 100644 index d85fc6c4490..00000000000 --- a/test/end2end/session/test_session.py +++ /dev/null @@ -1,299 +0,0 @@ -import time -from time import sleep -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session - -from ..minicroft import get_minicroft - - -class TestSessions(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-hello-world.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_no_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", # no session - f"{self.skill_id}.activate", - f"{self.skill_id}:HelloWorldIntent", - "mycroft.skill.handler.start", - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that "session" and "lang" is injected - # (missing in utterance message) and kept in all messages - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["lang"], "en-US") - - # verify skill is activated - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.activate") - # verify skill_id is now present in every message.context - for m in messages[1:]: - if m.msg_type == "ovos.session.update_default": - continue - self.assertEqual(m.context["skill_id"], self.skill_id) - # verify intent triggers - self.assertEqual(messages[2].msg_type, f"{self.skill_id}:HelloWorldIntent") - self.assertEqual(messages[2].data["intent_type"], f"{self.skill_id}:HelloWorldIntent") - self.assertEqual(messages[3].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[3].data["name"], "HelloWorldSkill.handle_hello_world_intent") - # intent complete - self.assertEqual(messages[-3].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[-3].data["name"], "HelloWorldSkill.handle_hello_world_intent") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - # test that active skills list has been updated - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.skill_id) - - def test_explicit_default_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - now = time.time() - SessionManager.default_session.active_skills = [(self.skill_id, now)] - SessionManager.default_session.pipeline = [ - "converse", - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}, - {"session": SessionManager.default_session.serialize(), # explicit - "xxx": "not-valid"}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.activate", - f"{self.skill_id}:HelloWorldIntent", - "mycroft.skill.handler.start", - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - "ovos.session.update_default" - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that contexts are kept around - for m in messages: - self.assertEqual(m.context["session"]["session_id"], "default") - self.assertEqual(m.context["xxx"], "not-valid") - - # verify ping/pong answer from hello world skill - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.converse.ping") - self.assertEqual(messages[2].msg_type, "skill.converse.pong") - self.assertEqual(messages[2].data["skill_id"], self.skill_id) - self.assertEqual(messages[2].context["skill_id"], self.skill_id) - self.assertFalse(messages[2].data["can_handle"]) - - # verify skill is activated - self.assertEqual(messages[4].msg_type, f"{self.skill_id}:HelloWorldIntent") - self.assertEqual(messages[4].data["intent_type"], f"{self.skill_id}:HelloWorldIntent") - # verify skill_id is now present in every message.context - for m in messages[4:]: - if m.msg_type == "ovos.session.update_default": - continue - self.assertEqual(m.context["skill_id"], self.skill_id) - - self.assertEqual(messages[5].msg_type, "mycroft.skill.handler.start") - - # intent complete - self.assertEqual(messages[-3].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[-3].data["name"], "HelloWorldSkill.handle_hello_world_intent") - - # verify default session is now updated - self.assertEqual(messages[-1].msg_type, "ovos.session.update_default") - self.assertEqual(messages[-1].data["session_data"]["session_id"], "default") - - # test deserialization of payload - sess = Session.deserialize(messages[-1].data["session_data"]) - self.assertEqual(sess.session_id, "default") - - # test that active skills list has been updated - self.assertEqual(messages[-1].data["session_data"]["active_skills"][0][0], self.skill_id) - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertNotEqual(sess.active_skills[0][1], now) - - def test_explicit_session(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "converse", - "adapt_high" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("test-session", - pipeline=[ - "converse", - "adapt_high" - ]) - now = time.time() - sess.active_skills = [(self.skill_id, now)] - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world"]}, - {"session": sess.serialize(), # explicit - "xxx": "not-valid"}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - f"{self.skill_id}.converse.ping", - "skill.converse.pong", - f"{self.skill_id}.activate", - f"{self.skill_id}:HelloWorldIntent", - "mycroft.skill.handler.start", - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled", # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify that contexts are kept around - for m in messages: - self.assertEqual(m.context["session"]["session_id"], sess.session_id) - self.assertEqual(m.context["xxx"], "not-valid") - - # verify ping/pong answer from hello world skill - self.assertEqual(messages[1].msg_type, f"{self.skill_id}.converse.ping") - self.assertEqual(messages[2].msg_type, "skill.converse.pong") - self.assertEqual(messages[2].data["skill_id"], self.skill_id) - self.assertEqual(messages[2].context["skill_id"], self.skill_id) - self.assertFalse(messages[2].data["can_handle"]) - # verify skill is activated - self.assertEqual(messages[3].msg_type, f"{self.skill_id}.activate") - # verify intent triggers - self.assertEqual(messages[4].msg_type, f"{self.skill_id}:HelloWorldIntent") - self.assertEqual(messages[4].data["intent_type"], f"{self.skill_id}:HelloWorldIntent") - # verify skill_id is now present in every message.context - for m in messages[3:]: - self.assertEqual(m.context["skill_id"], self.skill_id) - - # verify intent execution - self.assertEqual(messages[5].msg_type, "mycroft.skill.handler.start") - self.assertEqual(messages[5].data["name"], "HelloWorldSkill.handle_hello_world_intent") - - self.assertEqual(messages[6].msg_type, "speak") - self.assertEqual(messages[6].data["lang"], "en-US") - self.assertFalse(messages[6].data["expect_response"]) - self.assertEqual(messages[6].data["meta"]["dialog"], "hello.world") - self.assertEqual(messages[6].data["meta"]["skill"], self.skill_id) - - self.assertEqual(messages[-2].msg_type, "mycroft.skill.handler.complete") - self.assertEqual(messages[-2].data["name"], "HelloWorldSkill.handle_hello_world_intent") - - # test that active skills list has been updated - sess = Session.from_message(messages[-1]) - self.assertEqual(sess.active_skills[0][0], self.skill_id) - self.assertNotEqual(sess.active_skills[0][1], now) - # test that default session remains unchanged - self.assertEqual(SessionManager.default_session.active_skills, []) diff --git a/test/end2end/session/test_stop.py b/test/end2end/session/test_stop.py deleted file mode 100644 index a30bfc1cf84..00000000000 --- a/test/end2end/session/test_stop.py +++ /dev/null @@ -1,392 +0,0 @@ -import time -from time import sleep -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ..minicroft import get_minicroft - - -class TestSessions(TestCase): - - def setUp(self): - self.skill_id = "skill-old-stop.openvoiceos" - self.new_skill_id = "skill-new-stop.openvoiceos" - self.core = get_minicroft([self.skill_id, self.new_skill_id]) - - def tearDown(self) -> None: - self.core.stop() - - def test_old_stop(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "stop_high", - "adapt_high" - "stop_medium" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("123", - pipeline=[ - "stop_high", - "adapt_high", - "stop_medium" - ]) - ######################################## - # STEP 1 - # nothing to stop - # old style global stop, even if nothing active - utt = Message("recognizer_loop:utterance", - {"utterances": ["stop"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # global stop trigger - "mycroft.stop", - "common_query.openvoiceos.stop.response", - "ovos.common_play.stop.response", - f"{self.skill_id}.stop.response", - # sanity check in test skill that method was indeed called - "speak", # "utterance":"old stop called" - f"{self.new_skill_id}.stop.response", # nothing to stop - - "ovos.utterance.handled" - ] - - wait_for_n_messages(len(expected_messages)) - - mtypes = [m.msg_type for m in messages] - for m in expected_messages: - self.assertTrue(m in mtypes) - - # sanity check stop triggered - for m in messages: - if m.msg_type == "speak": - self.assertEqual(m.data["utterance"], "old stop called") - - messages = [] - - ######################################## - # STEP 2 - # get the skill in active list - utt = Message("recognizer_loop:utterance", - {"utterances": ["old world"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # skill selected - f"{self.skill_id}.activate", - f"{self.skill_id}:OldWorldIntent", - "mycroft.skill.handler.start", - # skill code executing - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # sanity check correct intent triggered - self.assertEqual(messages[-3].data["utterance"], "hello world") - - # test that active skills list has been updated - sess = Session.deserialize(messages[-1].context["session"]) - self.assertEqual(sess.active_skills[0][0], self.skill_id) - - messages = [] - - ######################################## - # STEP 3 - # stop should now go over active skills list - utt = Message("recognizer_loop:utterance", - {"utterances": ["stop"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - - # stop_high - f"{self.skill_id}.stop.ping", # check if active skill wants to stop - "skill.stop.pong", # "can_handle":true - f"{self.skill_id}.stop", # skill specific stop trigger - "speak", # "old stop called" in the test skill stop method - f"{self.skill_id}.stop.response", # skill stops and reports back - - # skill reports it stopped, so core ensures any threaded activity is also killed - "mycroft.skills.abort_question", # core kills any ongoing get_response - "ovos.skills.converse.force_timeout", # core kills any ongoing converse - "mycroft.audio.speech.stop", # core kills any ongoing TTS - - f"{self.skill_id}.activate", # update of skill last usage timestamp - "ovos.utterance.handled" - - ] - - wait_for_n_messages(len(expected_messages)) - - mtypes = [m.msg_type for m in messages] - for m in expected_messages: - self.assertTrue(m in mtypes) - - # confirm all skills self.stop methods called - for m in messages: - # sanity check stop triggered - if m.msg_type == "speak": - self.assertIn(m.data["utterance"], - ["old stop called", "stop"]) - # confirm "skill-old-stop" was the one that reported success - if m.msg_type == "mycroft.stop.handled": - self.assertEqual(m.data["by"], f"skill:{self.skill_id}") - - messages = [] - - def test_new_stop(self): - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.pipeline = [ - "stop_high", - "adapt_high", - "stop_medium" - ] - - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed", "ovos.common_play.status"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - sess = Session("123", - pipeline=[ - "stop_high", - "adapt_high", - "stop_medium" - ]) - - ######################################## - # STEP 1 - # no skills active yet, nothing to stop - # old style global stop, even if nothing active - utt = Message("recognizer_loop:utterance", - {"utterances": ["stop"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # global stop trigger - "mycroft.stop", - "common_query.openvoiceos.stop.response", # common_query framework reporting nothing to stop - "ovos.common_play.stop.response", # OCP framework reporting nothing to stop - f"{self.skill_id}.stop.response", # skill reporting nothing to stop - - # sanity check in test skill that method was indeed called - "speak", # "utterance":"old stop called" - - f"{self.new_skill_id}.stop.response", # skill reporting it stopped - - "ovos.utterance.handled", - - ] - - wait_for_n_messages(len(expected_messages)) - - mtypes = [m.msg_type for m in messages] - for m in expected_messages: - self.assertTrue(m in mtypes) - - for m in messages: - # sanity check stop triggered - if m.msg_type == "speak": - self.assertEqual(m.data["utterance"], "old stop called") - - messages = [] - - ######################################## - # STEP 2 - # get a skill in active list - utt = Message("recognizer_loop:utterance", - {"utterances": ["new world"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - # skill selected - f"{self.new_skill_id}.activate", - f"{self.new_skill_id}:NewWorldIntent", - "mycroft.skill.handler.start", - # skill code executing - "speak", - "mycroft.skill.handler.complete", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # sanity check correct intent triggered - - for m in messages: - # sanity check stop triggered - if m.msg_type == "speak": - self.assertEqual(m.data["utterance"], "hello world") - - # test that active skills list has been updated - sess = Session.deserialize(messages[-1].context["session"]) - self.assertEqual(sess.active_skills[0][0], self.new_skill_id) - - messages = [] - - ######################################## - # STEP 3 - # we got active skills - # stop should now go over active skills list - # reports success - utt = Message("recognizer_loop:utterance", - {"utterances": ["stop"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - - # stop_high - f"{self.new_skill_id}.stop.ping", # check if active skill wants to stop - "skill.stop.pong", # "can_handle":true - f"{self.new_skill_id}.stop", # skill specific stop trigger - - # test session specific stop was called - "speak", # "utterance":"stop 123" - f"{self.new_skill_id}.stop.response", # skill reports it stopped (new style), - - "mycroft.skills.abort_question", # core kills any ongoing get_response - "ovos.skills.converse.force_timeout", # core kills any ongoing converse - "mycroft.audio.speech.stop", # core kills any ongoing TTS - f"{self.new_skill_id}.activate", # update timestamp of last interaction with skill - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - - wait_for_n_messages(len(expected_messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # confirm skill self.stop methods called - - for m in messages: - # sanity check stop triggered - if m.msg_type == "speak": - self.assertEqual(m.data["utterance"], "stop 123") - - # confirm "skill-new-stop" was the one that reported success - handler = messages[-6] - self.assertEqual(handler.msg_type, f"{self.new_skill_id}.stop.response") - self.assertEqual(handler.data["result"], True) - - messages = [] - - ######################################## - # STEP 4 - # skill already stopped - # reports failure - utt = Message("recognizer_loop:utterance", - {"utterances": ["stop"]}, - {"session": sess.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - - # stop_high - f"{self.new_skill_id}.stop.ping", # check if active skill wants to stop - "skill.stop.pong", # "can_handle":true - f"{self.new_skill_id}.stop", # skill specific stop trigger - "speak", # it's in the stop method even if it returns False! - f"{self.new_skill_id}.stop.response", # dont want to stop (new style) - - # rest of pipeline - # stop low - f"{self.new_skill_id}.stop.ping", - "skill.stop.pong", - f"{self.new_skill_id}.stop", # skill specific stop trigger - "speak", # it's in the stop method even if it returns False! - f"{self.new_skill_id}.stop.response", # dont want to stop (new style) - - # global stop fallback - "mycroft.stop", - "common_query.openvoiceos.stop.response", # dont want to stop - "ovos.common_play.stop.response", # dont want to stop - - f"{self.skill_id}.stop.response", # old style, never stops - "speak", # it's in the stop method even if it returns False! - f"{self.new_skill_id}.stop.response", # dont want to stop (new style) - - "ovos.utterance.handled" - ] - - wait_for_n_messages(len(expected_messages)) - - mtypes = [m.msg_type for m in messages] - for m in expected_messages: - self.assertTrue(m in mtypes) - - # confirm self.stop method called - for m in messages: - # sanity check stop triggered - if m.msg_type == "speak": - self.assertEqual(m.data["utterance"], "old stop called") - - messages = [] diff --git a/test/end2end/session/test_transformers.py b/test/end2end/session/test_transformers.py deleted file mode 100644 index 52584027943..00000000000 --- a/test/end2end/session/test_transformers.py +++ /dev/null @@ -1,166 +0,0 @@ -import time -from time import sleep -from unittest import TestCase, skip - -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session -from ovos_core.transformers import UtteranceTransformersService, MetadataTransformersService -from ..minicroft import get_minicroft - - -class TestTransformerPlugins(TestCase): - - def setUp(self): - self.skill_id = "skill-ovos-hello-world.openvoiceos" - self.core = get_minicroft(self.skill_id) - - def tearDown(self) -> None: - self.core.stop() - - def test_transformer_plugins(self): - # test plugins found - self.assertIn('ovos-utterance-plugin-cancel', - [k[0] for k in UtteranceTransformersService.find_plugins()], - UtteranceTransformersService.find_plugins()) - self.assertIn('ovos-metadata-test-plugin', - [k[0] for k in MetadataTransformersService.find_plugins()], - MetadataTransformersService.find_plugins()) - - def test_cancel(self): - - self.assertIn('ovos-utterance-plugin-cancel', self.core.intent_service.utterance_plugins.loaded_plugins) - - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.active_skills = [(self.skill_id, time.time())] - SessionManager.default_session.pipeline = [ - "stop_high", - "converse", - "padatious_high", - "adapt_high", - "fallback_high", - "stop_medium", - "adapt_medium", - "padatious_medium", - "adapt_low", - "common_qa", - "fallback_medium", - "fallback_low" - ] - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world , actually, cancel order"]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "mycroft.audio.play_sound", - "ovos.utterance.cancelled", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify the transformer metadata was injected - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") # session - self.assertEqual(m.context["cancel_word"], "cancel order") # cancel plugin - - # verify sound - self.assertEqual(messages[1].data["uri"], "snd/cancel.mp3") - - def test_meta(self): - self.assertNotIn('ovos-metadata-test-plugin', - self.core.intent_service.metadata_plugins.loaded_plugins) - self.core.load_metadata_transformers({"ovos-metadata-test-plugin": {}}) - self.assertIn('ovos-metadata-test-plugin', - self.core.intent_service.metadata_plugins.loaded_plugins, - self.core.intent_service.metadata_plugins.find_plugins()) - - SessionManager.sessions = {} - SessionManager.default_session = SessionManager.sessions["default"] = Session("default") - SessionManager.default_session.lang = "en-US" - SessionManager.default_session.active_skills = [(self.skill_id, time.time())] - SessionManager.default_session.pipeline = [ - "stop_high", - "converse", - "padatious_high", - "adapt_high", - "fallback_high", - "stop_medium", - "adapt_medium", - "padatious_medium", - "adapt_low", - "common_qa", - "fallback_medium", - "fallback_low" - ] - messages = [] - - def new_msg(msg): - nonlocal messages - m = Message.deserialize(msg) - if m.msg_type in ["ovos.skills.settings_changed"]: - return # skip these, only happen in 1st run - messages.append(m) - print(len(messages), msg) - - def wait_for_n_messages(n): - nonlocal messages - t = time.time() - while len(messages) < n: - sleep(0.1) - if time.time() - t > 10: - raise RuntimeError("did not get the number of expected messages under 10 seconds") - - self.core.bus.on("message", new_msg) - - utt = Message("recognizer_loop:utterance", - {"utterances": ["hello world , actually, cancel order"]}, - {"session": SessionManager.default_session.serialize()}) - self.core.bus.emit(utt) - - # confirm all expected messages are sent - expected_messages = [ - "recognizer_loop:utterance", - "mycroft.audio.play_sound", - "ovos.utterance.cancelled", - "ovos.utterance.handled" # handle_utterance returned (intent service) - ] - wait_for_n_messages(len(expected_messages)) - - self.assertEqual(len(expected_messages), len(messages)) - - for idx, m in enumerate(messages): - self.assertEqual(m.msg_type, expected_messages[idx]) - - # verify the transformer metadata was injected - for m in messages[1:]: - self.assertEqual(m.context["session"]["session_id"], "default") # session - self.assertEqual(m.context["metadata"], "test") # metadata plugin diff --git a/test/end2end/skill-converse_test/__init__.py b/test/end2end/skill-converse_test/__init__.py deleted file mode 100644 index 6241c34e5df..00000000000 --- a/test/end2end/skill-converse_test/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -from time import sleep - -from ovos_workshop.decorators import killable_intent, intent_handler -from ovos_workshop.skills.ovos import OVOSSkill - - -class TestAbortSkill(OVOSSkill): - """ - send "mycroft.skills.abort_question" and confirm only get_response is aborted - send "mycroft.skills.abort_execution" and confirm the full intent is aborted, except intent3 - send "my.own.abort.msg" and confirm intent3 is aborted - say "stop" and confirm all intents are aborted - """ - - def initialize(self): - self.stop_called = False - self._converse = False - self._converse_deactivate = False - self.items = [] - self.bus.on("test_activate", self.do_activate) - self.bus.on("test_deactivate", self.do_deactivate) - self.bus.on("converse_deactivate", self.do_deactivate_converse) - - def do_deactivate_converse(self, message): - self._converse_deactivate = True - - def do_activate(self, message): - self.activate() - - def do_deactivate(self, message): - self.deactivate() - - @intent_handler("deactivate.intent") - def handle_deactivated(self, message): - self.deactivate() - self.speak("deactivated") - - @intent_handler("converse_on.intent") - def handle_converse_on(self, message): - self._converse = True - self.speak("on") - - @intent_handler("converse_off.intent") - def handle_converse_off(self, message): - self._converse = False - self.speak("off") - - def handle_intent_aborted(self): - self.speak("I am dead") - - @intent_handler("test_get_response.intent") - def handle_test_get_response(self, message): - ans = self.get_response("get", num_retries=1) - self.speak(ans or "ERROR") - - @intent_handler("test_get_response3.intent") - def handle_test_get_response3(self, message): - ans = self.get_response(num_retries=3) - self.speak(ans or "ERROR") - - @intent_handler("test_get_response_cascade.intent") - def handle_test_get_response_cascade(self, message): - quit = False - self.items = [] - self.speak("give me items", wait=True) - while not quit: - response = self.get_response(num_retries=0) - if response is None: - quit = True - else: - self.items.append(response) - self.bus.emit(message.forward("skill_items", {"items": self.items})) - - @killable_intent(callback=handle_intent_aborted) - @intent_handler("test.intent") - def handle_test_abort_intent(self, message): - self.stop_called = False - self.my_special_var = "changed" - while True: - sleep(1) - self.speak("still here") - - @intent_handler("test2.intent") - @killable_intent(callback=handle_intent_aborted) - def handle_test_get_response_intent(self, message): - self.stop_called = False - self.my_special_var = "CHANGED" - ans = self.get_response("question", num_retries=99999) - self.log.debug("get_response returned: " + str(ans)) - if ans is None: - self.speak("question aborted") - - @killable_intent(msg="my.own.abort.msg", callback=handle_intent_aborted) - @intent_handler("test3.intent") - def handle_test_msg_intent(self, message): - self.stop_called = False - if self.my_special_var != "default": - self.speak("someone forgot to cleanup") - while True: - sleep(1) - self.speak("you can't abort me") - - def stop(self): - self.stop_called = True - - def converse(self, message): - if self._converse_deactivate: - self.deactivate() - self._converse_deactivate = False - return True - return self._converse diff --git a/test/end2end/skill-converse_test/locale/en-us/converse_off.intent b/test/end2end/skill-converse_test/locale/en-us/converse_off.intent deleted file mode 100644 index 54299a48fb3..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/converse_off.intent +++ /dev/null @@ -1 +0,0 @@ -no \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/converse_on.intent b/test/end2end/skill-converse_test/locale/en-us/converse_on.intent deleted file mode 100644 index 396a0ba2698..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/converse_on.intent +++ /dev/null @@ -1 +0,0 @@ -yes \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/deactivate.intent b/test/end2end/skill-converse_test/locale/en-us/deactivate.intent deleted file mode 100644 index 7a839444669..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/deactivate.intent +++ /dev/null @@ -1 +0,0 @@ -deactivate skill \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/get.dialog b/test/end2end/skill-converse_test/locale/en-us/get.dialog deleted file mode 100644 index 1dd99dc4e0a..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/get.dialog +++ /dev/null @@ -1 +0,0 @@ -give me an answer \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/question.dialog b/test/end2end/skill-converse_test/locale/en-us/question.dialog deleted file mode 100644 index f0fb83cc4f3..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/question.dialog +++ /dev/null @@ -1 +0,0 @@ -this is a question \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/test.intent b/test/end2end/skill-converse_test/locale/en-us/test.intent deleted file mode 100644 index 30d74d25844..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/test.intent +++ /dev/null @@ -1 +0,0 @@ -test \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/test2.intent b/test/end2end/skill-converse_test/locale/en-us/test2.intent deleted file mode 100644 index 5161aff4299..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/test2.intent +++ /dev/null @@ -1 +0,0 @@ -test again \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/test3.intent b/test/end2end/skill-converse_test/locale/en-us/test3.intent deleted file mode 100644 index 1fec3fd265b..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/test3.intent +++ /dev/null @@ -1 +0,0 @@ -one more test \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/test_get_response.intent b/test/end2end/skill-converse_test/locale/en-us/test_get_response.intent deleted file mode 100644 index 88cde3244be..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/test_get_response.intent +++ /dev/null @@ -1 +0,0 @@ -test get response \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/test_get_response3.intent b/test/end2end/skill-converse_test/locale/en-us/test_get_response3.intent deleted file mode 100644 index 484272aaa94..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/test_get_response3.intent +++ /dev/null @@ -1,2 +0,0 @@ -3 prompts -three prompts \ No newline at end of file diff --git a/test/end2end/skill-converse_test/locale/en-us/test_get_response_cascade.intent b/test/end2end/skill-converse_test/locale/en-us/test_get_response_cascade.intent deleted file mode 100644 index b8a7494fc50..00000000000 --- a/test/end2end/skill-converse_test/locale/en-us/test_get_response_cascade.intent +++ /dev/null @@ -1 +0,0 @@ -test get items \ No newline at end of file diff --git a/test/end2end/skill-converse_test/setup.py b/test/end2end/skill-converse_test/setup.py deleted file mode 100644 index b0e4fe1977f..00000000000 --- a/test/end2end/skill-converse_test/setup.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -from os import path, walk - -from setuptools import setup - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex") - base_dir = path.dirname(__file__) - package_data = ["skill.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -# skill_id=package_name:SkillClass -PLUGIN_ENTRY_POINT = 'ovos-tskill-abort.openvoiceos=ovos_tskill_abort:TestAbortSkill' - -setup( - # this is the package name that goes on pip - name='ovos-tskill-abort', - version='0.0.1', - description='this is a OVOS test skill for the killable_intents decorator', - url='https://github.com/OpenVoiceOS/skill-abort-test', - author='JarbasAi', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={"ovos_tskill_abort": ""}, - package_data={'ovos_tskill_abort': find_resource_files()}, - packages=['ovos_tskill_abort'], - include_package_data=True, - install_requires=["ovos-workshop"], - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-fake-fm/__init__.py b/test/end2end/skill-fake-fm/__init__.py deleted file mode 100644 index 79eb070d090..00000000000 --- a/test/end2end/skill-fake-fm/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from os.path import join, dirname - -from ovos_utils.ocp import MediaType, PlaybackType -from ovos_workshop.decorators.ocp import ocp_search -from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill - - -class FakeFMSkill(OVOSCommonPlaybackSkill): - - def __init__(self, *args, **kwargs): - super().__init__(supported_media = [MediaType.RADIO, - MediaType.GENERIC], - skill_icon=join(dirname(__file__), "ui", "fakefm.png"), - *args, **kwargs) - - @ocp_search() - def search_fakefm(self, phrase, media_type): - score = 30 - if "fake" in phrase: - score += 35 - if media_type == MediaType.RADIO: - score += 20 - else: - score -= 30 - - for i in range(5): - score = score + i - yield { - "match_confidence": score, - "media_type": MediaType.RADIO, - "uri": f"https://fake_{i}.mp3", - "playback": PlaybackType.AUDIO, - "image": f"https://fake_{i}.png", - "bg_image": f"https://fake_{i}.png", - "skill_icon": f"https://fakefm.png", - "title": f"fake station {i}", - "author": "FakeFM", - "length": 0 - } \ No newline at end of file diff --git a/test/end2end/skill-fake-fm/setup.py b/test/end2end/skill-fake-fm/setup.py deleted file mode 100755 index e8e602d24b7..00000000000 --- a/test/end2end/skill-fake-fm/setup.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -from os import walk, path - -from setuptools import setup - -URL = "https://github.com/OpenVoiceOS/skill-fake-fm" -SKILL_CLAZZ = "FakeFMSkill" # needs to match __init__.py class name - -# below derived from github url to ensure standard skill_id -SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") -SKILL_PKG = SKILL_NAME.lower().replace('-', '_') -PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' - - -# skill_id=package_name:SkillClass - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") - base_dir = path.dirname(__file__) - package_data = ["*.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -setup( - name="skill-fake-fm", - version="0.0.0", - long_description="test", - description='OVOS test plugin', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={SKILL_PKG: ""}, - package_data={SKILL_PKG: find_resource_files()}, - packages=[SKILL_PKG], - include_package_data=True, - install_requires=["ovos-workshop>=0.0.16a8"], - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-new-stop/__init__.py b/test/end2end/skill-new-stop/__init__.py deleted file mode 100644 index ef5a13408fd..00000000000 --- a/test/end2end/skill-new-stop/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from ovos_workshop.intents import IntentBuilder -from ovos_workshop.decorators import intent_handler -from ovos_workshop.skills import OVOSSkill -from ovos_bus_client.session import SessionManager, Session - - -class NewStopSkill(OVOSSkill): - - def initialize(self): - self.active = [] - - @intent_handler(IntentBuilder("NewWorldIntent").require("HelloWorldKeyword")) - def handle_hello_world_intent(self, message): - self.speak_dialog("hello.world") - sess = SessionManager.get(message) - self.active.append(sess.session_id) - - def stop_session(self, sess: Session): - if sess.session_id in self.active: - self.speak(f"stop {sess.session_id}") - self.active.remove(sess.session_id) - return True - return False - - def stop(self): - self.speak("old stop called") diff --git a/test/end2end/skill-new-stop/locale/en-us/vocab/HelloWorldKeyword.voc b/test/end2end/skill-new-stop/locale/en-us/vocab/HelloWorldKeyword.voc deleted file mode 100644 index 18091e85f31..00000000000 --- a/test/end2end/skill-new-stop/locale/en-us/vocab/HelloWorldKeyword.voc +++ /dev/null @@ -1 +0,0 @@ -new world \ No newline at end of file diff --git a/test/end2end/skill-new-stop/setup.py b/test/end2end/skill-new-stop/setup.py deleted file mode 100755 index a60431638ca..00000000000 --- a/test/end2end/skill-new-stop/setup.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -from os import walk, path - -from setuptools import setup - -URL = "https://github.com/OpenVoiceOS/skill-new-stop" -SKILL_CLAZZ = "NewStopSkill" # needs to match __init__.py class name - -# below derived from github url to ensure standard skill_id -SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") -SKILL_PKG = SKILL_NAME.lower().replace('-', '_') -PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' - - -# skill_id=package_name:SkillClass - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") - base_dir = path.dirname(__file__) - package_data = ["*.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -setup( - name="skill-new-stop", - version="0.0.0", - long_description="test", - description='OVOS test plugin', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={SKILL_PKG: ""}, - package_data={SKILL_PKG: find_resource_files()}, - packages=[SKILL_PKG], - include_package_data=True, - install_requires=["ovos-workshop>=0.0.16a8"], - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-old-stop/__init__.py b/test/end2end/skill-old-stop/__init__.py deleted file mode 100644 index 2ccbced0bb4..00000000000 --- a/test/end2end/skill-old-stop/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from ovos_workshop.intents import IntentBuilder -from ovos_workshop.decorators import intent_handler -from ovos_workshop.skills import OVOSSkill -from ovos_bus_client.session import SessionManager, Session - - -class OldStopSkill(OVOSSkill): - - def initialize(self): - self.active = False - - @intent_handler(IntentBuilder("OldWorldIntent").require("HelloWorldKeyword")) - def handle_hello_world_intent(self, message): - self.speak_dialog("hello.world") - self.active = True - - def stop(self): - if self.active: - self.speak("stop") - self.active = False - return True - return False diff --git a/test/end2end/skill-old-stop/locale/en-us/vocab/HelloWorldKeyword.voc b/test/end2end/skill-old-stop/locale/en-us/vocab/HelloWorldKeyword.voc deleted file mode 100644 index ba449ed4279..00000000000 --- a/test/end2end/skill-old-stop/locale/en-us/vocab/HelloWorldKeyword.voc +++ /dev/null @@ -1 +0,0 @@ -old world \ No newline at end of file diff --git a/test/end2end/skill-old-stop/setup.py b/test/end2end/skill-old-stop/setup.py deleted file mode 100755 index cf4491512d8..00000000000 --- a/test/end2end/skill-old-stop/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -from os import walk, path - -from setuptools import setup - -URL = "https://github.com/OpenVoiceOS/skill-old-stop" -SKILL_CLAZZ = "OldStopSkill" # needs to match __init__.py class name - -# below derived from github url to ensure standard skill_id -SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") -SKILL_PKG = SKILL_NAME.lower().replace('-', '_') -PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' - - -# skill_id=package_name:SkillClass - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") - base_dir = path.dirname(__file__) - package_data = ["*.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -setup( - name="skill-old-stop", - version="0.0.0", - long_description="test", - description='OVOS test plugin', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={SKILL_PKG: ""}, - package_data={SKILL_PKG: find_resource_files()}, - packages=[SKILL_PKG], - include_package_data=True, - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-ovos-fakewiki/__init__.py b/test/end2end/skill-ovos-fakewiki/__init__.py deleted file mode 100644 index 2ca23a25967..00000000000 --- a/test/end2end/skill-ovos-fakewiki/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from ovos_workshop.skills.common_query_skill import CommonQuerySkill, CQSMatchLevel - - -class UnWikiSkill(CommonQuerySkill): - - # common query integration - def CQS_match_query_phrase(self, utt): - response = "42" - return (utt, CQSMatchLevel.EXACT, response, - {'query': utt, 'answer': response}) - - def CQS_action(self, phrase, data): - """ If selected show gui """ - self.speak("selected") diff --git a/test/end2end/skill-ovos-fakewiki/setup.py b/test/end2end/skill-ovos-fakewiki/setup.py deleted file mode 100755 index e38e6177b5b..00000000000 --- a/test/end2end/skill-ovos-fakewiki/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -from setuptools import setup - -# skill_id=package_name:SkillClass -PLUGIN_ENTRY_POINT = 'ovos-skill-fakewiki.openvoiceos=ovos_skill_fakewiki:UnWikiSkill' - -setup( - # this is the package name that goes on pip - name='ovos-skill-fakewiki', - version='0.0.1', - description='this is a OVOS test skill for the common query framework', - url='https://github.com/OpenVoiceOS/ovos-core', - author='JarbasAi', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={"ovos_skill_fakewiki": ""}, - package_data={'ovos_skill_fakewiki': ['locale/*']}, - packages=['ovos_skill_fakewiki'], - include_package_data=True, - install_requires=["ovos-workshop"], - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-ovos-fallback-unknown/__init__.py b/test/end2end/skill-ovos-fallback-unknown/__init__.py deleted file mode 100644 index f3074adc0ee..00000000000 --- a/test/end2end/skill-ovos-fallback-unknown/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from ovos_workshop.decorators import fallback_handler -from ovos_workshop.skills.fallback import FallbackSkill - - -class UnknownSkill(FallbackSkill): - def initialize(self): - self._fallback_deactivate = False - self.add_event("fallback_deactivate", - self.do_deactivate_fallback) - - def do_deactivate_fallback(self, message): - self._fallback_deactivate = True - - @fallback_handler(priority=100) - def handle_fallback(self, message): - self.speak_dialog('unknown') - if self._fallback_deactivate: - self._fallback_deactivate = False - self.deactivate() - return True diff --git a/test/end2end/skill-ovos-fallback-unknown/locale/en-us/unknown.dialog b/test/end2end/skill-ovos-fallback-unknown/locale/en-us/unknown.dialog deleted file mode 100755 index 3f31d7bbc9a..00000000000 --- a/test/end2end/skill-ovos-fallback-unknown/locale/en-us/unknown.dialog +++ /dev/null @@ -1,9 +0,0 @@ -I'm sorry, I don't understand. -I don't know what that means. -I don't understand, but I'm learning new things everyday. -Sorry, I didn't catch that. -Sorry, I don't understand. -I don't understand. -I'm not sure I understood you. -You might have to say that a different way. -Please rephrase your request. diff --git a/test/end2end/skill-ovos-fallback-unknown/setup.py b/test/end2end/skill-ovos-fallback-unknown/setup.py deleted file mode 100755 index 21c97f9462d..00000000000 --- a/test/end2end/skill-ovos-fallback-unknown/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -from os import walk, path - -from setuptools import setup - -URL = "https://github.com/OpenVoiceOS/skill-ovos-fallback-unknown" -SKILL_CLAZZ = "UnknownSkill" # needs to match __init__.py class name -PYPI_NAME = "ovos-skill-fallback-unknown" # pip install PYPI_NAME - -# below derived from github url to ensure standard skill_id -SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") -SKILL_PKG = SKILL_NAME.lower().replace('-', '_') -PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' - - -# skill_id=package_name:SkillClass - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") - base_dir = path.dirname(__file__) - package_data = ["*.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -setup( - name=PYPI_NAME, - version="0.0.0", - package_dir={SKILL_PKG: ""}, - package_data={SKILL_PKG: find_resource_files()}, - packages=[SKILL_PKG], - include_package_data=True, - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-ovos-hello-world/MANIFEST.in b/test/end2end/skill-ovos-hello-world/MANIFEST.in deleted file mode 100644 index b9ecb5807a5..00000000000 --- a/test/end2end/skill-ovos-hello-world/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -recursive-include dialog * -recursive-include vocab * -recursive-include locale * -recursive-include res * -recursive-include ui * -include *.json -include *.txt \ No newline at end of file diff --git a/test/end2end/skill-ovos-hello-world/__init__.py b/test/end2end/skill-ovos-hello-world/__init__.py deleted file mode 100644 index b8df1a9e9fb..00000000000 --- a/test/end2end/skill-ovos-hello-world/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from ovos_workshop.intents import IntentBuilder -from ovos_workshop.decorators import intent_handler -from ovos_workshop.skills import OVOSSkill - - -class HelloWorldSkill(OVOSSkill): - - @intent_handler(IntentBuilder("HelloWorldIntent").require("HelloWorldKeyword")) - def handle_hello_world_intent(self, message): - self.speak_dialog("hello.world") diff --git a/test/end2end/skill-ovos-hello-world/locale/en-us/dialog/hello.world.dialog b/test/end2end/skill-ovos-hello-world/locale/en-us/dialog/hello.world.dialog deleted file mode 100644 index 811f098f322..00000000000 --- a/test/end2end/skill-ovos-hello-world/locale/en-us/dialog/hello.world.dialog +++ /dev/null @@ -1,3 +0,0 @@ -Hello world -Hello -Hi to you too diff --git a/test/end2end/skill-ovos-hello-world/locale/en-us/vocab/HelloWorldKeyword.voc b/test/end2end/skill-ovos-hello-world/locale/en-us/vocab/HelloWorldKeyword.voc deleted file mode 100644 index 5ffa264b919..00000000000 --- a/test/end2end/skill-ovos-hello-world/locale/en-us/vocab/HelloWorldKeyword.voc +++ /dev/null @@ -1,2 +0,0 @@ -hello world -greetings diff --git a/test/end2end/skill-ovos-hello-world/setup.py b/test/end2end/skill-ovos-hello-world/setup.py deleted file mode 100755 index ad2d35c4218..00000000000 --- a/test/end2end/skill-ovos-hello-world/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -from os import walk, path - -from setuptools import setup - -URL = "https://github.com/OpenVoiceOS/skill-ovos-hello-world" -SKILL_CLAZZ = "HelloWorldSkill" # needs to match __init__.py class name - -# below derived from github url to ensure standard skill_id -SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") -SKILL_PKG = SKILL_NAME.lower().replace('-', '_') -PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' - - -# skill_id=package_name:SkillClass - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") - base_dir = path.dirname(__file__) - package_data = ["*.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -setup( - name="ovos-skill-hello-world", - version="0.0.0", - long_description="test", - description='OVOS hello world skill plugin', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={SKILL_PKG: ""}, - package_data={SKILL_PKG: find_resource_files()}, - packages=[SKILL_PKG], - include_package_data=True, - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-ovos-schedule/MANIFEST.in b/test/end2end/skill-ovos-schedule/MANIFEST.in deleted file mode 100644 index b9ecb5807a5..00000000000 --- a/test/end2end/skill-ovos-schedule/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -recursive-include dialog * -recursive-include vocab * -recursive-include locale * -recursive-include res * -recursive-include ui * -include *.json -include *.txt \ No newline at end of file diff --git a/test/end2end/skill-ovos-schedule/__init__.py b/test/end2end/skill-ovos-schedule/__init__.py deleted file mode 100644 index 60fcb3ab2f3..00000000000 --- a/test/end2end/skill-ovos-schedule/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from ovos_workshop.intents import IntentBuilder -from ovos_workshop.decorators import intent_handler -from ovos_workshop.skills import OVOSSkill - - -class ScheduleSkill(OVOSSkill): - - def handle_event(self, message): - self.speak_dialog("trigger") - - @intent_handler(IntentBuilder("ScheduleIntent").require("Schedule")) - def handle_sched_intent(self, message): - self.speak_dialog("done") - self.schedule_event(self.handle_event, 3, name="my_event") diff --git a/test/end2end/skill-ovos-schedule/locale/en-us/dialog/done.dialog b/test/end2end/skill-ovos-schedule/locale/en-us/dialog/done.dialog deleted file mode 100644 index 3fc39363831..00000000000 --- a/test/end2end/skill-ovos-schedule/locale/en-us/dialog/done.dialog +++ /dev/null @@ -1 +0,0 @@ -it has been scheduled \ No newline at end of file diff --git a/test/end2end/skill-ovos-schedule/locale/en-us/dialog/trigger.dialog b/test/end2end/skill-ovos-schedule/locale/en-us/dialog/trigger.dialog deleted file mode 100644 index b711e1a1c8d..00000000000 --- a/test/end2end/skill-ovos-schedule/locale/en-us/dialog/trigger.dialog +++ /dev/null @@ -1 +0,0 @@ -this is the event triggering \ No newline at end of file diff --git a/test/end2end/skill-ovos-schedule/locale/en-us/vocab/Schedule.voc b/test/end2end/skill-ovos-schedule/locale/en-us/vocab/Schedule.voc deleted file mode 100644 index 5864a517f9a..00000000000 --- a/test/end2end/skill-ovos-schedule/locale/en-us/vocab/Schedule.voc +++ /dev/null @@ -1 +0,0 @@ -schedule event \ No newline at end of file diff --git a/test/end2end/skill-ovos-schedule/setup.py b/test/end2end/skill-ovos-schedule/setup.py deleted file mode 100755 index 19be23c9b5f..00000000000 --- a/test/end2end/skill-ovos-schedule/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -from os import walk, path - -from setuptools import setup - -URL = "https://github.com/OpenVoiceOS/skill-ovos-schedule" -SKILL_CLAZZ = "ScheduleSkill" # needs to match __init__.py class name - -# below derived from github url to ensure standard skill_id -SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") -SKILL_PKG = SKILL_NAME.lower().replace('-', '_') -PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' - - -# skill_id=package_name:SkillClass - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") - base_dir = path.dirname(__file__) - package_data = ["*.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -setup( - name="ovos-skill-schedule", - version="0.0.0", - long_description="test", - description='OVOS schedule skill plugin', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={SKILL_PKG: ""}, - package_data={SKILL_PKG: find_resource_files()}, - packages=[SKILL_PKG], - include_package_data=True, - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/skill-ovos-slow-fallback/__init__.py b/test/end2end/skill-ovos-slow-fallback/__init__.py deleted file mode 100644 index 3b3da0b17b6..00000000000 --- a/test/end2end/skill-ovos-slow-fallback/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -import time - -from ovos_workshop.decorators import fallback_handler -from ovos_workshop.skills.fallback import FallbackSkill - - -class SlowFallbackSkill(FallbackSkill): - """test skill that would block - converse/fallback forever if not killed""" - - @fallback_handler(priority=20) - def handle_fallback(self, message): - while True: # busy skill - time.sleep(0.1) - return True - - def converse(self, message): - while True: # busy skill - time.sleep(0.1) - return True diff --git a/test/end2end/skill-ovos-slow-fallback/setup.py b/test/end2end/skill-ovos-slow-fallback/setup.py deleted file mode 100755 index 323eecd7a40..00000000000 --- a/test/end2end/skill-ovos-slow-fallback/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -from os import walk, path - -from setuptools import setup - -URL = "https://github.com/OpenVoiceOS/ovos-skill-slow-fallback" -SKILL_CLAZZ = "SlowFallbackSkill" # needs to match __init__.py class name -PYPI_NAME = "ovos-skill-slow-fallback" # pip install PYPI_NAME - -# below derived from github url to ensure standard skill_id -SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") -SKILL_PKG = SKILL_NAME.lower().replace('-', '_') -PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' - - -# skill_id=package_name:SkillClass - - -def find_resource_files(): - resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") - base_dir = path.dirname(__file__) - package_data = ["*.json"] - for res in resource_base_dirs: - if path.isdir(path.join(base_dir, res)): - for (directory, _, files) in walk(path.join(base_dir, res)): - if files: - package_data.append( - path.join(directory.replace(base_dir, "").lstrip('/'), - '*')) - return package_data - - -setup( - name=PYPI_NAME, - version="0.0.0", - package_dir={SKILL_PKG: ""}, - package_data={SKILL_PKG: find_resource_files()}, - packages=[SKILL_PKG], - include_package_data=True, - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/end2end/test_helloworld.py b/test/end2end/test_helloworld.py new file mode 100644 index 00000000000..b99caeebf38 --- /dev/null +++ b/test/end2end/test_helloworld.py @@ -0,0 +1,243 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session + +from ovoscope import End2EndTest + + +class TestAdaptIntent(TestCase): + + def test_adapt_match(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["adapt_high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message(f"{skill_id}.activate", + data={}, + context={"skill_id": skill_id}), + Message(f"{skill_id}:HelloWorldIntent", + data={"utterance": "hello world", "lang": "en-US"}, + context={"skill_id": skill_id}), + Message("mycroft.skill.handler.start", + data={"name": "HelloWorldSkill.handle_hello_world_intent"}, + context={"skill_id": skill_id}), + Message("speak", + data={"utterance": "Hello world", + "lang": "en-US", + "expect_response": False, + "meta": { + "dialog": "hello.world", + "data": {}, + "skill": skill_id + }}, + context={"skill_id": skill_id}), + Message("mycroft.skill.handler.complete", + data={"name": "HelloWorldSkill.handle_hello_world_intent"}, + context={"skill_id": skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_skill_blacklist(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["adapt_high"] + session.blacklisted_skills = [skill_id] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_intent_blacklist(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["adapt_high"] + session.blacklisted_intents = [f"{skill_id}:HelloWorldIntent"] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_padatious_no_match(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["padatious_high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + +class TestPadatiousIntent(TestCase): + + def test_padatious_match(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["padatious_high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message(f"{skill_id}.activate", + data={}, + context={"skill_id": skill_id}), + Message(f"{skill_id}:Greetings.intent", + data={"utterance": "good morning", "lang": "en-US"}, + context={"skill_id": skill_id}), + Message("mycroft.skill.handler.start", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": skill_id}), + Message("speak", + data={"lang": "en-US", + "expect_response": False, + "meta": { + "dialog": "hello", + "data": {}, + "skill": skill_id + }}, + context={"skill_id": skill_id}), + Message("mycroft.skill.handler.complete", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_skill_blacklist(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["padatious_high"] + session.blacklisted_skills = [skill_id] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_intent_blacklist(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["padatious_high"] + session.blacklisted_intents = [f"{skill_id}:Greetings.intent"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_adapt_no_match(self): + skill_id = "ovos-skill-hello-world.openvoiceos" + session = Session("123") + session.pipeline = ["adapt_high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[skill_id], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) diff --git a/test/end2end/test_no_skills.py b/test/end2end/test_no_skills.py new file mode 100644 index 00000000000..f0404c574dc --- /dev/null +++ b/test/end2end/test_no_skills.py @@ -0,0 +1,48 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message + +from ovoscope import End2EndTest + + +class TestNoSkills(TestCase): + + def test_complete_failure(self): + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"]}) + + test = End2EndTest( + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}), + ] + ) + + test.execute() + + def test_routing(self): + # this test will validate source and destination are handled properly + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"]}, + {"source": "A", "destination": "B"}) + + test = End2EndTest( + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}), + ] + ) + + test.execute() diff --git a/test/integrationtests/__init__.py b/test/integrationtests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/integrationtests/common_query/__init__.py b/test/integrationtests/common_query/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/integrationtests/common_query/ovos_tskill_fakewiki/__init__.py b/test/integrationtests/common_query/ovos_tskill_fakewiki/__init__.py deleted file mode 100644 index ca8fee4af70..00000000000 --- a/test/integrationtests/common_query/ovos_tskill_fakewiki/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -from ovos_adapt.intent import IntentBuilder - -from ovos_workshop.skills.common_query_skill import CommonQuerySkill, CQSMatchLevel -from ovos_workshop.decorators import intent_handler - - -class FakeWikiSkill(CommonQuerySkill): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.displayed = False - self.idx = 0 - self.results = [] - - # explicit intents - @intent_handler("search_fakewiki.intent") - def handle_search(self, message): - query = message.data["query"] - self.ask_the_wiki(query) - if self.results: - self.speak_result() - else: - self.speak_dialog("no_answer") - - @intent_handler(IntentBuilder("FakeWikiMore").require("More"). - require("FakeWikiKnows")) - def handle_tell_more(self, message): - """ Follow up query handler, "tell me more".""" - self.speak_result() - - def speak_result(self): - if self.idx + 1 > len(self.results): - self.speak_dialog("thats all") - self.remove_context("FakeWikiKnows") - self.idx = 0 - else: - self.display_fakewiki() - ans = self.results[self.idx] - self.speak(ans) - self.idx += 1 - - # common query integration - def CQS_match_query_phrase(self, utt): - self.log.debug("FakeWiki query: " + utt) - response = self.ask_the_wiki(utt)[0] - self.idx += 1 # spoken by common query framework - return (utt, CQSMatchLevel.GENERAL, response, - {'query': utt, 'answer': response}) - - def CQS_action(self, phrase, data): - """ If selected show gui """ - self.display_fakewiki() - - # fakewiki integration - def ask_the_wiki(self, query): - # context for follow up questions - self.set_context("FakeWikiKnows", query) - self.idx = 0 - self.results = ["answer 1", "answer 2"] - return self.results - - def display_fakewiki(self): - self.displayed = True - - -def create_skill(): - return FakeWikiSkill() diff --git a/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/More.voc b/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/More.voc deleted file mode 100644 index b484c505afc..00000000000 --- a/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/More.voc +++ /dev/null @@ -1,4 +0,0 @@ -know more -tell me more -tell more -continue \ No newline at end of file diff --git a/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/no_answer.dialog b/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/no_answer.dialog deleted file mode 100644 index 7166625bef0..00000000000 --- a/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/no_answer.dialog +++ /dev/null @@ -1 +0,0 @@ -the archives are incomplete \ No newline at end of file diff --git a/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/search_fakewiki.intent b/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/search_fakewiki.intent deleted file mode 100644 index da62a296df6..00000000000 --- a/test/integrationtests/common_query/ovos_tskill_fakewiki/locale/en-us/search_fakewiki.intent +++ /dev/null @@ -1,9 +0,0 @@ -search wiki for {query} -search wiki for {query} -ask the wiki about {query} -ask the wiki about {query} -what does wiki say about {query} -what does wiki say about {query} -ask the wiki {query} -search the wiki for {query} -what does the wiki say about {query} \ No newline at end of file diff --git a/test/integrationtests/common_query/ovos_tskill_fakewiki/setup.py b/test/integrationtests/common_query/ovos_tskill_fakewiki/setup.py deleted file mode 100755 index fac8fab9e72..00000000000 --- a/test/integrationtests/common_query/ovos_tskill_fakewiki/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -from setuptools import setup - -# skill_id=package_name:SkillClass -PLUGIN_ENTRY_POINT = 'ovos-tskill-fakewiki.openvoiceos=ovos_tskill_fakewiki:FakeWikiSkill' - -setup( - # this is the package name that goes on pip - name='ovos-tskill-fakewiki', - version='0.0.1', - description='this is a OVOS test skill for the common query framework', - url='https://github.com/OpenVoiceOS/ovos-core', - author='JarbasAi', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={"ovos_tskill_fakewiki": ""}, - package_data={'ovos_tskill_fakewiki': ['locale/*']}, - packages=['ovos_tskill_fakewiki'], - include_package_data=True, - install_requires=["ovos-workshop"], - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/integrationtests/common_query/test_continuous_dialog.py b/test/integrationtests/common_query/test_continuous_dialog.py deleted file mode 100644 index b8d25ba2b4e..00000000000 --- a/test/integrationtests/common_query/test_continuous_dialog.py +++ /dev/null @@ -1,82 +0,0 @@ -import json -import unittest - -from ovos_tskill_fakewiki import FakeWikiSkill -from ovos_utils.fakebus import FakeBus, FakeMessage as Message - - -class TestDialog(unittest.TestCase): - def setUp(self): - self.bus = FakeBus() - self.bus.emitted_msgs = [] - - def get_msg(msg): - m = json.loads(msg) - if "session" in m.get("context", {}): - m["context"].pop("session") # simplify tests - self.bus.emitted_msgs.append(m) - - self.bus.on("message", get_msg) - - self.skill = FakeWikiSkill() - self.skill._startup(self.bus, "wiki.test") - - self.skill.has_context = False - - def set_context(message): - self.skill.has_context = True - - def unset_context(message): - self.skill.has_context = False - - self.bus.on('add_context', set_context) - self.bus.on('remove_context', unset_context) - - def test_continuous_dialog(self): - self.bus.emitted_msgs = [] - - # "ask the wiki X" - self.assertFalse(self.skill.has_context) - self.skill.handle_search(Message("search_fakewiki.intent", - {"query": "what is the speed of light"})) - - self.assertEqual(self.bus.emitted_msgs[0], - {'context': {'skill_id': 'wiki.test'}, - 'data': {'context': 'wiki_testFakeWikiKnows', - 'origin': '', - 'word': 'what is the speed of light'}, - 'type': 'add_context'}) - self.assertEqual(self.bus.emitted_msgs[-1], - {'context': {'skill_id': 'wiki.test'}, - 'data': {'expect_response': False, - 'lang': 'en-US', - 'meta': {'skill': 'wiki.test'}, - 'utterance': 'answer 1'}, - 'type': 'speak'}) - - # "tell me more" - self.assertTrue(self.skill.has_context) - self.skill.handle_tell_more(Message("FakeWikiMore")) - - self.assertEqual(self.bus.emitted_msgs[-1], - {'context': {'skill_id': 'wiki.test'}, - 'data': {'expect_response': False, - 'lang': 'en-US', - 'meta': {'skill': 'wiki.test'}, - 'utterance': 'answer 2'}, - 'type': 'speak'}) - self.assertTrue(self.skill.has_context) - - # "tell me more" - no more data dialog - self.skill.handle_tell_more(Message("FakeWikiMore")) - - self.assertEqual(self.bus.emitted_msgs[-2]["type"], "speak") - self.assertEqual(self.bus.emitted_msgs[-2]["data"]["meta"], - {'skill': 'wiki.test'}) - - # removal of context to disable "tell me more" - self.assertEqual(self.bus.emitted_msgs[-1], - {'context': {'skill_id': 'wiki.test'}, - 'data': {'context': 'wiki_testFakeWikiKnows'}, - 'type': 'remove_context'}) - self.assertFalse(self.skill.has_context) diff --git a/test/integrationtests/common_query/test_skill.py b/test/integrationtests/common_query/test_skill.py deleted file mode 100644 index bd3f81fa5b8..00000000000 --- a/test/integrationtests/common_query/test_skill.py +++ /dev/null @@ -1,73 +0,0 @@ -import json -import unittest - -from ovos_utils.fakebus import FakeBus -from ovos_tskill_fakewiki import FakeWikiSkill -from ovos_workshop.skills.common_query_skill import CommonQuerySkill - - -class TestSkill(unittest.TestCase): - def setUp(self): - self.bus = FakeBus() - self.bus.emitted_msgs = [] - - def get_msg(msg): - self.bus.emitted_msgs.append(json.loads(msg)) - - self.bus.on("message", get_msg) - - self.skill = FakeWikiSkill() - self.skill._startup(self.bus, "wiki.test") - - def test_skill_id(self): - self.assertEqual(self.skill.skill_id, "wiki.test") - # if running in ovos-core every message will have the skill_id in context - for msg in self.bus.emitted_msgs: - self.assertEqual(msg["context"]["skill_id"], "wiki.test") - - def test_intent_register(self): - adapt_ents = ["wiki_testMore"] # why are you different :( - adapt_intents = ["wiki.test:FakeWikiMore"] - padatious_intents = ["wiki.test:search_fakewiki.intent"] - for msg in self.bus.emitted_msgs: - if msg["type"] == "register_vocab": - self.assertTrue(msg["data"]["entity_type"] in adapt_ents) - elif msg["type"] == "register_intent": - self.assertTrue(msg["data"]["name"] in adapt_intents) - elif msg["type"] == "padatious:register_intent": - self.assertTrue(msg["data"]["name"] in padatious_intents) - - def test_registered_events(self): - registered_events = [e[0] for e in self.skill.events] - - # common query event handlers - self.assertTrue(isinstance(self.skill, CommonQuerySkill)) - common_query = ['question:action', - 'question:query'] - for event in common_query: - self.assertTrue(event in registered_events) - - # intent events - intent_triggers = [f"{self.skill.skill_id}:FakeWikiMore", - f"{self.skill.skill_id}:search_fakewiki.intent"] - for event in intent_triggers: - self.assertTrue(event in registered_events) - - # base skill class events shared with mycroft-core - default_skill = ["mycroft.skill.enable_intent", - "mycroft.skill.disable_intent", - "mycroft.skill.set_cross_context", - "mycroft.skill.remove_cross_context", - "intent.service.skills.deactivated", - "intent.service.skills.activated", - "mycroft.skills.settings.changed"] - for event in default_skill: - self.assertTrue(event in registered_events) - - # base skill class events exclusive to ovos-core - default_ovos = [f"{self.skill.skill_id}.converse.ping", - f"{self.skill.skill_id}.converse.request", - f"{self.skill.skill_id}.activate", - f"{self.skill.skill_id}.deactivate"] - for event in default_ovos: - self.assertTrue(event in registered_events) diff --git a/test/integrationtests/ovos_tskill_abort/__init__.py b/test/integrationtests/ovos_tskill_abort/__init__.py deleted file mode 100644 index f18c11d81f4..00000000000 --- a/test/integrationtests/ovos_tskill_abort/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -from ovos_workshop.decorators import killable_intent -from ovos_workshop.skills.ovos import OVOSSkill -from ovos_workshop.decorators import intent_handler -from time import sleep - - -class TestAbortSkill(OVOSSkill): - """ - send "mycroft.skills.abort_question" and confirm only get_response is aborted - send "mycroft.skills.abort_execution" and confirm the full intent is aborted, except intent3 - send "my.own.abort.msg" and confirm intent3 is aborted - say "stop" and confirm all intents are aborted - """ - def __init__(self, *args, **kwargs): - super(TestAbortSkill, self).__init__(*args, **kwargs) - self.my_special_var = "default" - self.stop_called = False - - def handle_intent_aborted(self): - self.speak("I am dead") - # handle any cleanup the skill might need, since intent was killed - # at an arbitrary place of code execution some variables etc. might - # end up in unexpected states - self.my_special_var = "default" - - @killable_intent(callback=handle_intent_aborted) - @intent_handler("test.intent") - def handle_test_abort_intent(self, message): - self.stop_called = False - self.my_special_var = "changed" - while True: - sleep(1) - self.speak("still here") - - @intent_handler("test2.intent") - @killable_intent(callback=handle_intent_aborted) - def handle_test_get_response_intent(self, message): - self.stop_called = False - self.my_special_var = "CHANGED" - ans = self.get_response("question", num_retries=99999) - self.log.debug("get_response returned: " + str(ans)) - if ans is None: - self.speak("question aborted") - - @killable_intent(msg="my.own.abort.msg", callback=handle_intent_aborted) - @intent_handler("test3.intent") - def handle_test_msg_intent(self, message): - self.stop_called = False - if self.my_special_var != "default": - self.speak("someone forgot to cleanup") - while True: - sleep(1) - self.speak("you can't abort me") - - def stop(self): - self.stop_called = True - - -def create_skill(): - return TestAbortSkill() diff --git a/test/integrationtests/ovos_tskill_abort/locale/en-us/question.dialog b/test/integrationtests/ovos_tskill_abort/locale/en-us/question.dialog deleted file mode 100644 index f0fb83cc4f3..00000000000 --- a/test/integrationtests/ovos_tskill_abort/locale/en-us/question.dialog +++ /dev/null @@ -1 +0,0 @@ -this is a question \ No newline at end of file diff --git a/test/integrationtests/ovos_tskill_abort/locale/en-us/test.intent b/test/integrationtests/ovos_tskill_abort/locale/en-us/test.intent deleted file mode 100644 index 30d74d25844..00000000000 --- a/test/integrationtests/ovos_tskill_abort/locale/en-us/test.intent +++ /dev/null @@ -1 +0,0 @@ -test \ No newline at end of file diff --git a/test/integrationtests/ovos_tskill_abort/locale/en-us/test2.intent b/test/integrationtests/ovos_tskill_abort/locale/en-us/test2.intent deleted file mode 100644 index 5161aff4299..00000000000 --- a/test/integrationtests/ovos_tskill_abort/locale/en-us/test2.intent +++ /dev/null @@ -1 +0,0 @@ -test again \ No newline at end of file diff --git a/test/integrationtests/ovos_tskill_abort/locale/en-us/test3.intent b/test/integrationtests/ovos_tskill_abort/locale/en-us/test3.intent deleted file mode 100644 index 1fec3fd265b..00000000000 --- a/test/integrationtests/ovos_tskill_abort/locale/en-us/test3.intent +++ /dev/null @@ -1 +0,0 @@ -one more test \ No newline at end of file diff --git a/test/integrationtests/ovos_tskill_abort/readme.md b/test/integrationtests/ovos_tskill_abort/readme.md deleted file mode 100644 index add1af272c6..00000000000 --- a/test/integrationtests/ovos_tskill_abort/readme.md +++ /dev/null @@ -1 +0,0 @@ -skill for testing https://github.com/OpenVoiceOS/ovos_utils/pull/34 \ No newline at end of file diff --git a/test/integrationtests/ovos_tskill_abort/setup.py b/test/integrationtests/ovos_tskill_abort/setup.py deleted file mode 100755 index fb2af910514..00000000000 --- a/test/integrationtests/ovos_tskill_abort/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -from setuptools import setup - -# skill_id=package_name:SkillClass -PLUGIN_ENTRY_POINT = 'ovos-tskill-abort.openvoiceos=ovos_tskill_abort:TestAbortSkill' - -setup( - # this is the package name that goes on pip - name='ovos-tskill-abort', - version='0.0.1', - description='this is a OVOS test skill for the killable_intents decorator', - url='https://github.com/OpenVoiceOS/skill-abort-test', - author='JarbasAi', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - package_dir={"ovos_tskill_abort": ""}, - package_data={'ovos_tskill_abort': ['locale/*']}, - packages=['ovos_tskill_abort'], - include_package_data=True, - install_requires=["ovos-workshop"], - keywords='ovos skill plugin', - entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} -) diff --git a/test/integrationtests/test_workshop.py b/test/integrationtests/test_workshop.py deleted file mode 100644 index af20b99f298..00000000000 --- a/test/integrationtests/test_workshop.py +++ /dev/null @@ -1,275 +0,0 @@ -import json -import unittest -from os.path import dirname -from time import sleep -from ovos_workshop.skill_launcher import SkillLoader -from ovos_workshop.skills.ovos import OVOSSkill -from ovos_utils.fakebus import FakeBus, FakeMessage as Message - -# tests taken from ovos_workshop - - -class TestSkill(unittest.TestCase): - def setUp(self): - self.bus = FakeBus() - self.bus.emitted_msgs = [] - - def get_msg(msg): - msg = json.loads(msg) - self.bus.emitted_msgs.append(msg) - - self.bus.on("message", get_msg) - - self.skill = SkillLoader(self.bus, f"{dirname(__file__)}/ovos_tskill_abort") - self.skill.skill_id = "abort.test" - self.bus.emitted_msgs = [] - - self.skill.load() - - def test_skill_id(self): - self.assertTrue(isinstance(self.skill.instance, OVOSSkill)) - - self.assertEqual(self.skill.skill_id, "abort.test") - # if running in ovos-core every message will have the skill_id in context - for msg in self.bus.emitted_msgs: - if msg["type"] == 'mycroft.skills.loaded': # emitted by SkillLoader, not by skill - continue - self.assertEqual(msg["context"]["skill_id"], "abort.test") - - def test_intent_register(self): - padatious_intents = ["abort.test:test.intent", - "abort.test:test2.intent", - "abort.test:test3.intent"] - for msg in self.bus.emitted_msgs: - if msg["type"] == "padatious:register_intent": - self.assertTrue(msg["data"]["name"] in padatious_intents) - - def test_registered_events(self): - registered_events = [e[0] for e in self.skill.instance.events] - - # intent events - intent_triggers = [f"{self.skill.skill_id}:test.intent", - f"{self.skill.skill_id}:test2.intent", - f"{self.skill.skill_id}:test3.intent" - ] - for event in intent_triggers: - self.assertTrue(event in registered_events) - - # base skill class events shared with mycroft-core - default_skill = ["mycroft.skill.enable_intent", - "mycroft.skill.disable_intent", - "mycroft.skill.set_cross_context", - "mycroft.skill.remove_cross_context", - "mycroft.skills.settings.changed"] - for event in default_skill: - self.assertTrue(event in registered_events) - - # base skill class events exclusive to ovos-core - default_ovos = [f"{self.skill.skill_id}.converse.ping", - f"{self.skill.skill_id}.converse.request", - "intent.service.skills.activated", - "intent.service.skills.deactivated", - f"{self.skill.skill_id}.activate", - f"{self.skill.skill_id}.deactivate"] - for event in default_ovos: - self.assertTrue(event in registered_events) - - def tearDown(self) -> None: - self.skill.unload() - - -class TestKillableIntents(unittest.TestCase): - def setUp(self): - self.bus = FakeBus() - self.bus.emitted_msgs = [] - - def get_msg(msg): - m = json.loads(msg) - m.pop("context") - self.bus.emitted_msgs.append(m) - - self.bus.on("message", get_msg) - - self.skill = SkillLoader(self.bus, f"{dirname(__file__)}/ovos_tskill_abort") - self.skill.skill_id = "abort.test" - self.skill.load() - - def test_skills_abort_event(self): - self.bus.emitted_msgs = [] - # skill will enter a infinite loop unless aborted - self.assertTrue(self.skill.instance.my_special_var == "default") - self.bus.emit(Message(f"{self.skill.skill_id}:test.intent")) - sleep(2) - # check that intent triggered - start_msg = {'type': 'mycroft.skill.handler.start', - 'data': {'name': 'TestAbortSkill.handle_test_abort_intent'}} - speak_msg = {'type': 'speak', - 'data': {'utterance': 'still here', 'expect_response': False, - 'meta': {'skill': 'abort.test'}, - 'lang': 'en-US'}} - self.assertIn(start_msg, self.bus.emitted_msgs) - self.assertIn(speak_msg, self.bus.emitted_msgs) - self.assertTrue(self.skill.instance.my_special_var == "changed") - - # check that intent reacts to mycroft.skills.abort_execution - # eg, gui can emit this event if some option was selected - # on screen to abort the current voice interaction - self.bus.emitted_msgs = [] - self.bus.emit(Message(f"mycroft.skills.abort_execution")) - sleep(2) - - # check that stop method was called - self.assertTrue(self.skill.instance.stop_called) - - # check that TTS stop message was emmited - tts_stop = {'type': 'mycroft.audio.speech.stop', 'data': {}} - self.assertIn(tts_stop, self.bus.emitted_msgs) - - # check that cleanup callback was called - speak_msg = {'type': 'speak', - 'data': {'utterance': 'I am dead', 'expect_response': False, - 'meta': {'skill': 'abort.test'}, - 'lang': 'en-US'}} - self.assertIn(speak_msg, self.bus.emitted_msgs) - self.assertTrue(self.skill.instance.my_special_var == "default") - - # check that we are not getting speak messages anymore - self.bus.emitted_msgs = [] - sleep(2) - self.assertTrue(self.bus.emitted_msgs == []) - - def test_skill_stop(self): - self.bus.emitted_msgs = [] - # skill will enter a infinite loop unless aborted - self.assertTrue(self.skill.instance.my_special_var == "default") - self.bus.emit(Message(f"{self.skill.skill_id}:test.intent")) - sleep(2) - # check that intent triggered - start_msg = {'type': 'mycroft.skill.handler.start', - 'data': {'name': 'TestAbortSkill.handle_test_abort_intent'}} - speak_msg = {'type': 'speak', - 'data': {'utterance': 'still here', 'expect_response': False, - 'meta': {'skill': 'abort.test'}, 'lang': 'en-US'}} - self.assertIn(start_msg, self.bus.emitted_msgs) - self.assertIn(speak_msg, self.bus.emitted_msgs) - self.assertTrue(self.skill.instance.my_special_var == "changed") - - # check that intent reacts to skill specific stop message - # this is also emitted on mycroft.stop if using OvosSkill class - self.bus.emitted_msgs = [] - self.bus.emit(Message(f"{self.skill.skill_id}.stop")) - sleep(2) - - # check that stop method was called - self.assertTrue(self.skill.instance.stop_called) - - # check that TTS stop message was emmited - tts_stop = {'type': 'mycroft.audio.speech.stop', 'data': {}} - self.assertIn(tts_stop, self.bus.emitted_msgs) - - # check that cleanup callback was called - speak_msg = {'type': 'speak', - 'data': {'utterance': 'I am dead', 'expect_response': False, - 'meta': {'skill': 'abort.test'}, - 'lang': 'en-US'}} - - self.assertIn(speak_msg, self.bus.emitted_msgs) - self.assertTrue(self.skill.instance.my_special_var == "default") - - # check that we are not getting speak messages anymore - self.bus.emitted_msgs = [] - sleep(2) - self.assertTrue(self.bus.emitted_msgs == []) - - def test_get_response(self): - """ send "mycroft.skills.abort_question" and - confirm only get_response is aborted, speech after is still spoken""" - self.bus.emitted_msgs = [] - # skill will enter a infinite loop unless aborted - self.bus.emit(Message(f"{self.skill.skill_id}:test2.intent")) - sleep(1) # mock wait=True in speak_dialog - self.bus.emit(Message("recognizer_loop:audio_output_end")) - sleep(1) - - # check that intent triggered - start_msg = {'type': 'mycroft.skill.handler.start', - 'data': {'name': 'TestAbortSkill.handle_test_get_response_intent'}} - speak_msg = {'type': 'speak', - 'data': {'utterance': 'this is a question', - 'expect_response': True, - 'meta': {'dialog': 'question', 'data': {}, 'skill': 'abort.test'}, - 'lang': 'en-US'}} - activate_msg = {'type': 'intent.service.skills.activate', 'data': {'skill_id': 'abort.test'}} - - self.assertIn(start_msg, self.bus.emitted_msgs) - self.assertIn(speak_msg, self.bus.emitted_msgs) - #self.assertIn(activate_msg, self.bus.emitted_msgs) - - # check that get_response loop is aborted - # but intent continues executing - self.bus.emitted_msgs = [] - self.bus.emit(Message(f"mycroft.skills.abort_question")) - sleep(1) - - # check that stop method was NOT called - self.assertFalse(self.skill.instance.stop_called) - - # check that speak message after get_response loop was spoken - speak_msg = {'type': 'speak', - 'data': {'utterance': 'question aborted', - 'expect_response': False, - 'meta': {'skill': 'abort.test'}, - 'lang': 'en-US'}} - self.assertIn(speak_msg, self.bus.emitted_msgs) - - def test_developer_stop_msg(self): - """ send "my.own.abort.msg" and confirm intent3 is aborted - send "mycroft.skills.abort_execution" and confirm intent3 ignores it""" - self.bus.emitted_msgs = [] - # skill will enter a infinite loop unless aborted - self.bus.emit(Message(f"{self.skill.skill_id}:test3.intent")) - sleep(2) - # check that intent triggered - start_msg = {'type': 'mycroft.skill.handler.start', - 'data': {'name': 'TestAbortSkill.handle_test_msg_intent'}} - speak_msg = {'type': 'speak', - 'data': {'utterance': "you can't abort me", - 'expect_response': False, - 'meta': {'skill': 'abort.test'}, - 'lang': 'en-US'}} - self.assertIn(start_msg, self.bus.emitted_msgs) - self.assertIn(speak_msg, self.bus.emitted_msgs) - - # check that intent does NOT react to mycroft.skills.abort_execution - # developer requested a dedicated abort message - self.bus.emitted_msgs = [] - self.bus.emit(Message(f"mycroft.skills.abort_execution")) - sleep(1) - - # check that stop method was NOT called - self.assertFalse(self.skill.instance.stop_called) - - # check that intent reacts to my.own.abort.msg - self.bus.emitted_msgs = [] - self.bus.emit(Message(f"my.own.abort.msg")) - sleep(2) - - # check that stop method was called - self.assertTrue(self.skill.instance.stop_called) - - # check that TTS stop message was emmited - tts_stop = {'type': 'mycroft.audio.speech.stop', 'data': {}} - self.assertIn(tts_stop, self.bus.emitted_msgs) - - # check that cleanup callback was called - speak_msg = {'type': 'speak', - 'data': {'utterance': 'I am dead', 'expect_response': False, - 'meta': {'skill': 'abort.test'}, - 'lang': 'en-US'}} - self.assertIn(speak_msg, self.bus.emitted_msgs) - self.assertTrue(self.skill.instance.my_special_var == "default") - - # check that we are not getting speak messages anymore - self.bus.emitted_msgs = [] - sleep(2) - self.assertTrue(self.bus.emitted_msgs == []) From 9f7cfa6725bd5983861d521f38082f0a4e3356fd Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Jun 2025 18:33:23 +0000 Subject: [PATCH 11/57] Increment Version to 1.5.0a3 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index db89a7a8ac2..2281ba1a129 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 1 VERSION_MINOR = 5 VERSION_BUILD = 0 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK # for compat with old imports From 6bb655a02b93c3ec8ee58eef2514f469a50e3ce1 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Jun 2025 18:34:07 +0000 Subject: [PATCH 12/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf17ac024e9..a4671c093a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.5.0a3](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.0a3) (2025-06-09) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.0a2...1.5.0a3) + +**Merged pull requests:** + +- refactor: ovoscope [\#691](https://github.com/OpenVoiceOS/ovos-core/pull/691) ([JarbasAl](https://github.com/JarbasAl)) + ## [1.5.0a2](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.0a2) (2025-06-09) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.0a1...1.5.0a2) From 1453c652284ef7239486e60e86c9cbee9ddd73ba Mon Sep 17 00:00:00 2001 From: miro Date: Mon, 9 Jun 2025 20:00:55 +0100 Subject: [PATCH 13/57] coverage --- .github/workflows/gh_pages_coverage.yml | 1 + .gitignore | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gh_pages_coverage.yml b/.github/workflows/gh_pages_coverage.yml index a7962377f7b..2ad89ef90e6 100644 --- a/.github/workflows/gh_pages_coverage.yml +++ b/.github/workflows/gh_pages_coverage.yml @@ -36,6 +36,7 @@ jobs: run: | coverage run -m pytest test/ coverage html + rm ./htmlcov/.gitignore - name: Deploy coverage report to GitHub Pages uses: peaceiris/actions-gh-pages@v3 diff --git a/.gitignore b/.gitignore index abdde43c26a..1aaa34fc97e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,11 +17,11 @@ tornado.ioloop .venv/ # Created by unit tests -coverage.xml +#coverage.xml pytest.ini -.coverage +#.coverage .testmondata* .pytest_cache/ -/htmlcov +#/htmlcov /.gtm/ !/.ruff_cache/ From e332145d5189e9b10ea93833676acaaadc2bffad Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:08:12 +0100 Subject: [PATCH 14/57] fix: skills-internet.txt (#695) messed up package names in previous PR --- requirements/skills-internet.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/skills-internet.txt b/requirements/skills-internet.txt index ac4ad92cf7e..3d3198b22a6 100644 --- a/requirements/skills-internet.txt +++ b/requirements/skills-internet.txt @@ -1,7 +1,7 @@ # skills that require internet connectivity, should not be installed in offline devices ovos-skill-weather>=0.1.11,<2.0.0 -skill-ddg>=0.1.9,<1.0.0 -skill-wolfie>=0.2.9,<1.0.0 +ovos-skill-ddg>=0.1.9,<1.0.0 +ovos-skill-wolfie>=0.2.9,<1.0.0 ovos-skill-wikipedia>=0.5.3,<1.0.0 ovos-skill-wikihow>=0.2.5,<1.0.0 ovos-skill-speedtest>=0.3.2,<1.0.0 From 7d23e6854e6279be4278a8095a17801c2f9745b6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Jun 2025 19:08:42 +0000 Subject: [PATCH 15/57] Increment Version to 1.5.1a1 --- ovos_core/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 2281ba1a129..d7ba2748152 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 1 VERSION_MINOR = 5 -VERSION_BUILD = 0 -VERSION_ALPHA = 3 +VERSION_BUILD = 1 +VERSION_ALPHA = 1 # END_VERSION_BLOCK # for compat with old imports From a916266b278121def01c4342789465caaa39e77c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 9 Jun 2025 19:09:29 +0000 Subject: [PATCH 16/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4671c093a6..476f023c306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.5.1a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.1a1) (2025-06-09) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.0a3...1.5.1a1) + +**Merged pull requests:** + +- fix: skills-internet.txt [\#695](https://github.com/OpenVoiceOS/ovos-core/pull/695) ([JarbasAl](https://github.com/JarbasAl)) + ## [1.5.0a3](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.0a3) (2025-06-09) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.0a2...1.5.0a3) From 62024dbf984f34248a68e4a16049f2eed500f73c Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 10 Jun 2025 03:51:41 +0100 Subject: [PATCH 17/57] refactor!: drop old pipeline plugins and deprecated methods (#690) * feat:pipeline plugin factory loads pipeline plugins from config :tada: no longer tied to adapt/padatious * migrate pipeline names to new style * logs * add model2vec test * add model2vec test * ensure working fallback skill version * reuse minicroft instance to speed up tests * rework pipeline matchers * fix tearDown of tests * log level to help debug failing tests * explicitly check that intent service is ready + remove deprecated skill loading from folder * update tests * fallback skill tests * feat: standalone intent service support * pipeline plugin shutdown * coderabbit suggestions * coderabbit suggestions * coderabbit suggestions * test converse * test converse * fix: sess injection --- .github/workflows/unit_tests.yml | 8 +- ovos_core/intent_services/__init__.py | 974 +----------------- ovos_core/intent_services/adapt_service.py | 12 - ovos_core/intent_services/commonqa_service.py | 11 - ovos_core/intent_services/converse_service.py | 139 +-- ovos_core/intent_services/fallback_service.py | 105 +- ovos_core/intent_services/ocp_service.py | 12 - .../intent_services/padacioso_service.py | 13 - .../intent_services/padatious_service.py | 12 - ovos_core/intent_services/service.py | 633 ++++++++++++ ovos_core/intent_services/stop_service.py | 207 ++-- ovos_core/skill_manager.py | 309 +----- ovos_core/transformers.py | 6 +- requirements/lgpl.txt | 4 +- requirements/mycroft.txt | 9 +- requirements/plugins.txt | 14 +- requirements/requirements.txt | 10 +- requirements/skills-audio.txt | 5 +- requirements/skills-desktop.txt | 2 +- requirements/skills-en.txt | 2 + requirements/skills-essential.txt | 7 +- requirements/skills-extra.txt | 15 +- requirements/skills-gui.txt | 2 +- requirements/skills-internet.txt | 12 +- requirements/skills-media.txt | 8 +- requirements/tests.txt | 2 +- setup.py | 3 +- test/end2end/test_converse.py | 142 +++ test/end2end/test_fallback.py | 59 ++ test/end2end/test_helloworld.py | 231 +++-- test/end2end/test_no_skills.py | 15 +- test/unittests/test_intent_service.py | 39 +- test/unittests/test_manager.py | 30 - test/unittests/test_skill_manager.py | 17 +- 34 files changed, 1295 insertions(+), 1774 deletions(-) delete mode 100644 ovos_core/intent_services/adapt_service.py delete mode 100644 ovos_core/intent_services/commonqa_service.py delete mode 100644 ovos_core/intent_services/ocp_service.py delete mode 100644 ovos_core/intent_services/padacioso_service.py delete mode 100644 ovos_core/intent_services/padatious_service.py create mode 100644 ovos_core/intent_services/service.py create mode 100644 test/end2end/test_converse.py create mode 100644 test/end2end/test_fallback.py diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 3eae6909a8c..160667d9d80 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -29,10 +29,6 @@ on: jobs: unit_tests: - strategy: - max-parallel: 3 - matrix: - python-version: ["3.11", "3.12"] runs-on: ubuntu-latest permissions: # Gives the action the necessary permissions for publishing new @@ -45,10 +41,10 @@ jobs: timeout-minutes: 35 steps: - uses: actions/checkout@v4 - - name: Set up python ${{ matrix.python-version }} + - name: Set up python uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: "3.11" - name: Install System Dependencies run: | sudo apt-get update diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index 4c46650fcfb..b729cc75da8 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -1,973 +1 @@ -# Copyright 2017 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. -# - -import json -import warnings -import time -from collections import defaultdict -from typing import Tuple, Callable, Union, List - -import requests -from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager -from ovos_bus_client.util import get_message_lang -from ovos_plugin_manager.templates.pipeline import PipelineMatch, IntentHandlerMatch -from ovos_utils.lang import standardize_lang_tag -from ovos_utils.log import LOG, log_deprecation, deprecated -from ovos_utils.metrics import Stopwatch -from ovos_utils.thread_utils import create_daemon -from padacioso.opm import PadaciosoPipeline as PadaciosoService - -from ocp_pipeline.opm import OCPPipelineMatcher -from ovos_adapt.opm import AdaptPipeline -from ovos_commonqa.opm import CommonQAService -from ovos_config.config import Configuration -from ovos_config.locale import get_valid_languages -from ovos_core.intent_services.converse_service import ConverseService -from ovos_core.intent_services.fallback_service import FallbackService -from ovos_core.intent_services.stop_service import StopService -from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService, IntentTransformersService -from ovos_persona import PersonaService - -# TODO - to be dropped once pluginified -# just a placeholder during alphas until https://github.com/OpenVoiceOS/ovos-core/pull/570 -try: - from ovos_ollama_intent_pipeline import LLMIntentPipeline -except ImportError: - LLMIntentPipeline = None -try: - from ovos_m2v_pipeline import Model2VecIntentPipeline -except ImportError: - Model2VecIntentPipeline = None - - -class IntentService: - """OVOS intent service. parses utterances using a variety of systems. - - The intent service also provides the internal API for registering and - querying the intent service. - """ - - def __init__(self, bus, config=None): - """ - Initializes the IntentService with all intent parsing pipelines, transformer services, and messagebus event handlers. - - Args: - bus: The messagebus connection used for event-driven communication. - config: Optional configuration dictionary for intent services. - - Sets up skill name mapping, loads all supported intent matching pipelines (including Adapt, Padatious, Padacioso, Fallback, Converse, CommonQA, Stop, OCP, Persona, and optionally LLM and Model2Vec pipelines), initializes utterance and metadata transformer services, connects the session manager, and registers all relevant messagebus event handlers for utterance processing, context management, intent queries, and skill deactivation tracking. - """ - self.bus = bus - self.config = config or Configuration().get("intents", {}) - - # Dictionary for translating a skill id to a name - self.skill_names = {} - - self._adapt_service = None - self._padatious_service = None - self._padacioso_service = None - self._fallback = None - self._converse = None - self._common_qa = None - self._stop = None - self._ocp = None - self._ollama = None - self._m2v = None - self._load_pipeline_plugins() - - self.utterance_plugins = UtteranceTransformersService(bus) - self.metadata_plugins = MetadataTransformersService(bus) - self.intent_plugins = IntentTransformersService(bus) - - # connection SessionManager to the bus, - # this will sync default session across all components - SessionManager.connect_to_bus(self.bus) - - self.bus.on('recognizer_loop:utterance', self.handle_utterance) - - # Context related handlers - 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) - - # 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('mycroft.skills.loaded', self.update_skill_name_dict) - - # internal, track skills that call self.deactivate to avoid reactivating them again - self._deactivations = defaultdict(list) - self.bus.on('intent.service.skills.deactivate', self._handle_deactivate) - - def _load_pipeline_plugins(self): - # TODO - replace with plugin loader from OPM - """ - Initializes and configures all intent matching pipeline plugins for the service. - - Sets up Adapt, Padatious, Padacioso, Fallback, Converse, CommonQA, Stop, OCP, Persona, and optionally LLM and Model2Vec intent pipelines based on the current configuration. Handles conditional loading and disabling of Padatious and Padacioso pipelines, and logs relevant status or errors. - """ - self._adapt_service = AdaptPipeline(bus=self.bus, config=self.config.get("adapt", {})) - if "padatious" not in self.config: - self.config["padatious"] = Configuration().get("padatious", {}) - try: - if self.config["padatious"].get("disabled"): - LOG.info("padatious forcefully disabled in config") - else: - from ovos_padatious.opm import PadatiousPipeline - if "instant_train" not in self.config["padatious"]: - self.config["padatious"]["instant_train"] = False - self._padatious_service = PadatiousPipeline(self.bus, self.config["padatious"]) - except ImportError: - LOG.error(f'Failed to create padatious intent handlers, padatious not installed') - - # by default only load padacioso is padatious is not available - # save memory if padacioso isnt needed - disable_padacioso = self.config.get("disable_padacioso", self._padatious_service is not None) - if not disable_padacioso: - self._padacioso_service = PadaciosoService(self.bus, self.config["padatious"]) - elif "disable_padacioso" not in self.config: - LOG.debug("Padacioso pipeline is disabled, only padatious is loaded. " - "set 'disable_padacioso': false in mycroft.conf if you want it to load alongside padatious") - self._fallback = FallbackService(self.bus) - self._converse = ConverseService(self.bus) - self._common_qa = CommonQAService(self.bus, self.config.get("common_query")) - self._stop = StopService(self.bus) - self._ocp = OCPPipelineMatcher(self.bus, config=self.config.get("OCP", {})) - self._persona = PersonaService(self.bus, config=self.config.get("persona", {})) - if LLMIntentPipeline is not None: - try: - self._ollama = LLMIntentPipeline(self.bus, config=self.config.get("ovos-ollama-intent-pipeline", {})) - except Exception as e: - LOG.error(f"Failed to load LLMIntentPipeline ({e})") - if Model2VecIntentPipeline is not None: - try: - self._m2v = Model2VecIntentPipeline(self.bus, config=self.config.get("ovos-m2v-pipeline", {})) - except Exception as e: - LOG.error(f"Failed to load Model2VecIntentPipeline ({e})") - - LOG.debug(f"Default pipeline: {SessionManager.get().pipeline}") - - def update_skill_name_dict(self, message): - """ - Updates the internal mapping of skill IDs to skill names from a message event. - - Args: - message: A message object containing 'id' and 'name' fields for the skill. - """ - 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 _handle_transformers(self, message): - """ - Pipe utterance through transformer plugins to get more metadata. - Utterances may be modified by any parser and context overwritten - """ - lang = get_message_lang(message) # per query lang or default Configuration lang - original = utterances = message.data.get('utterances', []) - message.context["lang"] = lang - utterances, message.context = self.utterance_plugins.transform(utterances, message.context) - if original != utterances: - message.data["utterances"] = utterances - LOG.debug(f"utterances transformed: {original} -> {utterances}") - message.context = self.metadata_plugins.transform(message.context) - return message - - @staticmethod - def disambiguate_lang(message): - """ disambiguate language of the query via pre-defined context keys - 1 - stt_lang -> tagged in stt stage (STT used this lang to transcribe speech) - 2 - request_lang -> tagged in source message (wake word/request volunteered lang info) - 3 - detected_lang -> tagged by transformers (text classification, free form chat) - 4 - config lang (or from message.data) - """ - default_lang = get_message_lang(message) - valid_langs = get_valid_languages() - valid_langs = [standardize_lang_tag(l) for l in valid_langs] - lang_keys = ["stt_lang", - "request_lang", - "detected_lang"] - for k in lang_keys: - if k in message.context: - v = standardize_lang_tag(message.context[k]) - if v in valid_langs: # TODO - use lang distance instead to choose best dialect - if v != default_lang: - LOG.info(f"replaced {default_lang} with {k}: {v}") - return v - else: - LOG.warning(f"ignoring {k}, {v} is not in enabled languages: {valid_langs}") - - return default_lang - - def get_pipeline(self, skips=None, session=None) -> Tuple[str, Callable]: - """ - Constructs and returns the ordered list of intent matcher functions for the current session. - - The pipeline sequence is determined by the session's configuration and may be filtered by - an optional list of pipeline keys to skip. Each entry in the returned list is a tuple of - the pipeline key and its corresponding matcher function, in the order they will be applied - for intent matching. If a requested pipeline component is unavailable, it is skipped and a - warning is logged. - - Args: - skips: Optional list of pipeline keys to exclude from the matcher sequence. - session: Optional session object; if not provided, the current session is used. - - Returns: - A list of (pipeline_key, matcher_function) tuples representing the active intent - matching pipeline for the session. - """ - session = session or SessionManager.get() - - # Create matchers - # TODO - from plugins - padatious_matcher = None - if self._padatious_service is None: - needs_pada = any("padatious" in p for p in session.pipeline) - if self._padacioso_service is not None: - if needs_pada: - LOG.warning("padatious is not available! using padacioso in it's place, " - "intent matching will be extremely slow in comparison") - padatious_matcher = self._padacioso_service - elif needs_pada: - LOG.warning("padatious is not available! only adapt (keyword based) intents will match!") - else: - padatious_matcher = self._padatious_service - - matchers = { - "converse": self._converse.converse_with_skills, - "stop_high": self._stop.match_stop_high, - "stop_medium": self._stop.match_stop_medium, - "stop_low": self._stop.match_stop_low, - "adapt_high": self._adapt_service.match_high, - "common_qa": self._common_qa.match, - "fallback_high": self._fallback.high_prio, - "adapt_medium": self._adapt_service.match_medium, - "fallback_medium": self._fallback.medium_prio, - "adapt_low": self._adapt_service.match_low, - "fallback_low": self._fallback.low_prio, - "ovos-persona-pipeline-plugin-high": self._persona.match_high, - "ovos-persona-pipeline-plugin-low": self._persona.match_low - } - if self._ollama is not None: - matchers["ovos-ollama-intent-pipeline"] = self._ollama.match_low - if self._m2v is not None: - matchers["ovos-m2v-pipeline-high"] = self._m2v.match_high - matchers["ovos-m2v-pipeline-medium"] = self._m2v.match_medium - matchers["ovos-m2v-pipeline-low"] = self._m2v.match_low - if self._padacioso_service is not None: - matchers.update({ - "padacioso_high": self._padacioso_service.match_high, - "padacioso_medium": self._padacioso_service.match_medium, - "padacioso_low": self._padacioso_service.match_low, - - }) - if padatious_matcher is not None: - matchers.update({ - "padatious_high": padatious_matcher.match_high, - "padatious_medium": padatious_matcher.match_medium, - "padatious_low": padatious_matcher.match_low, - - }) - if self._ocp is not None: - matchers.update({ - "ocp_high": self._ocp.match_high, - "ocp_medium": self._ocp.match_medium, - "ocp_fallback": self._ocp.match_fallback, - "ocp_legacy": self._ocp.match_legacy}) - skips = skips or [] - pipeline = [k for k in session.pipeline if k not in skips] - if any(k not in matchers for k in pipeline): - LOG.warning(f"Requested some invalid pipeline components! " - f"filtered {[k for k in pipeline if k not in matchers]}") - pipeline = [k for k in pipeline if k in matchers] - LOG.debug(f"Session pipeline: {pipeline}") - return [(k, matchers[k]) for k in pipeline] - - @staticmethod - def _validate_session(message, lang): - # get session - lang = standardize_lang_tag(lang) - sess = SessionManager.get(message) - if sess.session_id == "default": - updated = False - # Default session, check if it needs to be (re)-created - if sess.expired(): - sess = SessionManager.reset_default_session() - updated = True - if lang != sess.lang: - sess.lang = lang - updated = True - if updated: - SessionManager.update(sess) - SessionManager.sync(message) - else: - sess.lang = lang - SessionManager.update(sess) - sess.touch() - return sess - - def _handle_deactivate(self, message): - """internal helper, track if a skill asked to be removed from active list during intent match - in this case we want to avoid reactivating it again - This only matters in PipelineMatchers, such as fallback and converse - in those cases the activation is only done AFTER the match, not before unlike intents - """ - sess = SessionManager.get(message) - skill_id = message.data.get("skill_id") - self._deactivations[sess.session_id].append(skill_id) - - def _emit_match_message(self, match: Union[IntentHandlerMatch, PipelineMatch], message: Message, lang: str): - """ - Emit a reply message for a matched intent, updating session and skill activation. - - This method processes matched intents from either a pipeline matcher or an intent handler, - creating a reply message with matched intent details and managing skill activation. - - Args: - match (Union[IntentHandlerMatch, PipelineMatch]): The matched intent object containing - utterance and matching information. - message (Message): The original messagebus message that triggered the intent match. - lang (str): The language of the pipeline plugin match - - Details: - - Handles two types of matches: PipelineMatch and IntentHandlerMatch - - Creates a reply message with matched intent data - - Activates the corresponding skill if not previously deactivated - - Updates session information - - Emits the reply message on the messagebus - - Side Effects: - - Modifies session state - - Emits a messagebus event - - Can trigger skill activation events - - Returns: - None - """ - try: - match = self.intent_plugins.transform(match) - except Exception as e: - LOG.error(f"Error in IntentTransformers: {e}") - - reply = None - sess = match.updated_session or SessionManager.get(message) - sess.lang = lang # ensure it is updated - - # utterance fully handled by pipeline matcher - if isinstance(match, PipelineMatch): - if match.handled: - reply = message.reply("ovos.utterance.handled", {"skill_id": match.skill_id}) - - # upload intent metrics if enabled - create_daemon(self._upload_match_data, (match.utterance, - match.skill_id, - lang, - match.match_data)) - - # Launch skill if not handled by the match function - elif isinstance(match, IntentHandlerMatch) and match.match_type: - # keep all original message.data and update with intent match - data = dict(message.data) - data.update(match.match_data) - reply = message.reply(match.match_type, data) - - # upload intent metrics if enabled - create_daemon(self._upload_match_data, (match.utterance, - match.match_type, - lang, - match.match_data)) - - if reply is not None: - reply.data["utterance"] = match.utterance - reply.data["lang"] = lang - - # update active skill list - if match.skill_id: - # ensure skill_id is present in message.context - reply.context["skill_id"] = match.skill_id - - # NOTE: do not re-activate if the skill called self.deactivate - # we could also skip activation if skill is already active, - # but we still want to update the timestamp - was_deactivated = match.skill_id in self._deactivations[sess.session_id] - if not was_deactivated: - sess.activate_skill(match.skill_id) - # emit event for skills callback -> self.handle_activate - self.bus.emit(reply.forward(f"{match.skill_id}.activate")) - - # update Session if modified by pipeline - reply.context["session"] = sess.serialize() - - # finally emit reply message - self.bus.emit(reply) - - else: # upload intent metrics if enabled - create_daemon(self._upload_match_data, (match.utterance, - "complete_intent_failure", - lang, - match.match_data)) - - @staticmethod - def _upload_match_data(utterance: str, intent: str, lang: str, match_data: dict): - """if enabled upload the intent match data to a server, allowing users and developers - to collect metrics/datasets to improve the pipeline plugins and skills. - - There isn't a default server to upload things too, users needs to explicitly configure one - - https://github.com/OpenVoiceOS/ovos-opendata-server - """ - config = Configuration().get("open_data", {}) - endpoints: List[str] = config.get("intent_urls", []) # eg. "http://localhost:8000/intents" - if not endpoints: - return # user didn't configure any endpoints to upload metrics to - if isinstance(endpoints, str): - endpoints = [endpoints] - headers = {"Content-Type": "application/x-www-form-urlencoded", - "User-Agent": config.get("user_agent", "ovos-metrics")} - data = { - "utterance": utterance, - "intent": intent, - "lang": lang, - "match_data": json.dumps(match_data, ensure_ascii=False) - } - for url in endpoints: - try: - # Add a timeout to prevent hanging - response = requests.post(url, data=data, headers=headers, timeout=3) - LOG.info(f"Uploaded intent metrics to '{url}' - Response: {response.status_code}") - except Exception as e: - LOG.warning(f"Failed to upload metrics: {e}") - - def send_cancel_event(self, message): - """ - Emit events and play a sound when an utterance is canceled. - - Logs the cancellation with the specific cancel word, plays a predefined cancel sound, - and emits multiple events to signal the utterance cancellation. - - Parameters: - message (Message): The original message that triggered the cancellation. - - Events Emitted: - - 'mycroft.audio.play_sound': Plays a cancel sound from configuration - - 'ovos.utterance.cancelled': Signals that the utterance was canceled - - 'ovos.utterance.handled': Indicates the utterance processing is complete - - Notes: - - Uses the default cancel sound path 'snd/cancel.mp3' if not specified in configuration - - Ensures events are sent as replies to the original message - """ - LOG.info("utterance canceled, cancel_word:" + message.context.get("cancel_word")) - # play dedicated cancel sound - sound = Configuration().get('sounds', {}).get('cancel', "snd/cancel.mp3") - # NOTE: message.reply to ensure correct message destination - self.bus.emit(message.reply('mycroft.audio.play_sound', {"uri": sound})) - self.bus.emit(message.reply("ovos.utterance.cancelled")) - self.bus.emit(message.reply("ovos.utterance.handled")) - - def handle_utterance(self, message: Message): - """Main entrypoint for handling user utterances - - Monitor the messagebus for 'recognizer_loop:utterance', typically - generated by a spoken interaction but potentially also from a CLI - or other method of injecting a 'user utterance' into the system. - - Utterances then work through this sequence to be handled: - 1) UtteranceTransformers can modify the utterance and metadata in message.context - 2) MetadataTransformers can modify the metadata in message.context - 3) Language is extracted from message - 4) Active skills attempt to handle using converse() - 5) Padatious high match intents (conf > 0.95) - 6) Adapt intent handlers - 7) CommonQuery Skills - 8) High Priority Fallbacks - 9) Padatious near match intents (conf > 0.8) - 10) General Fallbacks - 11) Padatious loose match intents (conf > 0.5) - 12) Catch all fallbacks including Unknown intent handler - - If all these fail the complete_intent_failure message will be sent - and a generic error sound played. - - Args: - message (Message): The messagebus data - """ - # Get utterance utterance_plugins additional context - message = self._handle_transformers(message) - - if message.context.get("canceled"): - self.send_cancel_event(message) - return - - # tag language of this utterance - lang = self.disambiguate_lang(message) - - utterances = message.data.get('utterances', []) - LOG.info(f"Parsing utterance: {utterances}") - - stopwatch = Stopwatch() - - # get session - sess = self._validate_session(message, lang) - message.context["session"] = sess.serialize() - - # match - match = None - with stopwatch: - self._deactivations[sess.session_id] = [] - # Loop through the matching functions until a match is found. - for pipeline, match_func in self.get_pipeline(session=sess): - langs = [lang] - if self.config.get("multilingual_matching"): - # if multilingual matching is enabled, attempt to match all user languages if main fails - langs += [l for l in get_valid_languages() if l != lang] - for intent_lang in langs: - match = match_func(utterances, intent_lang, message) - if match: - LOG.info(f"{pipeline} match ({intent_lang}): {match}") - if match.skill_id and match.skill_id in sess.blacklisted_skills: - LOG.debug( - f"ignoring match, skill_id '{match.skill_id}' blacklisted by Session '{sess.session_id}'") - continue - if isinstance(match, IntentHandlerMatch) and match.match_type in sess.blacklisted_intents: - LOG.debug( - f"ignoring match, intent '{match.match_type}' blacklisted by Session '{sess.session_id}'") - continue - try: - self._emit_match_message(match, message, intent_lang) - break - except: - LOG.exception(f"{match_func} returned an invalid match") - else: - LOG.debug(f"no match from {match_func}") - continue - break - else: - # Nothing was able to handle the intent - # Ask politely for forgiveness for failing in this vital task - self.send_complete_intent_failure(message) - - LOG.debug(f"intent matching took: {stopwatch.time}") - - # sync any changes made to the default session, eg by ConverseService - if sess.session_id == "default": - SessionManager.sync(message) - elif sess.session_id in self._deactivations: - self._deactivations.pop(sess.session_id) - return match, message.context, stopwatch - - def send_complete_intent_failure(self, message): - """Send a message that no skill could handle the utterance. - - Args: - message (Message): original message to forward from - """ - sound = Configuration().get('sounds', {}).get('error', "snd/error.mp3") - # NOTE: message.reply to ensure correct message destination - self.bus.emit(message.reply('mycroft.audio.play_sound', {"uri": sound})) - self.bus.emit(message.reply('complete_intent_failure')) - self.bus.emit(message.reply("ovos.utterance.handled")) - - @staticmethod - def handle_add_context(message: Message): - """Add context - - Args: - message: data contains the 'context' item to add - optionally can include 'word' to be injected as - an alias for the context item. - """ - entity = {'confidence': 1.0} - context = message.data.get('context') - word = message.data.get('word') or '' - origin = message.data.get('origin') or '' - # if not a string type try creating a string from it - if not isinstance(word, str): - word = str(word) - entity['data'] = [(word, context)] - entity['match'] = word - entity['key'] = word - entity['origin'] = origin - sess = SessionManager.get(message) - sess.context.inject_context(entity) - - @staticmethod - def handle_remove_context(message: Message): - """Remove specific context - - Args: - message: data contains the 'context' item to remove - """ - context = message.data.get('context') - if context: - sess = SessionManager.get(message) - sess.context.remove_context(context) - - @staticmethod - def handle_clear_context(message: Message): - """Clears all keywords from context """ - sess = SessionManager.get(message) - sess.context.clear_context() - - def handle_get_intent(self, message): - """Get intent from either adapt or padatious. - - Args: - message (Message): message containing utterance - """ - utterance = message.data["utterance"] - lang = get_message_lang(message) - sess = SessionManager.get(message) - match = None - # Loop through the matching functions until a match is found. - for pipeline, match_func in self.get_pipeline(skips=["converse", - "common_qa", - "fallback_high", - "fallback_medium", - "fallback_low"], - session=sess): - s = time.monotonic() - match = match_func([utterance], lang, message) - LOG.debug(f"matching '{pipeline}' took: {time.monotonic() - s} seconds") - if match: - if match.match_type: - intent_data = dict(match.match_data) - intent_data["intent_name"] = match.match_type - intent_data["intent_service"] = pipeline - intent_data["skill_id"] = match.skill_id - intent_data["handler"] = match_func.__name__ - LOG.debug(f"final intent match: {intent_data}") - m = message.reply("intent.service.intent.reply", - {"intent": intent_data, "utterance": utterance}) - self.bus.emit(m) - return - LOG.error(f"bad pipeline match! {match}") - # signal intent failure - self.bus.emit(message.reply("intent.service.intent.reply", - {"intent": None, "utterance": utterance})) - - 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})) - - def shutdown(self): - self.utterance_plugins.shutdown() - self.metadata_plugins.shutdown() - self._adapt_service.shutdown() - if self._padacioso_service: - self._padacioso_service.shutdown() - if self._padatious_service: - self._padatious_service.shutdown() - self._common_qa.shutdown() - self._converse.shutdown() - self._fallback.shutdown() - if self._ocp: - self._ocp.shutdown() - - self.bus.remove('recognizer_loop:utterance', self.handle_utterance) - self.bus.remove('add_context', self.handle_add_context) - self.bus.remove('remove_context', self.handle_remove_context) - self.bus.remove('clear_context', self.handle_clear_context) - self.bus.remove('mycroft.skills.loaded', self.update_skill_name_dict) - self.bus.remove('intent.service.intent.get', self.handle_get_intent) - self.bus.remove('intent.service.skills.get', self.handle_get_skills) - - ########### - # DEPRECATED STUFF - @property - def registered_intents(self): - log_deprecation("direct access to self.adapt_service is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - warnings.warn( - "direct access to self.adapt_service is deprecated", - DeprecationWarning, - stacklevel=2, - ) - lang = get_message_lang() - return [parser.__dict__ - for parser in self._adapt_service.engines[lang].intent_parsers] - - @property - def adapt_service(self): - warnings.warn( - "direct access to self.adapt_service is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.adapt_service is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._adapt_service - - @property - def padatious_service(self): - warnings.warn( - "direct access to self.padatious_service is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.padatious_service is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._padatious_service - - @property - def padacioso_service(self): - warnings.warn( - "direct access to self.padatious_service is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.padacioso_service is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._padacioso_service - - @property - def fallback(self): - warnings.warn( - "direct access to self.fallback is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.fallback is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._fallback - - @property - def converse(self): - warnings.warn( - "direct access to self.converse is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.converse is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._converse - - @property - def common_qa(self): - warnings.warn( - "direct access to self.common_qa is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.common_qa is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._common_qa - - @property - def stop(self): - warnings.warn( - "direct access to self.stop is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.stop is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._stop - - @property - def ocp(self): - warnings.warn( - "direct access to self.ocp is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.ocp is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - return self._ocp - - @adapt_service.setter - def adapt_service(self, value): - warnings.warn( - "direct access to self.adapt_service is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.adapt_service is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._adapt_service = value - - @padatious_service.setter - def padatious_service(self, value): - warnings.warn( - "direct access to self.padatious_service is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.padatious_service is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._padatious_service = value - - @padacioso_service.setter - def padacioso_service(self, value): - warnings.warn( - "direct access to self.padacioso_service is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.padacioso_service is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._padacioso_service = value - - @fallback.setter - def fallback(self, value): - warnings.warn( - "direct access to self.fallback is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.fallback is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._fallback = value - - @converse.setter - def converse(self, value): - warnings.warn( - "direct access to self.converse is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.converse is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._converse = value - - @common_qa.setter - def common_qa(self, value): - warnings.warn( - "direct access to self.common_qa is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.common_qa is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._common_qa = value - - @stop.setter - def stop(self, value): - warnings.warn( - "direct access to self.stop is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.stop is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._stop = value - - @ocp.setter - def ocp(self, value): - warnings.warn( - "direct access to self.ocp is deprecated", - DeprecationWarning, - stacklevel=2, - ) - log_deprecation("direct access to self.ocp is deprecated, " - "pipelines are in the progress of being replaced with plugins", "1.0.0") - self._ocp = value - - @deprecated("handle_get_adapt moved to adapt service, this method does nothing", "1.0.0") - def handle_get_adapt(self, message: Message): - warnings.warn( - "moved to adapt service, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_adapt_manifest moved to adapt service, this method does nothing", "1.0.0") - def handle_adapt_manifest(self, message): - warnings.warn( - "moved to adapt service, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_vocab_manifest moved to adapt service, this method does nothing", "1.0.0") - def handle_vocab_manifest(self, message): - warnings.warn( - "moved to adapt service, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_get_padatious moved to padatious service, this method does nothing", "1.0.0") - def handle_get_padatious(self, message): - warnings.warn( - "moved to padatious service, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_padatious_manifest moved to padatious service, this method does nothing", "1.0.0") - def handle_padatious_manifest(self, message): - warnings.warn( - "moved to padatious service, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_entity_manifest moved to padatious service, this method does nothing", "1.0.0") - def handle_entity_manifest(self, message): - warnings.warn( - "moved to padatious service, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_register_vocab moved to individual pipeline services, this method does nothing", "1.0.0") - def handle_register_vocab(self, message): - warnings.warn( - "moved to pipeline plugins, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_register_intent moved to individual pipeline services, this method does nothing", "1.0.0") - def handle_register_intent(self, message): - warnings.warn( - "moved to pipeline plugins, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_detach_intent moved to individual pipeline services, this method does nothing", "1.0.0") - def handle_detach_intent(self, message): - warnings.warn( - "moved to pipeline plugins, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("handle_detach_skill moved to individual pipeline services, this method does nothing", "1.0.0") - def handle_detach_skill(self, message): - warnings.warn( - "moved to pipeline plugins, this method does nothing", - DeprecationWarning, - stacklevel=2, - ) +from ovos_core.intent_services.service import IntentService diff --git a/ovos_core/intent_services/adapt_service.py b/ovos_core/intent_services/adapt_service.py deleted file mode 100644 index 982fe78831d..00000000000 --- a/ovos_core/intent_services/adapt_service.py +++ /dev/null @@ -1,12 +0,0 @@ -# backwards compat import -from ovos_adapt.opm import AdaptPipeline as AdaptService -from ovos_utils.log import log_deprecation -log_deprecation("adapt service moved to 'ovos-adapt-pipeline-plugin'. this import is deprecated", "1.0.0") - -import warnings - -warnings.warn( - "adapt service moved to 'ovos-adapt-pipeline-plugin'", - DeprecationWarning, - stacklevel=2, -) \ No newline at end of file diff --git a/ovos_core/intent_services/commonqa_service.py b/ovos_core/intent_services/commonqa_service.py deleted file mode 100644 index d292ceacd6f..00000000000 --- a/ovos_core/intent_services/commonqa_service.py +++ /dev/null @@ -1,11 +0,0 @@ -from ovos_commonqa.opm import Query, CommonQAService -from ovos_utils.log import log_deprecation -log_deprecation("adapt service moved to 'ovos-common-query-pipeline-plugin'. this import is deprecated", "1.0.0") - -import warnings - -warnings.warn( - "adapt service moved to 'ovos-common-query-pipeline-plugin'", - DeprecationWarning, - stacklevel=2, -) \ No newline at end of file diff --git a/ovos_core/intent_services/converse_service.py b/ovos_core/intent_services/converse_service.py index 4d6421f97ed..26c92bc7871 100644 --- a/ovos_core/intent_services/converse_service.py +++ b/ovos_core/intent_services/converse_service.py @@ -1,33 +1,33 @@ import time from threading import Event -from typing import Optional, List +from typing import Optional, Dict, List, Union +from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message from ovos_bus_client.session import SessionManager, UtteranceState, Session -from ovos_bus_client.util import get_message_lang from ovos_config.config import Configuration -from ovos_config.locale import setup_locale -from ovos_plugin_manager.templates.pipeline import PipelineMatch, PipelinePlugin from ovos_utils import flatten_list +from ovos_utils.fakebus import FakeBus from ovos_utils.lang import standardize_lang_tag from ovos_utils.log import LOG + +from ovos_plugin_manager.templates.pipeline import PipelinePlugin, IntentHandlerMatch from ovos_workshop.permissions import ConverseMode, ConverseActivationMode class ConverseService(PipelinePlugin): """Intent Service handling conversational skills.""" - def __init__(self, bus): - self.bus = bus + def __init__(self, bus: Optional[Union[MessageBusClient, FakeBus]] = None, + config: Optional[Dict] = None): + config = config or Configuration().get("skills", {}).get("converse", {}) + super().__init__(bus, config) self._consecutive_activations = {} - 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) - super().__init__(config=Configuration().get("skills", {}).get("converse") or {}) @property def active_skills(self): @@ -209,17 +209,15 @@ def _converse_allowed(self, skill_id: str) -> bool: def _collect_converse_skills(self, message: Message) -> List[str]: """use the messagebus api to determine which skills want to converse - This includes all skills and external applications""" - session = SessionManager.get(message) + Individual skills respond to this request via the `can_converse` method""" 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() + want_converse = [] + session = SessionManager.get(message) + # note: this is sorted by priority already + active_skills = [skill_id for skill_id in self.get_active_skills(message) + if session.utterance_states.get(skill_id, UtteranceState.INTENT) == UtteranceState.INTENT] if not active_skills: return want_converse @@ -246,8 +244,7 @@ def handle_ack(msg): # 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})) + self.bus.emit(message.forward(f"{skill_id}.converse.ping", {**message.data, "skill_id": skill_id})) # wait for all skills to acknowledge they want to converse event.wait(timeout=0.5) @@ -264,65 +261,17 @@ def _check_converse_timeout(self, message: Message): skill for skill in session.active_skills if time.time() - skill[1] <= timeouts.get(skill[0], def_timeout)] - def converse(self, utterances: List[str], skill_id: str, lang: str, message: Message) -> bool: - """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. - """ - lang = standardize_lang_tag(lang) - 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', - timeout=self.config.get("max_skill_runtime", 10)) - 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) - else: - # abort any ongoing converse - # if skill crashed or returns False, all good - # if it is just taking a long time, more than 1 skill would end up answering - self.bus.emit(message.forward("ovos.skills.converse.force_timeout", - {"skill_id": skill_id})) - LOG.warning(f"{skill_id} took too long to answer, " - f'increasing "max_skill_runtime" in mycroft.conf might help alleviate this issue') - return False - - def converse_with_skills(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: + def match(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: """ Attempt to converse with active skills for a given set of utterances. - + Iterates through active skills to find one that can handle the utterance. Filters skills based on timeout and blacklist status. - + Args: utterances (List[str]): List of utterance strings to process lang (str): 4-letter ISO language code for the utterances message (Message): Message context for generating a reply - + Returns: PipelineMatch: Match details if a skill successfully handles the utterance, otherwise None - handled (bool): Whether the utterance was fully handled @@ -330,7 +279,7 @@ def converse_with_skills(self, utterances: List[str], lang: str, message: Messag - skill_id (str): ID of the skill that handled the utterance - updated_session (Session): Current session state after skill interaction - utterance (str): The original utterance processed - + Notes: - Standardizes language tag - Filters out blacklisted skills @@ -342,22 +291,43 @@ def converse_with_skills(self, utterances: List[str], lang: str, message: Messag # we call flatten in case someone is sending the old style list of tuples utterances = flatten_list(utterances) + + # note: this is sorted by priority already + gr_skills = [skill_id for skill_id in self.get_active_skills(message) + if session.utterance_states.get(skill_id, UtteranceState.INTENT) == UtteranceState.RESPONSE] + + # check if any skill wants to capture utterance for self.get_response method + for skill_id in gr_skills: + if skill_id in session.blacklisted_skills: + LOG.debug(f"ignoring match, skill_id '{skill_id}' blacklisted by Session '{session.session_id}'") + continue + LOG.debug(f"utterance captured by skill.get_response method: {skill_id}") + return IntentHandlerMatch( + match_type=f"{skill_id}.converse.get_response", + match_data={"utterances": utterances, "lang": lang}, + skill_id=skill_id, + utterance=utterances[0], + updated_session=session + ) + # filter allowed skills self._check_converse_timeout(message) - # check if any skill wants to handle utterance + + # check if any skill wants to converse for skill_id in self._collect_converse_skills(message): if skill_id in session.blacklisted_skills: LOG.debug(f"ignoring match, skill_id '{skill_id}' blacklisted by Session '{session.session_id}'") continue LOG.debug(f"Attempting to converse with skill: {skill_id}") - if self.converse(utterances, skill_id, lang, message): - state = session.utterance_states.get(skill_id, UtteranceState.INTENT) - return PipelineMatch(handled=state != UtteranceState.RESPONSE, - # handled == True -> emit "ovos.utterance.handled" - match_data={}, - skill_id=skill_id, - updated_session=session, - utterance=utterances[0]) + if self._converse_allowed(skill_id): + return IntentHandlerMatch( + match_type=f"{skill_id}.converse.request", + match_data={"utterances": utterances, "lang": lang}, + skill_id=skill_id, + utterance=utterances[0], + updated_session=session + ) + return None @staticmethod @@ -400,11 +370,6 @@ def handle_deactivate_skill_request(self, message: Message): if sess.session_id == "default": SessionManager.sync(message) - def reset_converse(self, message: Message): - """Let skills know there was a problem with speech recognition""" - lang = get_message_lang() - self.converse_with_skills([], lang, message) - def handle_get_active_skills(self, message: Message): """Send active skills to caller. @@ -415,10 +380,8 @@ def handle_get_active_skills(self, message: Message): {"skills": self.get_active_skills(message)})) def shutdown(self): - self.bus.remove('mycroft.speech.recognition.unknown', self.reset_converse) self.bus.remove('intent.service.skills.deactivate', self.handle_deactivate_skill_request) self.bus.remove('intent.service.skills.activate', self.handle_activate_skill_request) - self.bus.remove('active_skill_request', self.handle_activate_skill_request) # TODO backwards compat, deprecate self.bus.remove('intent.service.active_skills.get', self.handle_get_active_skills) self.bus.remove("skill.converse.get_response.enable", self.handle_get_response_enable) self.bus.remove("skill.converse.get_response.disable", self.handle_get_response_disable) diff --git a/ovos_core/intent_services/fallback_service.py b/ovos_core/intent_services/fallback_service.py index df2d5cb042f..ed28d18474c 100644 --- a/ovos_core/intent_services/fallback_service.py +++ b/ovos_core/intent_services/fallback_service.py @@ -12,17 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Intent service for Mycroft's fallback system.""" import operator import time from collections import namedtuple -from typing import Optional, List +from typing import Optional, Dict, List, Union +from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message from ovos_bus_client.session import SessionManager from ovos_config import Configuration -from ovos_plugin_manager.templates.pipeline import PipelineMatch, PipelinePlugin +from ovos_plugin_manager.templates.pipeline import ConfidenceMatcherPipeline, IntentHandlerMatch from ovos_utils import flatten_list +from ovos_utils.fakebus import FakeBus from ovos_utils.lang import standardize_lang_tag from ovos_utils.log import LOG from ovos_workshop.permissions import FallbackMode @@ -30,23 +31,23 @@ FallbackRange = namedtuple('FallbackRange', ['start', 'stop']) -class FallbackService(PipelinePlugin): +class FallbackService(ConfidenceMatcherPipeline): """Intent Service handling fallback skills.""" - def __init__(self, bus): - self.bus = bus - self.fallback_config = Configuration()["skills"].get("fallbacks", {}) + def __init__(self, bus: Optional[Union[MessageBusClient, FakeBus]] = None, + config: Optional[Dict] = None): + config = config or Configuration().get("skills", {}).get("fallbacks", {}) + super().__init__(bus, config) self.registered_fallbacks = {} # skill_id: priority self.bus.on("ovos.skills.fallback.register", self.handle_register_fallback) self.bus.on("ovos.skills.fallback.deregister", self.handle_deregister_fallback) - super().__init__(self.fallback_config) def handle_register_fallback(self, message: Message): skill_id = message.data.get("skill_id") priority = message.data.get("priority") or 101 # check if .conf is overriding the priority for this skill - priority_overrides = self.fallback_config.get("fallback_priorities", {}) + priority_overrides = self.config.get("fallback_priorities", {}) if skill_id in priority_overrides: new_priority = priority_overrides.get(skill_id) LOG.info(f"forcing {skill_id} fallback priority from {priority} to {new_priority}") @@ -71,19 +72,23 @@ def _fallback_allowed(self, skill_id: str) -> bool: Returns: permitted (bool): True if skill can fallback """ - opmode = self.fallback_config.get("fallback_mode", FallbackMode.ACCEPT_ALL) + opmode = self.config.get("fallback_mode", FallbackMode.ACCEPT_ALL) if opmode == FallbackMode.BLACKLIST and skill_id in \ - self.fallback_config.get("fallback_blacklist", []): + self.config.get("fallback_blacklist", []): return False elif opmode == FallbackMode.WHITELIST and skill_id not in \ - self.fallback_config.get("fallback_whitelist", []): + self.config.get("fallback_whitelist", []): return False return True def _collect_fallback_skills(self, message: Message, - fb_range: FallbackRange = FallbackRange(0, 100)) -> List[str]: + fb_range: Optional[FallbackRange] = None) -> List[str]: """use the messagebus api to determine which skills have registered fallback handlers - This includes all skills and external applications""" + + Individual skills respond to this request via the `can_answer` method + """ + if fb_range is None: + fb_range = FallbackRange(0, 100) skill_ids = [] # skill_ids that already answered to ping fallback_skills = [] # skill_ids that want to handle fallback @@ -109,7 +114,7 @@ def handle_ack(msg): if in_range: # no need to search if no skills available self.bus.on("ovos.skills.fallback.pong", handle_ack) - LOG.info("checking for FallbackSkillsV2 candidates") + LOG.info("checking for FallbackSkill candidates") message.data["range"] = (fb_range.start, fb_range.stop) # wait for all skills to acknowledge they want to answer fallback queries self.bus.emit(message.forward("ovos.skills.fallback.ping", @@ -122,50 +127,8 @@ def handle_ack(msg): self.bus.remove("ovos.skills.fallback.pong", handle_ack) return fallback_skills - def attempt_fallback(self, utterances: List[str], skill_id: str, lang: str, message: Message) -> bool: - """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. - """ - sess = SessionManager.get(message) - if skill_id in sess.blacklisted_skills: - LOG.debug(f"ignoring match, skill_id '{skill_id}' blacklisted by Session '{sess.session_id}'") - return False - if self._fallback_allowed(skill_id): - fb_msg = message.reply(f"ovos.skills.fallback.{skill_id}.request", - {"skill_id": skill_id, - "utterances": utterances, - "utterance": utterances[0], # backwards compat, we send all transcripts now - "lang": lang}) - result = self.bus.wait_for_response(fb_msg, - f"ovos.skills.fallback.{skill_id}.response", - timeout=self.fallback_config.get("max_skill_runtime", 10)) - 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) - else: - # abort any ongoing fallback - # if skill crashed or returns False, all good - # if it is just taking a long time, more than 1 fallback would end up answering - self.bus.emit(message.forward("ovos.skills.fallback.force_timeout", - {"skill_id": skill_id})) - LOG.warning(f"{skill_id} took too long to answer, " - f'increasing "max_skill_runtime" in mycroft.conf might help alleviate this issue') - return False - def _fallback_range(self, utterances: List[str], lang: str, - message: Message, fb_range: FallbackRange) -> Optional[PipelineMatch]: + message: Message, fb_range: FallbackRange) -> Optional[IntentHandlerMatch]: """Send fallback request for a specified priority range. Args: @@ -190,29 +153,35 @@ def _fallback_range(self, utterances: List[str], lang: str, fallbacks = [(k, v) for k, v in self.registered_fallbacks.items() if k in available_skills] sorted_handlers = sorted(fallbacks, key=operator.itemgetter(1)) + for skill_id, prio in sorted_handlers: if skill_id in sess.blacklisted_skills: LOG.debug(f"ignoring match, skill_id '{skill_id}' blacklisted by Session '{sess.session_id}'") continue - result = self.attempt_fallback(utterances, skill_id, lang, message) - if result: - return PipelineMatch(handled=True, - match_data={}, - skill_id=skill_id, - utterance=utterances[0]) + + if self._fallback_allowed(skill_id): + return IntentHandlerMatch( + match_type=f"ovos.skills.fallback.{skill_id}.request", + match_data={"skill_id": skill_id, + "utterances": utterances, + "lang": lang}, + utterance=utterances[0], + updated_session=sess + ) + return None - def high_prio(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: - """Pre-padatious fallbacks.""" + def match_high(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: + """High confidence/quality matchers.""" return self._fallback_range(utterances, lang, message, FallbackRange(0, 5)) - def medium_prio(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: + def match_medium(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: """General fallbacks.""" return self._fallback_range(utterances, lang, message, FallbackRange(5, 90)) - def low_prio(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: + def match_low(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: """Low prio fallbacks with general matching such as chat-bot.""" return self._fallback_range(utterances, lang, message, FallbackRange(90, 101)) diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py deleted file mode 100644 index 790e79f5d60..00000000000 --- a/ovos_core/intent_services/ocp_service.py +++ /dev/null @@ -1,12 +0,0 @@ -# backwards compat imports -from ocp_pipeline.opm import OCPPipelineMatcher, OCPFeaturizer, OCPPlayerProxy -from ovos_utils.log import log_deprecation -log_deprecation("adapt service moved to 'ovos-ocp-pipeline-plugin'. this import is deprecated", "1.0.0") - -import warnings - -warnings.warn( - "adapt service moved to 'ovos-ocp-pipeline-plugin'", - DeprecationWarning, - stacklevel=2, -) \ No newline at end of file diff --git a/ovos_core/intent_services/padacioso_service.py b/ovos_core/intent_services/padacioso_service.py deleted file mode 100644 index 7bd3fd645d1..00000000000 --- a/ovos_core/intent_services/padacioso_service.py +++ /dev/null @@ -1,13 +0,0 @@ -# backwards compat imports -from padacioso.opm import PadaciosoPipeline as PadaciosoService, PadaciosoIntent -from padacioso import IntentContainer as FallbackIntentContainer -from ovos_utils.log import log_deprecation -log_deprecation("adapt service moved to 'padacioso.opm'. this import is deprecated", "1.0.0") - -import warnings - -warnings.warn( - "adapt service moved to 'padacioso'", - DeprecationWarning, - stacklevel=2, -) \ No newline at end of file diff --git a/ovos_core/intent_services/padatious_service.py b/ovos_core/intent_services/padatious_service.py deleted file mode 100644 index b0f421b732c..00000000000 --- a/ovos_core/intent_services/padatious_service.py +++ /dev/null @@ -1,12 +0,0 @@ -# backwards compat imports -from ovos_padatious.opm import PadatiousMatcher, PadatiousPipeline as PadatiousService -from ovos_utils.log import log_deprecation -log_deprecation("adapt service moved to 'ovos-padatious-pipeline-plugin'. this import is deprecated", "1.0.0") - -import warnings - -warnings.warn( - "adapt service moved to 'ovos-padatious-pipeline-plugin'", - DeprecationWarning, - stacklevel=2, -) \ No newline at end of file diff --git a/ovos_core/intent_services/service.py b/ovos_core/intent_services/service.py new file mode 100644 index 00000000000..1659ab770b0 --- /dev/null +++ b/ovos_core/intent_services/service.py @@ -0,0 +1,633 @@ +# Copyright 2017 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. +# + +import json +import re +import time +from collections import defaultdict +from typing import Tuple, Callable, List + +import requests +from ovos_bus_client.message import Message +from ovos_bus_client.session import SessionManager +from ovos_bus_client.util import get_message_lang +from ovos_config.config import Configuration +from ovos_config.locale import get_valid_languages +from ovos_utils.lang import standardize_lang_tag +from ovos_utils.log import LOG +from ovos_utils.metrics import Stopwatch +from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap +from ovos_utils.thread_utils import create_daemon + +from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService, IntentTransformersService +from ovos_plugin_manager.pipeline import OVOSPipelineFactory +from ovos_plugin_manager.templates.pipeline import IntentHandlerMatch, ConfidenceMatcherPipeline + + +def on_started(): + LOG.info('IntentService is starting up.') + + +def on_alive(): + LOG.info('IntentService is alive.') + + +def on_ready(): + LOG.info('IntentService is ready.') + + +def on_error(e='Unknown'): + LOG.info(f'IntentService failed to launch ({e})') + + +def on_stopping(): + LOG.info('IntentService is shutting down...') + + +class IntentService: + """OVOS intent service. parses utterances using a variety of systems. + + The intent service also provides the internal API for registering and + querying the intent service. + """ + + def __init__(self, bus, config=None, preload_pipelines=True, + alive_hook=on_alive, started_hook=on_started, + ready_hook=on_ready, + error_hook=on_error, stopping_hook=on_stopping): + """ + Initializes the IntentService with all intent parsing pipelines, transformer services, and messagebus event handlers. + + Args: + bus: The messagebus connection used for event-driven communication. + config: Optional configuration dictionary for intent services. + + Sets up skill name mapping, loads all supported intent matching pipelines (including Adapt, Padatious, Padacioso, Fallback, Converse, CommonQA, Stop, OCP, Persona, and optionally LLM and Model2Vec pipelines), initializes utterance and metadata transformer services, connects the session manager, and registers all relevant messagebus event handlers for utterance processing, context management, intent queries, and skill deactivation tracking. + """ + callbacks = StatusCallbackMap(on_started=started_hook, + on_alive=alive_hook, + on_ready=ready_hook, + on_error=error_hook, + on_stopping=stopping_hook) + self.bus = bus + self.status = ProcessStatus('intents', bus=self.bus, callback_map=callbacks) + self.status.set_started() + self.config = config or Configuration().get("intents", {}) + + # load and cache the plugins right away so they receive all bus messages + self.pipeline_plugins = {} + + self.utterance_plugins = UtteranceTransformersService(bus) + self.metadata_plugins = MetadataTransformersService(bus) + self.intent_plugins = IntentTransformersService(bus) + + # connection SessionManager to the bus, + # this will sync default session across all components + SessionManager.connect_to_bus(self.bus) + + self.bus.on('recognizer_loop:utterance', self.handle_utterance) + + # Context related handlers + 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) + + # Intents API + self.bus.on('intent.service.intent.get', self.handle_get_intent) + + # internal, track skills that call self.deactivate to avoid reactivating them again + self._deactivations = defaultdict(list) + self.bus.on('intent.service.skills.deactivate', self._handle_deactivate) + self.bus.on('intent.service.pipelines.reload', self.handle_reload_pipelines) + + self.status.set_alive() + if preload_pipelines: + self.bus.emit(Message('intent.service.pipelines.reload')) + + def handle_reload_pipelines(self, message: Message): + pipeline_plugins = OVOSPipelineFactory.get_installed_pipeline_ids() + LOG.debug(f"Installed pipeline plugins: {pipeline_plugins}") + for p in pipeline_plugins: + try: + self.pipeline_plugins[p] = OVOSPipelineFactory.load_plugin(p, bus=self.bus) + LOG.debug(f"Loaded pipeline plugin: '{p}'") + except Exception as e: + LOG.error(f"Failed to load pipeline plugin '{p}': {e}") + self.status.set_ready() + + def _handle_transformers(self, message): + """ + Pipe utterance through transformer plugins to get more metadata. + Utterances may be modified by any parser and context overwritten + """ + lang = get_message_lang(message) # per query lang or default Configuration lang + original = utterances = message.data.get('utterances', []) + message.context["lang"] = lang + utterances, message.context = self.utterance_plugins.transform(utterances, message.context) + if original != utterances: + message.data["utterances"] = utterances + LOG.debug(f"utterances transformed: {original} -> {utterances}") + message.context = self.metadata_plugins.transform(message.context) + return message + + @staticmethod + def disambiguate_lang(message): + """ disambiguate language of the query via pre-defined context keys + 1 - stt_lang -> tagged in stt stage (STT used this lang to transcribe speech) + 2 - request_lang -> tagged in source message (wake word/request volunteered lang info) + 3 - detected_lang -> tagged by transformers (text classification, free form chat) + 4 - config lang (or from message.data) + """ + default_lang = get_message_lang(message) + valid_langs = get_valid_languages() + valid_langs = [standardize_lang_tag(l) for l in valid_langs] + lang_keys = ["stt_lang", + "request_lang", + "detected_lang"] + for k in lang_keys: + if k in message.context: + v = standardize_lang_tag(message.context[k]) + if v in valid_langs: # TODO - use lang distance instead to choose best dialect + if v != default_lang: + LOG.info(f"replaced {default_lang} with {k}: {v}") + return v + else: + LOG.warning(f"ignoring {k}, {v} is not in enabled languages: {valid_langs}") + + return default_lang + + def get_pipeline_matcher(self, matcher_id: str): + """ + Retrieve a matcher function for a given pipeline matcher ID. + + Args: + matcher_id: The configured matcher ID (e.g. `adapt_high`). + + Returns: + A callable matcher function. + """ + migration_map = { + "converse": "ovos-converse-pipeline-plugin", + "common_qa": "ovos-common-query-pipeline-plugin", + "fallback_high": "ovos-fallback-pipeline-plugin-high", + "fallback_medium": "ovos-fallback-pipeline-plugin-medium", + "fallback_low": "ovos-fallback-pipeline-plugin-low", + "stop_high": "ovos-stop-pipeline-plugin-high", + "stop_medium": "ovos-stop-pipeline-plugin-medium", + "stop_low": "ovos-stop-pipeline-plugin-low", + "adapt_high": "ovos-adapt-pipeline-plugin-high", + "adapt_medium": "ovos-adapt-pipeline-plugin-medium", + "adapt_low": "ovos-adapt-pipeline-plugin-low", + "padacioso_high": "ovos-padacioso-pipeline-plugin-high", + "padacioso_medium": "ovos-padacioso-pipeline-plugin-medium", + "padacioso_low": "ovos-padacioso-pipeline-plugin-low", + "padatious_high": "ovos-padatious-pipeline-plugin-high", + "padatious_medium": "ovos-padatious-pipeline-plugin-medium", + "padatious_low": "ovos-padatious-pipeline-plugin-low", + "ocp_high": "ovos-ocp-pipeline-plugin-high", + "ocp_medium": "ovos-ocp-pipeline-plugin-medium", + "ocp_low": "ovos-ocp-pipeline-plugin-low", + "ocp_legacy": "ovos-ocp-pipeline-plugin-legacy" + } + + matcher_id = migration_map.get(matcher_id, matcher_id) + pipe_id = re.sub(r'-(high|medium|low)$', '', matcher_id) + plugin = self.pipeline_plugins.get(pipe_id) + if not plugin: + LOG.error(f"Unknown pipeline matcher: {matcher_id}") + return None + + if isinstance(plugin, ConfidenceMatcherPipeline): + if matcher_id.endswith("-high"): + return plugin.match_high + if matcher_id.endswith("-medium"): + return plugin.match_medium + if matcher_id.endswith("-low"): + return plugin.match_low + return plugin.match + + def get_pipeline(self, session=None) -> List[Tuple[str, Callable]]: + """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""" + session = session or SessionManager.get() + matchers = [(p, self.get_pipeline_matcher(p)) for p in session.pipeline] + matchers = [m for m in matchers if m[1] is not None] # filter any that failed to load + final_pipeline = [k[0] for k in matchers] + if session.pipeline != final_pipeline: + LOG.warning(f"Requested some invalid pipeline components! " + f"filtered: {[k for k in session.pipeline if k not in final_pipeline]}") + LOG.debug(f"Session final pipeline: {final_pipeline}") + return matchers + + @staticmethod + def _validate_session(message, lang): + # get session + lang = standardize_lang_tag(lang) + sess = SessionManager.get(message) + if sess.session_id == "default": + updated = False + # Default session, check if it needs to be (re)-created + if sess.expired(): + sess = SessionManager.reset_default_session() + updated = True + if lang != sess.lang: + sess.lang = lang + updated = True + if updated: + SessionManager.update(sess) + SessionManager.sync(message) + else: + sess.lang = lang + SessionManager.update(sess) + sess.touch() + return sess + + def _handle_deactivate(self, message): + """internal helper, track if a skill asked to be removed from active list during intent match + in this case we want to avoid reactivating it again + This only matters in PipelineMatchers, such as fallback and converse + in those cases the activation is only done AFTER the match, not before unlike intents + """ + sess = SessionManager.get(message) + skill_id = message.data.get("skill_id") + self._deactivations[sess.session_id].append(skill_id) + + def _emit_match_message(self, match: IntentHandlerMatch, message: Message, lang: str): + """ + Emit a reply message for a matched intent, updating session and skill activation. + + This method processes matched intents from either a pipeline matcher or an intent handler, + creating a reply message with matched intent details and managing skill activation. + + Args: + match (IntentHandlerMatch): The matched intent object containing + utterance and matching information. + message (Message): The original messagebus message that triggered the intent match. + lang (str): The language of the pipeline plugin match + + Details: + - Handles two types of matches: PipelineMatch and IntentHandlerMatch + - Creates a reply message with matched intent data + - Activates the corresponding skill if not previously deactivated + - Updates session information + - Emits the reply message on the messagebus + + Side Effects: + - Modifies session state + - Emits a messagebus event + - Can trigger skill activation events + + Returns: + None + """ + try: + match = self.intent_plugins.transform(match) + except Exception as e: + LOG.error(f"Error in IntentTransformers: {e}") + + reply = None + sess = match.updated_session or SessionManager.get(message) + sess.lang = lang # ensure it is updated + + # Launch intent handler + if match.match_type: + # keep all original message.data and update with intent match + data = dict(message.data) + data.update(match.match_data) + reply = message.reply(match.match_type, data) + + # upload intent metrics if enabled + create_daemon(self._upload_match_data, (match.utterance, + match.match_type, + lang, + match.match_data)) + + if reply is not None: + reply.data["utterance"] = match.utterance + reply.data["lang"] = lang + + # update active skill list + if match.skill_id: + # ensure skill_id is present in message.context + reply.context["skill_id"] = match.skill_id + + # NOTE: do not re-activate if the skill called self.deactivate + # we could also skip activation if skill is already active, + # but we still want to update the timestamp + was_deactivated = match.skill_id in self._deactivations[sess.session_id] + if not was_deactivated: + sess.activate_skill(match.skill_id) + # emit event for skills callback -> self.handle_activate + self.bus.emit(reply.forward(f"{match.skill_id}.activate")) + + # update Session if modified by pipeline + reply.context["session"] = sess.serialize() + + # finally emit reply message + self.bus.emit(reply) + + else: # upload intent metrics if enabled + create_daemon(self._upload_match_data, (match.utterance, + "complete_intent_failure", + lang, + match.match_data)) + + @staticmethod + def _upload_match_data(utterance: str, intent: str, lang: str, match_data: dict): + """if enabled upload the intent match data to a server, allowing users and developers + to collect metrics/datasets to improve the pipeline plugins and skills. + + There isn't a default server to upload things too, users needs to explicitly configure one + + https://github.com/OpenVoiceOS/ovos-opendata-server + """ + config = Configuration().get("open_data", {}) + endpoints: List[str] = config.get("intent_urls", []) # eg. "http://localhost:8000/intents" + if not endpoints: + return # user didn't configure any endpoints to upload metrics to + if isinstance(endpoints, str): + endpoints = [endpoints] + headers = {"Content-Type": "application/x-www-form-urlencoded", + "User-Agent": config.get("user_agent", "ovos-metrics")} + data = { + "utterance": utterance, + "intent": intent, + "lang": lang, + "match_data": json.dumps(match_data, ensure_ascii=False) + } + for url in endpoints: + try: + # Add a timeout to prevent hanging + response = requests.post(url, data=data, headers=headers, timeout=3) + LOG.info(f"Uploaded intent metrics to '{url}' - Response: {response.status_code}") + except Exception as e: + LOG.warning(f"Failed to upload metrics: {e}") + + def send_cancel_event(self, message): + """ + Emit events and play a sound when an utterance is canceled. + + Logs the cancellation with the specific cancel word, plays a predefined cancel sound, + and emits multiple events to signal the utterance cancellation. + + Parameters: + message (Message): The original message that triggered the cancellation. + + Events Emitted: + - 'mycroft.audio.play_sound': Plays a cancel sound from configuration + - 'ovos.utterance.cancelled': Signals that the utterance was canceled + - 'ovos.utterance.handled': Indicates the utterance processing is complete + + Notes: + - Uses the default cancel sound path 'snd/cancel.mp3' if not specified in configuration + - Ensures events are sent as replies to the original message + """ + LOG.info("utterance canceled, cancel_word:" + message.context.get("cancel_word")) + # play dedicated cancel sound + sound = Configuration().get('sounds', {}).get('cancel', "snd/cancel.mp3") + # NOTE: message.reply to ensure correct message destination + self.bus.emit(message.reply('mycroft.audio.play_sound', {"uri": sound})) + self.bus.emit(message.reply("ovos.utterance.cancelled")) + self.bus.emit(message.reply("ovos.utterance.handled")) + + def handle_utterance(self, message: Message): + """Main entrypoint for handling user utterances + + Monitor the messagebus for 'recognizer_loop:utterance', typically + generated by a spoken interaction but potentially also from a CLI + or other method of injecting a 'user utterance' into the system. + + Utterances then work through this sequence to be handled: + 1) UtteranceTransformers can modify the utterance and metadata in message.context + 2) MetadataTransformers can modify the metadata in message.context + 3) Language is extracted from message + 4) Active skills attempt to handle using converse() + 5) Padatious high match intents (conf > 0.95) + 6) Adapt intent handlers + 7) CommonQuery Skills + 8) High Priority Fallbacks + 9) Padatious near match intents (conf > 0.8) + 10) General Fallbacks + 11) Padatious loose match intents (conf > 0.5) + 12) Catch all fallbacks including Unknown intent handler + + If all these fail the complete_intent_failure message will be sent + and a generic error sound played. + + Args: + message (Message): The messagebus data + """ + # Get utterance utterance_plugins additional context + message = self._handle_transformers(message) + + if message.context.get("canceled"): + self.send_cancel_event(message) + return + + # tag language of this utterance + lang = self.disambiguate_lang(message) + + utterances = message.data.get('utterances', []) + LOG.info(f"Parsing utterance: {utterances}") + + stopwatch = Stopwatch() + + # get session + sess = self._validate_session(message, lang) + message.context["session"] = sess.serialize() + + # match + match = None + with stopwatch: + self._deactivations[sess.session_id] = [] + # Loop through the matching functions until a match is found. + for pipeline, match_func in self.get_pipeline(session=sess): + langs = [lang] + if self.config.get("multilingual_matching"): + # if multilingual matching is enabled, attempt to match all user languages if main fails + langs += [l for l in get_valid_languages() if l != lang] + for intent_lang in langs: + match = match_func(utterances, intent_lang, message) + if match: + LOG.info(f"{pipeline} match ({intent_lang}): {match}") + if match.skill_id and match.skill_id in sess.blacklisted_skills: + LOG.debug( + f"ignoring match, skill_id '{match.skill_id}' blacklisted by Session '{sess.session_id}'") + continue + if isinstance(match, IntentHandlerMatch) and match.match_type in sess.blacklisted_intents: + LOG.debug( + f"ignoring match, intent '{match.match_type}' blacklisted by Session '{sess.session_id}'") + continue + try: + self._emit_match_message(match, message, intent_lang) + break + except: + LOG.exception(f"{match_func} returned an invalid match") + else: + LOG.debug(f"no match from {match_func}") + continue + break + else: + # Nothing was able to handle the intent + # Ask politely for forgiveness for failing in this vital task + self.send_complete_intent_failure(message) + + LOG.debug(f"intent matching took: {stopwatch.time}") + + # sync any changes made to the default session, eg by ConverseService + if sess.session_id == "default": + SessionManager.sync(message) + elif sess.session_id in self._deactivations: + self._deactivations.pop(sess.session_id) + return match, message.context, stopwatch + + def send_complete_intent_failure(self, message): + """Send a message that no skill could handle the utterance. + + Args: + message (Message): original message to forward from + """ + sound = Configuration().get('sounds', {}).get('error', "snd/error.mp3") + # NOTE: message.reply to ensure correct message destination + self.bus.emit(message.reply('mycroft.audio.play_sound', {"uri": sound})) + self.bus.emit(message.reply('complete_intent_failure')) + self.bus.emit(message.reply("ovos.utterance.handled")) + + @staticmethod + def handle_add_context(message: Message): + """Add context + + Args: + message: data contains the 'context' item to add + optionally can include 'word' to be injected as + an alias for the context item. + """ + entity = {'confidence': 1.0} + context = message.data.get('context') + word = message.data.get('word') or '' + origin = message.data.get('origin') or '' + # if not a string type try creating a string from it + if not isinstance(word, str): + word = str(word) + entity['data'] = [(word, context)] + entity['match'] = word + entity['key'] = word + entity['origin'] = origin + sess = SessionManager.get(message) + sess.context.inject_context(entity) + + @staticmethod + def handle_remove_context(message: Message): + """Remove specific context + + Args: + message: data contains the 'context' item to remove + """ + context = message.data.get('context') + if context: + sess = SessionManager.get(message) + sess.context.remove_context(context) + + @staticmethod + def handle_clear_context(message: Message): + """Clears all keywords from context """ + sess = SessionManager.get(message) + sess.context.clear_context() + + def handle_get_intent(self, message): + """Get intent from either adapt or padatious. + + Args: + message (Message): message containing utterance + """ + utterance = message.data["utterance"] + lang = get_message_lang(message) + sess = SessionManager.get(message) + match = None + # Loop through the matching functions until a match is found. + for pipeline, match_func in self.get_pipeline(session=sess): + s = time.monotonic() + match = match_func([utterance], lang, message) + LOG.debug(f"matching '{pipeline}' took: {time.monotonic() - s} seconds") + if match: + if match.match_type: + intent_data = dict(match.match_data) + intent_data["intent_name"] = match.match_type + intent_data["intent_service"] = pipeline + intent_data["skill_id"] = match.skill_id + intent_data["handler"] = match_func.__name__ + LOG.debug(f"final intent match: {intent_data}") + m = message.reply("intent.service.intent.reply", + {"intent": intent_data, "utterance": utterance}) + self.bus.emit(m) + return + LOG.error(f"bad pipeline match! {match}") + # signal intent failure + self.bus.emit(message.reply("intent.service.intent.reply", + {"intent": None, "utterance": utterance})) + + def shutdown(self): + self.utterance_plugins.shutdown() + self.metadata_plugins.shutdown() + for pipeline in self.pipeline_plugins.values(): + if hasattr(pipeline, "stop"): + try: + pipeline.stop() + except Exception as e: + LOG.warning(f"Failed to stop pipeline {pipeline}: {e}") + continue + if hasattr(pipeline, "shutdown"): + try: + pipeline.shutdown() + except Exception as e: + LOG.warning(f"Failed to shutdown pipeline {pipeline}: {e}") + continue + + self.bus.remove('recognizer_loop:utterance', self.handle_utterance) + self.bus.remove('add_context', self.handle_add_context) + self.bus.remove('remove_context', self.handle_remove_context) + self.bus.remove('clear_context', self.handle_clear_context) + self.bus.remove('intent.service.intent.get', self.handle_get_intent) + + self.status.set_stopping() + + +def launch_standalone(): + from ovos_bus_client import MessageBusClient + from ovos_utils import wait_for_exit_signal + from ovos_config.locale import setup_locale + from ovos_utils.log import init_service_logger + + LOG.info("Launching IntentService in standalone mode") + init_service_logger("intents") + setup_locale() + + bus = MessageBusClient() + bus.run_in_thread() + bus.connected_event.wait() + + intents = IntentService(bus) + + wait_for_exit_signal() + + intents.shutdown() + + LOG.info('IntentService shutdown complete!') + + +if __name__ == "__main__": + launch_standalone() \ No newline at end of file diff --git a/ovos_core/intent_services/stop_service.py b/ovos_core/intent_services/stop_service.py index e48463fc61f..109759bf2ad 100644 --- a/ovos_core/intent_services/stop_service.py +++ b/ovos_core/intent_services/stop_service.py @@ -2,29 +2,32 @@ import re from os.path import dirname from threading import Event -from typing import Optional, List +from typing import Optional, Dict, List, Union from langcodes import closest_match - +from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message from ovos_bus_client.session import SessionManager + from ovos_config.config import Configuration -from ovos_plugin_manager.templates.pipeline import PipelineMatch, PipelinePlugin +from ovos_plugin_manager.templates.pipeline import ConfidenceMatcherPipeline, IntentHandlerMatch from ovos_utils import flatten_list +from ovos_utils.fakebus import FakeBus from ovos_utils.bracket_expansion import expand_template from ovos_utils.lang import standardize_lang_tag from ovos_utils.log import LOG from ovos_utils.parse import match_one -class StopService(PipelinePlugin): +class StopService(ConfidenceMatcherPipeline): """Intent Service thats handles stopping skills.""" - def __init__(self, bus): - self.bus = bus + def __init__(self, bus: Optional[Union[MessageBusClient, FakeBus]] = None, + config: Optional[Dict] = None): + config = config or Configuration().get("skills", {}).get("stop") or {} + super().__init__(config=config, bus=bus) self._voc_cache = {} self.load_resource_files() - super().__init__(config=Configuration().get("skills", {}).get("stop") or {}) def load_resource_files(self): base = f"{dirname(__file__)}/locale" @@ -52,17 +55,19 @@ def get_active_skills(message: Optional[Message] = None) -> List[str]: def _collect_stop_skills(self, message: Message) -> List[str]: """ Collect skills that can be stopped based on a ping-pong mechanism. - + This method determines which active skills can handle a stop request by sending a stop ping to each active skill and waiting for their acknowledgment. - + + Individual skills respond to this request via the `can_stop` method + Parameters: message (Message): The original message triggering the stop request. - + Returns: List[str]: A list of skill IDs that can be stopped. If no skills explicitly indicate they can stop, returns all active skills. - + Notes: - Excludes skills that are blacklisted in the current session - Uses a non-blocking event mechanism to collect skill responses @@ -85,17 +90,17 @@ def _collect_stop_skills(self, message: Message) -> List[str]: def handle_ack(msg): """ Handle acknowledgment from skills during the stop process. - + This method is a nested function used in skill stopping negotiation. It validates and tracks skill responses to a stop request. - + Parameters: msg (Message): Message containing skill acknowledgment details. - + Side Effects: - Modifies the `want_stop` list with skills that can handle stopping - Updates the `skill_ids` list to track which skills have responded - Sets the threading event when all active skills have responded - + Notes: - Checks if a skill can handle stopping based on multiple conditions - Ensures all active skills provide a response before proceeding @@ -129,77 +134,41 @@ def handle_ack(msg): self.bus.remove("skill.stop.pong", handle_ack) return want_stop or active_skills - def stop_skill(self, skill_id: str, message: Message) -> bool: - """ - Stop a skill's ongoing activities and manage its session state. - - Sends a stop command to a specific skill and handles its response, ensuring - that any active interactions or processes are terminated. The method checks - for errors, verifies the skill's stopped status, and emits additional signals - to forcibly abort ongoing actions like conversations, questions, or speech. - - Args: - skill_id (str): Unique identifier of the skill to be stopped. - message (Message): The original message context containing interaction details. - - Returns: - bool: True if the skill was successfully stopped, False otherwise. - - Raises: - Logs error if skill stop request encounters an issue. - - Notes: - - Emits multiple bus messages to ensure complete skill termination - - Checks and handles different skill interaction states - - Supports force-stopping of conversations, questions, and speech - """ - stop_msg = message.reply(f"{skill_id}.stop") - result = self.bus.wait_for_response(stop_msg, f"{skill_id}.stop.response") - if result and 'error' in result.data: - error_msg = result.data['error'] + def handle_stop_confirmation(self, message: Message): + skill_id = (message.data.get("skill_id") or + message.context.get("skill_id") or + message.msg_type.split(".stop.response")[0]) + if 'error' in message.data: + error_msg = message.data['error'] LOG.error(f"{skill_id}: {error_msg}") - return False - elif result is not None: - stopped = result.data.get('result', False) - else: - stopped = False - - if stopped: - sess = SessionManager.get(message) - state = sess.utterance_states.get(skill_id, "intent") - LOG.debug(f"skill response status: {state}") - if state == "response": # TODO this is never happening and it should... - LOG.debug(f"stopping {skill_id} in middle of get_response!") - + elif message.data.get('result', False): # force-kill any ongoing get_response/converse/TTS - see @killable_event decorator self.bus.emit(message.forward("mycroft.skills.abort_question", {"skill_id": skill_id})) self.bus.emit(message.forward("ovos.skills.converse.force_timeout", {"skill_id": skill_id})) # TODO - track if speech is coming from this skill! not currently tracked - self.bus.emit(message.reply("mycroft.audio.speech.stop",{"skill_id": skill_id})) + self.bus.emit(message.reply("mycroft.audio.speech.stop", {"skill_id": skill_id})) - return stopped - - def match_stop_high(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: + def match_high(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: """ Handles high-confidence stop requests by matching exact stop vocabulary and managing skill stopping. - + Attempts to stop skills when an exact "stop" or "global_stop" command is detected. Performs the following actions: - Identifies the closest language match for vocabulary - Checks for global stop command when no active skills exist - Emits a global stop message if applicable - Attempts to stop individual skills if a stop command is detected - Disables response mode for stopped skills - + Parameters: utterances (List[str]): List of user utterances to match against stop vocabulary lang (str): Four-letter ISO language code for language-specific matching message (Message): Message context for generating appropriate responses - + Returns: Optional[PipelineMatch]: Match result indicating whether stop was handled, with optional skill and session information - Returns None if no stop action could be performed - Returns PipelineMatch with handled=True for successful global or skill-specific stop - + Raises: No explicit exceptions raised, but may log debug/info messages during processing """ @@ -221,43 +190,47 @@ def match_stop_high(self, utterances: List[str], lang: str, message: Message) -> if is_global_stop: LOG.info(f"Emitting global stop, {len(self.get_active_skills(message))} active skills") # emit a global stop, full stop anything OVOS is doing - self.bus.emit(message.reply("mycroft.stop", {})) - return PipelineMatch(handled=True, - match_data={"conf": conf}, - skill_id=None, - utterance=utterance) + return IntentHandlerMatch( + match_type="mycroft.stop", + match_data={"conf": conf}, + updated_session=sess, + utterance=utterance, + skill_id="stop.openvoiceos" + ) if is_stop: # check if any skill can stop for skill_id in self._collect_stop_skills(message): - LOG.debug(f"Checking if skill wants to stop: {skill_id}") - if self.stop_skill(skill_id, message): - LOG.info(f"Skill stopped: {skill_id}") - sess.disable_response_mode(skill_id) - return PipelineMatch(handled=True, - match_data={"conf": conf}, - skill_id=skill_id, - utterance=utterance, - updated_session=sess) + LOG.debug(f"Telling skill to stop: {skill_id}") + sess.disable_response_mode(skill_id) + self.bus.once(f"{skill_id}.stop.response", self.handle_stop_confirmation) + return IntentHandlerMatch( + match_type=f"{skill_id}.stop", + match_data={"conf": conf}, + updated_session=sess, + utterance=utterance, + skill_id="stop.openvoiceos" + ) + return None - def match_stop_medium(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: + def match_medium(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: """ Handle stop intent with additional context beyond simple stop commands. - + This method processes utterances that contain "stop" or global stop vocabulary but may include additional words not explicitly defined in intent files. It performs a medium-confidence intent matching for stop requests. - + Parameters: utterances (List[str]): List of input utterances to analyze lang (str): Four-letter ISO language code for localization message (Message): Message context for generating appropriate responses - + Returns: Optional[PipelineMatch]: A pipeline match if the stop intent is successfully processed, otherwise None if no stop intent is detected - + Notes: - Attempts to match stop vocabulary with fuzzy matching - Falls back to low-confidence matching if medium-confidence match is inconclusive @@ -277,34 +250,22 @@ def match_stop_medium(self, utterances: List[str], lang: str, message: Message) if not is_global_stop: return None - return self.match_stop_low(utterances, lang, message) + return self.match_low(utterances, lang, message) - def _get_closest_lang(self, lang: str) -> Optional[str]: - if self._voc_cache: - lang = standardize_lang_tag(lang) - closest, score = closest_match(lang, list(self._voc_cache.keys())) - # https://langcodes-hickford.readthedocs.io/en/sphinx/index.html#distance-values - # 0 -> These codes represent the same language, possibly after filling in values and normalizing. - # 1- 3 -> These codes indicate a minor regional difference. - # 4 - 10 -> These codes indicate a significant but unproblematic regional difference. - if score < 10: - return closest - return None - - def match_stop_low(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: + def match_low(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: """ Perform a low-confidence fuzzy match for stop intent before fallback processing. - + This method attempts to match stop-related vocabulary with low confidence and handle stopping of active skills. - + Parameters: utterances (List[str]): List of input utterances to match against stop vocabulary lang (str): Four-letter ISO language code for vocabulary matching message (Message): Message context used for generating replies and managing session - + Returns: Optional[PipelineMatch]: A pipeline match object if a stop action is handled, otherwise None - + Notes: - Increases confidence if active skills are present - Attempts to stop individual skills before emitting a global stop signal @@ -328,23 +289,38 @@ def match_stop_low(self, utterances: List[str], lang: str, message: Message) -> # check if any skill can stop for skill_id in self._collect_stop_skills(message): - LOG.debug(f"Checking if skill wants to stop: {skill_id}") - if self.stop_skill(skill_id, message): - sess.disable_response_mode(skill_id) - return PipelineMatch(handled=True, - match_data={"conf": conf}, - skill_id=skill_id, - utterance=utterance, - updated_session=sess) + LOG.debug(f"Telling skill to stop: {skill_id}") + sess.disable_response_mode(skill_id) + self.bus.once(f"{skill_id}.stop.response", self.handle_stop_confirmation) + return IntentHandlerMatch( + match_type=f"{skill_id}.stop", + match_data={"conf": conf}, + updated_session=sess, + utterance=utterance, + skill_id="stop.openvoiceos" + ) # emit a global stop, full stop anything OVOS is doing LOG.debug(f"Emitting global stop signal, {len(self.get_active_skills(message))} active skills") - self.bus.emit(message.reply("mycroft.stop", {})) - return PipelineMatch(handled=True, - # emit instead of intent message {"conf": conf}, - match_data={"conf": conf}, - skill_id=None, - utterance=utterance) + return IntentHandlerMatch( + match_type="mycroft.stop", + match_data={"conf": conf}, + updated_session=sess, + utterance=utterance, + skill_id="stop.openvoiceos" + ) + + def _get_closest_lang(self, lang: str) -> Optional[str]: + if self._voc_cache: + lang = standardize_lang_tag(lang) + closest, score = closest_match(lang, list(self._voc_cache.keys())) + # https://langcodes-hickford.readthedocs.io/en/sphinx/index.html#distance-values + # 0 -> These codes represent the same language, possibly after filling in values and normalizing. + # 1- 3 -> These codes indicate a minor regional difference. + # 4 - 10 -> These codes indicate a significant but unproblematic regional difference. + if score < 10: + return closest + return None def voc_match(self, utt: str, voc_filename: str, lang: str, exact: bool = False): @@ -389,3 +365,4 @@ def voc_match(self, utt: str, voc_filename: str, lang: str, return any([re.match(r'.*\b' + i + r'\b.*', utt, re.IGNORECASE) for i in _vocs]) return False + diff --git a/ovos_core/skill_manager.py b/ovos_core/skill_manager.py index 059da67b84e..c7602f2cc5f 100644 --- a/ovos_core/skill_manager.py +++ b/ovos_core/skill_manager.py @@ -14,46 +14,22 @@ # """Load, update and manage skills on this device.""" import os -from os.path import basename -from threading import Thread, Event, Lock -from time import monotonic +import threading +from threading import Thread, Event from ovos_bus_client.apis.enclosure import EnclosureAPI from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message from ovos_config.config import Configuration from ovos_config.locations import get_xdg_config_save_path -from ovos_plugin_manager.skills import find_skill_plugins -from ovos_plugin_manager.skills import get_skill_directories from ovos_utils.file_utils import FileWatcher from ovos_utils.gui import is_gui_connected -from ovos_utils.log import LOG, deprecated +from ovos_utils.log import LOG from ovos_utils.network_utils import is_connected_http from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap, ProcessState -from ovos_workshop.skill_launcher import SKILL_MAIN_MODULE -from ovos_workshop.skill_launcher import SkillLoader, PluginSkillLoader -import warnings - - -def _shutdown_skill(instance): - """Shutdown a skill. - - Call the default_shutdown method of the skill, will produce a warning if - the shutdown process takes longer than 1 second. - - Args: - instance (MycroftSkill): Skill instance to shutdown - """ - try: - ref_time = monotonic() - # Perform the shutdown - instance.default_shutdown() +from ovos_workshop.skill_launcher import PluginSkillLoader - shutdown_time = monotonic() - ref_time - if shutdown_time > 1: - LOG.warning(f'{instance.skill_id} shutdown took {shutdown_time} seconds') - except Exception: - LOG.exception(f'Failed to shut down skill: {instance.skill_id}') +from ovos_plugin_manager.skills import find_skill_plugins def on_started(): @@ -105,7 +81,6 @@ def __init__(self, bus, watchdog=None, alive_hook=on_alive, started_hook=on_star self.status = ProcessStatus('skills', callback_map=callbacks) self.status.set_started() - self._lock = Lock() self._setup_event = Event() self._stop_event = Event() self._connected_event = Event() @@ -124,7 +99,6 @@ def __init__(self, bus, watchdog=None, alive_hook=on_alive, started_hook=on_star self.config = Configuration() - self.skill_loaders = {} self.plugin_skills = {} self.enclosure = EnclosureAPI(bus) self.num_install_retries = 0 @@ -143,8 +117,7 @@ def blacklist(self): Returns: list: List of blacklisted skill ids. """ - return Configuration().get("skills", {}).get("blacklisted_skills", - ["skill-ovos-stop.openvoiceos"]) + return Configuration().get("skills", {}).get("blacklisted_skills", []) def _init_filewatcher(self): """Initialize the file watcher to monitor skill settings files for changes.""" @@ -295,7 +268,6 @@ def load_plugin_skills(self, network=None, internet=None): if internet is None: internet = self._connected_event.is_set() plugins = find_skill_plugins() - loaded_skill_ids = [basename(p) for p in self.skill_loaders] for skill_id, plug in plugins.items(): if skill_id in self.blacklist: if skill_id not in self._logged_skill_warnings: @@ -303,7 +275,7 @@ def load_plugin_skills(self, network=None, internet=None): LOG.warning(f"{skill_id} is blacklisted, it will NOT be loaded") LOG.info(f"Consider uninstalling {skill_id} instead of blacklisting it") continue - if skill_id not in self.plugin_skills and skill_id not in loaded_skill_ids: + if skill_id not in self.plugin_skills: skill_loader = self._get_plugin_skill_loader(skill_id, init_bus=False, skill_class=plug) requirements = skill_loader.runtime_requirements @@ -371,10 +343,26 @@ def _load_plugin_skill(self, skill_id, skill_plugin): return skill_loader if load_status else None + def wait_for_intent_service(self): + """ensure IntentService reported ready to accept skill messages""" + while not self._stop_event.is_set(): + response = self.bus.wait_for_response( + Message('mycroft.intents.is_ready', + context={"source": "skills", "destination": "intents"}), + timeout=5) + if response and response.data.get('status'): + return + threading.Event().wait(1) + raise RuntimeError("Skill manager stopped while waiting for intent service") + def run(self): """Run the skill manager thread.""" self.status.set_alive() + LOG.debug("Waiting for IntentService startup") + self.wait_for_intent_service() + LOG.debug("IntentService reported ready") + self._load_on_startup() # trigger a sync so we dont need to wait for the plugin to volunteer info @@ -397,7 +385,6 @@ def run(self): # unload the existing version from memory and reload from the disk. while not self._stop_event.wait(30): try: - self._unload_removed_skills() self._load_new_skills() self._watchdog() except Exception: @@ -422,39 +409,15 @@ def _load_on_internet(self): def _unload_on_network_disconnect(self): """Unload skills that require a network connection to work.""" - with self._lock: - for skill_dir in self._get_skill_directories(): - skill_id = os.path.basename(skill_dir) - skill_loader = self._get_skill_loader(skill_dir, init_bus=False) - requirements = skill_loader.runtime_requirements - if requirements.requires_network and \ - not requirements.no_network_fallback: - # Unload skills until the network is back - self._unload_skill(skill_dir) + # TODO - implementation missing def _unload_on_internet_disconnect(self): """Unload skills that require an internet connection to work.""" - with self._lock: - for skill_dir in self._get_skill_directories(): - skill_id = os.path.basename(skill_dir) - skill_loader = self._get_skill_loader(skill_dir, init_bus=False) - requirements = skill_loader.runtime_requirements - if requirements.requires_internet and \ - not requirements.no_internet_fallback: - # Unload skills until the internet is back - self._unload_skill(skill_dir) + # TODO - implementation missing def _unload_on_gui_disconnect(self): """Unload skills that require a GUI to work.""" - with self._lock: - for skill_dir in self._get_skill_directories(): - skill_id = os.path.basename(skill_dir) - skill_loader = self._get_skill_loader(skill_dir, init_bus=False) - requirements = skill_loader.runtime_requirements - if requirements.requires_gui and \ - not requirements.no_gui_fallback: - # Unload skills until the GUI is back - self._unload_skill(skill_dir) + # TODO - implementation missing def _load_on_startup(self): """Handle offline skills load on startup.""" @@ -477,164 +440,22 @@ def _load_new_skills(self, network=None, internet=None, gui=None): if gui is None: gui = self._gui_event.is_set() or is_gui_connected(self.bus) - # A lock is used because this can be called via state events or as part of the main loop. - # There is a possible race condition where this handler would be executing several times otherwise. - with self._lock: - - loaded_new = self.load_plugin_skills(network=network, internet=internet) - - for skill_dir in self._get_skill_directories(): - replaced_skills = [] - skill_id = os.path.basename(skill_dir) - skill_loader = self._get_skill_loader(skill_dir, init_bus=False) - requirements = skill_loader.runtime_requirements - if not network and requirements.network_before_load: - continue - if not internet and requirements.internet_before_load: - continue - if not gui and requirements.gui_before_load: - # TODO - companion PR adding this one - continue - - # A local source install is replacing this plugin, unload it! - if skill_id in self.plugin_skills: - LOG.info(f"{skill_id} plugin will be replaced by a local version: {skill_dir}") - self._unload_plugin_skill(skill_id) - - for old_skill_dir, skill_loader in self.skill_loaders.items(): - if old_skill_dir != skill_dir and \ - skill_loader.skill_id == skill_id: - # A higher priority equivalent has been detected! - replaced_skills.append(old_skill_dir) - - for old_skill_dir in replaced_skills: - # Unload the old skill - self._unload_skill(old_skill_dir) - - if skill_dir not in self.skill_loaders: - self._load_skill(skill_dir) - loaded_new = True + loaded_new = self.load_plugin_skills(network=network, internet=internet) if loaded_new: - LOG.info("Requesting padatious intent training") + LOG.debug("Requesting pipeline intent training") try: response = self.bus.wait_for_response(Message("mycroft.skills.train"), "mycroft.skills.trained", timeout=60) # 60 second timeout if not response: - LOG.error("Padatious training timed out") + LOG.error("Intent training timed out") elif response.data.get('error'): - LOG.error(f"Padatious training failed: {response.data['error']}") + LOG.error(f"Intent training failed: {response.data['error']}") + else: + LOG.debug(f"pipelines trained and ready to go") except Exception as e: - LOG.exception(f"Error during padatious training: {e}") - - def _get_skill_loader(self, skill_directory, init_bus=True): - """Get a skill loader instance. - - Args: - skill_directory (str): Directory path of the skill. - init_bus (bool): Whether to initialize the internal skill bus. - - Returns: - SkillLoader: Skill loader instance. - """ - bus = None - if init_bus: - bus = self._get_internal_skill_bus() - return SkillLoader(bus, skill_directory) - - def _load_skill(self, skill_directory): - """Load an old-style skill. - - Args: - skill_directory (str): Directory path of the skill. - - Returns: - SkillLoader: Loaded skill loader instance if successful, None otherwise. - """ - LOG.warning(f"Found deprecated skill directory: {skill_directory}\n" - f"please create a setup.py for this skill") - skill_id = basename(skill_directory) - if skill_id in self.blacklist: - if skill_id not in self._logged_skill_warnings: - self._logged_skill_warnings.append(skill_id) - LOG.warning(f"{skill_id} is blacklisted, it will NOT be loaded") - LOG.info(f"Consider deleting {skill_directory} instead of blacklisting it") - return None - - skill_loader = self._get_skill_loader(skill_directory) - try: - load_status = skill_loader.load() - except Exception: - LOG.exception(f'Load of skill {skill_directory} failed!') - load_status = False - finally: - self.skill_loaders[skill_directory] = skill_loader - if load_status: - LOG.info(f"Loaded old style skill: {skill_directory}") - else: - LOG.error(f"Failed to load old style skill: {skill_directory}") - return skill_loader if load_status else None - - def _unload_skill(self, skill_dir): - """Unload a skill. - - Args: - skill_dir (str): Directory path of the skill. - """ - if skill_dir in self.skill_loaders: - skill = self.skill_loaders[skill_dir] - LOG.info(f'Removing {skill.skill_id}') - try: - skill.unload() - except Exception: - LOG.exception('Failed to shutdown skill ' + skill.id) - del self.skill_loaders[skill_dir] - - def _get_skill_directories(self): - """Get valid skill directories. - - Returns: - list: List of valid skill directories. - """ - skillmap = {} - valid_skill_roots = ["/opt/mycroft/skills"] + get_skill_directories() - for skills_dir in valid_skill_roots: - if not os.path.isdir(skills_dir): - continue - for skill_id in os.listdir(skills_dir): - skill = os.path.join(skills_dir, skill_id) - # NOTE: empty folders mean the skill should NOT be loaded - if os.path.isdir(skill): - skillmap[skill_id] = skill - - for skill_id, skill_dir in skillmap.items(): - # TODO: all python packages must have __init__.py! Better way? - # check if folder is a skill (must have __init__.py) - if SKILL_MAIN_MODULE in os.listdir(skill_dir): - if skill_dir in self.empty_skill_dirs: - self.empty_skill_dirs.discard(skill_dir) - else: - if skill_dir not in self.empty_skill_dirs: - self.empty_skill_dirs.add(skill_dir) - LOG.debug('Found skills directory with no skill: ' + - skill_dir) - - return skillmap.values() - - def _unload_removed_skills(self): - """Shutdown removed skills. - - Finds and unloads skills that were removed from the disk. - """ - skill_dirs = self._get_skill_directories() - # Find loaded skills that don't exist on disk - removed_skills = [ - s for s in self.skill_loaders.keys() if s not in skill_dirs - ] - for skill_dir in removed_skills: - self._unload_skill(skill_dir) - return removed_skills + LOG.exception(f"Error during Intent training: {e}") def _unload_plugin_skill(self, skill_id): """Unload a plugin skill. @@ -665,8 +486,7 @@ def send_skill_list(self, message=None): try: message_data = {} # TODO handle external skills, OVOSAbstractApp/Hivemind skills are not accounted for - skills = {**self.skill_loaders, **self.plugin_skills} - + skills = self.plugin_skills for skill_loader in skills.values(): message_data[skill_loader.skill_id] = { "active": skill_loader.active and skill_loader.loaded, @@ -680,7 +500,7 @@ def deactivate_skill(self, message): """Deactivate a skill.""" try: # TODO handle external skills, OVOSAbstractApp/Hivemind skills are not accounted for - skills = {**self.skill_loaders, **self.plugin_skills} + skills = self.plugin_skills for skill_loader in skills.values(): if message.data['skill'] == skill_loader.skill_id: LOG.info("Deactivating skill: " + skill_loader.skill_id) @@ -696,7 +516,7 @@ def deactivate_except(self, message): skill_to_keep = message.data['skill'] LOG.info(f'Deactivating all skills except {skill_to_keep}') # TODO handle external skills, OVOSAbstractApp/Hivemind skills are not accounted for - skills = {**self.skill_loaders, **self.plugin_skills} + skills = self.plugin_skills for skill in skills.values(): if skill.skill_id != skill_to_keep: skill.deactivate() @@ -708,7 +528,7 @@ def activate_skill(self, message): """Activate a deactivated skill.""" try: # TODO handle external skills, OVOSAbstractApp/Hivemind skills are not accounted for - skills = {**self.skill_loaders, **self.plugin_skills} + skills = self.plugin_skills for skill_loader in skills.values(): if (message.data['skill'] in ('all', skill_loader.skill_id) and not skill_loader.active): @@ -724,63 +544,8 @@ def stop(self): self._stop_event.set() # Do a clean shutdown of all skills - for skill_loader in self.skill_loaders.values(): - if skill_loader.instance is not None: - _shutdown_skill(skill_loader.instance) - - # Do a clean shutdown of all plugin skills for skill_id in list(self.plugin_skills.keys()): self._unload_plugin_skill(skill_id) if self._settings_watchdog: self._settings_watchdog.shutdown() - - ############ - # Deprecated stuff - @deprecated("priority skills have been deprecated for a long time", "1.0.0") - def load_priority(self): - warnings.warn( - "priority skills have been deprecated", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("mycroft.ready event has moved to finished booting skill", "1.0.0") - def is_device_ready(self): - """Check if the device is ready by waiting for various services to start. - - Returns: - bool: True if the device is ready, False otherwise. - Raises: - TimeoutError: If the device is not ready within a specified timeout. - """ - warnings.warn( - "mycroft.ready event has moved to finished booting skill", - DeprecationWarning, - stacklevel=2, - ) - return True - - @deprecated("mycroft.ready event has moved to finished booting skill", "1.0.0") - def handle_check_device_readiness(self, message): - warnings.warn( - "mycroft.ready event has moved to finished booting skill", - DeprecationWarning, - stacklevel=2, - ) - - @deprecated("mycroft.ready event has moved to finished booting skill", "1.0.0") - def check_services_ready(self, services): - """Report if all specified services are ready. - - Args: - services (iterable): Service names to check. - Returns: - bool: True if all specified services are ready, False otherwise. - """ - warnings.warn( - "mycroft.ready event has moved to finished booting skill", - DeprecationWarning, - stacklevel=2, - ) - return True diff --git a/ovos_core/transformers.py b/ovos_core/transformers.py index 3bd10887265..3ac676dabdc 100644 --- a/ovos_core/transformers.py +++ b/ovos_core/transformers.py @@ -1,10 +1,10 @@ -from typing import Optional, List, Union +from typing import Optional, List from ovos_config import Configuration from ovos_plugin_manager.intent_transformers import find_intent_transformer_plugins from ovos_plugin_manager.metadata_transformers import find_metadata_transformer_plugins from ovos_plugin_manager.text_transformers import find_utterance_transformer_plugins -from ovos_plugin_manager.templates.pipeline import IntentHandlerMatch, PipelineMatch +from ovos_plugin_manager.templates.pipeline import IntentHandlerMatch from ovos_utils.json_helper import merge_dict from ovos_utils.log import LOG @@ -203,7 +203,7 @@ def shutdown(self): except: pass - def transform(self, intent: Union[IntentHandlerMatch, PipelineMatch]) -> Union[IntentHandlerMatch, PipelineMatch]: + def transform(self, intent: IntentHandlerMatch) -> IntentHandlerMatch: """ Sequentially applies all loaded intent transformer plugins to the given intent object. diff --git a/requirements/lgpl.txt b/requirements/lgpl.txt index a69b6b849b6..106b0b899ca 100644 --- a/requirements/lgpl.txt +++ b/requirements/lgpl.txt @@ -1,2 +1,2 @@ -ovos_padatious>=1.1.0, <2.0.0 -fann2>=1.0.7, < 1.1.0 +ovos_padatious>=1.4.2,<2.0.0 +fann2>=1.0.7,<1.1.0 diff --git a/requirements/mycroft.txt b/requirements/mycroft.txt index 433a17466f8..ff4ad1a3184 100644 --- a/requirements/mycroft.txt +++ b/requirements/mycroft.txt @@ -1,6 +1,7 @@ # all ovos core modules, a full install like mycroft-core used to do -ovos_PHAL[extras]>=0.2.7,<1.0.0 -ovos-audio[extras]>=0.3.1,<1.0.0 -ovos-gui[extras]>=0.2.2,<2.0.0 +ovos_PHAL[extras]>=0.2.9,<1.0.0 +ovos-audio[extras]>=1.0.1,<2.0.0 +ovos-audio>=1.0.1,<2.0.0 +ovos-gui[extras]>=1.3.3,<2.0.0 ovos-messagebus>=0.0.7,<1.0.0 -ovos-dinkum-listener[extras]>=0.3.2,<1.0.0 \ No newline at end of file +ovos-dinkum-listener[extras]>=0.4.1,<1.0.0 \ No newline at end of file diff --git a/requirements/plugins.txt b/requirements/plugins.txt index 816c8fcfd7f..2ac495659ed 100644 --- a/requirements/plugins.txt +++ b/requirements/plugins.txt @@ -2,10 +2,18 @@ ovos-utterance-corrections-plugin>=0.0.2, <1.0.0 ovos-utterance-plugin-cancel>=0.2.3, <1.0.0 ovos-bidirectional-translation-plugin>=0.1.0, <1.0.0 ovos-translate-server-plugin>=0.0.2, <1.0.0 -ovos-utterance-normalizer>=0.2.1, <1.0.0 +ovos-utterance-normalizer>=0.2.2, <1.0.0 ovos-number-parser>=0.0.1,<1.0.0 ovos-date-parser>=0.0.3,<1.0.0 -ovos-m2v-pipeline>=0.0.5,<1.0.0 -ovos-ollama-intent-pipeline-plugin>=0.0.1,<1.0.0 + +# pipeline plugins +ovos-m2v-pipeline>=0.0.6,<1.0.0 +ovos-common-query-pipeline-plugin>=1.1.8, <2.0.0 +ovos-adapt-parser>=1.0.6, <2.0.0 +ovos_ocp_pipeline_plugin>=1.1.16, <2.0.0 +ovos-persona>=0.6.23,<1.0.0 +padacioso>=1.0.0, <2.0.0 + +# intent transformer plugins keyword-template-matcher>=0.0.1,<1.0.0 ahocorasick-ner>=0.0.1,<1.0.0 \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c3d6fb7ffda..9bf7cc18c6d 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,14 +3,8 @@ python-dateutil>=2.6, <3.0 watchdog>=2.1, <3.0 combo-lock>=0.2.2, <0.4 -padacioso>=1.0.0, <2.0.0 -ovos-adapt-parser>=1.0.5, <2.0.0 -ovos_ocp_pipeline_plugin>=1.0.10, <2.0.0 -ovos-common-query-pipeline-plugin>=1.0.5,<2.0.0 -ovos-persona>=0.4.4,<1.0.0 - ovos-utils[extras]>=0.6.0,<1.0.0 ovos_bus_client>=0.1.4,<2.0.0 -ovos-plugin-manager>=0.9.0,<1.0.0 +ovos-plugin-manager>=1.0.3,<2.0.0 ovos-config>=0.0.13,<2.0.0 -ovos-workshop>=3.3.4,<4.0.0 +ovos-workshop>=7.0.2,<8.0.0 diff --git a/requirements/skills-audio.txt b/requirements/skills-audio.txt index 3ccf0f146f6..8fe86baf32a 100644 --- a/requirements/skills-audio.txt +++ b/requirements/skills-audio.txt @@ -2,6 +2,5 @@ ovos-skill-boot-finished>=0.4.8,<1.0.0 ovos-skill-audio-recording>=0.2.4,<1.0.0 ovos-skill-dictation>=0.2.5,<1.0.0 -ovos-skill-parrot>=0.1.9,<1.0.0 -ovos-skill-volume>=0.1.7,<1.0.0 -ovos-skill-naptime>=0.3.8,<1.0.0 +ovos-skill-volume>=0.1.16,<1.0.0 +ovos-skill-naptime>=0.3.15,<1.0.0 diff --git a/requirements/skills-desktop.txt b/requirements/skills-desktop.txt index 822399564f6..35c09b68cf1 100644 --- a/requirements/skills-desktop.txt +++ b/requirements/skills-desktop.txt @@ -1,4 +1,4 @@ # skills that require a linux desktop environment -ovos-skill-application-launcher>=0.5.6,<1.0.0 +ovos-skill-application-launcher>=0.5.14,<1.0.0 ovos-skill-wallpapers>=1.0.2,<3.0.0 ovos-skill-screenshot>=0.0.2,<1.0.0 diff --git a/requirements/skills-en.txt b/requirements/skills-en.txt index 09714f0e766..35507b62e2a 100644 --- a/requirements/skills-en.txt +++ b/requirements/skills-en.txt @@ -1,2 +1,4 @@ # skills providing english specific functionality ovos-skill-word-of-the-day +# skills below need translation before they are moved to skill-extras.txt +ovos-skill-days-in-history>=0.3.11,<1.0.0 diff --git a/requirements/skills-essential.txt b/requirements/skills-essential.txt index a2db9274f22..8633b97db80 100644 --- a/requirements/skills-essential.txt +++ b/requirements/skills-essential.txt @@ -1,8 +1,9 @@ # skills providing core functionality (offline) -ovos-skill-fallback-unknown>=0.1.5,<1.0.0 +ovos-skill-fallback-unknown>=0.1.9,<1.0.0 ovos-skill-alerts>=0.1.10,<1.0.0 -ovos-skill-personal>=0.1.7,<1.0.0 -ovos-skill-date-time>=0.4.2,<2.0.0 +ovos-skill-personal>=0.1.19,<1.0.0 +ovos-skill-date-time>=1.1.3,<2.0.0 ovos-skill-hello-world>=0.1.10,<1.0.0 ovos-skill-spelling>=0.2.5,<1.0.0 ovos-skill-diagnostics>=0.0.2,<1.0.0 +ovos-skill-parrot>=0.1.25,<1.0.0 diff --git a/requirements/skills-extra.txt b/requirements/skills-extra.txt index 12c049da2ee..2e034ca983c 100644 --- a/requirements/skills-extra.txt +++ b/requirements/skills-extra.txt @@ -1,12 +1,11 @@ # skills providing non essential functionality -ovos-skill-wordnet>=0.0.10,<1.0.0 +ovos-skill-wordnet>=0.2.5,<1.0.0 ovos-skill-randomness>=0.1.1,<1.0.0; python_version >= "3.10" -ovos-skill-days-in-history>=0.3.6,<1.0.0 ovos-skill-laugh>=0.1.1,<1.0.0 -ovos-skill-number-facts>=0.1.4,<1.0.0 -ovos-skill-iss-location>=0.2.2,<1.0.0 -ovos-skill-cmd>=0.2.5,<1.0.0 -ovos-skill-moviemaster>=0.0.7,<1.0.0 -ovos-skill-confucius-quotes>=0.1.7,<1.0.0 -ovos-skill-icanhazdadjokes>=0.3.1,<1.0.0 +ovos-skill-number-facts>=0.1.12,<1.0.0 +ovos-skill-iss-location>=0.2.16,<1.0.0 +ovos-skill-cmd>=0.2.11,<1.0.0 +ovos-skill-moviemaster>=0.0.12,<1.0.0 +ovos-skill-confucius-quotes>=0.1.13,<1.0.0 +ovos-skill-icanhazdadjokes>=0.3.7,<1.0.0 ovos-skill-camera diff --git a/requirements/skills-gui.txt b/requirements/skills-gui.txt index b214c582386..e6544b7d6c7 100644 --- a/requirements/skills-gui.txt +++ b/requirements/skills-gui.txt @@ -1,3 +1,3 @@ -ovos-skill-homescreen>=3.0.2,<4.0.0 +ovos-skill-homescreen>=3.0.3,<4.0.0 ovos-skill-screenshot>=0.0.2,<1.0.0 ovos-skill-color-picker>=0.0.2,<1.0.0 \ No newline at end of file diff --git a/requirements/skills-internet.txt b/requirements/skills-internet.txt index 3d3198b22a6..4ff3ee3e5c6 100644 --- a/requirements/skills-internet.txt +++ b/requirements/skills-internet.txt @@ -1,8 +1,8 @@ # skills that require internet connectivity, should not be installed in offline devices -ovos-skill-weather>=0.1.11,<2.0.0 -ovos-skill-ddg>=0.1.9,<1.0.0 -ovos-skill-wolfie>=0.2.9,<1.0.0 -ovos-skill-wikipedia>=0.5.3,<1.0.0 -ovos-skill-wikihow>=0.2.5,<1.0.0 -ovos-skill-speedtest>=0.3.2,<1.0.0 +ovos-skill-weather>=1.0.3,<2.0.0 +ovos-skill-ddg>=0.3.5,<1.0.0 +ovos-skill-wolfie>=0.5.8,<1.0.0 +ovos-skill-wikipedia>=0.8.13,<1.0.0 +ovos-skill-wikihow>=0.3.3,<1.0.0 +ovos-skill-speedtest>=0.3.6,<1.0.0 ovos-skill-ip>=0.2.5,<1.0.0 diff --git a/requirements/skills-media.txt b/requirements/skills-media.txt index ad229eba61d..c19a3838900 100644 --- a/requirements/skills-media.txt +++ b/requirements/skills-media.txt @@ -1,6 +1,6 @@ # skills for OCP, require audio playback plugins (usually mpv) ovos-skill-somafm>=0.1.3,<1.0.0 -ovos-skill-news>=0.1.8,<1.0.0 -ovos-skill-pyradios>=0.1.4,<1.0.0 -ovos-skill-local-media>=0.2.4,<1.0.0 -ovos-skill-youtube-music>=0.1.6,<1.0.0 +ovos-skill-news>=0.4.5,<1.0.0 +ovos-skill-pyradios>=0.1.5,<1.0.0 +ovos-skill-local-media>=0.2.12,<1.0.0 +ovos-skill-youtube-music>=0.1.7,<1.0.0 diff --git a/requirements/tests.txt b/requirements/tests.txt index 098263f32ea..44ab4eddb6f 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -5,4 +5,4 @@ pytest-cov>=2.8.1 pytest-testmon>=2.1.3 pytest-randomly>=3.16.0 cov-core>=1.15.0 -ovoscope>=0.3.0,<1.0.0 \ No newline at end of file +ovoscope>=0.5.1,<1.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 697c087b602..b453a160a19 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,8 @@ def required(requirements_file): entry_points={ 'opm.pipeline': PLUGIN_ENTRY_POINT, 'console_scripts': [ - 'ovos-core=ovos_core.__main__:main' + 'ovos-core=ovos_core.__main__:main', + 'ovos-intent-service=ovos_core.intent_services.service:launch_standalone' ] } ) diff --git a/test/end2end/test_converse.py b/test/end2end/test_converse.py new file mode 100644 index 00000000000..03d85d3f8d3 --- /dev/null +++ b/test/end2end/test_converse.py @@ -0,0 +1,142 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG + +from ovoscope import End2EndTest, get_minicroft + + +class TestConverse(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-parrot.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) # reuse for speed, but beware if skills keeping internal state + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_parrot_mode(self): + session = Session("123") + session.pipeline = ["ovos-converse-pipeline-plugin", "ovos-padatious-pipeline-plugin-high"] + + message1 = Message("recognizer_loop:utterance", + {"utterances": ["start parrot mode"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + # NOTE: we dont pass session, End2EndTest will inject/update the session from message1 + message2 = Message("recognizer_loop:utterance", + {"utterances": ["echo test"], "lang": "en-US"}, + {"source": "A", "destination": "B"}) + message3 = Message("recognizer_loop:utterance", + {"utterances": ["stop parrot"], "lang": "en-US"}, + {"source": "A", "destination": "B"}) + message4 = Message("recognizer_loop:utterance", + {"utterances": ["echo test"], "lang": "en-US"}, + {"source": "A", "destination": "B"}) + + expected1 = [ + message1, + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:start_parrot.intent", + data={"utterance": "start parrot mode", "lang": "en-US"}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.start", + data={"name": "ParrotSkill.handle_start_parrot_intent"}, + context={"skill_id": self.skill_id}), + Message("speak", + data={"expect_response": False, + "meta": { + "dialog": "parrot_start", + "data": {}, + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.complete", + data={"name": "ParrotSkill.handle_start_parrot_intent"}, + context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": self.skill_id}), + ] + expected2 = [ + message2, + Message(f"{self.skill_id}.converse.ping", + data={"utterances": ["echo test"], "skill_id": self.skill_id}, + context={}), + Message("skill.converse.pong", + data={"can_handle": True, "skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.converse.request", + data={"utterances": ["echo test"], "lang": "en-US"}, + context={"skill_id": self.skill_id}), + Message("speak", + data={"utterance": "echo test", + "expect_response": False, + "lang": "en-US", + "meta": { + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message("skill.converse.response", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + ] + expected3 = [ + message3, + Message(f"{self.skill_id}.converse.ping", + data={"utterances": ["stop parrot"], "skill_id": self.skill_id}, + context={}), + Message("skill.converse.pong", + data={"can_handle": True, "skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + + Message(f"{self.skill_id}.converse.request", + data={"utterances": ["stop parrot"], "lang": "en-US"}, + context={"skill_id": self.skill_id}), + + Message("speak", + data={"expect_response": False, + "lang": "en-US", + "meta": { + "dialog": "parrot_stop", + "data": {}, + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message("skill.converse.response", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}) + ] + expected4 = [ + message4, + Message(f"{self.skill_id}.converse.ping", + data={"utterances": ["echo test"], "skill_id": self.skill_id}, + context={}), + Message("skill.converse.pong", + data={"can_handle": False, "skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message("mycroft.audio.play_sound", data={"uri": "snd/error.mp3"}), + Message("complete_intent_failure"), + Message("ovos.utterance.handled") + ] + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + flip_points=["recognizer_loop:utterance"], + source_message=[message1, message2, message3, message4], + expected_messages=expected1 + expected2 + expected3 + expected4, + activation_points={f"{self.skill_id}:start_parrot.intent": self.skill_id}, + keep_original_src=[f"{self.skill_id}.converse.ping", "skill.converse.response"] + ) + test.execute(timeout=10) diff --git a/test/end2end/test_fallback.py b/test/end2end/test_fallback.py new file mode 100644 index 00000000000..a11601692fd --- /dev/null +++ b/test/end2end/test_fallback.py @@ -0,0 +1,59 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG + +from ovoscope import End2EndTest, get_minicroft + + +class TestFallback(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-fallback-unknown.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) # reuse for speed, but beware if skills keeping internal state + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_fallback_match(self): + session = Session("123") + session.pipeline = ['ovos-fallback-pipeline-plugin-low'] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + keep_original_src=["ovos.skills.fallback.ping"], # for routing tests this is an exception + source_message=message, + expected_messages=[ + message, + Message("ovos.skills.fallback.ping", + {"utterances": ["hello world"], "lang": "en-US", "range": [90, 101]}), + Message("ovos.skills.fallback.pong", {"skill_id": self.skill_id, "can_handle": True}), + Message(f"ovos.skills.fallback.{self.skill_id}.request", + {"utterances": ["hello world"], "lang": "en-US", "range": [90, 101], "skill_id": self.skill_id}), + Message(f"ovos.skills.fallback.{self.skill_id}.start", {}), + Message("speak", + data={"lang": "en-US", + "expect_response": False, + "meta": { + "dialog": "unknown", + "data": {}, + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message(f"ovos.skills.fallback.{self.skill_id}.response", + data={"fallback_handler":"UnknownSkill.handle_fallback"}, + context={"skill_id": self.skill_id}), + + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) diff --git a/test/end2end/test_helloworld.py b/test/end2end/test_helloworld.py index b99caeebf38..b228387e71e 100644 --- a/test/end2end/test_helloworld.py +++ b/test/end2end/test_helloworld.py @@ -2,36 +2,44 @@ from ovos_bus_client.message import Message from ovos_bus_client.session import Session - -from ovoscope import End2EndTest +from ovos_utils.log import LOG +from ovoscope import End2EndTest, get_minicroft class TestAdaptIntent(TestCase): + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-hello-world.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) # reuse for speed, but beware if skills keeping internal state + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + def test_adapt_match(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["adapt_high"] + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] message = Message("recognizer_loop:utterance", {"utterances": ["hello world"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, - Message(f"{skill_id}.activate", + Message(f"{self.skill_id}.activate", data={}, - context={"skill_id": skill_id}), - Message(f"{skill_id}:HelloWorldIntent", + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:HelloWorldIntent", data={"utterance": "hello world", "lang": "en-US"}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("mycroft.skill.handler.start", data={"name": "HelloWorldSkill.handle_hello_world_intent"}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("speak", data={"utterance": "Hello world", "lang": "en-US", @@ -39,33 +47,31 @@ def test_adapt_match(self): "meta": { "dialog": "hello.world", "data": {}, - "skill": skill_id + "skill": self.skill_id }}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("mycroft.skill.handler.complete", data={"name": "HelloWorldSkill.handle_hello_world_intent"}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("ovos.utterance.handled", data={}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), ] ) test.execute(timeout=10) def test_skill_blacklist(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["adapt_high"] - session.blacklisted_skills = [skill_id] + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + session.blacklisted_skills = [self.skill_id] message = Message("recognizer_loop:utterance", {"utterances": ["hello world"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, @@ -78,18 +84,16 @@ def test_skill_blacklist(self): test.execute(timeout=10) def test_intent_blacklist(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["adapt_high"] - session.blacklisted_intents = [f"{skill_id}:HelloWorldIntent"] + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + session.blacklisted_intents = [f"{self.skill_id}:HelloWorldIntent"] message = Message("recognizer_loop:utterance", {"utterances": ["hello world"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, @@ -102,17 +106,15 @@ def test_intent_blacklist(self): test.execute(timeout=10) def test_padatious_no_match(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["padatious_high"] + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] message = Message("recognizer_loop:utterance", {"utterances": ["hello world"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, @@ -127,63 +129,69 @@ def test_padatious_no_match(self): class TestPadatiousIntent(TestCase): + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-hello-world.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + def test_padatious_match(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["padatious_high"] + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] message = Message("recognizer_loop:utterance", {"utterances": ["good morning"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, - Message(f"{skill_id}.activate", + Message(f"{self.skill_id}.activate", data={}, - context={"skill_id": skill_id}), - Message(f"{skill_id}:Greetings.intent", + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:Greetings.intent", data={"utterance": "good morning", "lang": "en-US"}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("mycroft.skill.handler.start", data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("speak", data={"lang": "en-US", "expect_response": False, "meta": { "dialog": "hello", "data": {}, - "skill": skill_id + "skill": self.skill_id }}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("mycroft.skill.handler.complete", data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), Message("ovos.utterance.handled", data={}, - context={"skill_id": skill_id}), + context={"skill_id": self.skill_id}), ] ) test.execute(timeout=10) def test_skill_blacklist(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["padatious_high"] - session.blacklisted_skills = [skill_id] + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] + session.blacklisted_skills = [self.skill_id] message = Message("recognizer_loop:utterance", {"utterances": ["good morning"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, @@ -196,18 +204,16 @@ def test_skill_blacklist(self): test.execute(timeout=10) def test_intent_blacklist(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["padatious_high"] - session.blacklisted_intents = [f"{skill_id}:Greetings.intent"] + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] + session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] message = Message("recognizer_loop:utterance", {"utterances": ["good morning"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, @@ -220,17 +226,114 @@ def test_intent_blacklist(self): test.execute(timeout=10) def test_adapt_no_match(self): - skill_id = "ovos-skill-hello-world.openvoiceos" session = Session("123") - session.pipeline = ["adapt_high"] + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + +class TestModel2VecIntent(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-hello-world.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_m2v_match(self): + session = Session("123") + session.pipeline = ["ovos-m2v-pipeline-high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:Greetings.intent", + data={"utterance": "good morning", "lang": "en-US"}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.start", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": self.skill_id}), + Message("speak", + data={"lang": "en-US", + "expect_response": False, + "meta": { + "dialog": "hello", + "data": {}, + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.complete", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": self.skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_skill_blacklist(self): + session = Session("123") + session.pipeline = ["ovos-m2v-pipeline-high"] + session.blacklisted_skills = [self.skill_id] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_intent_blacklist(self): + session = Session("123") + session.pipeline = ["ovos-m2v-pipeline-high"] + session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] message = Message("recognizer_loop:utterance", {"utterances": ["good morning"], "lang": "en-US"}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( - skill_ids=[skill_id], - eof_msgs=["ovos.utterance.handled"], - flip_points=["recognizer_loop:utterance"], + minicroft=self.minicroft, + skill_ids=[self.skill_id], source_message=message, expected_messages=[ message, diff --git a/test/end2end/test_no_skills.py b/test/end2end/test_no_skills.py index f0404c574dc..ccbb25aa600 100644 --- a/test/end2end/test_no_skills.py +++ b/test/end2end/test_no_skills.py @@ -1,17 +1,28 @@ from unittest import TestCase from ovos_bus_client.message import Message +from ovos_utils.log import LOG -from ovoscope import End2EndTest +from ovoscope import End2EndTest, get_minicroft class TestNoSkills(TestCase): + def setUp(self): + LOG.set_level("DEBUG") + self.minicroft = get_minicroft([]) # reuse for speed, but beware if skills keeping internal state + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + def test_complete_failure(self): message = Message("recognizer_loop:utterance", {"utterances": ["hello world"]}) test = End2EndTest( + minicroft=self.minicroft, skill_ids=[], eof_msgs=["ovos.utterance.handled"], flip_points=["recognizer_loop:utterance"], @@ -28,11 +39,13 @@ def test_complete_failure(self): def test_routing(self): # this test will validate source and destination are handled properly + # done automatically if "source" and "destination" are in message.context message = Message("recognizer_loop:utterance", {"utterances": ["hello world"]}, {"source": "A", "destination": "B"}) test = End2EndTest( + minicroft=self.minicroft, skill_ids=[], eof_msgs=["ovos.utterance.handled"], flip_points=["recognizer_loop:utterance"], diff --git a/test/unittests/test_intent_service.py b/test/unittests/test_intent_service.py index b8f2a38a270..b5da55b6ccf 100644 --- a/test/unittests/test_intent_service.py +++ b/test/unittests/test_intent_service.py @@ -13,6 +13,7 @@ # limitations under the License. # import time +import unittest from copy import deepcopy from unittest import TestCase, mock @@ -80,41 +81,3 @@ def test_lang_exists(self): msg = Message('test msg', data={'lang': 'sv-se'}) self.assertEqual(get_message_lang(msg), 'sv-SE') - -class TestIntentServiceApi(TestCase): - def setUp(self): - self.bus = FakeBus() - self.emitted = [] - - def on_msg(m): - self.emitted.append(Message.deserialize(m)) - - self.bus.on("message", on_msg) - - self.intent_service = IntentService(self.bus) - - msg = Message('register_vocab', - {'entity_value': 'test', 'entity_type': 'testKeyword'}) - self.intent_service._adapt_service.handle_register_vocab(msg) - - intent = IntentBuilder('skill:testIntent').require('testKeyword') - msg = Message('register_intent', intent.__dict__) - self.intent_service._adapt_service.handle_register_intent(msg) - - def test_get_intent_no_match(self): - """Check that if the intent doesn't match at all None is returned.""" - # Check that no intent is matched - msg = Message('intent.service.intent.get', - data={'utterance': 'five'}) - self.intent_service.handle_get_intent(msg) - reply = self.emitted[-1] - self.assertEqual(reply.data['intent'], None) - - def test_get_intent_match(self): - # Check that intent is matched - msg = Message('intent.service.intent.get', - data={'utterance': 'test'}) - self.intent_service.handle_get_intent(msg) - reply = self.emitted[-1] - time.sleep(3) - self.assertEqual(reply.data['intent']['intent_name'], 'skill:testIntent') diff --git a/test/unittests/test_manager.py b/test/unittests/test_manager.py index 4f67d073a56..d85fb43bb19 100644 --- a/test/unittests/test_manager.py +++ b/test/unittests/test_manager.py @@ -157,36 +157,6 @@ def test_get_internal_skill_bus_not_shared_connection(self, mock_MessageBusClien mock_MessageBusClient.assert_called_once_with(cache=True) self.assertTrue(result.run_in_thread.called) - @patch('ovos_core.skill_manager.LOG') - def test_load_new_skills_with_blacklisted_skill(self, mock_log): - # Mocking find_skill_plugins to return a blacklisted skill - with patch('ovos_core.skill_manager.find_skill_plugins', return_value={'blacklisted_skill': ''}): - # Mocking _load_skill method to prevent actual loading - with patch.object(self.skill_manager, '_load_skill', return_value=None): - self.skill_manager._load_skill = MagicMock() - - # Setting up blacklisted skill in the configuration - self.skill_manager.config['skills']['blacklisted_skills'] = ['blacklisted_skill'] - - # Calling _load_new_skills - self.skill_manager._load_new_skills(network=True, internet=True, gui=True) - self.assertEqual(self.skill_manager._logged_skill_warnings, ["blacklisted_skill"]) - self.skill_manager._load_new_skills(network=True, internet=True, gui=True) - - # Assert that a warning log message is generated once for the blacklisted skill - mock_log.warning.assert_called_once_with("blacklisted_skill is blacklisted, it will NOT be loaded") - mock_log.info.assert_called_once_with( - "Consider uninstalling blacklisted_skill instead of blacklisting it") - - # Mock loading a local directory that is blacklisted - self.skill_manager.config['skills']['blacklisted_skills'].append("local_skill.test") - test_skill_path = join(dirname(__file__), 'local_skill.test') - self.skill_manager._load_skill(test_skill_path) - mock_log.warning.assert_called_with("local_skill.test is blacklisted, it will NOT be loaded") - mock_log.info.assert_called_with( - f"Consider deleting {test_skill_path} instead of blacklisting it") - self.assertIn("local_skill.test", self.skill_manager._logged_skill_warnings) - if __name__ == '__main__': unittest.main() diff --git a/test/unittests/test_skill_manager.py b/test/unittests/test_skill_manager.py index 9bbab883dee..5665fe176f8 100644 --- a/test/unittests/test_skill_manager.py +++ b/test/unittests/test_skill_manager.py @@ -89,7 +89,7 @@ def _mock_skill_loader_instance(self): self.skill_loader_mock.instance.converse = Mock() self.skill_loader_mock.instance.converse.return_value = True self.skill_loader_mock.skill_id = 'test_skill' - self.skill_manager.skill_loaders = { + self.skill_manager.plugin_skills = { str(self.skill_dir): self.skill_loader_mock } @@ -114,11 +114,6 @@ def test_instantiate(self): self.assertListEqual(expected_result, self.message_bus_mock.event_handlers) - def test_unload_removed_skills(self): - self.skill_manager._unload_removed_skills() - - self.assertDictEqual({}, self.skill_manager.skill_loaders) - self.skill_loader_mock.unload.assert_called_once_with() def test_send_skill_list(self): self.skill_loader_mock.active = True @@ -158,9 +153,9 @@ def test_deactivate_except(self): foo2_skill_loader.skill_id = 'foo2' test_skill_loader = Mock(spec=SkillLoader) test_skill_loader.skill_id = 'test_skill' - self.skill_manager.skill_loaders['foo'] = foo_skill_loader - self.skill_manager.skill_loaders['foo2'] = foo2_skill_loader - self.skill_manager.skill_loaders['test_skill'] = test_skill_loader + self.skill_manager.plugin_skills['foo'] = foo_skill_loader + self.skill_manager.plugin_skills['foo2'] = foo2_skill_loader + self.skill_manager.plugin_skills['test_skill'] = test_skill_loader self.skill_manager.deactivate_except(message) foo_skill_loader.deactivate.assert_called_once() @@ -174,8 +169,8 @@ def test_activate_skill(self): test_skill_loader.skill_id = 'test_skill' test_skill_loader.active = False - self.skill_manager.skill_loaders = {} - self.skill_manager.skill_loaders['test_skill'] = test_skill_loader + self.skill_manager.plugin_skills = {} + self.skill_manager.plugin_skills['test_skill'] = test_skill_loader self.skill_manager.activate_skill(message) test_skill_loader.activate.assert_called_once() From 4508108e8be4ed11e6ebee6cc141e139be4c58bb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 10 Jun 2025 02:52:11 +0000 Subject: [PATCH 18/57] Increment Version to 2.0.0a1 --- ovos_core/version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index d7ba2748152..720db92cbca 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,7 +1,7 @@ # START_VERSION_BLOCK -VERSION_MAJOR = 1 -VERSION_MINOR = 5 -VERSION_BUILD = 1 +VERSION_MAJOR = 2 +VERSION_MINOR = 0 +VERSION_BUILD = 0 VERSION_ALPHA = 1 # END_VERSION_BLOCK From 3c398468939a1c019225bec7c4720ea92f09b5e8 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 10 Jun 2025 02:52:56 +0000 Subject: [PATCH 19/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476f023c306..6f0b335e9e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.0a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.0a1) (2025-06-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.1a1...2.0.0a1) + +**Breaking changes:** + +- refactor!: drop old pipeline plugins and deprecated methods [\#690](https://github.com/OpenVoiceOS/ovos-core/pull/690) ([JarbasAl](https://github.com/JarbasAl)) + ## [1.5.1a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.5.1a1) (2025-06-09) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.0a3...1.5.1a1) From 1eef060b560a7ad0f7672cb6e65065056b1cd277 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 12 Jun 2025 22:19:37 +0100 Subject: [PATCH 20/57] fix: minimum requirements + add galician skills extras (#699) --- requirements/plugins.txt | 4 ++-- requirements/skills-gl.txt | 2 ++ setup.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 requirements/skills-gl.txt diff --git a/requirements/plugins.txt b/requirements/plugins.txt index 2ac495659ed..6981ac1d896 100644 --- a/requirements/plugins.txt +++ b/requirements/plugins.txt @@ -15,5 +15,5 @@ ovos-persona>=0.6.23,<1.0.0 padacioso>=1.0.0, <2.0.0 # intent transformer plugins -keyword-template-matcher>=0.0.1,<1.0.0 -ahocorasick-ner>=0.0.1,<1.0.0 \ No newline at end of file +keyword-template-matcher>=0.1.1,<1.0.0 +ahocorasick-ner>=0.1.1,<1.0.0 \ No newline at end of file diff --git a/requirements/skills-gl.txt b/requirements/skills-gl.txt new file mode 100644 index 00000000000..f9ec9d061f9 --- /dev/null +++ b/requirements/skills-gl.txt @@ -0,0 +1,2 @@ +# skills providing galician specific functionality +ovos-skill-word-of-the-day>=0.2.0 diff --git a/setup.py b/setup.py index b453a160a19..28ec89f3496 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,6 @@ def get_version(): """ Find the version of ovos-core""" - version = None version_file = os.path.join(BASEDIR, 'ovos_core', 'version.py') major, minor, build, alpha = (0, 0, 0, 0) with open(version_file) as f: @@ -87,6 +86,7 @@ def required(requirements_file): 'skills-media': required('requirements/skills-media.txt'), 'skills-ca': required('requirements/skills-ca.txt'), 'skills-pt': required('requirements/skills-pt.txt'), + 'skills-gl': required('requirements/skills-gl.txt'), 'skills-en': required('requirements/skills-en.txt') }, packages=find_packages(include=['ovos_core*']), From 8628bcd9461767751d0deacd63837834bf9c724e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 12 Jun 2025 21:19:53 +0000 Subject: [PATCH 21/57] Update translations --- .../locale/nl-nl/global_stop.intent | 42 +++++++++---------- .../intent_services/locale/nl-nl/stop.intent | 18 ++++---- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/ovos_core/intent_services/locale/nl-nl/global_stop.intent b/ovos_core/intent_services/locale/nl-nl/global_stop.intent index fbe3afc8b57..d030bdac5c7 100644 --- a/ovos_core/intent_services/locale/nl-nl/global_stop.intent +++ b/ovos_core/intent_services/locale/nl-nl/global_stop.intent @@ -1,31 +1,31 @@ -Annuleer alle lopende acties -Annuleer alle taken -Beëindig alle acties -Beëindig alle lopende acties +Alle lopende processen afbreken +Alle taken annuleren +Beëindig alle bewerkingen Beëindig alle processen -Rond alle activiteiten af -Stop alle activiteiten +Stop alle acties Stop alle huidige taken -Stop alle lopende acties -Stop alle lopende processen -Stop alle lopende processen -Stop met alle acties -Stop nu met alles -Stop onmiddellijk alle acties -Voltooi alle openstaande taken +Stop nu alles +Stop onmiddellijk alle activiteiten +Voltooi alle activiteiten +alles afbreken +alles afbreken +alles afmaken +alles afmaken +alles annuleren alles annuleren alles beëindigen alles beëindigen alles stoppen -alles stoppen -alles stoppen -alles stoppen -alles stoppen -alles stopzetten -annuleer alles -beëindig alles beëindig alles +genoeg +hou op +hou op met praten +kappen +kappen nu +niet meer praten +nu ophouden +stop alles stop alles stop alles stop alles -stop met alles \ No newline at end of file +stop met praten \ No newline at end of file diff --git a/ovos_core/intent_services/locale/nl-nl/stop.intent b/ovos_core/intent_services/locale/nl-nl/stop.intent index 15e78b7c979..a3c1ff8dd4e 100644 --- a/ovos_core/intent_services/locale/nl-nl/stop.intent +++ b/ovos_core/intent_services/locale/nl-nl/stop.intent @@ -1,17 +1,17 @@ -(kun|kan) je nu stoppen Annuleer de huidige taak -Beëindig de huidige actie Beëindig de huidige taak -Beëindig the current action -Ga niet verder -Hou daar alsjeblieft mee op -Maak er een einde aan -Stop de huidige actie +Kun je nu stoppen? +Maak er alsjeblieft een einde aan +Stop a.u.b. +Stop alstublieft met de huidige actie +Stop daar alsjeblieft mee Stop de huidige actie +Stop de huidige activiteit Stop het lopende proces Stop met het uitvoeren van de huidige opdracht Stop met het uitvoeren van die taak -Stop waar je mee bezig bent +Stop wat je doet +Stoppen maar stop -stop dit +stop daarmee stop ermee \ No newline at end of file From a64a2b44e19ba625f0b7c7dbe67cfb3b8fbea636 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 12 Jun 2025 21:20:04 +0000 Subject: [PATCH 22/57] Increment Version to 2.0.1a1 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 720db92cbca..99dbb0420ca 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,7 +1,7 @@ # START_VERSION_BLOCK VERSION_MAJOR = 2 VERSION_MINOR = 0 -VERSION_BUILD = 0 +VERSION_BUILD = 1 VERSION_ALPHA = 1 # END_VERSION_BLOCK From b35de09da0dd61b285d26060ae2a7025375de216 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 12 Jun 2025 21:20:44 +0000 Subject: [PATCH 23/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0b335e9e6..606cfb70f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.1a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.1a1) (2025-06-12) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.0a1...2.0.1a1) + +**Merged pull requests:** + +- fix: minimum requirements + add galician skills extras [\#699](https://github.com/OpenVoiceOS/ovos-core/pull/699) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.0a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.0a1) (2025-06-10) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/1.5.1a1...2.0.0a1) From 8ca55b3808ea437bb002463ff680242758ac6b9b Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 12 Jun 2025 23:09:26 +0100 Subject: [PATCH 24/57] Update dependabot.yml --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 26e59a20206..636f346535d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,4 @@ updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/requirements" # Location of package manifests schedule: - interval: "weekly" + interval: "daily" From cc9db5eb6775b0792dd1074207cb4e94c9bd37ba Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 16 Jun 2025 00:44:51 +0100 Subject: [PATCH 25/57] refactor: improve_stop (#702) * refactor: improve_stop * simplify * feat: test message number * fix: fallback test --- ovos_core/intent_services/stop_service.py | 10 +- requirements/plugins.txt | 4 +- requirements/skills-essential.txt | 1 + requirements/tests.txt | 2 +- test/end2end/test_fallback.py | 2 +- test/end2end/test_stop.py | 269 ++++++++++++++++++++++ 6 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 test/end2end/test_stop.py diff --git a/ovos_core/intent_services/stop_service.py b/ovos_core/intent_services/stop_service.py index 109759bf2ad..0b9bf98d012 100644 --- a/ovos_core/intent_services/stop_service.py +++ b/ovos_core/intent_services/stop_service.py @@ -28,6 +28,12 @@ def __init__(self, bus: Optional[Union[MessageBusClient, FakeBus]] = None, super().__init__(config=config, bus=bus) self._voc_cache = {} self.load_resource_files() + self.bus.on("stop:global", self.handle_global_stop) + + def handle_global_stop(self, message: Message): + self.bus.emit(message.forward("mycroft.stop")) + # TODO - this needs a confirmation dialog if nothing was stopped + self.bus.emit(message.forward("ovos.utterance.handled")) def load_resource_files(self): base = f"{dirname(__file__)}/locale" @@ -191,7 +197,7 @@ def match_high(self, utterances: List[str], lang: str, message: Message) -> Opti LOG.info(f"Emitting global stop, {len(self.get_active_skills(message))} active skills") # emit a global stop, full stop anything OVOS is doing return IntentHandlerMatch( - match_type="mycroft.stop", + match_type="stop:global", match_data={"conf": conf}, updated_session=sess, utterance=utterance, @@ -303,7 +309,7 @@ def match_low(self, utterances: List[str], lang: str, message: Message) -> Optio # emit a global stop, full stop anything OVOS is doing LOG.debug(f"Emitting global stop signal, {len(self.get_active_skills(message))} active skills") return IntentHandlerMatch( - match_type="mycroft.stop", + match_type="stop:global", match_data={"conf": conf}, updated_session=sess, utterance=utterance, diff --git a/requirements/plugins.txt b/requirements/plugins.txt index 6981ac1d896..f7e0e9cc24b 100644 --- a/requirements/plugins.txt +++ b/requirements/plugins.txt @@ -1,7 +1,7 @@ -ovos-utterance-corrections-plugin>=0.0.2, <1.0.0 +ovos-utterance-corrections-plugin>=0.1.1, <1.0.0 ovos-utterance-plugin-cancel>=0.2.3, <1.0.0 ovos-bidirectional-translation-plugin>=0.1.0, <1.0.0 -ovos-translate-server-plugin>=0.0.2, <1.0.0 +ovos-translate-server-plugin>=0.0.4, <1.0.0 ovos-utterance-normalizer>=0.2.2, <1.0.0 ovos-number-parser>=0.0.1,<1.0.0 ovos-date-parser>=0.0.3,<1.0.0 diff --git a/requirements/skills-essential.txt b/requirements/skills-essential.txt index 8633b97db80..ba81dc0cb7a 100644 --- a/requirements/skills-essential.txt +++ b/requirements/skills-essential.txt @@ -7,3 +7,4 @@ ovos-skill-hello-world>=0.1.10,<1.0.0 ovos-skill-spelling>=0.2.5,<1.0.0 ovos-skill-diagnostics>=0.0.2,<1.0.0 ovos-skill-parrot>=0.1.25,<1.0.0 +ovos-skill-count>=0.0.1,<1.0.0 \ No newline at end of file diff --git a/requirements/tests.txt b/requirements/tests.txt index 44ab4eddb6f..789bea8775b 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -5,4 +5,4 @@ pytest-cov>=2.8.1 pytest-testmon>=2.1.3 pytest-randomly>=3.16.0 cov-core>=1.15.0 -ovoscope>=0.5.1,<1.0.0 \ No newline at end of file +ovoscope>=0.6.0,<1.0.0 \ No newline at end of file diff --git a/test/end2end/test_fallback.py b/test/end2end/test_fallback.py index a11601692fd..ce287eef950 100644 --- a/test/end2end/test_fallback.py +++ b/test/end2end/test_fallback.py @@ -52,7 +52,7 @@ def test_fallback_match(self): data={"fallback_handler":"UnknownSkill.handle_fallback"}, context={"skill_id": self.skill_id}), - Message("ovos.utterance.handled", {}) + #vMessage("ovos.utterance.handled", {}) # TODO (ovos-workshop) - missing ] ) diff --git a/test/end2end/test_stop.py b/test/end2end/test_stop.py new file mode 100644 index 00000000000..6c8adc5327c --- /dev/null +++ b/test/end2end/test_stop.py @@ -0,0 +1,269 @@ +import time +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils import create_daemon +from ovos_utils.log import LOG + +from ovoscope import End2EndTest, get_minicroft + + +class TestStopNoSkills(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.minicroft = get_minicroft([]) # reuse for speed, but beware if skills keeping internal state # to make tests easier to grok + self.ignore_messages = ["speak", + "ovos.common_play.stop.response", + "common_query.openvoiceos.stop.response", + "persona.openvoiceos.stop.response" + ] + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_exact(self): + session = Session("123") + session.pipeline = ['ovos-stop-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"utterances": ["stop"], "lang": "en-US"}, + {"session": session.serialize()}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + ignore_messages=self.ignore_messages, + source_message=message, + expected_messages=[ + message, + Message("stop.openvoiceos.activate", {}), # stop pipeline counts as active_skill + + Message("stop:global", {}), # global stop, no active skill + Message("mycroft.stop", {}), + + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute() + + def test_not_exact_high(self): + session = Session("123") + session.pipeline = ['ovos-stop-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"utterances": ["could you stop that"], "lang": "en-US"}, + {"session": session.serialize()}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + ignore_messages=self.ignore_messages, + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}), + ] + ) + + test.execute() + + def test_not_exact_med(self): + session = Session("123") + session.pipeline = ['ovos-stop-pipeline-plugin-medium'] + message = Message("recognizer_loop:utterance", + {"utterances": ["could you stop that"], "lang": "en-US"}, + {"session": session.serialize()}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + ignore_messages=self.ignore_messages, + expected_messages=[ + message, + Message("stop.openvoiceos.activate", {}), # stop pipeline counts as active_skill + + Message("stop:global", {}), # global stop, no active skill + Message("mycroft.stop", {}), + + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute() + + +class TestCountSkills(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-count.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) # reuse for speed, but beware if skills keeping internal state + # to make tests easier to grok + self.ignore_messages = ["speak", + "ovos.common_play.stop.response", + "common_query.openvoiceos.stop.response", + "persona.openvoiceos.stop.response" + ] + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_count(self): + session = Session("123") + session.pipeline = ['ovos-stop-pipeline-plugin-high', "ovos-padatious-pipeline-plugin-high"] + + message = Message("recognizer_loop:utterance", + {"utterances": ["count to 3"], "lang": "en-US"}, + {"session": session.serialize()}) + + # first count to 10 to validate skill is working + activate_skill = [ + message, + Message("ovos-skill-count.openvoiceos.activate", {}), # skill is activated + Message("ovos-skill-count.openvoiceos:count_to_N.intent", {}), # intent triggers + + Message("mycroft.skill.handler.start", { + "name": "CountSkill.handle_how_are_you_intent" + }), + # here would be N speak messages, but we ignore them in this test + Message("mycroft.skill.handler.complete", { + "name": "CountSkill.handle_how_are_you_intent" + }), + + Message("ovos.utterance.handled", {}) + ] + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + ignore_messages=self.ignore_messages, + source_message=message, + expected_messages=activate_skill + ) + test.execute() + + def test_count_infinity_active(self): + session = Session("123") + session.pipeline = ['ovos-stop-pipeline-plugin-high', + "ovos-padatious-pipeline-plugin-high"] + + def make_it_count(): + nonlocal session + message = Message("recognizer_loop:utterance", + {"utterances": ["count to infinity"], "lang": "en-US"}, + {"session": session.serialize()}) + session.activate_skill(self.skill_id) # ensure in active skill list + self.minicroft.bus.emit(message) + + # count to infinity, the skill will keep running in the background + create_daemon(make_it_count) + + time.sleep(3) + + message = Message("recognizer_loop:utterance", + {"utterances": ["stop"], "lang": "en-US"}, + {"session": session.serialize()}) # skill in active list now + + stop_skill_active = [ + message, + Message("ovos-skill-count.openvoiceos.stop.ping", + {"skill_id":self.skill_id}), + Message("skill.stop.pong", + {"skill_id": self.skill_id, "can_handle": True}, + {"skill_id": self.skill_id}), + + Message("stop.openvoiceos.activate", + context={"skill_id": "stop.openvoiceos"}), + Message(f"{self.skill_id}.stop", + context={"skill_id": "stop.openvoiceos"}), + Message(f"{self.skill_id}.stop.response", + {"skill_id": self.skill_id, "result": True}, + {"skill_id": self.skill_id}), + + # skill callback to stop everything + # TODO - clean up! most arent needed/can check session if needed (ovos-workshop) + Message("mycroft.skills.abort_question", {"skill_id": self.skill_id}, + {"skill_id": self.skill_id}), + Message("ovos.skills.converse.force_timeout", {"skill_id": self.skill_id}, + {"skill_id": self.skill_id}), + Message("mycroft.audio.speech.stop", {"skill_id": self.skill_id}, + {"skill_id": self.skill_id}), + + # the intent running in the daemon thread exits cleanly + Message("mycroft.skill.handler.complete", + {"name": "CountSkill.handle_how_are_you_intent"}, + {"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + {"name": "CountSkill.handle_how_are_you_intent"}, + {"skill_id": self.skill_id}) + ] + test = End2EndTest( + minicroft=self.minicroft, + # inject_active=[self.skill_id], # ensure this skill is in active skills list for the test + skill_ids=[], + eof_msgs=[], + flip_points=["recognizer_loop:utterance"], + ignore_messages=self.ignore_messages, + source_message=message, + expected_messages=stop_skill_active + ) + test.execute() + + def test_count_infinity_global(self): + session = Session("123") + session.pipeline = ['ovos-stop-pipeline-plugin-high', + "ovos-padatious-pipeline-plugin-high"] + + def make_it_count(): + message = Message("recognizer_loop:utterance", + {"utterances": ["count to infinity"], "lang": "en-US"}, + {"session": session.serialize()}) + self.minicroft.bus.emit(message) + + # count to infinity, the skill will keep running in the background + create_daemon(make_it_count) + + time.sleep(3) + + # NOTE: skill not in active skill list for this Session, global stop will match instead + # this doesnt typically happen at runtime, but possible since clients send whatever Session they want + message = Message("recognizer_loop:utterance", + {"utterances": ["stop"], "lang": "en-US"}, + {"session": session.serialize()}) + stop_skill_from_global = [ + message, + Message("stop.openvoiceos.activate", {}), # stop pipeline counts as active_skill + + Message("stop:global", {}), # global stop, no active skill + Message("mycroft.stop", {}), + + Message(f"{self.skill_id}.stop.response", + {"skill_id": self.skill_id, "result": True}), + Message("ovos.utterance.handled", {}) + ] + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + ignore_messages=self.ignore_messages, + source_message=message, + expected_messages=stop_skill_from_global + ) + test.execute() + From da5d14eb135b8a4e3ed02fe5b395f54db0c071f6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 15 Jun 2025 23:45:17 +0000 Subject: [PATCH 26/57] Increment Version to 2.0.1a2 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 99dbb0420ca..fc3a2f98833 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 2 VERSION_MINOR = 0 VERSION_BUILD = 1 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK # for compat with old imports From 7dbb8d309bcc1e800a3ce2c4b3f4de651c6e1dc4 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 15 Jun 2025 23:46:03 +0000 Subject: [PATCH 27/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 606cfb70f0b..721f2c63081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.1a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.1a2) (2025-06-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.1a1...2.0.1a2) + +**Merged pull requests:** + +- refactor: improve\_stop [\#702](https://github.com/OpenVoiceOS/ovos-core/pull/702) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.1a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.1a1) (2025-06-12) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.0a1...2.0.1a1) From 9ab5ec5bb8f4e56e32576cdcde8f7b797f5b15c6 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 16 Jun 2025 01:59:11 +0100 Subject: [PATCH 28/57] fix: improve language disambiguation (#704) * tests/increase_coverage coverage for language disambiguation * improve lang matching * test invalid lang detection --- ovos_core/intent_services/service.py | 22 +++-- test/end2end/test_lang_detect.py | 131 +++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 test/end2end/test_lang_detect.py diff --git a/ovos_core/intent_services/service.py b/ovos_core/intent_services/service.py index 1659ab770b0..841acf28282 100644 --- a/ovos_core/intent_services/service.py +++ b/ovos_core/intent_services/service.py @@ -20,6 +20,7 @@ from typing import Tuple, Callable, List import requests +from langcodes import closest_match from ovos_bus_client.message import Message from ovos_bus_client.session import SessionManager from ovos_bus_client.util import get_message_lang @@ -151,20 +152,24 @@ def disambiguate_lang(message): 4 - config lang (or from message.data) """ default_lang = get_message_lang(message) - valid_langs = get_valid_languages() + valid_langs = message.context.get("valid_langs") or get_valid_languages() valid_langs = [standardize_lang_tag(l) for l in valid_langs] lang_keys = ["stt_lang", "request_lang", "detected_lang"] for k in lang_keys: if k in message.context: - v = standardize_lang_tag(message.context[k]) - if v in valid_langs: # TODO - use lang distance instead to choose best dialect - if v != default_lang: - LOG.info(f"replaced {default_lang} with {k}: {v}") - return v - else: + try: + v = standardize_lang_tag(message.context[k]) + best_lang, _ = closest_match(v, valid_langs, max_distance=10) + except: + v = message.context[k] + best_lang = "und" + if best_lang == "und": LOG.warning(f"ignoring {k}, {v} is not in enabled languages: {valid_langs}") + continue + LOG.info(f"replaced {default_lang} with {k}: {v}") + return v return default_lang @@ -484,6 +489,7 @@ def handle_utterance(self, message: Message): else: # Nothing was able to handle the intent # Ask politely for forgiveness for failing in this vital task + message.data["lang"] = lang self.send_complete_intent_failure(message) LOG.debug(f"intent matching took: {stopwatch.time}") @@ -504,7 +510,7 @@ def send_complete_intent_failure(self, message): sound = Configuration().get('sounds', {}).get('error', "snd/error.mp3") # NOTE: message.reply to ensure correct message destination self.bus.emit(message.reply('mycroft.audio.play_sound', {"uri": sound})) - self.bus.emit(message.reply('complete_intent_failure')) + self.bus.emit(message.reply('complete_intent_failure', message.data)) self.bus.emit(message.reply("ovos.utterance.handled")) @staticmethod diff --git a/test/end2end/test_lang_detect.py b/test/end2end/test_lang_detect.py new file mode 100644 index 00000000000..b1cebd98a76 --- /dev/null +++ b/test/end2end/test_lang_detect.py @@ -0,0 +1,131 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG + +from ovoscope import End2EndTest, get_minicroft + + +class TestLangDisambiguation(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.minicroft = get_minicroft([]) # reuse for speed, but beware if skills keeping internal state + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_stt_lang(self): + session = Session("123") + session.lang = "en-US" + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": session.lang}, + {"session": session.serialize()}) + lang_keys = { + "stt_lang": "ca-ES", # lang detection from audio plugin + "request_lang": "pt-PT", # lang tagged in source message (wake word config) + "detected_lang": "nl-NL" # lang detection from utterance (text) plugin + } + message.context.update(lang_keys) + message.context["valid_langs"] = list(lang_keys.values()) + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {"lang": lang_keys["stt_lang"]}), + Message("ovos.utterance.handled", {}), + ] + ) + + test.execute() + + + def test_lang_text_detection(self): + session = Session("123") + session.lang = "en-US" + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": session.lang}, + {"session": session.serialize()}) + lang_keys = { + "detected_lang": "nl-NL" # lang detection from utterance (text) plugin + } + message.context.update(lang_keys) + message.context["valid_langs"] = list(lang_keys.values()) + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {"lang": lang_keys["detected_lang"]}), + Message("ovos.utterance.handled", {}), + ] + ) + + test.execute() + + def test_metadata_preferred_over_text_detection(self): + session = Session("123") + session.lang = "en-US" + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": session.lang}, + {"session": session.serialize()}) + lang_keys = { + "request_lang": "pt-PT", # lang tagged in source message (wake word config) + "detected_lang": "nl-NL" # lang detection from utterance (text) plugin + } + message.context.update(lang_keys) + message.context["valid_langs"] = list(lang_keys.values()) + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {"lang": lang_keys["request_lang"]}), + Message("ovos.utterance.handled", {}), + ] + ) + + test.execute() + + def test_invalid_lang_detection(self): + session = Session("123") + session.lang = "en-US" + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": session.lang}, + {"session": session.serialize()}) + lang_keys = { + "detected_lang": "nl-NL" + } + message.context.update(lang_keys) + message.context["valid_langs"] = [session.lang] # no nl-NL + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=["ovos.utterance.handled"], + flip_points=["recognizer_loop:utterance"], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {"lang": session.lang}), + Message("ovos.utterance.handled", {}), + ] + ) + + test.execute() From 393c891d92dbb2e5304966393057b3a9e6909409 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 00:59:40 +0000 Subject: [PATCH 29/57] Increment Version to 2.0.2a1 --- ovos_core/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index fc3a2f98833..b6cb9ed6c86 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 2 VERSION_MINOR = 0 -VERSION_BUILD = 1 -VERSION_ALPHA = 2 +VERSION_BUILD = 2 +VERSION_ALPHA = 1 # END_VERSION_BLOCK # for compat with old imports From 8c904c4f60615ab3043f2caa092fa1fb3314e144 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 01:00:23 +0000 Subject: [PATCH 30/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721f2c63081..b79b53ab401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.2a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.2a1) (2025-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.1a2...2.0.2a1) + +**Merged pull requests:** + +- fix: improve language disambiguation [\#704](https://github.com/OpenVoiceOS/ovos-core/pull/704) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.1a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.1a2) (2025-06-15) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.1a1...2.0.1a2) From 4951b0b88a18612bae043dd71f0a233970157545 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:30:15 +0100 Subject: [PATCH 31/57] Update ovos-config requirement in /requirements (#707) Updates the requirements on [ovos-config](https://github.com/OpenVoiceOS/ovos-config) to permit the latest version. - [Release notes](https://github.com/OpenVoiceOS/ovos-config/releases) - [Changelog](https://github.com/OpenVoiceOS/ovos-config/blob/dev/CHANGELOG.md) - [Commits](https://github.com/OpenVoiceOS/ovos-config/compare/0.1.0a1...2.0.0) --- updated-dependencies: - dependency-name: ovos-config dependency-version: 2.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9bf7cc18c6d..cb5da128e92 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,5 +6,5 @@ combo-lock>=0.2.2, <0.4 ovos-utils[extras]>=0.6.0,<1.0.0 ovos_bus_client>=0.1.4,<2.0.0 ovos-plugin-manager>=1.0.3,<2.0.0 -ovos-config>=0.0.13,<2.0.0 +ovos-config>=0.0.13,<3.0.0 ovos-workshop>=7.0.2,<8.0.0 From e5347711253f8d1837cdb6566cd7957c73369396 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 10:30:43 +0000 Subject: [PATCH 32/57] Increment Version to 2.0.2a2 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index b6cb9ed6c86..8625e7e020d 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 2 VERSION_MINOR = 0 VERSION_BUILD = 2 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK # for compat with old imports From d5dbd309ba4fd2f908720b0a4b324e814737e92c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 10:31:28 +0000 Subject: [PATCH 33/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b79b53ab401..99d2731c9f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.2a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.2a2) (2025-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.2a1...2.0.2a2) + +**Merged pull requests:** + +- Update ovos-config requirement from \<2.0.0,\>=0.0.13 to \>=0.0.13,\<3.0.0 in /requirements [\#707](https://github.com/OpenVoiceOS/ovos-core/pull/707) ([dependabot[bot]](https://github.com/apps/dependabot)) + ## [2.0.2a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.2a1) (2025-06-16) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.1a2...2.0.2a1) From 4863f27cfe38a9546726a43317a34e3df615db77 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:37:38 +0100 Subject: [PATCH 34/57] fix: stop message.context source/destination (#706) * refactor: reduce bus spam * fix: stop message routing --- ovos_core/intent_services/stop_service.py | 35 ++++++++++++++++------- test/end2end/test_stop.py | 35 ++++++++++++++++------- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/ovos_core/intent_services/stop_service.py b/ovos_core/intent_services/stop_service.py index 0b9bf98d012..74f34bb92ca 100644 --- a/ovos_core/intent_services/stop_service.py +++ b/ovos_core/intent_services/stop_service.py @@ -7,7 +7,7 @@ from langcodes import closest_match from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager +from ovos_bus_client.session import SessionManager, UtteranceState from ovos_config.config import Configuration from ovos_plugin_manager.templates.pipeline import ConfidenceMatcherPipeline, IntentHandlerMatch @@ -29,12 +29,17 @@ def __init__(self, bus: Optional[Union[MessageBusClient, FakeBus]] = None, self._voc_cache = {} self.load_resource_files() self.bus.on("stop:global", self.handle_global_stop) + self.bus.on("stop:skill", self.handle_skill_stop) def handle_global_stop(self, message: Message): self.bus.emit(message.forward("mycroft.stop")) # TODO - this needs a confirmation dialog if nothing was stopped self.bus.emit(message.forward("ovos.utterance.handled")) + def handle_skill_stop(self, message: Message): + skill_id = message.data["skill_id"] + self.bus.emit(message.reply(f"{skill_id}.stop")) + def load_resource_files(self): base = f"{dirname(__file__)}/locale" for lang in os.listdir(base): @@ -148,11 +153,21 @@ def handle_stop_confirmation(self, message: Message): error_msg = message.data['error'] LOG.error(f"{skill_id}: {error_msg}") elif message.data.get('result', False): - # force-kill any ongoing get_response/converse/TTS - see @killable_event decorator - self.bus.emit(message.forward("mycroft.skills.abort_question", {"skill_id": skill_id})) - self.bus.emit(message.forward("ovos.skills.converse.force_timeout", {"skill_id": skill_id})) - # TODO - track if speech is coming from this skill! not currently tracked - self.bus.emit(message.reply("mycroft.audio.speech.stop", {"skill_id": skill_id})) + sess = SessionManager.get(message) + utt_state = sess.utterance_states.get(skill_id, UtteranceState.INTENT) + if utt_state == UtteranceState.RESPONSE: + LOG.debug("Forcing get_response timeout") + # force-kill any ongoing get_response - see @killable_event decorator (ovos-workshop) + self.bus.emit(message.reply("mycroft.skills.abort_question", {"skill_id": skill_id})) + if sess.is_active(skill_id): + LOG.debug("Forcing converse timeout") + # force-kill any ongoing converse - see @killable_event decorator (ovos-workshop) + self.bus.emit(message.reply("ovos.skills.converse.force_timeout", {"skill_id": skill_id})) + + # TODO - track if speech is coming from this skill! not currently tracked (ovos-audio) + if sess.is_speaking: + # force-kill any ongoing TTS + self.bus.emit(message.forward("mycroft.audio.speech.stop", {"skill_id": skill_id})) def match_high(self, utterances: List[str], lang: str, message: Message) -> Optional[IntentHandlerMatch]: """ @@ -211,8 +226,8 @@ def match_high(self, utterances: List[str], lang: str, message: Message) -> Opti sess.disable_response_mode(skill_id) self.bus.once(f"{skill_id}.stop.response", self.handle_stop_confirmation) return IntentHandlerMatch( - match_type=f"{skill_id}.stop", - match_data={"conf": conf}, + match_type="stop:skill", + match_data={"conf": conf, "skill_id": skill_id}, updated_session=sess, utterance=utterance, skill_id="stop.openvoiceos" @@ -299,8 +314,8 @@ def match_low(self, utterances: List[str], lang: str, message: Message) -> Optio sess.disable_response_mode(skill_id) self.bus.once(f"{skill_id}.stop.response", self.handle_stop_confirmation) return IntentHandlerMatch( - match_type=f"{skill_id}.stop", - match_data={"conf": conf}, + match_type="stop:skill", + match_data={"conf": conf, "skill_id": skill_id}, updated_session=sess, utterance=utterance, skill_id="stop.openvoiceos" diff --git a/test/end2end/test_stop.py b/test/end2end/test_stop.py index 6c8adc5327c..2e7c2b5545f 100644 --- a/test/end2end/test_stop.py +++ b/test/end2end/test_stop.py @@ -166,7 +166,7 @@ def make_it_count(): nonlocal session message = Message("recognizer_loop:utterance", {"utterances": ["count to infinity"], "lang": "en-US"}, - {"session": session.serialize()}) + {"session": session.serialize(), "source": "A", "destination": "B"}) session.activate_skill(self.skill_id) # ensure in active skill list self.minicroft.bus.emit(message) @@ -177,11 +177,11 @@ def make_it_count(): message = Message("recognizer_loop:utterance", {"utterances": ["stop"], "lang": "en-US"}, - {"session": session.serialize()}) # skill in active list now + {"session": session.serialize(), "source": "A", "destination": "B"}) stop_skill_active = [ message, - Message("ovos-skill-count.openvoiceos.stop.ping", + Message(f"{self.skill_id}.stop.ping", {"skill_id":self.skill_id}), Message("skill.stop.pong", {"skill_id": self.skill_id, "can_handle": True}, @@ -189,21 +189,29 @@ def make_it_count(): Message("stop.openvoiceos.activate", context={"skill_id": "stop.openvoiceos"}), + Message("stop:skill", + context={"skill_id": "stop.openvoiceos"}), Message(f"{self.skill_id}.stop", context={"skill_id": "stop.openvoiceos"}), Message(f"{self.skill_id}.stop.response", {"skill_id": self.skill_id, "result": True}, {"skill_id": self.skill_id}), - # skill callback to stop everything - # TODO - clean up! most arent needed/can check session if needed (ovos-workshop) - Message("mycroft.skills.abort_question", {"skill_id": self.skill_id}, - {"skill_id": self.skill_id}), - Message("ovos.skills.converse.force_timeout", {"skill_id": self.skill_id}, - {"skill_id": self.skill_id}), - Message("mycroft.audio.speech.stop", {"skill_id": self.skill_id}, + # stop pipeline callback to stop everything + + # if skill is in middle of get_response + #Message("mycroft.skills.abort_question", {"skill_id": self.skill_id}, + # {"skill_id": self.skill_id}), + + # if skill is in active_list + Message("ovos.skills.converse.force_timeout", + {"skill_id": self.skill_id}, {"skill_id": self.skill_id}), + # if skill is executing TTS + #Message("mycroft.audio.speech.stop", {"skill_id": self.skill_id}, + # {"skill_id": self.skill_id}), + # the intent running in the daemon thread exits cleanly Message("mycroft.skill.handler.complete", {"name": "CountSkill.handle_how_are_you_intent"}, @@ -214,10 +222,15 @@ def make_it_count(): ] test = End2EndTest( minicroft=self.minicroft, - # inject_active=[self.skill_id], # ensure this skill is in active skills list for the test skill_ids=[], eof_msgs=[], flip_points=["recognizer_loop:utterance"], + # messages in 'keep_original_src' would not be sent to hivemind clients + # i.e. they are directed towards ovos-core + keep_original_src=[f"{self.skill_id}.stop.ping", + f"{self.skill_id}.stop", + "mycroft.skills.abort_question", + "ovos.skills.converse.force_timeout"], ignore_messages=self.ignore_messages, source_message=message, expected_messages=stop_skill_active From 447d578b000cd97ab1edde56b81915e90584e626 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 11:38:04 +0000 Subject: [PATCH 35/57] Increment Version to 2.0.3a1 --- ovos_core/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 8625e7e020d..b48ba5b6d80 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 2 VERSION_MINOR = 0 -VERSION_BUILD = 2 -VERSION_ALPHA = 2 +VERSION_BUILD = 3 +VERSION_ALPHA = 1 # END_VERSION_BLOCK # for compat with old imports From bcfd03699206963bc40fe0e34065f6e33a6d4c85 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 11:38:45 +0000 Subject: [PATCH 36/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d2731c9f2..dcded5b16bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.3a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.3a1) (2025-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.2a2...2.0.3a1) + +**Merged pull requests:** + +- fix: stop message.context source/destination [\#706](https://github.com/OpenVoiceOS/ovos-core/pull/706) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.2a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.2a2) (2025-06-16) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.2a1...2.0.2a2) From f4a6d49c67e105289d1e2630fdc0c8f293c24604 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:11:27 +0100 Subject: [PATCH 37/57] tests: test cancel plugin (#710) --- test/end2end/test_cancel_plugin.py | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 test/end2end/test_cancel_plugin.py diff --git a/test/end2end/test_cancel_plugin.py b/test/end2end/test_cancel_plugin.py new file mode 100644 index 00000000000..87895711488 --- /dev/null +++ b/test/end2end/test_cancel_plugin.py @@ -0,0 +1,62 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG +from ovoscope import End2EndTest, get_minicroft + + +class TestCancelIntentMidSentence(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-hello-world.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_cancel_match(self): + session = Session("123") + message = Message("recognizer_loop:utterance", + {"utterances": ["can you tell me the...ummm...oh, nevermind that"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + # utterance cancelled -> no complete_intent_failure + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/cancel.mp3"}), + Message("ovos.utterance.cancelled", {}), + Message("ovos.utterance.handled", {}), + + ] + ) + + test.execute(timeout=10) + + # ensure hello world doesnt match either + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world cancel command"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/cancel.mp3"}), + Message("ovos.utterance.cancelled", {}), + Message("ovos.utterance.handled", {}), + + ] + ) + + test.execute(timeout=10) + From f202bebb766861a549b61defe72a1c87bb1596db Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 12:12:04 +0000 Subject: [PATCH 38/57] Increment Version to 2.0.3a2 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index b48ba5b6d80..2aab5475d35 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 2 VERSION_MINOR = 0 VERSION_BUILD = 3 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK # for compat with old imports From 81ebd4f25516d348e1adf2de66aff36e75d9e85c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 12:13:23 +0000 Subject: [PATCH 39/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcded5b16bc..e2cbd0afc5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.3a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.3a2) (2025-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.3a1...2.0.3a2) + +**Merged pull requests:** + +- tests: test cancel plugin [\#710](https://github.com/OpenVoiceOS/ovos-core/pull/710) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.3a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.3a1) (2025-06-16) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.2a2...2.0.3a1) From f9c55f51ea0f89aa7945d27a2e5c41e82ac3965b Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:04:29 +0100 Subject: [PATCH 40/57] fix: converse_routing (#712) * fix: converse_routing * fix: converse_routing * fix: converse_routing * fix: missing status message in fallback handling * min ovos-workshop version * min ovos-workshop version * fix: clean shutdown --- ovos_core/intent_services/converse_service.py | 10 +- ovos_core/intent_services/stop_service.py | 3 + requirements/requirements.txt | 2 +- test/end2end/test_adapt.py | 128 +++++++ test/end2end/test_converse.py | 27 +- test/end2end/test_fallback.py | 2 +- test/end2end/test_helloworld.py | 346 ------------------ test/end2end/test_m2v.py | 106 ++++++ test/end2end/test_padatious.py | 127 +++++++ 9 files changed, 395 insertions(+), 356 deletions(-) create mode 100644 test/end2end/test_adapt.py delete mode 100644 test/end2end/test_helloworld.py create mode 100644 test/end2end/test_m2v.py create mode 100644 test/end2end/test_padatious.py diff --git a/ovos_core/intent_services/converse_service.py b/ovos_core/intent_services/converse_service.py index 26c92bc7871..80b1444e38b 100644 --- a/ovos_core/intent_services/converse_service.py +++ b/ovos_core/intent_services/converse_service.py @@ -28,6 +28,11 @@ def __init__(self, bus: Optional[Union[MessageBusClient, FakeBus]] = None, 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) + self.bus.on("converse:skill", self.handle_converse) + + def handle_converse(self, message: Message): + skill_id = message.data["skill_id"] + self.bus.emit(message.reply(f"{skill_id}.converse.request", message.data)) @property def active_skills(self): @@ -321,8 +326,8 @@ def match(self, utterances: List[str], lang: str, message: Message) -> Optional[ LOG.debug(f"Attempting to converse with skill: {skill_id}") if self._converse_allowed(skill_id): return IntentHandlerMatch( - match_type=f"{skill_id}.converse.request", - match_data={"utterances": utterances, "lang": lang}, + match_type="converse:skill", + match_data={"utterances": utterances, "lang": lang, "skill_id": skill_id}, skill_id=skill_id, utterance=utterances[0], updated_session=session @@ -380,6 +385,7 @@ def handle_get_active_skills(self, message: Message): {"skills": self.get_active_skills(message)})) def shutdown(self): + self.bus.remove("converse:skill", self.handle_converse) self.bus.remove('intent.service.skills.deactivate', self.handle_deactivate_skill_request) self.bus.remove('intent.service.skills.activate', self.handle_activate_skill_request) self.bus.remove('intent.service.active_skills.get', self.handle_get_active_skills) diff --git a/ovos_core/intent_services/stop_service.py b/ovos_core/intent_services/stop_service.py index 74f34bb92ca..fcf8f6a5b0c 100644 --- a/ovos_core/intent_services/stop_service.py +++ b/ovos_core/intent_services/stop_service.py @@ -387,3 +387,6 @@ def voc_match(self, utt: str, voc_filename: str, lang: str, for i in _vocs]) return False + def shutdown(self): + self.bus.remove("stop:global", self.handle_global_stop) + self.bus.remove("stop:skill", self.handle_skill_stop) \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cb5da128e92..f799cb8cfb8 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,4 +7,4 @@ ovos-utils[extras]>=0.6.0,<1.0.0 ovos_bus_client>=0.1.4,<2.0.0 ovos-plugin-manager>=1.0.3,<2.0.0 ovos-config>=0.0.13,<3.0.0 -ovos-workshop>=7.0.2,<8.0.0 +ovos-workshop>=7.0.4,<8.0.0 \ No newline at end of file diff --git a/test/end2end/test_adapt.py b/test/end2end/test_adapt.py new file mode 100644 index 00000000000..4d48e874190 --- /dev/null +++ b/test/end2end/test_adapt.py @@ -0,0 +1,128 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG + +from ovoscope import End2EndTest, get_minicroft + + +class TestAdaptIntent(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-hello-world.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) # reuse for speed, but beware if skills keeping internal state + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_adapt_match(self): + session = Session("123") + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:HelloWorldIntent", + data={"utterance": "hello world", "lang": "en-US"}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.start", + data={"name": "HelloWorldSkill.handle_hello_world_intent"}, + context={"skill_id": self.skill_id}), + Message("speak", + data={"utterance": "Hello world", + "lang": "en-US", + "expect_response": False, + "meta": { + "dialog": "hello.world", + "data": {}, + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.complete", + data={"name": "HelloWorldSkill.handle_hello_world_intent"}, + context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": self.skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_skill_blacklist(self): + session = Session("123") + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + session.blacklisted_skills = [self.skill_id] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_intent_blacklist(self): + session = Session("123") + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + session.blacklisted_intents = [f"{self.skill_id}:HelloWorldIntent"] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_padatious_no_match(self): + session = Session("123") + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["hello world"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) diff --git a/test/end2end/test_converse.py b/test/end2end/test_converse.py index 03d85d3f8d3..93eaec3284b 100644 --- a/test/end2end/test_converse.py +++ b/test/end2end/test_converse.py @@ -43,7 +43,7 @@ def test_parrot_mode(self): data={}, context={"skill_id": self.skill_id}), Message(f"{self.skill_id}:start_parrot.intent", - data={"utterance": "start parrot mode", "lang": "en-US"}, + data={"utterance": "start parrot mode", "lang": session.lang}, context={"skill_id": self.skill_id}), Message("mycroft.skill.handler.start", data={"name": "ParrotSkill.handle_start_parrot_intent"}, @@ -74,13 +74,16 @@ def test_parrot_mode(self): Message(f"{self.skill_id}.activate", data={}, context={"skill_id": self.skill_id}), + Message("converse:skill", + data={"utterances": ["echo test"], "lang": session.lang, "skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), Message(f"{self.skill_id}.converse.request", - data={"utterances": ["echo test"], "lang": "en-US"}, + data={"utterances": ["echo test"], "lang": session.lang}, context={"skill_id": self.skill_id}), Message("speak", data={"utterance": "echo test", "expect_response": False, - "lang": "en-US", + "lang": session.lang, "meta": { "skill": self.skill_id }}, @@ -88,6 +91,9 @@ def test_parrot_mode(self): Message("skill.converse.response", data={"skill_id": self.skill_id}, context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": self.skill_id}) ] expected3 = [ message3, @@ -101,13 +107,16 @@ def test_parrot_mode(self): data={}, context={"skill_id": self.skill_id}), + Message("converse:skill", + data={"utterances": ["stop parrot"], "lang": session.lang, "skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), Message(f"{self.skill_id}.converse.request", - data={"utterances": ["stop parrot"], "lang": "en-US"}, + data={"utterances": ["stop parrot"], "lang": session.lang}, context={"skill_id": self.skill_id}), Message("speak", data={"expect_response": False, - "lang": "en-US", + "lang": session.lang, "meta": { "dialog": "parrot_stop", "data": {}, @@ -116,6 +125,9 @@ def test_parrot_mode(self): context={"skill_id": self.skill_id}), Message("skill.converse.response", data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, context={"skill_id": self.skill_id}) ] expected4 = [ @@ -133,10 +145,13 @@ def test_parrot_mode(self): test = End2EndTest( minicroft=self.minicroft, skill_ids=[self.skill_id], + eof_msgs=["ovos.utterance.handled"], flip_points=["recognizer_loop:utterance"], source_message=[message1, message2, message3, message4], expected_messages=expected1 + expected2 + expected3 + expected4, activation_points={f"{self.skill_id}:start_parrot.intent": self.skill_id}, - keep_original_src=[f"{self.skill_id}.converse.ping", "skill.converse.response"] + # messages internal to ovos-core, i.e. would not be sent to clients such as hivemind + keep_original_src=[f"{self.skill_id}.converse.ping", + f"{self.skill_id}.converse.request"] ) test.execute(timeout=10) diff --git a/test/end2end/test_fallback.py b/test/end2end/test_fallback.py index ce287eef950..a11601692fd 100644 --- a/test/end2end/test_fallback.py +++ b/test/end2end/test_fallback.py @@ -52,7 +52,7 @@ def test_fallback_match(self): data={"fallback_handler":"UnknownSkill.handle_fallback"}, context={"skill_id": self.skill_id}), - #vMessage("ovos.utterance.handled", {}) # TODO (ovos-workshop) - missing + Message("ovos.utterance.handled", {}) ] ) diff --git a/test/end2end/test_helloworld.py b/test/end2end/test_helloworld.py deleted file mode 100644 index b228387e71e..00000000000 --- a/test/end2end/test_helloworld.py +++ /dev/null @@ -1,346 +0,0 @@ -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import Session -from ovos_utils.log import LOG -from ovoscope import End2EndTest, get_minicroft - - -class TestAdaptIntent(TestCase): - - def setUp(self): - LOG.set_level("DEBUG") - self.skill_id = "ovos-skill-hello-world.openvoiceos" - self.minicroft = get_minicroft([self.skill_id]) # reuse for speed, but beware if skills keeping internal state - - def tearDown(self): - if self.minicroft: - self.minicroft.stop() - LOG.set_level("CRITICAL") - - def test_adapt_match(self): - session = Session("123") - session.pipeline = ['ovos-adapt-pipeline-plugin-high'] - message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message(f"{self.skill_id}.activate", - data={}, - context={"skill_id": self.skill_id}), - Message(f"{self.skill_id}:HelloWorldIntent", - data={"utterance": "hello world", "lang": "en-US"}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.start", - data={"name": "HelloWorldSkill.handle_hello_world_intent"}, - context={"skill_id": self.skill_id}), - Message("speak", - data={"utterance": "Hello world", - "lang": "en-US", - "expect_response": False, - "meta": { - "dialog": "hello.world", - "data": {}, - "skill": self.skill_id - }}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.complete", - data={"name": "HelloWorldSkill.handle_hello_world_intent"}, - context={"skill_id": self.skill_id}), - Message("ovos.utterance.handled", - data={}, - context={"skill_id": self.skill_id}), - ] - ) - - test.execute(timeout=10) - - def test_skill_blacklist(self): - session = Session("123") - session.pipeline = ['ovos-adapt-pipeline-plugin-high'] - session.blacklisted_skills = [self.skill_id] - message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - def test_intent_blacklist(self): - session = Session("123") - session.pipeline = ['ovos-adapt-pipeline-plugin-high'] - session.blacklisted_intents = [f"{self.skill_id}:HelloWorldIntent"] - message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - def test_padatious_no_match(self): - session = Session("123") - session.pipeline = ["ovos-padatious-pipeline-plugin-high"] - message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - -class TestPadatiousIntent(TestCase): - - def setUp(self): - LOG.set_level("DEBUG") - self.skill_id = "ovos-skill-hello-world.openvoiceos" - self.minicroft = get_minicroft([self.skill_id]) - - def tearDown(self): - if self.minicroft: - self.minicroft.stop() - LOG.set_level("CRITICAL") - - def test_padatious_match(self): - session = Session("123") - session.pipeline = ["ovos-padatious-pipeline-plugin-high"] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message(f"{self.skill_id}.activate", - data={}, - context={"skill_id": self.skill_id}), - Message(f"{self.skill_id}:Greetings.intent", - data={"utterance": "good morning", "lang": "en-US"}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.start", - data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": self.skill_id}), - Message("speak", - data={"lang": "en-US", - "expect_response": False, - "meta": { - "dialog": "hello", - "data": {}, - "skill": self.skill_id - }}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.complete", - data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": self.skill_id}), - Message("ovos.utterance.handled", - data={}, - context={"skill_id": self.skill_id}), - ] - ) - - test.execute(timeout=10) - - def test_skill_blacklist(self): - session = Session("123") - session.pipeline = ["ovos-padatious-pipeline-plugin-high"] - session.blacklisted_skills = [self.skill_id] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - def test_intent_blacklist(self): - session = Session("123") - session.pipeline = ["ovos-padatious-pipeline-plugin-high"] - session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - def test_adapt_no_match(self): - session = Session("123") - session.pipeline = ['ovos-adapt-pipeline-plugin-high'] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - -class TestModel2VecIntent(TestCase): - - def setUp(self): - LOG.set_level("DEBUG") - self.skill_id = "ovos-skill-hello-world.openvoiceos" - self.minicroft = get_minicroft([self.skill_id]) - - def tearDown(self): - if self.minicroft: - self.minicroft.stop() - LOG.set_level("CRITICAL") - - def test_m2v_match(self): - session = Session("123") - session.pipeline = ["ovos-m2v-pipeline-high"] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message(f"{self.skill_id}.activate", - data={}, - context={"skill_id": self.skill_id}), - Message(f"{self.skill_id}:Greetings.intent", - data={"utterance": "good morning", "lang": "en-US"}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.start", - data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": self.skill_id}), - Message("speak", - data={"lang": "en-US", - "expect_response": False, - "meta": { - "dialog": "hello", - "data": {}, - "skill": self.skill_id - }}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.complete", - data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": self.skill_id}), - Message("ovos.utterance.handled", - data={}, - context={"skill_id": self.skill_id}), - ] - ) - - test.execute(timeout=10) - - def test_skill_blacklist(self): - session = Session("123") - session.pipeline = ["ovos-m2v-pipeline-high"] - session.blacklisted_skills = [self.skill_id] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - def test_intent_blacklist(self): - session = Session("123") - session.pipeline = ["ovos-m2v-pipeline-high"] - session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) diff --git a/test/end2end/test_m2v.py b/test/end2end/test_m2v.py new file mode 100644 index 00000000000..0e83b100bd7 --- /dev/null +++ b/test/end2end/test_m2v.py @@ -0,0 +1,106 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG + +from ovoscope import End2EndTest, get_minicroft + + +class TestModel2VecIntent(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-hello-world.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_m2v_match(self): + session = Session("123") + session.pipeline = ["ovos-m2v-pipeline-high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:Greetings.intent", + data={"utterance": "good morning", "lang": "en-US"}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.start", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": self.skill_id}), + Message("speak", + data={"lang": "en-US", + "expect_response": False, + "meta": { + "dialog": "hello", + "data": {}, + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.complete", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": self.skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_skill_blacklist(self): + session = Session("123") + session.pipeline = ["ovos-m2v-pipeline-high"] + session.blacklisted_skills = [self.skill_id] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_intent_blacklist(self): + session = Session("123") + session.pipeline = ["ovos-m2v-pipeline-high"] + session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) diff --git a/test/end2end/test_padatious.py b/test/end2end/test_padatious.py new file mode 100644 index 00000000000..118810f862e --- /dev/null +++ b/test/end2end/test_padatious.py @@ -0,0 +1,127 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG + +from ovoscope import End2EndTest, get_minicroft + + +class TestPadatiousIntent(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "ovos-skill-hello-world.openvoiceos" + self.minicroft = get_minicroft([self.skill_id]) + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_padatious_match(self): + session = Session("123") + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:Greetings.intent", + data={"utterance": "good morning", "lang": "en-US"}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.start", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": self.skill_id}), + Message("speak", + data={"lang": "en-US", + "expect_response": False, + "meta": { + "dialog": "hello", + "data": {}, + "skill": self.skill_id + }}, + context={"skill_id": self.skill_id}), + Message("mycroft.skill.handler.complete", + data={"name": "HelloWorldSkill.handle_greetings"}, + context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": self.skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_skill_blacklist(self): + session = Session("123") + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] + session.blacklisted_skills = [self.skill_id] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_intent_blacklist(self): + session = Session("123") + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] + session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + def test_adapt_no_match(self): + session = Session("123") + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"utterances": ["good morning"], "lang": "en-US"}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + expected_messages=[ + message, + Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) From fe7efe77bce6af214a170133fe80523d50468e24 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 14:04:57 +0000 Subject: [PATCH 41/57] Increment Version to 2.0.4a1 --- ovos_core/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 2aab5475d35..4661119d3de 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 2 VERSION_MINOR = 0 -VERSION_BUILD = 3 -VERSION_ALPHA = 2 +VERSION_BUILD = 4 +VERSION_ALPHA = 1 # END_VERSION_BLOCK # for compat with old imports From b41a03e51523c0a6b4ca42ad201010e467259d04 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 14:05:43 +0000 Subject: [PATCH 42/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cbd0afc5d..cf3de9d4c33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.4a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a1) (2025-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.3a2...2.0.4a1) + +**Merged pull requests:** + +- fix: converse\_routing [\#712](https://github.com/OpenVoiceOS/ovos-core/pull/712) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.3a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.3a2) (2025-06-16) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.3a1...2.0.3a2) From 388ab1f7a6cbf59af35dd2d9186ae0c6bfd2d563 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:37:09 +0100 Subject: [PATCH 43/57] refactor: launcher args (#714) * refactor: skill manager add args to launcher and unify all components under the main SkillManager service * feat: standalone skill installer * keep default values * fix mock * coverage for stop-low pipeline * drop m2v test * standardize language handling in tests (dont depend on mycroft.conf being set to en-US) --- ovos_core/__main__.py | 58 +++++++++----- ovos_core/skill_installer.py | 35 +++++++- ovos_core/skill_manager.py | 60 ++++++++++++-- setup.py | 3 +- test/end2end/test_adapt.py | 16 ++-- test/end2end/test_cancel_plugin.py | 3 +- test/end2end/test_converse.py | 9 ++- test/end2end/test_fallback.py | 9 ++- test/end2end/test_m2v.py | 106 ------------------------- test/end2end/test_padatious.py | 16 ++-- test/end2end/test_stop.py | 102 ++++++++++++++++++++++-- test/unittests/test_skill_installer.py | 3 + 12 files changed, 252 insertions(+), 168 deletions(-) delete mode 100644 test/end2end/test_m2v.py diff --git a/ovos_core/__main__.py b/ovos_core/__main__.py index 2a779a29da6..425704d0899 100644 --- a/ovos_core/__main__.py +++ b/ovos_core/__main__.py @@ -19,18 +19,20 @@ """ from ovos_bus_client import MessageBusClient -from ovos_bus_client.util.scheduler import EventScheduler from ovos_config.locale import setup_locale -from ovos_core.intent_services import IntentService -from ovos_core.skill_installer import SkillsStore -from ovos_core.skill_manager import SkillManager, on_error, on_stopping, on_ready, on_alive, on_started from ovos_utils import wait_for_exit_signal from ovos_utils.log import LOG, init_service_logger -from ovos_workshop.skills.api import SkillApi + +from ovos_core.skill_manager import SkillManager, on_error, on_stopping, on_ready, on_alive, on_started def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, - error_hook=on_error, stopping_hook=on_stopping, watchdog=None): + error_hook=on_error, stopping_hook=on_stopping, watchdog=None, + enable_file_watcher=True, + enable_skill_api=True, + enable_intent_service=True, + enable_installer=True, + enable_event_scheduler=True): """Create a thread that monitors the loaded skills, looking for updates Returns: @@ -40,21 +42,17 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, setup_locale() - # Connect this process to the Mycroft message bus + # Connect this process to the OpenVoiceOS message bus bus = MessageBusClient() bus.run_in_thread() bus.connected_event.wait() - intents = IntentService(bus) - - event_scheduler = EventScheduler(bus, autostart=False) - event_scheduler.daemon = True - event_scheduler.start() - - osm = SkillsStore(bus) - - SkillApi.connect_bus(bus) skill_manager = SkillManager(bus, watchdog, + enable_file_watcher=enable_file_watcher, + enable_skill_api=enable_skill_api, + enable_intent_service=enable_intent_service, + enable_installer=enable_installer, + enable_event_scheduler=enable_event_scheduler, alive_hook=alive_hook, started_hook=started_hook, stopping_hook=stopping_hook, @@ -65,13 +63,31 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, wait_for_exit_signal() - intents.shutdown() - osm.shutdown() - skill_manager.stop() - event_scheduler.shutdown() + skill_manager.shutdown() LOG.info('Skills service shutdown complete!') if __name__ == "__main__": - main() + import argparse + + parser = argparse.ArgumentParser(description="Start the OpenVoiceOS Skill Manager") + + parser.add_argument("--disable-file-watcher", action="store_false", dest="enable_file_watcher", + help="Disable automatic file watching for skill settings.json") + parser.add_argument("--disable-skill-api", action="store_false", dest="enable_skill_api", + help="Disable the Skill bus API (microservices provided by skills)") + parser.add_argument("--disable-intent-service", action="store_false", dest="enable_intent_service", + help="Disable the intent service") + parser.add_argument("--disable-installer", action="store_false", dest="enable_installer", + help="Disable skill installer") + parser.add_argument("--disable-event-scheduler", action="store_false", dest="enable_event_scheduler", + help="Disable the bus event scheduler") + + args = parser.parse_args() + + main(enable_file_watcher=args.enable_file_watcher, + enable_skill_api=args.enable_skill_api, + enable_intent_service=args.enable_intent_service, + enable_installer=args.enable_installer, + enable_event_scheduler=args.enable_event_scheduler) diff --git a/ovos_core/skill_installer.py b/ovos_core/skill_installer.py index 3f4f5304fbd..ce5cb5941ca 100644 --- a/ovos_core/skill_installer.py +++ b/ovos_core/skill_installer.py @@ -8,12 +8,12 @@ import requests from combo_lock import NamedLock - -import ovos_plugin_manager from ovos_bus_client import Message from ovos_config.config import Configuration from ovos_utils.log import LOG +import ovos_plugin_manager + class InstallError(str, enum.Enum): DISABLED = "pip disabled in mycroft.conf" @@ -37,7 +37,10 @@ def __init__(self, bus, config=None): self.bus.on("ovos.pip.uninstall", self.handle_uninstall_python) def shutdown(self): - pass + self.bus.remove("ovos.skills.install", self.handle_install_skill) + self.bus.remove("ovos.skills.uninstall", self.handle_uninstall_skill) + self.bus.remove("ovos.pip.install", self.handle_install_python) + self.bus.remove("ovos.pip.uninstall", self.handle_uninstall_python) def play_error_sound(self): snd = self.config.get("sounds", {}).get("pip_error", "snd/error.mp3") @@ -265,3 +268,29 @@ def handle_uninstall_python(self, message: Message): else: self.bus.emit(message.reply("ovos.pip.uninstall.failed", {"error": InstallError.NO_PKGS.value})) + + +def launch_standalone(): + # TODO - add docker detection and warn user + from ovos_bus_client import MessageBusClient + from ovos_utils import wait_for_exit_signal + from ovos_utils.log import init_service_logger + + LOG.info("Launching SkillsStore in standalone mode") + init_service_logger("skill-installer") + + bus = MessageBusClient() + bus.run_in_thread() + bus.connected_event.wait() + + store = SkillsStore(bus) + + wait_for_exit_signal() + + store.shutdown() + + LOG.info('SkillsStore shutdown complete!') + + +if __name__ == "__main__": + launch_standalone() diff --git a/ovos_core/skill_manager.py b/ovos_core/skill_manager.py index c7602f2cc5f..e8f8b21eb8a 100644 --- a/ovos_core/skill_manager.py +++ b/ovos_core/skill_manager.py @@ -20,6 +20,7 @@ from ovos_bus_client.apis.enclosure import EnclosureAPI from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message +from ovos_bus_client.util.scheduler import EventScheduler from ovos_config.config import Configuration from ovos_config.locations import get_xdg_config_save_path from ovos_utils.file_utils import FileWatcher @@ -28,6 +29,9 @@ from ovos_utils.network_utils import is_connected_http from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap, ProcessState from ovos_workshop.skill_launcher import PluginSkillLoader +from ovos_core.skill_installer import SkillsStore +from ovos_core.intent_services import IntentService +from ovos_workshop.skills.api import SkillApi from ovos_plugin_manager.skills import find_skill_plugins @@ -56,7 +60,12 @@ class SkillManager(Thread): """Manages the loading, activation, and deactivation of Mycroft skills.""" def __init__(self, bus, watchdog=None, alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, - error_hook=on_error, stopping_hook=on_stopping): + error_hook=on_error, stopping_hook=on_stopping, + enable_installer=False, + enable_intent_service=False, + enable_event_scheduler=False, + enable_file_watcher=True, + enable_skill_api=False): """Constructor Args: @@ -108,7 +117,18 @@ def __init__(self, bus, watchdog=None, alive_hook=on_alive, started_hook=on_star self.daemon = True self.status.bind(self.bus) - self._init_filewatcher() + + # init subsystems + self.osm = SkillsStore(self.bus) if enable_installer else None + self.event_scheduler = EventScheduler(self.bus, autostart=False) if enable_event_scheduler else None + if self.event_scheduler: + self.event_scheduler.daemon = True # TODO - add kwarg in EventScheduler + self.event_scheduler.start() + self.intents = IntentService(self.bus) if enable_intent_service else None + if enable_skill_api: + SkillApi.connect_bus(self.bus) + if enable_file_watcher: + self._init_filewatcher() @property def blacklist(self): @@ -503,7 +523,7 @@ def deactivate_skill(self, message): skills = self.plugin_skills for skill_loader in skills.values(): if message.data['skill'] == skill_loader.skill_id: - LOG.info("Deactivating skill: " + skill_loader.skill_id) + LOG.info("Deactivating (unloading) skill: " + skill_loader.skill_id) skill_loader.deactivate() self.bus.emit(message.response()) except Exception as err: @@ -514,7 +534,7 @@ def deactivate_except(self, message): """Deactivate all skills except the provided.""" try: skill_to_keep = message.data['skill'] - LOG.info(f'Deactivating all skills except {skill_to_keep}') + LOG.info(f'Deactivating (unloading) all skills except {skill_to_keep}') # TODO handle external skills, OVOSAbstractApp/Hivemind skills are not accounted for skills = self.plugin_skills for skill in skills.values(): @@ -535,17 +555,41 @@ def activate_skill(self, message): skill_loader.activate() self.bus.emit(message.response()) except Exception as err: - LOG.exception(f'Couldn\'t activate skill {message.data["skill"]}') + LOG.exception(f'Couldn\'t activate (load) skill {message.data["skill"]}') self.bus.emit(message.response({'error': f'failed: {err}'})) def stop(self): + """alias for shutdown (backwards compat)""" + return self.shutdown() + + def shutdown(self): """Tell the manager to shutdown.""" self.status.set_stopping() self._stop_event.set() # Do a clean shutdown of all skills for skill_id in list(self.plugin_skills.keys()): - self._unload_plugin_skill(skill_id) - + try: + self._unload_plugin_skill(skill_id) + except Exception as e: + LOG.error(f"Failed to cleanly unload skill '{skill_id}' ({e})") + if self.intents: + try: + self.intents.shutdown() + except Exception as e: + LOG.error(f"Failed to cleanly unload intent service ({e})") + if self.osm: + try: + self.osm.shutdown() + except Exception as e: + LOG.error(f"Failed to cleanly unload skill installer ({e})") + if self.event_scheduler: + try: + self.event_scheduler.shutdown() + except Exception as e: + LOG.error(f"Failed to cleanly unload event scheduler ({e})") if self._settings_watchdog: - self._settings_watchdog.shutdown() + try: + self._settings_watchdog.shutdown() + except Exception as e: + LOG.error(f"Failed to cleanly unload settings watchdog ({e})") diff --git a/setup.py b/setup.py index 28ec89f3496..4e7f696e5ab 100644 --- a/setup.py +++ b/setup.py @@ -100,7 +100,8 @@ def required(requirements_file): 'opm.pipeline': PLUGIN_ENTRY_POINT, 'console_scripts': [ 'ovos-core=ovos_core.__main__:main', - 'ovos-intent-service=ovos_core.intent_services.service:launch_standalone' + 'ovos-intent-service=ovos_core.intent_services.service:launch_standalone', + 'ovos-skill-installer=ovos_core.skill_installer:launch_standalone' ] } ) diff --git a/test/end2end/test_adapt.py b/test/end2end/test_adapt.py index 4d48e874190..86050bcca47 100644 --- a/test/end2end/test_adapt.py +++ b/test/end2end/test_adapt.py @@ -21,9 +21,10 @@ def tearDown(self): def test_adapt_match(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-adapt-pipeline-plugin-high'] message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, + {"utterances": ["hello world"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( @@ -36,14 +37,14 @@ def test_adapt_match(self): data={}, context={"skill_id": self.skill_id}), Message(f"{self.skill_id}:HelloWorldIntent", - data={"utterance": "hello world", "lang": "en-US"}, + data={"utterance": "hello world", "lang": session.lang}, context={"skill_id": self.skill_id}), Message("mycroft.skill.handler.start", data={"name": "HelloWorldSkill.handle_hello_world_intent"}, context={"skill_id": self.skill_id}), Message("speak", data={"utterance": "Hello world", - "lang": "en-US", + "lang": session.lang, "expect_response": False, "meta": { "dialog": "hello.world", @@ -64,10 +65,11 @@ def test_adapt_match(self): def test_skill_blacklist(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-adapt-pipeline-plugin-high'] session.blacklisted_skills = [self.skill_id] message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, + {"utterances": ["hello world"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( @@ -86,10 +88,11 @@ def test_skill_blacklist(self): def test_intent_blacklist(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-adapt-pipeline-plugin-high'] session.blacklisted_intents = [f"{self.skill_id}:HelloWorldIntent"] message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, + {"utterances": ["hello world"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( @@ -108,9 +111,10 @@ def test_intent_blacklist(self): def test_padatious_no_match(self): session = Session("123") + session.lang = "en-US" session.pipeline = ["ovos-padatious-pipeline-plugin-high"] message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, + {"utterances": ["hello world"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( diff --git a/test/end2end/test_cancel_plugin.py b/test/end2end/test_cancel_plugin.py index 87895711488..3fda369015a 100644 --- a/test/end2end/test_cancel_plugin.py +++ b/test/end2end/test_cancel_plugin.py @@ -20,8 +20,9 @@ def tearDown(self): def test_cancel_match(self): session = Session("123") + session.lang = "en-US" message = Message("recognizer_loop:utterance", - {"utterances": ["can you tell me the...ummm...oh, nevermind that"], "lang": "en-US"}, + {"utterances": ["can you tell me the...ummm...oh, nevermind that"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) # utterance cancelled -> no complete_intent_failure diff --git a/test/end2end/test_converse.py b/test/end2end/test_converse.py index 93eaec3284b..ffdf4991c13 100644 --- a/test/end2end/test_converse.py +++ b/test/end2end/test_converse.py @@ -21,20 +21,21 @@ def tearDown(self): def test_parrot_mode(self): session = Session("123") + session.lang = "en-US" session.pipeline = ["ovos-converse-pipeline-plugin", "ovos-padatious-pipeline-plugin-high"] message1 = Message("recognizer_loop:utterance", - {"utterances": ["start parrot mode"], "lang": "en-US"}, + {"utterances": ["start parrot mode"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) # NOTE: we dont pass session, End2EndTest will inject/update the session from message1 message2 = Message("recognizer_loop:utterance", - {"utterances": ["echo test"], "lang": "en-US"}, + {"utterances": ["echo test"], "lang": session.lang}, {"source": "A", "destination": "B"}) message3 = Message("recognizer_loop:utterance", - {"utterances": ["stop parrot"], "lang": "en-US"}, + {"utterances": ["stop parrot"], "lang": session.lang}, {"source": "A", "destination": "B"}) message4 = Message("recognizer_loop:utterance", - {"utterances": ["echo test"], "lang": "en-US"}, + {"utterances": ["echo test"], "lang": session.lang}, {"source": "A", "destination": "B"}) expected1 = [ diff --git a/test/end2end/test_fallback.py b/test/end2end/test_fallback.py index a11601692fd..f8959fa404b 100644 --- a/test/end2end/test_fallback.py +++ b/test/end2end/test_fallback.py @@ -21,9 +21,10 @@ def tearDown(self): def test_fallback_match(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-fallback-pipeline-plugin-low'] message = Message("recognizer_loop:utterance", - {"utterances": ["hello world"], "lang": "en-US"}, + {"utterances": ["hello world"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( @@ -34,13 +35,13 @@ def test_fallback_match(self): expected_messages=[ message, Message("ovos.skills.fallback.ping", - {"utterances": ["hello world"], "lang": "en-US", "range": [90, 101]}), + {"utterances": ["hello world"], "lang": session.lang, "range": [90, 101]}), Message("ovos.skills.fallback.pong", {"skill_id": self.skill_id, "can_handle": True}), Message(f"ovos.skills.fallback.{self.skill_id}.request", - {"utterances": ["hello world"], "lang": "en-US", "range": [90, 101], "skill_id": self.skill_id}), + {"utterances": ["hello world"], "lang": session.lang, "range": [90, 101], "skill_id": self.skill_id}), Message(f"ovos.skills.fallback.{self.skill_id}.start", {}), Message("speak", - data={"lang": "en-US", + data={"lang": session.lang, "expect_response": False, "meta": { "dialog": "unknown", diff --git a/test/end2end/test_m2v.py b/test/end2end/test_m2v.py deleted file mode 100644 index 0e83b100bd7..00000000000 --- a/test/end2end/test_m2v.py +++ /dev/null @@ -1,106 +0,0 @@ -from unittest import TestCase - -from ovos_bus_client.message import Message -from ovos_bus_client.session import Session -from ovos_utils.log import LOG - -from ovoscope import End2EndTest, get_minicroft - - -class TestModel2VecIntent(TestCase): - - def setUp(self): - LOG.set_level("DEBUG") - self.skill_id = "ovos-skill-hello-world.openvoiceos" - self.minicroft = get_minicroft([self.skill_id]) - - def tearDown(self): - if self.minicroft: - self.minicroft.stop() - LOG.set_level("CRITICAL") - - def test_m2v_match(self): - session = Session("123") - session.pipeline = ["ovos-m2v-pipeline-high"] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message(f"{self.skill_id}.activate", - data={}, - context={"skill_id": self.skill_id}), - Message(f"{self.skill_id}:Greetings.intent", - data={"utterance": "good morning", "lang": "en-US"}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.start", - data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": self.skill_id}), - Message("speak", - data={"lang": "en-US", - "expect_response": False, - "meta": { - "dialog": "hello", - "data": {}, - "skill": self.skill_id - }}, - context={"skill_id": self.skill_id}), - Message("mycroft.skill.handler.complete", - data={"name": "HelloWorldSkill.handle_greetings"}, - context={"skill_id": self.skill_id}), - Message("ovos.utterance.handled", - data={}, - context={"skill_id": self.skill_id}), - ] - ) - - test.execute(timeout=10) - - def test_skill_blacklist(self): - session = Session("123") - session.pipeline = ["ovos-m2v-pipeline-high"] - session.blacklisted_skills = [self.skill_id] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) - - def test_intent_blacklist(self): - session = Session("123") - session.pipeline = ["ovos-m2v-pipeline-high"] - session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] - message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - - test = End2EndTest( - minicroft=self.minicroft, - skill_ids=[self.skill_id], - source_message=message, - expected_messages=[ - message, - Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), - Message("complete_intent_failure", {}), - Message("ovos.utterance.handled", {}) - ] - ) - - test.execute(timeout=10) diff --git a/test/end2end/test_padatious.py b/test/end2end/test_padatious.py index 118810f862e..31751a02711 100644 --- a/test/end2end/test_padatious.py +++ b/test/end2end/test_padatious.py @@ -21,9 +21,10 @@ def tearDown(self): def test_padatious_match(self): session = Session("123") + session.lang = "en-US" session.pipeline = ["ovos-padatious-pipeline-plugin-high"] message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, + {"utterances": ["good morning"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( @@ -36,13 +37,13 @@ def test_padatious_match(self): data={}, context={"skill_id": self.skill_id}), Message(f"{self.skill_id}:Greetings.intent", - data={"utterance": "good morning", "lang": "en-US"}, + data={"utterance": "good morning", "lang": session.lang}, context={"skill_id": self.skill_id}), Message("mycroft.skill.handler.start", data={"name": "HelloWorldSkill.handle_greetings"}, context={"skill_id": self.skill_id}), Message("speak", - data={"lang": "en-US", + data={"lang": session.lang, "expect_response": False, "meta": { "dialog": "hello", @@ -63,10 +64,11 @@ def test_padatious_match(self): def test_skill_blacklist(self): session = Session("123") + session.lang = "en-US" session.pipeline = ["ovos-padatious-pipeline-plugin-high"] session.blacklisted_skills = [self.skill_id] message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, + {"utterances": ["good morning"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( @@ -85,10 +87,11 @@ def test_skill_blacklist(self): def test_intent_blacklist(self): session = Session("123") + session.lang = "en-US" session.pipeline = ["ovos-padatious-pipeline-plugin-high"] session.blacklisted_intents = [f"{self.skill_id}:Greetings.intent"] message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, + {"utterances": ["good morning"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( @@ -107,9 +110,10 @@ def test_intent_blacklist(self): def test_adapt_no_match(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-adapt-pipeline-plugin-high'] message = Message("recognizer_loop:utterance", - {"utterances": ["good morning"], "lang": "en-US"}, + {"utterances": ["good morning"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) test = End2EndTest( diff --git a/test/end2end/test_stop.py b/test/end2end/test_stop.py index 2e7c2b5545f..16980ed250e 100644 --- a/test/end2end/test_stop.py +++ b/test/end2end/test_stop.py @@ -27,9 +27,10 @@ def tearDown(self): def test_exact(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-stop-pipeline-plugin-high'] message = Message("recognizer_loop:utterance", - {"utterances": ["stop"], "lang": "en-US"}, + {"utterances": ["stop"], "lang": session.lang}, {"session": session.serialize()}) test = End2EndTest( @@ -54,9 +55,10 @@ def test_exact(self): def test_not_exact_high(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-stop-pipeline-plugin-high'] message = Message("recognizer_loop:utterance", - {"utterances": ["could you stop that"], "lang": "en-US"}, + {"utterances": ["could you stop that"], "lang": session.lang}, {"session": session.serialize()}) test = End2EndTest( @@ -78,9 +80,10 @@ def test_not_exact_high(self): def test_not_exact_med(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-stop-pipeline-plugin-medium'] message = Message("recognizer_loop:utterance", - {"utterances": ["could you stop that"], "lang": "en-US"}, + {"utterances": ["could you stop that"], "lang": session.lang}, {"session": session.serialize()}) test = End2EndTest( @@ -124,10 +127,11 @@ def tearDown(self): def test_count(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-stop-pipeline-plugin-high', "ovos-padatious-pipeline-plugin-high"] message = Message("recognizer_loop:utterance", - {"utterances": ["count to 3"], "lang": "en-US"}, + {"utterances": ["count to 3"], "lang": session.lang}, {"session": session.serialize()}) # first count to 10 to validate skill is working @@ -159,13 +163,14 @@ def test_count(self): def test_count_infinity_active(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-stop-pipeline-plugin-high', "ovos-padatious-pipeline-plugin-high"] def make_it_count(): nonlocal session message = Message("recognizer_loop:utterance", - {"utterances": ["count to infinity"], "lang": "en-US"}, + {"utterances": ["count to infinity"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) session.activate_skill(self.skill_id) # ensure in active skill list self.minicroft.bus.emit(message) @@ -176,7 +181,7 @@ def make_it_count(): time.sleep(3) message = Message("recognizer_loop:utterance", - {"utterances": ["stop"], "lang": "en-US"}, + {"utterances": ["stop"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) stop_skill_active = [ @@ -239,12 +244,13 @@ def make_it_count(): def test_count_infinity_global(self): session = Session("123") + session.lang = "en-US" session.pipeline = ['ovos-stop-pipeline-plugin-high', "ovos-padatious-pipeline-plugin-high"] def make_it_count(): message = Message("recognizer_loop:utterance", - {"utterances": ["count to infinity"], "lang": "en-US"}, + {"utterances": ["count to infinity"], "lang": session.lang}, {"session": session.serialize()}) self.minicroft.bus.emit(message) @@ -256,7 +262,7 @@ def make_it_count(): # NOTE: skill not in active skill list for this Session, global stop will match instead # this doesnt typically happen at runtime, but possible since clients send whatever Session they want message = Message("recognizer_loop:utterance", - {"utterances": ["stop"], "lang": "en-US"}, + {"utterances": ["stop"], "lang": session.lang}, {"session": session.serialize()}) stop_skill_from_global = [ message, @@ -280,3 +286,83 @@ def make_it_count(): ) test.execute() + def test_count_infinity_stop_low(self): + session = Session("123") + session.lang = "en-US" + session.pipeline = ["ovos-padatious-pipeline-plugin-high", + 'ovos-stop-pipeline-plugin-low'] + + def make_it_count(): + nonlocal session + message = Message("recognizer_loop:utterance", + {"utterances": ["count to infinity"], "lang": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + session.activate_skill(self.skill_id) # ensure in active skill list + self.minicroft.bus.emit(message) + + # count to infinity, the skill will keep running in the background + create_daemon(make_it_count) + + time.sleep(3) + + message = Message("recognizer_loop:utterance", + {"utterances": ["full stop"], "lang": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + stop_skill_active = [ + message, + Message(f"{self.skill_id}.stop.ping", + {"skill_id":self.skill_id}), + Message("skill.stop.pong", + {"skill_id": self.skill_id, "can_handle": True}, + {"skill_id": self.skill_id}), + + Message("stop.openvoiceos.activate", + context={"skill_id": "stop.openvoiceos"}), + Message("stop:skill", + context={"skill_id": "stop.openvoiceos"}), + Message(f"{self.skill_id}.stop", + context={"skill_id": "stop.openvoiceos"}), + Message(f"{self.skill_id}.stop.response", + {"skill_id": self.skill_id, "result": True}, + {"skill_id": self.skill_id}), + + # stop pipeline callback to stop everything + + # if skill is in middle of get_response + #Message("mycroft.skills.abort_question", {"skill_id": self.skill_id}, + # {"skill_id": self.skill_id}), + + # if skill is in active_list + Message("ovos.skills.converse.force_timeout", + {"skill_id": self.skill_id}, + {"skill_id": self.skill_id}), + + # if skill is executing TTS + #Message("mycroft.audio.speech.stop", {"skill_id": self.skill_id}, + # {"skill_id": self.skill_id}), + + # the intent running in the daemon thread exits cleanly + Message("mycroft.skill.handler.complete", + {"name": "CountSkill.handle_how_are_you_intent"}, + {"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + {"name": "CountSkill.handle_how_are_you_intent"}, + {"skill_id": self.skill_id}) + ] + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[], + eof_msgs=[], + flip_points=["recognizer_loop:utterance"], + # messages in 'keep_original_src' would not be sent to hivemind clients + # i.e. they are directed towards ovos-core + keep_original_src=[f"{self.skill_id}.stop.ping", + f"{self.skill_id}.stop", + "mycroft.skills.abort_question", + "ovos.skills.converse.force_timeout"], + ignore_messages=self.ignore_messages, + source_message=message, + expected_messages=stop_skill_active + ) + test.execute() diff --git a/test/unittests/test_skill_installer.py b/test/unittests/test_skill_installer.py index 08760e7e46d..a2c8f6c306c 100644 --- a/test/unittests/test_skill_installer.py +++ b/test/unittests/test_skill_installer.py @@ -25,6 +25,9 @@ def emit(self, message): def on(self, event, _): self.event_handlers.append(event) + def remove(self, event, _): + self.event_handlers.remove(event) + def once(self, event, _): self.event_handlers.append(event) From eb0bc781af5543839bcc3c5f78ad54f4ceed8c0b Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 15:37:39 +0000 Subject: [PATCH 44/57] Increment Version to 2.0.4a2 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 4661119d3de..231eae8ffac 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 2 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK # for compat with old imports From ee9a23db2e71344df0e91c8f8401e34232e89b70 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 16 Jun 2025 15:38:20 +0000 Subject: [PATCH 45/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3de9d4c33..05c1a0250d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.4a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a2) (2025-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a1...2.0.4a2) + +**Merged pull requests:** + +- refactor: launcher args [\#714](https://github.com/OpenVoiceOS/ovos-core/pull/714) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.4a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a1) (2025-06-16) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.3a2...2.0.4a1) From 5c0f6b1a55d11688f8d70ec4a9b03667cd00fbad Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 17 Jun 2025 01:00:34 +0100 Subject: [PATCH 46/57] improve skill shutdown (#716) * bump packages * Update tests.txt --- ovos_core/skill_manager.py | 6 +++++- requirements/requirements.txt | 2 +- requirements/tests.txt | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ovos_core/skill_manager.py b/ovos_core/skill_manager.py index e8f8b21eb8a..0e2c66050e4 100644 --- a/ovos_core/skill_manager.py +++ b/ovos_core/skill_manager.py @@ -487,10 +487,14 @@ def _unload_plugin_skill(self, skill_id): LOG.info('Unloading plugin skill: ' + skill_id) skill_loader = self.plugin_skills[skill_id] if skill_loader.instance is not None: + try: + skill_loader.instance.shutdown() + except Exception: + LOG.exception('Failed to run skill specific shutdown code: ' + skill_loader.skill_id) try: skill_loader.instance.default_shutdown() except Exception: - LOG.exception('Failed to shutdown plugin skill: ' + skill_loader.skill_id) + LOG.exception('Failed to shutdown skill: ' + skill_loader.skill_id) self.plugin_skills.pop(skill_id) def is_alive(self, message=None): diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f799cb8cfb8..d91887a4c4a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,4 +7,4 @@ ovos-utils[extras]>=0.6.0,<1.0.0 ovos_bus_client>=0.1.4,<2.0.0 ovos-plugin-manager>=1.0.3,<2.0.0 ovos-config>=0.0.13,<3.0.0 -ovos-workshop>=7.0.4,<8.0.0 \ No newline at end of file +ovos-workshop>=7.0.5,<8.0.0 \ No newline at end of file diff --git a/requirements/tests.txt b/requirements/tests.txt index 789bea8775b..c5634235c32 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -5,4 +5,4 @@ pytest-cov>=2.8.1 pytest-testmon>=2.1.3 pytest-randomly>=3.16.0 cov-core>=1.15.0 -ovoscope>=0.6.0,<1.0.0 \ No newline at end of file +ovoscope>=0.7.1,<1.0.0 From c1967a5b172ed2bc12dbf21dd05bdd7d586852e0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 17 Jun 2025 00:01:02 +0000 Subject: [PATCH 47/57] Increment Version to 2.0.4a3 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 231eae8ffac..41230f24a93 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 2 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK # for compat with old imports From d40b2f7ecc56b6f173710009424259997b37053e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 17 Jun 2025 00:01:43 +0000 Subject: [PATCH 48/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c1a0250d6..2821f74b12d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.4a3](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a3) (2025-06-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a2...2.0.4a3) + +**Merged pull requests:** + +- improve skill shutdown [\#716](https://github.com/OpenVoiceOS/ovos-core/pull/716) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.4a2](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a2) (2025-06-16) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a1...2.0.4a2) From a51f08bb66f77b761ea5d34dc2feb232a074cbd0 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:22:22 +0100 Subject: [PATCH 49/57] active skill end2end tests (#717) * active skill tests * test final session * fix stop tests - async messages support for when order is not known, eg. messages emitted from a thread --- requirements/requirements.txt | 2 +- requirements/tests.txt | 2 +- test/end2end/test_activate.py | 196 +++++++++++++++++++++++++++++ test/end2end/test_adapt.py | 11 +- test/end2end/test_cancel_plugin.py | 1 + test/end2end/test_converse.py | 29 +++-- test/end2end/test_fallback.py | 13 +- test/end2end/test_padatious.py | 11 +- test/end2end/test_stop.py | 55 +++++--- 9 files changed, 286 insertions(+), 34 deletions(-) create mode 100644 test/end2end/test_activate.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d91887a4c4a..cc408199d46 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,4 +7,4 @@ ovos-utils[extras]>=0.6.0,<1.0.0 ovos_bus_client>=0.1.4,<2.0.0 ovos-plugin-manager>=1.0.3,<2.0.0 ovos-config>=0.0.13,<3.0.0 -ovos-workshop>=7.0.5,<8.0.0 \ No newline at end of file +ovos-workshop>=7.0.6,<8.0.0 \ No newline at end of file diff --git a/requirements/tests.txt b/requirements/tests.txt index c5634235c32..4f0539f6640 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -5,4 +5,4 @@ pytest-cov>=2.8.1 pytest-testmon>=2.1.3 pytest-randomly>=3.16.0 cov-core>=1.15.0 -ovoscope>=0.7.1,<1.0.0 +ovoscope>=0.7.2,<1.0.0 diff --git a/test/end2end/test_activate.py b/test/end2end/test_activate.py new file mode 100644 index 00000000000..944c0950b95 --- /dev/null +++ b/test/end2end/test_activate.py @@ -0,0 +1,196 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovos_utils.log import LOG + +from ovos_workshop.skills.converse import ConversationalSkill +from ovoscope import End2EndTest, get_minicroft + + +class TestSkill(ConversationalSkill): + + def initialize(self): + self.add_event("test_activate", self.handle_activate_test) + self.add_event("test_deactivate", self.handle_deactivate_test) + + def handle_activate_test(self, message: Message): + self.activate() + + def handle_deactivate_test(self, message: Message): + self.deactivate() + + def can_converse(self, message: Message) -> bool: + return True + + def converse(self, message: Message): + self.log.debug("I dont wanna converse anymore") + self.deactivate() + + +class TestDeactivate(TestCase): + + def setUp(self): + LOG.set_level("DEBUG") + self.skill_id = "test_activation.openvoiceos" + self.minicroft = get_minicroft([self.skill_id], + extra_skills={self.skill_id: TestSkill}) + + def tearDown(self): + if self.minicroft: + self.minicroft.stop() + LOG.set_level("CRITICAL") + + def test_activate(self): + session = Session("123") + session.lang = "en-US" + session.deactivate_skill(self.skill_id) # start with skill inactive + + message = Message("test_activate", + context={"session": session.serialize(), + "source": "A", "destination": "B"}) + + final_session = Session("123") + final_session.lang = "en-US" + final_session.active_skills = [(self.skill_id, 0.0)] + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + deactivation_points=[message.msg_type], + final_session=final_session, + activation_points=["intent.service.skills.activated"], + # messages internal to ovos-core, i.e. would not be sent to clients such as hivemind + keep_original_src=[ + #"intent.service.skills.activate", # TODO + #f"{self.skill_id}.activate", # TODO + ], + expected_messages=[ + message, + # handler code + Message("intent.service.skills.activate", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message("intent.service.skills.activated", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_deactivate(self): + session = Session("123") + session.lang = "en-US" + session.activate_skill(self.skill_id) # start with skill active + + message = Message("test_deactivate", + context={"session": session.serialize(), + "source": "A", "destination": "B"}) + + final_session = Session("123") + final_session.lang = "en-US" + final_session.active_skills = [] + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + final_session=final_session, + activation_points=[message.msg_type], # starts activated + deactivation_points=["intent.service.skills.deactivated"], + # messages internal to ovos-core, i.e. would not be sent to clients such as hivemind + keep_original_src=[ + #"intent.service.skills.deactivate", # TODO + #f"{self.skill_id}.deactivate", # TODO + #f"{self.skill_id}.activate", # TODO + ], + expected_messages=[ + message, + # handler code + Message("intent.service.skills.deactivate", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message("intent.service.skills.deactivated", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.deactivate", + data={}, + context={"skill_id": self.skill_id}), + ] + ) + + test.execute(timeout=10) + + def test_deactivate_inside_converse(self): + session = Session("123") + session.lang = "en-US" + session.activate_skill(self.skill_id) # start with skill active + + message = Message("recognizer_loop:utterance", + {"utterances": ["deactivate skill from within converse"], "lang": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + final_session = Session("123") + final_session.lang = "en-US" + final_session.active_skills = [] + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + source_message=message, + final_session=final_session, + activation_points=[message.msg_type], # starts activated + deactivation_points=["intent.service.skills.deactivated"], + # messages internal to ovos-core, i.e. would not be sent to clients such as hivemind + keep_original_src=[ + f"{self.skill_id}.converse.ping", + f"{self.skill_id}.converse.request", + #"intent.service.skills.deactivate", # TODO + #f"{self.skill_id}.deactivate", # TODO + #f"{self.skill_id}.activate", # TODO + ], + expected_messages=[ + message, + Message(f"{self.skill_id}.converse.ping", + data={"utterances": ["deactivate skill from within converse"], "skill_id": self.skill_id}, + context={}), + Message("skill.converse.pong", + data={"can_handle": True, "skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.activate", + data={}, + context={"skill_id": self.skill_id}), + Message("converse:skill", + data={"utterances": ["deactivate skill from within converse"], "lang": session.lang, + "skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.converse.request", + data={"utterances": ["deactivate skill from within converse"], "lang": session.lang}, + context={"skill_id": self.skill_id}), + # converse handler code + Message("intent.service.skills.deactivate", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message("intent.service.skills.deactivated", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}.deactivate", + data={}, + context={"skill_id": self.skill_id}), + # post converse handler + Message("skill.converse.response", + data={"skill_id": self.skill_id}, + context={"skill_id": self.skill_id}), + Message("ovos.utterance.handled", + data={}, + context={"skill_id": self.skill_id}) + + ] + ) + + test.execute(timeout=10) diff --git a/test/end2end/test_adapt.py b/test/end2end/test_adapt.py index 86050bcca47..853a7b6676c 100644 --- a/test/end2end/test_adapt.py +++ b/test/end2end/test_adapt.py @@ -1,5 +1,5 @@ from unittest import TestCase - +from copy import deepcopy from ovos_bus_client.message import Message from ovos_bus_client.session import Session from ovos_utils.log import LOG @@ -27,10 +27,16 @@ def test_adapt_match(self): {"utterances": ["hello world"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) + final_session = deepcopy(session) + final_session.active_skills = [(self.skill_id, 0.0)] + test = End2EndTest( minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=final_session, + activation_points=[f"{self.skill_id}:HelloWorldIntent"], + # keep_original_src=[f"{self.skill_id}.activate"], # TODO expected_messages=[ message, Message(f"{self.skill_id}.activate", @@ -76,6 +82,7 @@ def test_skill_blacklist(self): minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=session, expected_messages=[ message, Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), @@ -99,6 +106,7 @@ def test_intent_blacklist(self): minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=session, expected_messages=[ message, Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), @@ -120,6 +128,7 @@ def test_padatious_no_match(self): test = End2EndTest( minicroft=self.minicroft, skill_ids=[self.skill_id], + final_session=session, source_message=message, expected_messages=[ message, diff --git a/test/end2end/test_cancel_plugin.py b/test/end2end/test_cancel_plugin.py index 3fda369015a..d940d0488e3 100644 --- a/test/end2end/test_cancel_plugin.py +++ b/test/end2end/test_cancel_plugin.py @@ -30,6 +30,7 @@ def test_cancel_match(self): minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=session, expected_messages=[ message, Message("mycroft.audio.play_sound", {"uri": "snd/cancel.mp3"}), diff --git a/test/end2end/test_converse.py b/test/end2end/test_converse.py index ffdf4991c13..a8799ca3c14 100644 --- a/test/end2end/test_converse.py +++ b/test/end2end/test_converse.py @@ -1,3 +1,4 @@ +from copy import deepcopy from unittest import TestCase from ovos_bus_client.message import Message @@ -25,15 +26,16 @@ def test_parrot_mode(self): session.pipeline = ["ovos-converse-pipeline-plugin", "ovos-padatious-pipeline-plugin-high"] message1 = Message("recognizer_loop:utterance", - {"utterances": ["start parrot mode"], "lang": session.lang}, - {"session": session.serialize(), "source": "A", "destination": "B"}) - # NOTE: we dont pass session, End2EndTest will inject/update the session from message1 + {"utterances": ["start parrot mode"], "lang": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + # NOTE: we dont pass session after first message + # End2EndTest will inject/update the session from message1 message2 = Message("recognizer_loop:utterance", - {"utterances": ["echo test"], "lang": session.lang}, - {"source": "A", "destination": "B"}) + {"utterances": ["echo test"], "lang": session.lang}, + {"source": "A", "destination": "B"}) message3 = Message("recognizer_loop:utterance", - {"utterances": ["stop parrot"], "lang": session.lang}, - {"source": "A", "destination": "B"}) + {"utterances": ["stop parrot"], "lang": session.lang}, + {"source": "A", "destination": "B"}) message4 = Message("recognizer_loop:utterance", {"utterances": ["echo test"], "lang": session.lang}, {"source": "A", "destination": "B"}) @@ -139,20 +141,27 @@ def test_parrot_mode(self): Message("skill.converse.pong", data={"can_handle": False, "skill_id": self.skill_id}, context={"skill_id": self.skill_id}), - Message("mycroft.audio.play_sound", data={"uri": "snd/error.mp3"}), + Message("mycroft.audio.play_sound", data={"uri": "snd/error.mp3"}), Message("complete_intent_failure"), Message("ovos.utterance.handled") ] + + final_session = deepcopy(session) + final_session.active_skills = [(self.skill_id, 0.0)] + test = End2EndTest( minicroft=self.minicroft, skill_ids=[self.skill_id], eof_msgs=["ovos.utterance.handled"], flip_points=["recognizer_loop:utterance"], + final_session=final_session, source_message=[message1, message2, message3, message4], expected_messages=expected1 + expected2 + expected3 + expected4, - activation_points={f"{self.skill_id}:start_parrot.intent": self.skill_id}, + activation_points=[f"{self.skill_id}:start_parrot.intent"], # messages internal to ovos-core, i.e. would not be sent to clients such as hivemind keep_original_src=[f"{self.skill_id}.converse.ping", - f"{self.skill_id}.converse.request"] + f"{self.skill_id}.converse.request" + # f"{self.skill_id}.activate", # TODO + ] ) test.execute(timeout=10) diff --git a/test/end2end/test_fallback.py b/test/end2end/test_fallback.py index f8959fa404b..de8a67b73e4 100644 --- a/test/end2end/test_fallback.py +++ b/test/end2end/test_fallback.py @@ -1,5 +1,5 @@ from unittest import TestCase - +from copy import deepcopy from ovos_bus_client.message import Message from ovos_bus_client.session import Session from ovos_utils.log import LOG @@ -27,10 +27,19 @@ def test_fallback_match(self): {"utterances": ["hello world"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) + final_session = deepcopy(session) + # final_session.active_skills = [(self.skill_id, 0.0)] # TODO - failing + + test = End2EndTest( minicroft=self.minicroft, skill_ids=[self.skill_id], - keep_original_src=["ovos.skills.fallback.ping"], # for routing tests this is an exception + final_session=final_session, + keep_original_src=[ + "ovos.skills.fallback.ping", + # "ovos.skills.fallback.pong", # TODO + ], + activation_points=[f"ovos.skills.fallback.{self.skill_id}.request"], source_message=message, expected_messages=[ message, diff --git a/test/end2end/test_padatious.py b/test/end2end/test_padatious.py index 31751a02711..e019676f35b 100644 --- a/test/end2end/test_padatious.py +++ b/test/end2end/test_padatious.py @@ -1,5 +1,5 @@ from unittest import TestCase - +from copy import deepcopy from ovos_bus_client.message import Message from ovos_bus_client.session import Session from ovos_utils.log import LOG @@ -27,10 +27,16 @@ def test_padatious_match(self): {"utterances": ["good morning"], "lang": session.lang}, {"session": session.serialize(), "source": "A", "destination": "B"}) + final_session = deepcopy(session) + final_session.active_skills = [(self.skill_id, 0.0)] + test = End2EndTest( minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=final_session, + activation_points=[f"{self.skill_id}:Greetings.intent"], + # keep_original_src=[f"{self.skill_id}.activate"], # TODO expected_messages=[ message, Message(f"{self.skill_id}.activate", @@ -75,6 +81,7 @@ def test_skill_blacklist(self): minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=session, expected_messages=[ message, Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), @@ -98,6 +105,7 @@ def test_intent_blacklist(self): minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=session, expected_messages=[ message, Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), @@ -120,6 +128,7 @@ def test_adapt_no_match(self): minicroft=self.minicroft, skill_ids=[self.skill_id], source_message=message, + final_session=session, expected_messages=[ message, Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}), diff --git a/test/end2end/test_stop.py b/test/end2end/test_stop.py index 16980ed250e..a9034469697 100644 --- a/test/end2end/test_stop.py +++ b/test/end2end/test_stop.py @@ -40,6 +40,7 @@ def test_exact(self): flip_points=["recognizer_loop:utterance"], ignore_messages=self.ignore_messages, source_message=message, + # keep_original_src=["stop.openvoiceos.activate"], # TODO expected_messages=[ message, Message("stop.openvoiceos.activate", {}), # stop pipeline counts as active_skill @@ -93,6 +94,7 @@ def test_not_exact_med(self): flip_points=["recognizer_loop:utterance"], source_message=message, ignore_messages=self.ignore_messages, + # keep_original_src=["stop.openvoiceos.activate"], # TODO expected_messages=[ message, Message("stop.openvoiceos.activate", {}), # stop pipeline counts as active_skill @@ -137,8 +139,8 @@ def test_count(self): # first count to 10 to validate skill is working activate_skill = [ message, - Message("ovos-skill-count.openvoiceos.activate", {}), # skill is activated - Message("ovos-skill-count.openvoiceos:count_to_N.intent", {}), # intent triggers + Message(f"{self.skill_id}.activate", {}), # skill is activated + Message(f"{self.skill_id}:count_to_N.intent", {}), # intent triggers Message("mycroft.skill.handler.start", { "name": "CountSkill.handle_how_are_you_intent" @@ -157,6 +159,7 @@ def test_count(self): flip_points=["recognizer_loop:utterance"], ignore_messages=self.ignore_messages, source_message=message, + # keep_original_src=[f"{self.skill_id}.activate"], # TODO expected_messages=activate_skill ) test.execute() @@ -178,7 +181,7 @@ def make_it_count(): # count to infinity, the skill will keep running in the background create_daemon(make_it_count) - time.sleep(3) + time.sleep(2) message = Message("recognizer_loop:utterance", {"utterances": ["stop"], "lang": session.lang}, @@ -202,19 +205,22 @@ def make_it_count(): {"skill_id": self.skill_id, "result": True}, {"skill_id": self.skill_id}), - # stop pipeline callback to stop everything + # async stop pipeline callback emits these messages + # but we cant guarantee where in the test they will be emitted # if skill is in middle of get_response - #Message("mycroft.skills.abort_question", {"skill_id": self.skill_id}, + #Message("mycroft.skills.abort_question", + # {"skill_id": self.skill_id}, # {"skill_id": self.skill_id}), # if skill is in active_list - Message("ovos.skills.converse.force_timeout", - {"skill_id": self.skill_id}, - {"skill_id": self.skill_id}), + #Message("ovos.skills.converse.force_timeout", + # {"skill_id": self.skill_id}, + # {"skill_id": self.skill_id}), # if skill is executing TTS - #Message("mycroft.audio.speech.stop", {"skill_id": self.skill_id}, + #Message("mycroft.audio.speech.stop", + # {"skill_id": self.skill_id}, # {"skill_id": self.skill_id}), # the intent running in the daemon thread exits cleanly @@ -235,7 +241,12 @@ def make_it_count(): keep_original_src=[f"{self.skill_id}.stop.ping", f"{self.skill_id}.stop", "mycroft.skills.abort_question", - "ovos.skills.converse.force_timeout"], + "ovos.skills.converse.force_timeout", + # "stop.openvoiceos.activate" # TODO + ], + async_messages=[ + "ovos.skills.converse.force_timeout" + ], # order that it wil be received unknown ignore_messages=self.ignore_messages, source_message=message, expected_messages=stop_skill_active @@ -282,7 +293,8 @@ def make_it_count(): flip_points=["recognizer_loop:utterance"], ignore_messages=self.ignore_messages, source_message=message, - expected_messages=stop_skill_from_global + expected_messages=stop_skill_from_global, + #keep_original_src=["stop.openvoiceos.activate"], # TODO ) test.execute() @@ -303,7 +315,7 @@ def make_it_count(): # count to infinity, the skill will keep running in the background create_daemon(make_it_count) - time.sleep(3) + time.sleep(2) message = Message("recognizer_loop:utterance", {"utterances": ["full stop"], "lang": session.lang}, @@ -327,19 +339,22 @@ def make_it_count(): {"skill_id": self.skill_id, "result": True}, {"skill_id": self.skill_id}), - # stop pipeline callback to stop everything + # async stop pipeline callback emits these messages + # but we cant guarantee where in the test they will be emitted # if skill is in middle of get_response - #Message("mycroft.skills.abort_question", {"skill_id": self.skill_id}, + #Message("mycroft.skills.abort_question", + # {"skill_id": self.skill_id}, # {"skill_id": self.skill_id}), # if skill is in active_list - Message("ovos.skills.converse.force_timeout", - {"skill_id": self.skill_id}, - {"skill_id": self.skill_id}), + #Message("ovos.skills.converse.force_timeout", + # {"skill_id": self.skill_id}, + # {"skill_id": self.skill_id}), # if skill is executing TTS - #Message("mycroft.audio.speech.stop", {"skill_id": self.skill_id}, + #Message("mycroft.audio.speech.stop", + # {"skill_id": self.skill_id}, # {"skill_id": self.skill_id}), # the intent running in the daemon thread exits cleanly @@ -360,8 +375,12 @@ def make_it_count(): keep_original_src=[f"{self.skill_id}.stop.ping", f"{self.skill_id}.stop", "mycroft.skills.abort_question", + # "stop.openvoiceos.activate", # TODO "ovos.skills.converse.force_timeout"], ignore_messages=self.ignore_messages, + async_messages=[ + "ovos.skills.converse.force_timeout" + ], # order that it wil be received unknown source_message=message, expected_messages=stop_skill_active ) From d4ea1228b0cd25a1fd37c178661655ce487d55e9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 17 Jun 2025 13:23:01 +0000 Subject: [PATCH 50/57] Increment Version to 2.0.4a4 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 41230f24a93..be3f97afa81 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 2 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK # for compat with old imports From cf3ceb1369048b726eaced7c0eb33eee3ee04bfd Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 17 Jun 2025 13:23:51 +0000 Subject: [PATCH 51/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2821f74b12d..d55bb8cbcd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.4a4](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a4) (2025-06-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a3...2.0.4a4) + +**Merged pull requests:** + +- active skill end2end tests [\#717](https://github.com/OpenVoiceOS/ovos-core/pull/717) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.4a3](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a3) (2025-06-17) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a2...2.0.4a3) From 56e359156cae1fd72ceea88abbc3d56fc98ca544 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 5 Sep 2025 22:16:32 +0100 Subject: [PATCH 52/57] Fix/orjson optional (#722) * fix: make orjson optional allow to run in more platforms, orjson fails to install in termux * fix: make orjson optional allow to run in more platforms, orjson fails to install in termux * fix: make orjson optional allow to run in more platforms, orjson fails to install in termux --- requirements/plugins.txt | 2 +- requirements/requirements.txt | 11 ++++++++--- requirements/skills-essential.txt | 3 ++- requirements/skills-extra.txt | 1 - requirements/skills-media.txt | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/requirements/plugins.txt b/requirements/plugins.txt index f7e0e9cc24b..f2ecca99679 100644 --- a/requirements/plugins.txt +++ b/requirements/plugins.txt @@ -10,7 +10,7 @@ ovos-date-parser>=0.0.3,<1.0.0 ovos-m2v-pipeline>=0.0.6,<1.0.0 ovos-common-query-pipeline-plugin>=1.1.8, <2.0.0 ovos-adapt-parser>=1.0.6, <2.0.0 -ovos_ocp_pipeline_plugin>=1.1.16, <2.0.0 +ovos_ocp_pipeline_plugin>=1.1.18a1, <2.0.0 ovos-persona>=0.6.23,<1.0.0 padacioso>=1.0.0, <2.0.0 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cc408199d46..2b7f7352211 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,8 +3,13 @@ python-dateutil>=2.6, <3.0 watchdog>=2.1, <3.0 combo-lock>=0.2.2, <0.4 -ovos-utils[extras]>=0.6.0,<1.0.0 -ovos_bus_client>=0.1.4,<2.0.0 +ovos-utils>=0.8.2a1,<1.0.0 +ovos_bus_client>=1.3.6a1,<2.0.0 ovos-plugin-manager>=1.0.3,<2.0.0 ovos-config>=0.0.13,<3.0.0 -ovos-workshop>=7.0.6,<8.0.0 \ No newline at end of file +ovos-workshop>=7.0.6,<8.0.0 + +rapidfuzz>=3.6,<4.0 +langcodes +timezonefinder +oauthlib~=3.2 \ No newline at end of file diff --git a/requirements/skills-essential.txt b/requirements/skills-essential.txt index ba81dc0cb7a..2ffaab76796 100644 --- a/requirements/skills-essential.txt +++ b/requirements/skills-essential.txt @@ -7,4 +7,5 @@ ovos-skill-hello-world>=0.1.10,<1.0.0 ovos-skill-spelling>=0.2.5,<1.0.0 ovos-skill-diagnostics>=0.0.2,<1.0.0 ovos-skill-parrot>=0.1.25,<1.0.0 -ovos-skill-count>=0.0.1,<1.0.0 \ No newline at end of file +ovos-skill-count>=0.0.1,<1.0.0 +ovos-skill-randomness>=0.1.2,<1.0.0; python_version >= "3.10" diff --git a/requirements/skills-extra.txt b/requirements/skills-extra.txt index 2e034ca983c..0dedcdc5f10 100644 --- a/requirements/skills-extra.txt +++ b/requirements/skills-extra.txt @@ -1,6 +1,5 @@ # skills providing non essential functionality ovos-skill-wordnet>=0.2.5,<1.0.0 -ovos-skill-randomness>=0.1.1,<1.0.0; python_version >= "3.10" ovos-skill-laugh>=0.1.1,<1.0.0 ovos-skill-number-facts>=0.1.12,<1.0.0 ovos-skill-iss-location>=0.2.16,<1.0.0 diff --git a/requirements/skills-media.txt b/requirements/skills-media.txt index c19a3838900..5a804f2a057 100644 --- a/requirements/skills-media.txt +++ b/requirements/skills-media.txt @@ -1,6 +1,6 @@ # skills for OCP, require audio playback plugins (usually mpv) ovos-skill-somafm>=0.1.3,<1.0.0 -ovos-skill-news>=0.4.5,<1.0.0 +ovos-skill-news>=0.4.6a1,<1.0.0 ovos-skill-pyradios>=0.1.5,<1.0.0 ovos-skill-local-media>=0.2.12,<1.0.0 ovos-skill-youtube-music>=0.1.7,<1.0.0 From 680ec99b2f5b6678a12166308e8f7f89a7764958 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 5 Sep 2025 21:16:59 +0000 Subject: [PATCH 53/57] Increment Version to 2.0.4a5 --- ovos_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index be3f97afa81..84150951d1c 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 2 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 4 +VERSION_ALPHA = 5 # END_VERSION_BLOCK # for compat with old imports From b01983980bf8df479745ad168e4c21d615babf7d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 5 Sep 2025 21:17:45 +0000 Subject: [PATCH 54/57] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d55bb8cbcd1..c9eef16aceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [2.0.4a5](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a5) (2025-09-05) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a4...2.0.4a5) + +**Closed issues:** + +- ovos-core should restart itself when it detects a new skill [\#720](https://github.com/OpenVoiceOS/ovos-core/issues/720) + +**Merged pull requests:** + +- Fix/orjson optional [\#722](https://github.com/OpenVoiceOS/ovos-core/pull/722) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.4a4](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a4) (2025-06-17) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a3...2.0.4a4) From acadd01fb2540a159247f4fa6489f1417f42cefe Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 5 Sep 2025 23:07:49 +0100 Subject: [PATCH 55/57] fix: less requirements (#724) --- requirements/requirements.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2b7f7352211..f922d2e1838 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -10,6 +10,4 @@ ovos-config>=0.0.13,<3.0.0 ovos-workshop>=7.0.6,<8.0.0 rapidfuzz>=3.6,<4.0 -langcodes -timezonefinder -oauthlib~=3.2 \ No newline at end of file +langcodes \ No newline at end of file From 98f8c55f628742e8707aba7a25fac2bacb743581 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 5 Sep 2025 22:08:15 +0000 Subject: [PATCH 56/57] Increment Version to 2.0.5a1 --- ovos_core/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_core/version.py b/ovos_core/version.py index 84150951d1c..683f8b06e58 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 2 VERSION_MINOR = 0 -VERSION_BUILD = 4 -VERSION_ALPHA = 5 +VERSION_BUILD = 5 +VERSION_ALPHA = 1 # END_VERSION_BLOCK # for compat with old imports From 807130d311da83d9eb95a495ab836060d35b72a4 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 5 Sep 2025 22:09:01 +0000 Subject: [PATCH 57/57] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9eef16aceb..e68463439c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.5a1](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.5a1) (2025-09-05) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a5...2.0.5a1) + +**Merged pull requests:** + +- fix: less requirements [\#724](https://github.com/OpenVoiceOS/ovos-core/pull/724) ([JarbasAl](https://github.com/JarbasAl)) + ## [2.0.4a5](https://github.com/OpenVoiceOS/ovos-core/tree/2.0.4a5) (2025-09-05) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a4...2.0.4a5)