diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4a9f8e8231ab..4df750748c44 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,19 +19,26 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev + sudo apt install python3-dev libssl-dev libfann-dev portaudio19-dev libpulse-dev python -m pip install build wheel - - name: Install repo + - name: Install core repo run: | pip install -e .[mycroft,deprecated] - name: Install test dependencies run: | - sudo apt install libssl-dev libfann-dev portaudio19-dev libpulse-dev pip install -r requirements/tests.txt pip install ./test/unittests/common_query/ovos_tskill_fakewiki + pip install ./test/end2end/session/skill-ovos-hello-world - name: Generate coverage report run: | - pytest --cov=./ovos_core --cov-report=xml + pytest --cov=ovos_core --cov-report xml test/unittests + pytest --cov-append --cov=ovos_core --cov-report xml test/end2end + pytest --cov-append --cov=ovos_core --cov-report xml test/integrationtests + - 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: diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d28b9129ad53..abab50a2879f 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -32,7 +32,7 @@ on: jobs: unit_tests: strategy: - max-parallel: 2 + max-parallel: 3 matrix: python-version: [ 3.7, 3.8, 3.9] runs-on: ubuntu-latest @@ -55,47 +55,28 @@ jobs: run: | pip install -r requirements/tests.txt pip install ./test/unittests/common_query/ovos_tskill_fakewiki + pip install ./test/end2end/session/skill-ovos-hello-world - name: Run unittests run: | pytest --cov=ovos_core --cov-report xml test/unittests # NOTE: additional pytest invocations should also add the --cov-append flag # or they will overwrite previous invocations' coverage reports # (for an example, see OVOS Skill Manager's workflow) + - name: Run full core unittests + run: | + pytest --cov-append --cov=ovos_core --cov-report xml test/end2end - name: Run integration tests run: | pytest --cov-append --cov=ovos_core --cov-report xml test/integrationtests - # NOTE: additional pytest invocations should also add the --cov-append flag - # or they will overwrite previous invocations' coverage reports - # (for an example, see OVOS Skill Manager's workflow) + - name: Install padatious + run: | + sudo apt install libfann-dev + pip install .[lgpl] + - name: Run unittests with padatious + run: | + pytest --cov=ovos_core --cov-report xml test/unittests/skills - name: Upload coverage if: "${{ matrix.python-version == '3.9' }}" env: CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - uses: codecov/codecov-action@v2 - backwards_compat_tests: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v2 - - name: Set up python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install System Dependencies - run: | - sudo apt-get update - sudo apt install python3-dev swig libssl-dev libfann-dev portaudio19-dev libpulse-dev - python -m pip install build wheel - - name: Install core repo - run: | - pip install .[mycroft,lgpl,deprecated] - - name: Install test dependencies - run: | - pip install -r requirements/tests.txt - pip install ./test/unittests/common_query/ovos_tskill_fakewiki - - name: Run unittests - run: | - pytest test/unittests - - name: Run integration tests - run: | - pytest test/integrationtests + uses: codecov/codecov-action@v2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3155be6df2a5..85bea1fe6d8a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ doc/_build/ test/unittests/skills/test_skill/settings.json test_conf.json .pytest_cache/ +/.gtm/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a55dfbb31a3..e98e61033e43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,76 @@ ## [Unreleased](https://github.com/OpenVoiceOS/ovos-core/tree/HEAD) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a32...HEAD) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a40...HEAD) + +**Fixed bugs:** + +- fix/session dropping active\_skills [\#355](https://github.com/OpenVoiceOS/ovos-core/pull/355) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a40](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a40) (2023-09-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a39...V0.0.8a40) + +## [V0.0.8a39](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a39) (2023-09-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a38...V0.0.8a39) + +**Implemented enhancements:** + +- Persona Initiative - Long Live the LLM [\#297](https://github.com/OpenVoiceOS/ovos-core/issues/297) +- default session managed by core [\#354](https://github.com/OpenVoiceOS/ovos-core/pull/354) ([JarbasAl](https://github.com/JarbasAl)) + +**Fixed bugs:** + +- fix docker build [\#267](https://github.com/OpenVoiceOS/ovos-core/issues/267) + +## [V0.0.8a38](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a38) (2023-09-22) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a37...V0.0.8a38) + +**Implemented enhancements:** + +- feat/pipeline\_session [\#352](https://github.com/OpenVoiceOS/ovos-core/pull/352) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a37](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a37) (2023-09-22) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a36...V0.0.8a37) + +**Merged pull requests:** + +- Update README.md: Update Link [\#351](https://github.com/OpenVoiceOS/ovos-core/pull/351) ([1Maxnet1](https://github.com/1Maxnet1)) + +## [V0.0.8a36](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a36) (2023-09-20) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a35...V0.0.8a36) + +**Implemented enhancements:** + +- feat/active skills from Session [\#350](https://github.com/OpenVoiceOS/ovos-core/pull/350) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a35](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a35) (2023-09-19) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a34...V0.0.8a35) + +**Implemented enhancements:** + +- feat/runtime skill installer [\#347](https://github.com/OpenVoiceOS/ovos-core/pull/347) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a34](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a34) (2023-09-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a33...V0.0.8a34) + +**Fixed bugs:** + +- unittests hang forever test\_event\_container.py [\#342](https://github.com/OpenVoiceOS/ovos-core/issues/342) + +**Merged pull requests:** + +- refactor/sound\_output\_in\_ovos\_audio [\#345](https://github.com/OpenVoiceOS/ovos-core/pull/345) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a33](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.8a33) (2023-08-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.8a32...V0.0.8a33) **Implemented enhancements:** @@ -554,11 +623,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.6a5...V0.0.6a6) -**Fixed bugs:** - -- port/fix/ intent name colision [\#233](https://github.com/OpenVoiceOS/ovos-core/issues/233) -- Handle multiple intents with the same name \(\#2921\) [\#235](https://github.com/OpenVoiceOS/ovos-core/pull/235) ([JarbasAl](https://github.com/JarbasAl)) - **Closed issues:** - Install confusion [\#239](https://github.com/OpenVoiceOS/ovos-core/issues/239) @@ -583,26 +647,14 @@ [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.6a2...V0.0.6a3) -**Implemented enhancements:** - -- page background color support for show image and show animated image [\#236](https://github.com/OpenVoiceOS/ovos-core/pull/236) ([AIIX](https://github.com/AIIX)) - ## [V0.0.6a2](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.6a2) (2022-11-25) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.6a1...V0.0.6a2) -**Fixed bugs:** - -- Fix show\_pages index api [\#234](https://github.com/OpenVoiceOS/ovos-core/pull/234) ([AIIX](https://github.com/AIIX)) - ## [V0.0.6a1](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.6a1) (2022-11-23) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.5...V0.0.6a1) -**Implemented enhancements:** - -- add factory reset ui [\#231](https://github.com/OpenVoiceOS/ovos-core/pull/231) ([AIIX](https://github.com/AIIX)) - ## [V0.0.5](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.5) (2022-11-16) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.5a39...V0.0.5) @@ -611,18 +663,10 @@ [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.5a38...V0.0.5a39) -**Implemented enhancements:** - -- Update alpha dependencies to stable versions [\#230](https://github.com/OpenVoiceOS/ovos-core/pull/230) ([NeonDaniel](https://github.com/NeonDaniel)) - ## [V0.0.5a38](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.5a38) (2022-11-11) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.5a37...V0.0.5a38) -**Fixed bugs:** - -- Replace / by \_ in name to avoid issue when writing the cache file. [\#228](https://github.com/OpenVoiceOS/ovos-core/pull/228) ([JarbasAl](https://github.com/JarbasAl)) - ## [V0.0.5a37](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.5a37) (2022-11-11) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.5a36...V0.0.5a37) @@ -631,10 +675,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.5a35...V0.0.5a36) -**Fixed bugs:** - -- emit theme get on theme change [\#227](https://github.com/OpenVoiceOS/ovos-core/pull/227) ([AIIX](https://github.com/AIIX)) - ## [V0.0.5a35](https://github.com/OpenVoiceOS/ovos-core/tree/V0.0.5a35) (2022-10-31) [Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/V0.0.5a34...V0.0.5a35) diff --git a/README.md b/README.md index bbe219256fe9..176da0889703 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # OVOS-core -[OpenVoiceOS](https://openvoiceos.com/) is an open source platform for smart speakers and other voice-centric devices. +[OpenVoiceOS](https://openvoiceos.org/) is an open source platform for smart speakers and other voice-centric devices. [Mycroft](https://mycroft.ai) is a hackable, open source voice assistant by MycroftAI. OVOS-core is a backwards-compatible descendant of [Mycroft-core](https://github.com/MycroftAI/mycroft-core), the central component of diff --git a/mycroft/__init__.py b/mycroft/__init__.py index fcea166b90b9..eb5a294fd114 100644 --- a/mycroft/__init__.py +++ b/mycroft/__init__.py @@ -18,7 +18,7 @@ from ovos_utils.intents import AdaptIntent, IntentBuilder, Intent from ovos_workshop.decorators import intent_handler, intent_file_handler, adds_context, removes_context -from ovos_workshop.skills import MycroftSkill +from ovos_workshop.skills.mycroft_skill import MycroftSkill from ovos_workshop.skills.fallback import FallbackSkill from ovos_utils.log import LOG diff --git a/ovos_core/__main__.py b/ovos_core/__main__.py index 6adff843c823..d76cd24acc0f 100644 --- a/ovos_core/__main__.py +++ b/ovos_core/__main__.py @@ -29,6 +29,7 @@ from ovos_utils import wait_for_exit_signal from ovos_utils.log import LOG, init_service_logger from ovos_utils.process_utils import reset_sigint_handler +from ovos_core.skill_installer import SkillsStore from ovos_workshop.skills.fallback import FallbackSkill @@ -52,6 +53,9 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, 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, alive_hook=alive_hook, @@ -64,7 +68,7 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, wait_for_exit_signal() - shutdown(skill_manager, event_scheduler) + shutdown(skill_manager, event_scheduler, osm) def _register_intent_services(bus): @@ -82,7 +86,7 @@ def _register_intent_services(bus): return service -def shutdown(skill_manager, event_scheduler): +def shutdown(skill_manager, event_scheduler, osm): LOG.info('Shutting down Skills service') if event_scheduler is not None: event_scheduler.shutdown() @@ -90,6 +94,8 @@ def shutdown(skill_manager, event_scheduler): if skill_manager is not None: skill_manager.stop() skill_manager.join() + if osm is not None: + osm.shutdown() LOG.info('Skills service shutdown complete!') diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index 074f6b17db40..fc4c04156bf5 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -26,10 +26,9 @@ from ovos_core.intent_services.padacioso_service import PadaciosoService from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService from ovos_utils.intents.intent_service_interface import open_intent_envelope -from ovos_utils.log import LOG +from ovos_utils.log import LOG, deprecated, log_deprecation from ovos_utils.messagebus import get_message_lang from ovos_utils.metrics import Stopwatch -from ovos_utils.sound import play_error_sound try: from ovos_core.intent_services.padatious_service import PadatiousService, PadatiousMatcher @@ -74,6 +73,9 @@ def __init__(self, bus): self.common_qa = CommonQAService(bus) self.utterance_plugins = UtteranceTransformersService(bus, config=config) self.metadata_plugins = MetadataTransformersService(bus, config=config) + # connection SessionManager to the bus, + # this will sync default session across all components + SessionManager.connect_to_bus(self.bus) self.bus.on('register_vocab', self.handle_register_vocab) self.bus.on('register_intent', self.handle_register_intent) @@ -86,22 +88,12 @@ def __init__(self, bus): self.bus.on('clear_context', self.handle_clear_context) # Converse method - self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse) self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict) - self.bus.on('intent.service.skills.activate', - self.handle_activate_skill_request) - self.bus.on('intent.service.skills.deactivate', - self.handle_deactivate_skill_request) - # TODO backwards compat, deprecate - self.bus.on('active_skill_request', self.handle_activate_skill_request) - # Intents API self.registered_vocab = [] self.bus.on('intent.service.intent.get', self.handle_get_intent) self.bus.on('intent.service.skills.get', self.handle_get_skills) - self.bus.on('intent.service.active_skills.get', - self.handle_get_active_skills) self.bus.on('intent.service.adapt.get', self.handle_get_adapt) self.bus.on('intent.service.adapt.manifest.get', self.handle_adapt_manifest) @@ -114,22 +106,6 @@ def __init__(self, bus): self.bus.on('intent.service.padatious.entities.manifest.get', self.handle_entity_manifest) - @property - def pipeline(self): - # List of functions to use to match the utterance with intent, listed in priority order. - config = Configuration().get("intents") or {} - return config.get("pipeline", [ - "converse", - "padacioso_high", - "adapt", - "common_qa", - "fallback_high", - "padacioso_medium", - "fallback_medium", - "padacioso_low", - "fallback_low" - ]) - @property def registered_intents(self): lang = get_message_lang() @@ -154,34 +130,32 @@ def get_skill_name(self, skill_id): # converse handling @property def active_skills(self): - return self.converse.active_skills # [skill_id , timestamp] - + log_deprecation("self.active_skills is deprecated! use Session instead", "0.0.9") + session = SessionManager.get() + return session.active_skills + + @active_skills.setter + def active_skills(self, val): + log_deprecation("self.active_skills is deprecated! use Session instead", "0.0.9") + session = SessionManager.get() + session.active_skills = [] + for skill_id, ts in val: + session.activate_skill(skill_id) + + @deprecated("handle_activate_skill_request moved to ConverseService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") def handle_activate_skill_request(self, message): - # TODO imperfect solution - only a skill can activate itself - # someone can forge this message and emit it raw, but in OpenVoiceOS all - # skill messages should have skill_id in context, so let's make sure - # this doesnt happen accidentally at very least - skill_id = message.data['skill_id'] - source_skill = message.context.get("skill_id") - self.converse.activate_skill(skill_id, source_skill) + self.converse.handle_activate_skill_request(message) + @deprecated("handle_deactivate_skill_request moved to ConverseService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") def handle_deactivate_skill_request(self, message): - # TODO imperfect solution - only a skill can deactivate itself - # someone can forge this message and emit it raw, but in ovos-core all - # skill message should have skill_id in context, so let's make sure - # this doesnt happen accidentally - skill_id = message.data['skill_id'] - source_skill = message.context.get("skill_id") or skill_id - self.converse.deactivate_skill(skill_id, source_skill) + self.converse.handle_deactivate_skill_request(message) + @deprecated("reset_converse moved to ConverseService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") def reset_converse(self, message): - """Let skills know there was a problem with speech recognition""" - lang = get_message_lang(message) - try: - setup_locale(lang) # restore default lang - except Exception as e: - LOG.exception(f"Failed to set lingua_franca default lang to {lang}") - self.converse.converse_with_skills([], lang, message) + self.converse.reset_converse(message) def _handle_transformers(self, message): """ @@ -206,16 +180,15 @@ def disambiguate_lang(message): 3 - detected_lang -> tagged by transformers (text classification, free form chat) 4 - config lang (or from message.data) """ - cfg = Configuration() + sess = SessionManager.get(message) default_lang = get_message_lang(message) - valid_langs = set([cfg.get("lang", "en-us")] + cfg.get("secondary_langs'", [])) lang_keys = ["stt_lang", "request_lang", "detected_lang"] for k in lang_keys: if k in message.context: v = message.context[k] - if v in valid_langs: + if v in sess.valid_languages: if v != default_lang: LOG.info(f"replaced {default_lang} with {k}: {v}") return v @@ -224,16 +197,17 @@ def disambiguate_lang(message): return default_lang - def get_pipeline(self, skips=None): + def get_pipeline(self, skips=None, session=None): """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() # Create matchers # TODO - from plugins if self.padatious_service is None: - if any("padatious" in p for p in self.pipeline): + if any("padatious" in p for p in session.pipeline): LOG.warning("padatious is not available! using padacioso in it's place") padatious_matcher = self.padacioso_service else: @@ -255,9 +229,29 @@ def get_pipeline(self, skips=None): "fallback_low": self.fallback.low_prio } skips = skips or [] - pipeline = [k for k in self.pipeline if k not in skips] + pipeline = [k for k in session.pipeline if k not in skips] return [matchers[k] for k in pipeline] + def _validate_session(self, message, lang): + # get session + 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) + return sess + def handle_utterance(self, message): """Main entrypoint for handling user utterances @@ -292,6 +286,7 @@ def handle_utterance(self, message): # tag language of this utterance lang = self.disambiguate_lang(message) + try: setup_locale(lang) except Exception as e: @@ -301,20 +296,25 @@ def handle_utterance(self, message): stopwatch = Stopwatch() + # get session + sess = self._validate_session(message, lang) + message.context["session"] = sess.serialize() + # match match = None with stopwatch: # Loop through the matching functions until a match is found. - for match_func in self.get_pipeline(): + for match_func in self.get_pipeline(session=sess): match = match_func(utterances, lang, message) if match: break + LOG.debug(f"intent matching took: {stopwatch.time}") if match: message.data["utterance"] = match.utterance if match.skill_id: - self.converse.activate_skill(match.skill_id) + self.converse.activate_skill(match.skill_id, message=message) message.context["skill_id"] = match.skill_id # If the service didn't report back the skill_id it # takes on the responsibility of making the skill "active" @@ -333,6 +333,9 @@ def handle_utterance(self, message): # Ask politely for forgiveness for failing in this vital task self.send_complete_intent_failure(message) + # sync any changes made to the default session, eg by ConverseService + if sess.session_id == "default": + SessionManager.sync(message) return match, message.context, stopwatch except Exception as err: @@ -344,7 +347,8 @@ def send_complete_intent_failure(self, message): Args: message (Message): original message to forward from """ - play_error_sound() + sound = Configuration().get('sounds', {}).get('error', "snd/error.mp3") + self.bus.emit(message.forward('mycroft.audio.play_sound', {"uri": sound})) self.bus.emit(message.forward('complete_intent_failure')) def handle_register_vocab(self, message): @@ -441,12 +445,14 @@ def handle_get_intent(self, message): """ utterance = message.data["utterance"] lang = get_message_lang(message) + sess = SessionManager.get(message) # Loop through the matching functions until a match is found. for match_func in self.get_pipeline(skips=["converse", "fallback_high", "fallback_medium", - "fallback_low"]): + "fallback_low"], + session=sess): match = match_func([utterance], lang, message) if match: if match.intent_type: @@ -472,14 +478,15 @@ def handle_get_skills(self, message): self.bus.emit(message.reply("intent.service.skills.reply", {"skills": self.skill_names})) + @deprecated("handle_get_active_skills moved to ConverseService, overriding this method has no effect, " + "it has been disconnected from the bus event", "0.0.8") def handle_get_active_skills(self, message): """Send active skills to caller. Argument: message: query message to reply to. """ - self.bus.emit(message.reply("intent.service.active_skills.reply", - {"skills": self.converse.active_skills})) + self.converse.handle_get_active_skills(message) def handle_get_adapt(self, message): """handler getting the adapt response for an utterance. diff --git a/ovos_core/intent_services/converse_service.py b/ovos_core/intent_services/converse_service.py index 0eb26934ad3b..c62b142df8bb 100644 --- a/ovos_core/intent_services/converse_service.py +++ b/ovos_core/intent_services/converse_service.py @@ -1,10 +1,14 @@ import time -from ovos_bus_client.message import Message + from ovos_config.config import Configuration +from ovos_config.locale import setup_locale import ovos_core.intent_services +from ovos_bus_client.message import Message +from ovos_bus_client.session import SessionManager, UtteranceState from ovos_utils import flatten_list from ovos_utils.log import LOG +from ovos_utils.messagebus import get_message_lang from ovos_workshop.permissions import ConverseMode, ConverseActivationMode @@ -14,7 +18,13 @@ class ConverseService: def __init__(self, bus): self.bus = bus self._consecutive_activations = {} - self.active_skills = [] # [skill_id , timestamp] + self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse) + self.bus.on('intent.service.skills.deactivate', self.handle_deactivate_skill_request) + self.bus.on('intent.service.skills.activate', self.handle_activate_skill_request) + self.bus.on('active_skill_request', self.handle_activate_skill_request) # TODO backwards compat, deprecate + self.bus.on('intent.service.active_skills.get', self.handle_get_active_skills) + self.bus.on("skill.converse.get_response.enable", self.handle_get_response_enable) + self.bus.on("skill.converse.get_response.disable", self.handle_get_response_disable) @property def config(self): @@ -24,16 +34,29 @@ def config(self): """ return Configuration().get("skills", {}).get("converse") or {} - def get_active_skills(self): + @property + def active_skills(self): + session = SessionManager.get() + return session.active_skills + + @active_skills.setter + def active_skills(self, val): + session = SessionManager.get() + session.active_skills = [] + for skill_id, ts in val: + session.activate_skill(skill_id) + + def get_active_skills(self, message=None): """Active skill ids ordered by converse priority this represents the order in which converse will be called Returns: active_skills (list): ordered list of skill_ids """ - return [skill[0] for skill in self.active_skills] + session = SessionManager.get(message) + return [skill[0] for skill in session.active_skills] - def deactivate_skill(self, skill_id, source_skill=None): + def deactivate_skill(self, skill_id, source_skill=None, message=None): """Remove a skill from being targetable by converse. Args: @@ -42,18 +65,24 @@ def deactivate_skill(self, skill_id, source_skill=None): """ source_skill = source_skill or skill_id if self._deactivate_allowed(skill_id, source_skill): - active_skills = self.get_active_skills() - if skill_id in active_skills: - idx = active_skills.index(skill_id) - self.active_skills.pop(idx) + session = SessionManager.get(message) + if session.is_active(skill_id): + # update converse session + if message: + session.update_history(message) + session.deactivate_skill(skill_id) + + # keep message.context + message = message or Message("") + message.context["session"] = session.serialize() # update session active skills + # send bus event self.bus.emit( - Message("intent.service.skills.deactivated", - data={"skill_id": skill_id}, - context={"skill_id": skill_id})) + message.forward("intent.service.skills.deactivated", + data={"skill_id": skill_id})) if skill_id in self._consecutive_activations: self._consecutive_activations[skill_id] = 0 - def activate_skill(self, skill_id, source_skill=None): + def activate_skill(self, skill_id, source_skill=None, message=None): """Add a skill or update the position of an active skill. The skill is added to the front of the list, if it's already in the @@ -65,20 +94,20 @@ def activate_skill(self, skill_id, source_skill=None): """ source_skill = source_skill or skill_id if self._activate_allowed(skill_id, source_skill): - # NOTE: do not call self.remove_active_skill - # do not want to send the deactivation bus event! - active_skills = self.get_active_skills() - if skill_id in active_skills: - idx = active_skills.index(skill_id) - self.active_skills.pop(idx) - - # add skill with timestamp to start of skill_list - self.active_skills.insert(0, [skill_id, time.time()]) - self.bus.emit( - Message("intent.service.skills.activated", - data={"skill_id": skill_id}, - context={"skill_id": skill_id})) - + # update converse session + session = SessionManager.get(message) + if message: + session.update_history(message) + session.activate_skill(skill_id) + + # keep message.context + message = message or Message("") + message.context["session"] = session.serialize() # update session active skills + message = message.forward("intent.service.skills.activated", + {"skill_id": skill_id}) + # send bus event + self.bus.emit(message) + # update activation counter self._consecutive_activations[skill_id] += 1 def _activate_allowed(self, skill_id, source_skill=None): @@ -115,7 +144,7 @@ def _activate_allowed(self, skill_id, source_skill=None): # define their default priority, this is a user/developer setting priority = prio.get(skill_id, 50) if any(p > priority for p in - [prio.get(s[0], 50) for s in self.active_skills]): + [prio.get(s, 50) for s in self.get_active_skills()]): return False elif acmode == ConverseActivationMode.BLACKLIST: if skill_id in self.config.get("converse_blacklist", []): @@ -181,16 +210,16 @@ def _converse_allowed(self, skill_id): return False return True - def _collect_converse_skills(self): + def _collect_converse_skills(self, message): """use the messagebus api to determine which skills want to converse This includes all skills and external applications""" skill_ids = [] want_converse = [] active_skills = self.get_active_skills() - def handle_ack(message): - skill_id = message.data["skill_id"] - if message.data.get("can_handle", True): + def handle_ack(msg): + skill_id = msg.data["skill_id"] + if msg.data.get("can_handle", True): if skill_id in active_skills: want_converse.append(skill_id) skill_ids.append(skill_id) @@ -198,7 +227,7 @@ def handle_ack(message): self.bus.on("skill.converse.pong", handle_ack) # wait for all skills to acknowledge they want to converse - self.bus.emit(Message("skill.converse.ping")) + self.bus.emit(message.forward("skill.converse.ping")) start = time.time() while not all(s in skill_ids for s in active_skills) \ and time.time() - start <= 0.5: @@ -207,12 +236,13 @@ def handle_ack(message): self.bus.remove("skill.converse.pong", handle_ack) return want_converse - def _check_converse_timeout(self): + def _check_converse_timeout(self, message): """ filter active skill list based on timestamps """ timeouts = self.config.get("skill_timeouts") or {} def_timeout = self.config.get("timeout", 300) - self.active_skills = [ - skill for skill in self.active_skills + session = SessionManager.get(message) + session.active_skills = [ + skill for skill in session.active_skills if time.time() - skill[1] <= timeouts.get(skill[0], def_timeout)] def converse(self, utterances, skill_id, lang, message): @@ -228,7 +258,21 @@ def converse(self, utterances, skill_id, lang, message): Returns: handled (bool): True if handled otherwise False. """ + session = SessionManager.get(message) + session.lang = lang + + state = session.utterance_states.get(skill_id, UtteranceState.INTENT) + if state == UtteranceState.RESPONSE: + session.update_history(message) + converse_msg = message.reply("skill.converse.get_response", + {"skill_id": skill_id, + "utterances": utterances, + "lang": lang}) + self.bus.emit(converse_msg) + return True + if self._converse_allowed(skill_id): + session.update_history(message) converse_msg = message.reply("skill.converse.request", {"skill_id": skill_id, "utterances": utterances, @@ -257,9 +301,60 @@ def converse_with_skills(self, utterances, lang, message): # we call flatten in case someone is sending the old style list of tuples utterances = flatten_list(utterances) # filter allowed skills - self._check_converse_timeout() + self._check_converse_timeout(message) # check if any skill wants to handle utterance - for skill_id in self._collect_converse_skills(): + for skill_id in self._collect_converse_skills(message): if self.converse(utterances, skill_id, lang, message): return ovos_core.intent_services.IntentMatch('Converse', None, None, skill_id, utterances[0]) return None + + def handle_get_response_enable(self, message): + skill_id = message.data["skill_id"] + session = SessionManager.get(message) + session.enable_response_mode(skill_id) + if session.session_id == "default": + SessionManager.sync(message) + + def handle_get_response_disable(self, message): + skill_id = message.data["skill_id"] + session = SessionManager.get(message) + session.disable_response_mode(skill_id) + if session.session_id == "default": + SessionManager.sync(message) + + def handle_activate_skill_request(self, message): + # TODO imperfect solution - only a skill can activate itself + # someone can forge this message and emit it raw, but in OpenVoiceOS all + # skill messages should have skill_id in context, so let's make sure + # this doesnt happen accidentally at very least + skill_id = message.data['skill_id'] + source_skill = message.context.get("skill_id") + self.activate_skill(skill_id, source_skill, message) + + def handle_deactivate_skill_request(self, message): + # TODO imperfect solution - only a skill can deactivate itself + # someone can forge this message and emit it raw, but in ovos-core all + # skill message should have skill_id in context, so let's make sure + # this doesnt happen accidentally + skill_id = message.data['skill_id'] + source_skill = message.context.get("skill_id") or skill_id + self.deactivate_skill(skill_id, source_skill, message) + + def reset_converse(self, message): + """Let skills know there was a problem with speech recognition""" + lang = get_message_lang(message) + try: + setup_locale(lang) # restore default lang + except Exception as e: + LOG.exception(f"Failed to set lingua_franca default lang to {lang}") + + self.converse_with_skills([], lang, message) + + def handle_get_active_skills(self, message): + """Send active skills to caller. + + Argument: + message: query message to reply to. + """ + self.bus.emit(message.reply("intent.service.active_skills.reply", + {"skills": self.get_active_skills(message)})) diff --git a/ovos_core/skill_installer.py b/ovos_core/skill_installer.py new file mode 100644 index 000000000000..4d3716ba089c --- /dev/null +++ b/ovos_core/skill_installer.py @@ -0,0 +1,228 @@ +import enum +import sys +from importlib import reload +from os.path import exists +from subprocess import Popen, PIPE +from typing import Optional + +from combo_lock import NamedLock +from ovos_config.config import Configuration + +import ovos_plugin_manager +from ovos_bus_client import Message +from ovos_utils.log import LOG + + +class InstallError(str, enum.Enum): + DISABLED = "pip disabled in mycroft.conf" + PIP_ERROR = "error in pip subprocess" + BAD_URL = "skill url validation failed" + NO_PKGS = "no packages to install" + + +class SkillsStore: + # default constraints to use if none are given + DEFAULT_CONSTRAINTS = '/etc/mycroft/constraints.txt' # TODO XDG paths, keep backwards compat for now with msm/osm + PIP_LOCK = NamedLock("ovos_pip.lock") + + def __init__(self, bus, config=None): + self.config = config or Configuration().get("skills", {}).get("installer", {}) + self.bus = bus + self.bus.on("ovos.skills.install", self.handle_install_skill) + self.bus.on("ovos.skills.uninstall", self.handle_uninstall_skill) + self.bus.on("ovos.pip.install", self.handle_install_python) + self.bus.on("ovos.pip.uninstall", self.handle_uninstall_python) + + def shutdown(self): + pass + + def play_error_sound(self): + snd = Configuration().get("sounds", {}).get("pip_error", "snd/error.mp3") + self.bus.emit(Message("mycroft.audio.play_sound", {"uri": snd})) + + def play_success_sound(self): + snd = Configuration().get("sounds", {}).get("pip_success", "snd/acknowledge.mp3") + self.bus.emit(Message("mycroft.audio.play_sound", {"uri": snd})) + + def pip_install(self, packages: list, + constraints: Optional[str] = None, + print_logs: bool = True): + if not len(packages): + LOG.error("no package list provided to install") + self.play_error_sound() + return False + # Use constraints to limit the installed versions + if constraints and not exists(constraints): + LOG.error('Couldn\'t find the constraints file') + self.play_error_sound() + return False + elif exists(SkillsStore.DEFAULT_CONSTRAINTS): + constraints = SkillsStore.DEFAULT_CONSTRAINTS + + pip_args = [sys.executable, '-m', 'pip', 'install'] + if constraints: + pip_args += ['-c', constraints] + if self.config.get("break_system_packages", False): + pip_args += ["--break-system-packages"] + + with SkillsStore.PIP_LOCK: + """ + Iterate over the individual Python packages and + install them one by one to enforce the order specified + in the manifest. + """ + for dependent_python_package in packages: + LOG.info("(pip) Installing " + dependent_python_package) + pip_command = pip_args + [dependent_python_package] + LOG.debug(" ".join(pip_command)) + if print_logs: + proc = Popen(pip_command) + else: + proc = Popen(pip_command, stdout=PIPE, stderr=PIPE) + pip_code = proc.wait() + if pip_code != 0: + stderr = proc.stderr.read().decode() + self.play_error_sound() + raise RuntimeError(stderr) + + reload(ovos_plugin_manager) # force core to pick new entry points + self.play_success_sound() + return True + + def pip_uninstall(self, packages: list, + constraints: Optional[str] = None, + print_logs: bool = True): + if not len(packages): + LOG.error("no package list provided to uninstall") + self.play_error_sound() + return False + + # Use constraints to limit package removal + if constraints and not exists(constraints): + LOG.error('Couldn\'t find the constraints file') + self.play_error_sound() + return False + elif exists(SkillsStore.DEFAULT_CONSTRAINTS): + constraints = SkillsStore.DEFAULT_CONSTRAINTS + + if constraints: + with open(constraints) as f: + # remove version pinning and normalize _ to - (pip accepts both) + cpkgs = [p.split("~")[0].split("<")[0].split(">")[0].split("=")[0].replace("_", "-") + for p in f.read().split("\n") if p.strip()] + else: + cpkgs = ["ovos-core", "ovos-utils", "ovos-plugin-manager", + "ovos-config", "ovos-bus-client", "ovos-workshop"] + + # normalize _ to - (pip accepts both) + if any(p.replace("_", "-") in cpkgs for p in packages): + LOG.error(f'tried to uninstall a protected package: {cpkgs}') + self.play_error_sound() + return False + + pip_args = [sys.executable, '-m', 'pip', 'uninstall', '-y'] + if self.config.get("break_system_packages", False): + pip_args += ["--break-system-packages"] + + with SkillsStore.PIP_LOCK: + """ + Iterate over the individual Python packages and + install them one by one to enforce the order specified + in the manifest. + """ + for dependent_python_package in packages: + LOG.info("(pip) Uninstalling " + dependent_python_package) + pip_command = pip_args + [dependent_python_package] + LOG.debug(" ".join(pip_command)) + if print_logs: + proc = Popen(pip_command) + else: + proc = Popen(pip_command, stdout=PIPE, stderr=PIPE) + pip_code = proc.wait() + if pip_code != 0: + stderr = proc.stderr.read().decode() + self.play_error_sound() + raise RuntimeError(stderr) + + reload(ovos_plugin_manager) # force core to pick new entry points + self.play_success_sound() + return True + + def validate_skill(self, url): + if not url.startswith("https://github.com/"): + return False + # TODO - check if setup.py + # TODO - check if not using MycroftSkill class + # TODO - check if not mycroft CommonPlay + return True + + def handle_install_skill(self, message: Message): + if not self.config.get("allow_pip"): + LOG.error(InstallError.DISABLED.value) + self.play_error_sound() + self.bus.emit(message.reply("ovos.skills.install.failed", + {"error": InstallError.DISABLED.value})) + return + + url = message.data["url"] + if self.validate_skill(url): + success = self.pip_install([f"git+{url}"]) + if success: + self.bus.emit(message.reply("ovos.skills.install.complete")) + else: + self.bus.emit(message.reply("ovos.skills.install.failed", + {"error": InstallError.PIP_ERROR.value})) + else: + LOG.error("invalid skill url, does not appear to be a github skill") + self.play_error_sound() + self.bus.emit(message.reply("ovos.skills.install.failed", + {"error": InstallError.BAD_URL.value})) + + def handle_uninstall_skill(self, message: Message): + if not self.config.get("allow_pip"): + LOG.error(InstallError.DISABLED.value) + self.play_error_sound() + self.bus.emit(message.reply("ovos.skills.uninstall.failed", + {"error": InstallError.DISABLED.value})) + return + # TODO + LOG.error("pip uninstall not yet implemented") + self.play_error_sound() + self.bus.emit(message.reply("ovos.skills.uninstall.failed", + {"error": "not implemented"})) + + def handle_install_python(self, message: Message): + if not self.config.get("allow_pip"): + LOG.error(InstallError.DISABLED.value) + self.play_error_sound() + self.bus.emit(message.reply("ovos.pip.install.failed", + {"error": InstallError.DISABLED.value})) + return + pkgs = message.data["packages"] + if pkgs: + if self.pip_install(pkgs): + self.bus.emit(message.reply("ovos.pip.install.complete")) + else: + self.bus.emit(message.reply("ovos.pip.install.failed", + {"error": InstallError.PIP_ERROR.value})) + else: + self.bus.emit(message.reply("ovos.pip.install.failed", + {"error": InstallError.NO_PKGS.value})) + + def handle_uninstall_python(self, message: Message): + if not self.config.get("allow_pip"): + LOG.error(InstallError.DISABLED.value) + self.play_error_sound() + self.bus.emit(message.reply("ovos.pip.uninstall.failed", + {"error": InstallError.DISABLED.value})) + return + pkgs = message.data["packages"] + if pkgs: + if self.pip_uninstall(pkgs): + self.bus.emit(message.reply("ovos.pip.uninstall.complete")) + else: + self.bus.emit(message.reply("ovos.pip.uninstall.failed", + {"error": InstallError.PIP_ERROR.value})) + else: + self.bus.emit(message.reply("ovos.pip.uninstall.failed", + {"error": InstallError.NO_PKGS.value})) diff --git a/ovos_core/version.py b/ovos_core/version.py index 9555237cd8c8..f78a2c4f6435 100644 --- a/ovos_core/version.py +++ b/ovos_core/version.py @@ -18,7 +18,7 @@ OVOS_VERSION_MAJOR = 0 OVOS_VERSION_MINOR = 0 OVOS_VERSION_BUILD = 8 -OVOS_VERSION_ALPHA = 33 +OVOS_VERSION_ALPHA = 41 # END_VERSION_BLOCK diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 87898e033b1a..97a7f181216c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,14 +6,13 @@ combo-lock>=0.2.2, <0.3 padacioso~=0.2, >=0.2.1a8 adapt-parser>=1.0.0, <2.0.0 -ovos-bus-client<0.1.0, >=0.0.6a4 -# ovos-utils<0.1.0, >=0.0.36a3 -ovos-utils@git+https://github.com/openvoiceos/ovos-utils@TEST_IntentServiceInterface -ovos-plugin-manager<0.1.0, >=0.0.24a5 -ovos-config~=0.0,>=0.0.11a9 +ovos-bus-client<0.1.0, >=0.0.6a9 +ovos-utils<0.1.0, >=0.0.36a8 +ovos-plugin-manager<0.1.0, >=0.0.24a9 +ovos-config~=0.0,>=0.0.11a13 ovos-lingua-franca>=0.4.7 -ovos_backend_client>=0.1.0a6 -ovos_workshop<0.1.0, >=0.0.12 +ovos-backend-client>=0.1.0a12 +ovos-workshop<0.1.0, >=0.0.13a5 # provides plugins and classic machine learning framework -ovos-classifiers<0.1.0, >=0.0.0a33 +ovos-classifiers<0.1.0, >=0.0.0a37 diff --git a/test/end2end/__init__.py b/test/end2end/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/end2end/session/__init__.py b/test/end2end/session/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/end2end/session/skill-ovos-hello-world/MANIFEST.in b/test/end2end/session/skill-ovos-hello-world/MANIFEST.in new file mode 100644 index 000000000000..b9ecb5807a59 --- /dev/null +++ b/test/end2end/session/skill-ovos-hello-world/MANIFEST.in @@ -0,0 +1,7 @@ +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/session/skill-ovos-hello-world/__init__.py b/test/end2end/session/skill-ovos-hello-world/__init__.py new file mode 100644 index 000000000000..df0104baa837 --- /dev/null +++ b/test/end2end/session/skill-ovos-hello-world/__init__.py @@ -0,0 +1,10 @@ +from ovos_utils.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/session/skill-ovos-hello-world/locale/en-us/dialog/hello.world.dialog b/test/end2end/session/skill-ovos-hello-world/locale/en-us/dialog/hello.world.dialog new file mode 100644 index 000000000000..811f098f322b --- /dev/null +++ b/test/end2end/session/skill-ovos-hello-world/locale/en-us/dialog/hello.world.dialog @@ -0,0 +1,3 @@ +Hello world +Hello +Hi to you too diff --git a/test/end2end/session/skill-ovos-hello-world/locale/en-us/vocab/HelloWorldKeyword.voc b/test/end2end/session/skill-ovos-hello-world/locale/en-us/vocab/HelloWorldKeyword.voc new file mode 100644 index 000000000000..5ffa264b9193 --- /dev/null +++ b/test/end2end/session/skill-ovos-hello-world/locale/en-us/vocab/HelloWorldKeyword.voc @@ -0,0 +1,2 @@ +hello world +greetings diff --git a/test/end2end/session/skill-ovos-hello-world/setup.py b/test/end2end/session/skill-ovos-hello-world/setup.py new file mode 100755 index 000000000000..6bc5a62e6ec7 --- /dev/null +++ b/test/end2end/session/skill-ovos-hello-world/setup.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +from setuptools import setup +from os import walk, path + +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/session/test_session.py b/test/end2end/session/test_session.py new file mode 100644 index 000000000000..81e55ede3dab --- /dev/null +++ b/test/end2end/session/test_session.py @@ -0,0 +1,604 @@ +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.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.messagebus import FakeBus +from ovos_utils.process_utils import ProcessState +from ovos_workshop.skills.fallback import FallbackSkill + + +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() + + 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() + SessionManager.bus = None + SessionManager.sessions = {} + SessionManager.default_session = SessionManager.sessions["default"] = Session("default") + + +def get_minicroft(skill_id): + croft1 = MiniCroft([skill_id]) + croft1.start() + while croft1.status.state != ProcessState.READY: + sleep(0.2) + return croft1 + + +class TestSessions(TestCase): + + def setUp(self): + self.skill_id = "skill-ovos-hello-world.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" + + 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 + while len(messages) < n: + sleep(0.1) + + 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 + "skill.converse.ping", # default session injected + "skill.converse.pong", + "intent.service.skills.activated", + f"{self.skill_id}.activate", + f"{self.skill_id}:HelloWorldIntent", + "mycroft.skill.handler.start", + "enclosure.active_skill", + "speak", + "mycroft.skill.handler.complete", + "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" 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 "lang" is injected by converse.ping + # (missing in utterance message) and kept in all messages + self.assertEqual(messages[1].msg_type, "skill.converse.ping") + for m in messages[1:]: + self.assertEqual(m.context["lang"], "en-us") + + # verify "pong" answer from hello world skill + 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 by intent service (intent pipeline matched) + self.assertEqual(messages[3].msg_type, "intent.service.skills.activated") + self.assertEqual(messages[3].data["skill_id"], self.skill_id) + self.assertEqual(messages[4].msg_type, f"{self.skill_id}.activate") + + # verify intent triggers + self.assertEqual(messages[5].msg_type, f"{self.skill_id}:HelloWorldIntent") + self.assertEqual(messages[5].data["intent_type"], f"{self.skill_id}:HelloWorldIntent") + # verify skill_id is now present in every message.context + for m in messages[5:]: + self.assertEqual(m.context["skill_id"], self.skill_id) + + # verify intent execution + self.assertEqual(messages[6].msg_type, "mycroft.skill.handler.start") + self.assertEqual(messages[6].data["name"], "HelloWorldSkill.handle_hello_world_intent") + self.assertEqual(messages[7].msg_type, "enclosure.active_skill") + self.assertEqual(messages[7].data["skill_id"], self.skill_id) + self.assertEqual(messages[8].msg_type, "speak") + self.assertEqual(messages[8].data["lang"], "en-us") + self.assertFalse(messages[8].data["expect_response"]) + self.assertEqual(messages[8].data["meta"]["dialog"], "hello.world") + self.assertEqual(messages[8].data["meta"]["skill"], self.skill_id) + self.assertEqual(messages[9].msg_type, "mycroft.skill.handler.complete") + self.assertEqual(messages[9].data["name"], "HelloWorldSkill.handle_hello_world_intent") + + # verify default session is now updated + self.assertEqual(messages[10].msg_type, "ovos.session.update_default") + self.assertEqual(messages[10].data["session_data"]["session_id"], "default") + # test deserialization of payload + sess = Session.deserialize(messages[10].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[10].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" + + 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 + while len(messages) < n: + sleep(0.1) + + 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", + "skill.converse.ping", + "skill.converse.pong", + "intent.service.skills.activated", + f"{self.skill_id}.activate", + f"{self.skill_id}:HelloWorldIntent", + "mycroft.skill.handler.start", + "enclosure.active_skill", + "speak", + "mycroft.skill.handler.complete", + "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["xxx"], "not-valid") + + # verify ping/pong answer from hello world skill + self.assertEqual(messages[1].msg_type, "skill.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 by intent service (intent pipeline matched) + self.assertEqual(messages[3].msg_type, "intent.service.skills.activated") + self.assertEqual(messages[3].data["skill_id"], self.skill_id) + self.assertEqual(messages[4].msg_type, f"{self.skill_id}.activate") + + # verify intent triggers + self.assertEqual(messages[5].msg_type, f"{self.skill_id}:HelloWorldIntent") + self.assertEqual(messages[5].data["intent_type"], f"{self.skill_id}:HelloWorldIntent") + # verify skill_id is now present in every message.context + for m in messages[5:]: + self.assertEqual(m.context["skill_id"], self.skill_id) + + # verify intent execution + self.assertEqual(messages[6].msg_type, "mycroft.skill.handler.start") + self.assertEqual(messages[6].data["name"], "HelloWorldSkill.handle_hello_world_intent") + self.assertEqual(messages[7].msg_type, "enclosure.active_skill") + self.assertEqual(messages[7].data["skill_id"], self.skill_id) + self.assertEqual(messages[8].msg_type, "speak") + self.assertEqual(messages[8].data["lang"], "en-us") + self.assertFalse(messages[8].data["expect_response"]) + self.assertEqual(messages[8].data["meta"]["dialog"], "hello.world") + self.assertEqual(messages[8].data["meta"]["skill"], self.skill_id) + self.assertEqual(messages[9].msg_type, "mycroft.skill.handler.complete") + self.assertEqual(messages[9].data["name"], "HelloWorldSkill.handle_hello_world_intent") + + # verify default session is now updated + self.assertEqual(messages[10].msg_type, "ovos.session.update_default") + self.assertEqual(messages[10].data["session_data"]["session_id"], "default") + + # test deserialization of payload + sess = Session.deserialize(messages[10].data["session_data"]) + self.assertEqual(sess.session_id, "default") + self.assertEqual(sess.valid_languages, ["en-us"]) + + # test that active skills list has been updated + self.assertEqual(messages[10].data["session_data"]["active_skills"][0][0], self.skill_id) + self.assertEqual(sess.active_skills[0][0], self.skill_id) + + def test_explicit_session(self): + SessionManager.sessions = {} + SessionManager.default_session = SessionManager.sessions["default"] = Session("default") + SessionManager.default_session.lang = "en-us" + + 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 + while len(messages) < n: + sleep(0.1) + + self.core.bus.on("message", new_msg) + + sess = Session() + 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", + "skill.converse.ping", + "skill.converse.pong", + "intent.service.skills.activated", + f"{self.skill_id}.activate", + f"{self.skill_id}:HelloWorldIntent", + "mycroft.skill.handler.start", + "enclosure.active_skill", + "speak", + "mycroft.skill.handler.complete" + ] + 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"], sess.session_id) + self.assertEqual(m.context["xxx"], "not-valid") + + # verify ping/pong answer from hello world skill + self.assertEqual(messages[1].msg_type, "skill.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 by intent service (intent pipeline matched) + self.assertEqual(messages[3].msg_type, "intent.service.skills.activated") + self.assertEqual(messages[3].data["skill_id"], self.skill_id) + self.assertEqual(messages[4].msg_type, f"{self.skill_id}.activate") + + # verify intent triggers + self.assertEqual(messages[5].msg_type, f"{self.skill_id}:HelloWorldIntent") + self.assertEqual(messages[5].data["intent_type"], f"{self.skill_id}:HelloWorldIntent") + # verify skill_id is now present in every message.context + for m in messages[5:]: + self.assertEqual(m.context["skill_id"], self.skill_id) + + # verify intent execution + self.assertEqual(messages[6].msg_type, "mycroft.skill.handler.start") + self.assertEqual(messages[6].data["name"], "HelloWorldSkill.handle_hello_world_intent") + self.assertEqual(messages[7].msg_type, "enclosure.active_skill") + self.assertEqual(messages[7].data["skill_id"], self.skill_id) + self.assertEqual(messages[8].msg_type, "speak") + self.assertEqual(messages[8].data["lang"], "en-us") + self.assertFalse(messages[8].data["expect_response"]) + self.assertEqual(messages[8].data["meta"]["dialog"], "hello.world") + self.assertEqual(messages[8].data["meta"]["skill"], self.skill_id) + self.assertEqual(messages[9].msg_type, "mycroft.skill.handler.complete") + self.assertEqual(messages[9].data["name"], "HelloWorldSkill.handle_hello_world_intent") + + # test that active skills list has been updated + sess = SessionManager.sessions[sess.session_id] + self.assertEqual(sess.active_skills[0][0], self.skill_id) + # test that default session remains unchanged + self.assertEqual(SessionManager.default_session.active_skills, []) + + def test_complete_failure(self): + SessionManager.sessions = {} + SessionManager.default_session = SessionManager.sessions["default"] = Session("default") + SessionManager.default_session.lang = "en-us" + 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 + while len(messages) < n: + sleep(0.1) + + 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", + "skill.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") + + # verify ping/pong answer from hello world skill + self.assertEqual(messages[1].msg_type, "skill.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"]) + + # high prio fallback + self.assertEqual(messages[3].msg_type, "mycroft.skills.fallback") + self.assertEqual(messages[3].data["fallback_range"], [0, 5]) + self.assertEqual(messages[4].msg_type, "mycroft.skill.handler.start") + self.assertEqual(messages[4].data["handler"], "fallback") + self.assertEqual(messages[5].msg_type, "mycroft.skill.handler.complete") + self.assertEqual(messages[5].data["handler"], "fallback") + self.assertEqual(messages[6].msg_type, "mycroft.skills.fallback.response") + self.assertFalse(messages[6].data["handled"]) + + # medium prio fallback + self.assertEqual(messages[7].msg_type, "mycroft.skills.fallback") + self.assertEqual(messages[7].data["fallback_range"], [5, 90]) + self.assertEqual(messages[8].msg_type, "mycroft.skill.handler.start") + self.assertEqual(messages[8].data["handler"], "fallback") + self.assertEqual(messages[9].msg_type, "mycroft.skill.handler.complete") + self.assertEqual(messages[9].data["handler"], "fallback") + self.assertEqual(messages[10].msg_type, "mycroft.skills.fallback.response") + self.assertFalse(messages[10].data["handled"]) + + # low prio fallback + self.assertEqual(messages[11].msg_type, "mycroft.skills.fallback") + self.assertEqual(messages[11].data["fallback_range"], [90, 101]) + self.assertEqual(messages[12].msg_type, "mycroft.skill.handler.start") + self.assertEqual(messages[12].data["handler"], "fallback") + self.assertEqual(messages[13].msg_type, "mycroft.skill.handler.complete") + self.assertEqual(messages[13].data["handler"], "fallback") + self.assertEqual(messages[14].msg_type, "mycroft.skills.fallback.response") + self.assertFalse(messages[14].data["handled"]) + + # complete intent failure + self.assertEqual(messages[15].msg_type, "mycroft.audio.play_sound") + self.assertEqual(messages[15].data["uri"], "snd/error.mp3") + self.assertEqual(messages[16].msg_type, "complete_intent_failure") + + # verify default session is now updated + self.assertEqual(messages[17].msg_type, "ovos.session.update_default") + self.assertEqual(messages[17].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" + + 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 + while len(messages) < n: + sleep(0.1) + + 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 + "skill.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, "skill.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/unittests/skills/test_intent_service.py b/test/unittests/skills/test_intent_service.py index e6dab4460026..80b238734713 100644 --- a/test/unittests/skills/test_intent_service.py +++ b/test/unittests/skills/test_intent_service.py @@ -98,8 +98,8 @@ def setUp(self): self.intent_service.converse.activate_skill('atari_skill') self.intent_service.converse.activate_skill('c64_skill') - def _collect(): - return [i[0] for i in self.intent_service.active_skills] + def _collect(message=None): + return [i[0] for i in self.intent_service.converse.active_skills] self.intent_service.converse._collect_converse_skills = _collect @@ -150,7 +150,7 @@ def response(message, return_msg_type): 'atari_skill': atari, 'amiga_skill': amiga} - return msgs[message.data['skill_id']] + return msgs.get(message.data['skill_id']) self.intent_service.converse.activate_skill('amiga_skill') self.intent_service.bus.wait_for_response.side_effect = response @@ -183,7 +183,7 @@ def response(message, return_msg_type): 'result': False}) msgs = {'c64_skill': c64, 'atari_skill': atari} - return msgs[message.data['skill_id']] + return msgs.get(message.data['skill_id'], None) reset_msg = Message('mycroft.speech.recognition.unknown', data={'lang': 'en-US'}) diff --git a/test/unittests/skills/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill.py index 6581c60f824f..fe03706bd669 100644 --- a/test/unittests/skills/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill.py @@ -23,12 +23,13 @@ from adapt.intent import IntentBuilder from ovos_config import Configuration -from ovos_utils.intents.intent_service_interface import open_intent_envelope from mycroft.skills.skill_data import (load_regex_from_file, load_regex, load_vocabulary, read_vocab_file) from ovos_bus_client.message import Message +from ovos_utils.intents.intent_service_interface import open_intent_envelope from ovos_workshop.decorators import resting_screen_handler, intent_handler +from ovos_workshop.skills.mycroft_skill import MycroftSkill from ovos_workshop.skills.ovos import OVOSSkill from test.util import base_config @@ -602,10 +603,10 @@ def test_native_langs(self): s.config_core['secondary_langs'] = ['en', 'en-us', 'en-AU', 'es', 'pt-PT'] self.assertEqual(s.lang, 'en-us') - self.assertEqual(s._secondary_langs, ['en', 'en-au', 'es', + self.assertEqual(s.secondary_langs, ['en', 'en-au', 'es', 'pt-pt']) - self.assertEqual(len(s._native_langs), len(set(s._native_langs))) - self.assertEqual(set(s._native_langs), {'en-us', 'en-au', 'pt-pt'}) + self.assertEqual(len(s.native_langs), len(set(s.native_langs))) + self.assertEqual(set(s.native_langs), {'en-us', 'en-au', 'pt-pt'}) s.config_core['lang'] = lang s.config_core['secondary_langs'] = secondary @@ -631,8 +632,9 @@ class _TestSkill(OVOSSkill): pass -class SimpleSkill1(_TestSkill): +class SimpleSkill1(MycroftSkill): """ Test skill for normal intent builder syntax """ + def initialize(self): self.handler_run = False i = IntentBuilder('a').require('Keyword').build() @@ -647,6 +649,7 @@ def stop(self): class SimpleSkill2(_TestSkill): """ Test skill for intent builder without .build() """ + def initialize(self): i = IntentBuilder('a').require('Keyword') self.register_intent(i, self.handler) @@ -660,6 +663,7 @@ def stop(self): class SimpleSkill3(_TestSkill): """ Test skill for invalid Intent for register_intent """ + def initialize(self): self.register_intent('string', self.handler) diff --git a/test/unittests/skills/test_mycroft_skill_get_response.py b/test/unittests/skills/test_mycroft_skill_get_response.py index 5cbea497d292..37b44296cb04 100644 --- a/test/unittests/skills/test_mycroft_skill_get_response.py +++ b/test/unittests/skills/test_mycroft_skill_get_response.py @@ -1,18 +1,16 @@ """Tests for the mycroft skill's get_response variations.""" +import time from os.path import dirname, join from threading import Thread -import time -from unittest import TestCase, mock +from unittest import TestCase, mock, skip from lingua_franca import load_language from mycroft.skills import MycroftSkill from ovos_bus_client.message import Message - from test.unittests.mocks import base_config, AnyCallable - load_language("en-us") @@ -57,6 +55,7 @@ def create_skill(mock_conf, lang='en-us'): class TestMycroftSkillWaitResponse(TestCase): + @skip("TODO - refactor for new event based get_response") def test_wait(self): """Ensure that _wait_response() returns the response from converse.""" skill = create_skill() @@ -162,13 +161,13 @@ def test_converse_detection(self): skill.speak_dialog = mock.Mock() def validator(*args, **kwargs): - self.assertTrue(skill._converse_is_implemented) + self.assertTrue(skill.converse_is_implemented) - self.assertFalse(skill._converse_is_implemented) + self.assertFalse(skill.converse_is_implemented) skill.get_response('what do you want', validator=validator) skill._wait_response.assert_called_with(AnyCallable(), validator, AnyCallable(), -1) - self.assertFalse(skill._converse_is_implemented) + self.assertFalse(skill.converse_is_implemented) class TestMycroftSkillAskYesNo(TestCase): @@ -230,7 +229,7 @@ def test_ask_yesno_other(self): response = skill.ask_yesno('Do you like breakfast') self.assertEqual(response, 'I am a fish') - @mock.patch('ovos_workshop.skills.base.dig_for_message') + @mock.patch('ovos_workshop.skills.ovos.dig_for_message') def test_ask_yesno_german(self, dig_mock): """Check that when the skill is set to german it responds to "ja".""" # lang is session based, it comes from originating message in ovos-core