diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000000..d4fa8be10b1b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +relative_files = true \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 26e59a202061..636f346535d6 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" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9ea6860ba439..62d26ac381a5 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 fe76a3ac7563..000000000000 --- 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/gh_pages_coverage.yml b/.github/workflows/gh_pages_coverage.yml new file mode 100644 index 000000000000..2ad89ef90e68 --- /dev/null +++ b/.github/workflows/gh_pages_coverage.yml @@ -0,0 +1,46 @@ +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 + rm ./htmlcov/.gitignore + + - 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/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml deleted file mode 100644 index 15bcdc8f521e..000000000000 --- 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 5e3c2374ccb8..160667d9d804 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -1,78 +1,73 @@ -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: jobs: unit_tests: - strategy: - max-parallel: 3 - matrix: - python-version: ["3.11"] 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 - - 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 - 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 4d7bd682c9c5..1aaa34fc97e4 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/CHANGELOG.md b/CHANGELOG.md index fae3217dff12..e68463439c18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,152 @@ # Changelog -## [1.3.1a1](https://github.com/OpenVoiceOS/ovos-core/tree/1.3.1a1) (2025-05-15) +## [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/1.3.0...1.3.1a1) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-core/compare/2.0.4a5...2.0.5a1) **Merged pull requests:** -- fix: update requirements skill package names [\#683](https://github.com/OpenVoiceOS/ovos-core/pull/683) ([JarbasAl](https://github.com/JarbasAl)) +- 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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**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) + +**Merged pull requests:** + +- feat: m2v pipeline [\#681](https://github.com/OpenVoiceOS/ovos-core/pull/681) ([JarbasAl](https://github.com/JarbasAl)) diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index a41de4b6a74c..000000000000 --- 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/ovos_core/__main__.py b/ovos_core/__main__.py index 2a779a29da6f..425704d0899a 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/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index b3ee90c9c40d..b729cc75da8e 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -1,943 +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 -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 -from ovos_persona import PersonaService - -try: - from ovos_ollama_intent_pipeline import LLMIntentPipeline -except ImportError: - LLMIntentPipeline = 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 intent parsing pipelines, transformer services, and messagebus event handlers. - - Args: - bus: The messagebus connection for event handling. - 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. - """ - 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._load_pipeline_plugins() - - self.utterance_plugins = UtteranceTransformersService(bus) - self.metadata_plugins = MetadataTransformersService(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 used by 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. - """ - 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: - self._ollama = LLMIntentPipeline(self.bus, config=self.config.get("ovos-ollama-intent-pipeline", {})) - - def update_skill_name_dict(self, message): - """ - Updates the mapping of skill IDs to skill names based on a messagebus event. - - Args: - message: A message 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]: - """ - Returns an ordered list of intent matcher functions for the current session pipeline. - - 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. - - 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. - """ - 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._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 - """ - 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) - 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) - - # 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): - match = match_func([utterance], lang, message) - if match: - if match.match_type: - intent_data = 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})) - return - - # signal intent failure - self.bus.emit(message.reply("intent.service.intent.reply", - {"intent": None})) - - def handle_get_skills(self, message): - """Send registered skills to caller. - - Argument: - message: query message to reply to. - """ - self.bus.emit(message.reply("intent.service.skills.reply", - {"skills": self.skill_names})) - - 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 982fe78831d8..000000000000 --- 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 d292ceacd6f2..000000000000 --- 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 4d6421f97ed3..80b1444e38b3 100644 --- a/ovos_core/intent_services/converse_service.py +++ b/ovos_core/intent_services/converse_service.py @@ -1,33 +1,38 @@ 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 {}) + 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): @@ -209,17 +214,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 +249,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 +266,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 +284,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 +296,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="converse:skill", + match_data={"utterances": utterances, "lang": lang, "skill_id": skill_id}, + skill_id=skill_id, + utterance=utterances[0], + updated_session=session + ) + return None @staticmethod @@ -400,11 +375,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 +385,9 @@ 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("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('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 df2d5cb042f3..ed28d18474cd 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/locale/nl-nl/global_stop.intent b/ovos_core/intent_services/locale/nl-nl/global_stop.intent index fbe3afc8b570..d030bdac5c75 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 15e78b7c9799..a3c1ff8dd4ef 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 diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py deleted file mode 100644 index 790e79f5d609..000000000000 --- 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 7bd3fd645d10..000000000000 --- 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 b0f421b732c5..000000000000 --- 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 000000000000..841acf282827 --- /dev/null +++ b/ovos_core/intent_services/service.py @@ -0,0 +1,639 @@ +# 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 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 +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 = 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: + 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 + + 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 + message.data["lang"] = lang + 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', message.data)) + 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 e48463fc61f3..fcf8f6a5b0c8 100644 --- a/ovos_core/intent_services/stop_service.py +++ b/ovos_core/intent_services/stop_service.py @@ -2,29 +2,43 @@ 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_bus_client.session import SessionManager, UtteranceState + 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 {}) + 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" @@ -52,17 +66,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 +101,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 +145,51 @@ 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: + elif message.data.get('result', False): 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!") - - # 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})) - - return stopped - - def match_stop_high(self, utterances: List[str], lang: str, message: Message) -> Optional[PipelineMatch]: + 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]: """ 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 +211,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="stop:global", + 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="stop:skill", + match_data={"conf": conf, "skill_id": skill_id}, + 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 +271,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) - - 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 + return self.match_low(utterances, lang, message) - 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 +310,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="stop:skill", + match_data={"conf": conf, "skill_id": skill_id}, + 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="stop:global", + 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 +386,7 @@ 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 + + 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/ovos_core/skill_installer.py b/ovos_core/skill_installer.py index 3f4f5304fbd3..ce5cb5941cad 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 059da67b84ed..0e2c66050e40 100644 --- a/ovos_core/skill_manager.py +++ b/ovos_core/skill_manager.py @@ -14,46 +14,26 @@ # """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_bus_client.util.scheduler import EventScheduler 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. +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 - 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() - - 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(): @@ -80,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: @@ -105,7 +90,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 +108,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 @@ -134,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): @@ -143,8 +137,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 +288,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 +295,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 +363,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 +405,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 +429,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 +460,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. @@ -646,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): @@ -665,8 +510,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,10 +524,10 @@ 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) + LOG.info("Deactivating (unloading) skill: " + skill_loader.skill_id) skill_loader.deactivate() self.bus.emit(message.response()) except Exception as err: @@ -694,9 +538,9 @@ 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.skill_loaders, **self.plugin_skills} + skills = self.plugin_skills for skill in skills.values(): if skill.skill_id != skill_to_keep: skill.deactivate() @@ -708,79 +552,48 @@ 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): 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_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) - + 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() - - ############ - # 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 + try: + self._settings_watchdog.shutdown() + except Exception as e: + LOG.error(f"Failed to cleanly unload settings watchdog ({e})") diff --git a/ovos_core/transformers.py b/ovos_core/transformers.py index 9984cc36275e..3ac676dabdcf 100644 --- a/ovos_core/transformers.py +++ b/ovos_core/transformers.py @@ -1,8 +1,10 @@ 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 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: IntentHandlerMatch) -> IntentHandlerMatch: + """ + 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/ovos_core/version.py b/ovos_core/version.py index dc55042d4398..683f8b06e58d 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_MAJOR = 2 +VERSION_MINOR = 0 +VERSION_BUILD = 5 +VERSION_ALPHA = 1 # END_VERSION_BLOCK # for compat with old imports diff --git a/requirements/lgpl.txt b/requirements/lgpl.txt index a69b6b849b6e..106b0b899ca8 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 433a17466f8e..ff4ad1a3184b 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 10351857f8d3..f2ecca996798 100644 --- a/requirements/plugins.txt +++ b/requirements/plugins.txt @@ -1,7 +1,19 @@ -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-utterance-normalizer>=0.2.1, <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 + +# 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.18a1, <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.1.1,<1.0.0 +ahocorasick-ner>=0.1.1,<1.0.0 \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cbe30a2a3486..f922d2e18381 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,14 +3,11 @@ 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>=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 -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-config>=0.0.13,<2.0.0 -ovos-workshop>=3.3.4,<4.0.0 +rapidfuzz>=3.6,<4.0 +langcodes \ No newline at end of file diff --git a/requirements/skills-audio.txt b/requirements/skills-audio.txt index 3ccf0f146f6a..8fe86baf32a2 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 822399564f61..35c09b68cf12 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 09714f0e766f..35507b62e2a9 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 997e979b12f2..2ffaab767963 100644 --- a/requirements/skills-essential.txt +++ b/requirements/skills-essential.txt @@ -1,8 +1,11 @@ # 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,<1.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 +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 12c049da2eec..0dedcdc5f106 100644 --- a/requirements/skills-extra.txt +++ b/requirements/skills-extra.txt @@ -1,12 +1,10 @@ # skills providing non essential functionality -ovos-skill-wordnet>=0.0.10,<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-wordnet>=0.2.5,<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-gl.txt b/requirements/skills-gl.txt new file mode 100644 index 000000000000..f9ec9d061f92 --- /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/requirements/skills-gui.txt b/requirements/skills-gui.txt index b214c5823864..e6544b7d6c77 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 7d6e55538184..4ff3ee3e5c64 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,<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 +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 ad229eba61db..5a804f2a057f 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.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 diff --git a/requirements/tests.txt b/requirements/tests.txt index 24ec8fa3ef09..4f0539f66407 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.7.2,<1.0.0 diff --git a/setup.py b/setup.py index 61ca37d87598..4e7f696e5ab1 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: @@ -74,6 +73,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'), @@ -86,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*']), @@ -98,7 +99,9 @@ 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', + 'ovos-skill-installer=ovos_core.skill_installer:launch_standalone' ] } ) 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 7b2e0f82941e..000000000000 --- 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 05f24d1d800c..000000000000 --- 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 23eacd74a8f2..000000000000 --- 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 e69de29bb2d1..000000000000 diff --git a/test/end2end/routing/test_sched.py b/test/end2end/routing/test_sched.py deleted file mode 100644 index 639b8ed40d91..000000000000 --- 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 f1cd842e3d3a..000000000000 --- 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 e69de29bb2d1..000000000000 diff --git a/test/end2end/session/test_blacklist.py b/test/end2end/session/test_blacklist.py deleted file mode 100644 index 9a57192fca3c..000000000000 --- 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 10da8f11d444..000000000000 --- 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 4520d73d67b0..000000000000 --- 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 7b4e33b533d4..000000000000 --- 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 b2320d41a16d..000000000000 --- 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 0ec4ecb6cbd3..000000000000 --- 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 a9378e3b9e73..000000000000 --- 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 d85fc6c4490e..000000000000 --- 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 a30bfc1cf84e..000000000000 --- 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 525840279432..000000000000 --- 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 6241c34e5df5..000000000000 --- 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 54299a48fb3a..000000000000 --- 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 396a0ba2698c..000000000000 --- 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 7a839444669f..000000000000 --- 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 1dd99dc4e0a3..000000000000 --- 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 f0fb83cc4f33..000000000000 --- 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 30d74d258442..000000000000 --- 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 5161aff42996..000000000000 --- 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 1fec3fd265bf..000000000000 --- 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 88cde3244be1..000000000000 --- 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 484272aaa949..000000000000 --- 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 b8a7494fc509..000000000000 --- 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 b0e4fe1977fa..000000000000 --- 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 79eb070d0907..000000000000 --- 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 e8e602d24b74..000000000000 --- 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 ef5a13408fde..000000000000 --- 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 18091e85f31d..000000000000 --- 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 a60431638ca2..000000000000 --- 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 2ccbced0bb4a..000000000000 --- 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 ba449ed42791..000000000000 --- 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 cf4491512d86..000000000000 --- 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 2ca23a259678..000000000000 --- 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 e38e6177b5bc..000000000000 --- 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 f3074adc0ee4..000000000000 --- 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 3f31d7bbc9aa..000000000000 --- 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 21c97f9462d5..000000000000 --- 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 b9ecb5807a59..000000000000 --- 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 b8df1a9e9fbf..000000000000 --- 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 811f098f322b..000000000000 --- 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 5ffa264b9193..000000000000 --- 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 ad2d35c42181..000000000000 --- 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 b9ecb5807a59..000000000000 --- 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 60fcb3ab2f34..000000000000 --- 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 3fc393638315..000000000000 --- 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 b711e1a1c8db..000000000000 --- 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 5864a517f9ac..000000000000 --- 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 19be23c9b5fc..000000000000 --- 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 3b3da0b17b67..000000000000 --- 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 323eecd7a40e..000000000000 --- 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_activate.py b/test/end2end/test_activate.py new file mode 100644 index 000000000000..944c0950b959 --- /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 new file mode 100644 index 000000000000..853a7b6676c4 --- /dev/null +++ b/test/end2end/test_adapt.py @@ -0,0 +1,141 @@ +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 + +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.lang = "en-US" + session.pipeline = ['ovos-adapt-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"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", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:HelloWorldIntent", + 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": session.lang, + "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.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": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + 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"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + 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": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + 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"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + 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": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + minicroft=self.minicroft, + skill_ids=[self.skill_id], + final_session=session, + 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_cancel_plugin.py b/test/end2end/test_cancel_plugin.py new file mode 100644 index 000000000000..d940d0488e39 --- /dev/null +++ b/test/end2end/test_cancel_plugin.py @@ -0,0 +1,64 @@ +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") + session.lang = "en-US" + message = Message("recognizer_loop:utterance", + {"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 + test = End2EndTest( + 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"}), + 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) + diff --git a/test/end2end/test_converse.py b/test/end2end/test_converse.py new file mode 100644 index 000000000000..a8799ca3c14f --- /dev/null +++ b/test/end2end/test_converse.py @@ -0,0 +1,167 @@ +from copy import deepcopy +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.lang = "en-US" + 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 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"}) + message3 = Message("recognizer_loop:utterance", + {"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"}) + + 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": session.lang}, + 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("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": session.lang}, + context={"skill_id": self.skill_id}), + Message("speak", + data={"utterance": "echo test", + "expect_response": False, + "lang": session.lang, + "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}), + Message("ovos.utterance.handled", + data={}, + 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("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": session.lang}, + context={"skill_id": self.skill_id}), + + Message("speak", + data={"expect_response": False, + "lang": session.lang, + "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}), + Message("ovos.utterance.handled", + data={}, + 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") + ] + + 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"], + # 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}.activate", # TODO + ] + ) + test.execute(timeout=10) diff --git a/test/end2end/test_fallback.py b/test/end2end/test_fallback.py new file mode 100644 index 000000000000..de8a67b73e4d --- /dev/null +++ b/test/end2end/test_fallback.py @@ -0,0 +1,69 @@ +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 + +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.lang = "en-US" + session.pipeline = ['ovos-fallback-pipeline-plugin-low'] + message = Message("recognizer_loop:utterance", + {"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], + 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, + Message("ovos.skills.fallback.ping", + {"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": session.lang, "range": [90, 101], "skill_id": self.skill_id}), + Message(f"ovos.skills.fallback.{self.skill_id}.start", {}), + Message("speak", + data={"lang": session.lang, + "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_lang_detect.py b/test/end2end/test_lang_detect.py new file mode 100644 index 000000000000..b1cebd98a768 --- /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() diff --git a/test/end2end/test_no_skills.py b/test/end2end/test_no_skills.py new file mode 100644 index 000000000000..ccbb25aa6000 --- /dev/null +++ b/test/end2end/test_no_skills.py @@ -0,0 +1,61 @@ +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_utils.log import LOG + +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"], + 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 + # 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"], + 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/end2end/test_padatious.py b/test/end2end/test_padatious.py new file mode 100644 index 000000000000..e019676f35b2 --- /dev/null +++ b/test/end2end/test_padatious.py @@ -0,0 +1,140 @@ +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 + +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.lang = "en-US" + session.pipeline = ["ovos-padatious-pipeline-plugin-high"] + message = Message("recognizer_loop:utterance", + {"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", + data={}, + context={"skill_id": self.skill_id}), + Message(f"{self.skill_id}:Greetings.intent", + 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": session.lang, + "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.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": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + 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"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + 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": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + 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"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) + + 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": session.lang}, + {"session": session.serialize(), "source": "A", "destination": "B"}) + + test = End2EndTest( + 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"}), + Message("complete_intent_failure", {}), + Message("ovos.utterance.handled", {}) + ] + ) + + test.execute(timeout=10) diff --git a/test/end2end/test_stop.py b/test/end2end/test_stop.py new file mode 100644 index 000000000000..a90344696970 --- /dev/null +++ b/test/end2end/test_stop.py @@ -0,0 +1,387 @@ +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.lang = "en-US" + session.pipeline = ['ovos-stop-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"utterances": ["stop"], "lang": session.lang}, + {"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, + # keep_original_src=["stop.openvoiceos.activate"], # TODO + 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.lang = "en-US" + session.pipeline = ['ovos-stop-pipeline-plugin-high'] + message = Message("recognizer_loop:utterance", + {"utterances": ["could you stop that"], "lang": session.lang}, + {"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.lang = "en-US" + session.pipeline = ['ovos-stop-pipeline-plugin-medium'] + message = Message("recognizer_loop:utterance", + {"utterances": ["could you stop that"], "lang": session.lang}, + {"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, + # keep_original_src=["stop.openvoiceos.activate"], # TODO + 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.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": session.lang}, + {"session": session.serialize()}) + + # first count to 10 to validate skill is working + activate_skill = [ + message, + 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" + }), + # 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, + # keep_original_src=[f"{self.skill_id}.activate"], # TODO + expected_messages=activate_skill + ) + test.execute() + + 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": 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(2) + + message = Message("recognizer_loop:utterance", + {"utterances": ["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}), + + # 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}, + # {"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", + # "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 + ) + test.execute() + + 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": session.lang}, + {"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": session.lang}, + {"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, + #keep_original_src=["stop.openvoiceos.activate"], # TODO + ) + 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(2) + + 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}), + + # 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}, + # {"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", + # "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 + ) + test.execute() diff --git a/test/integrationtests/__init__.py b/test/integrationtests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/test/integrationtests/common_query/__init__.py b/test/integrationtests/common_query/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 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 ca8fee4af70f..000000000000 --- 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 b484c505afc0..000000000000 --- 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 7166625bef00..000000000000 --- 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 da62a296df60..000000000000 --- 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 fac8fab9e72d..000000000000 --- 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 b8d25ba2b4e5..000000000000 --- 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 bd3f81fa5b8f..000000000000 --- 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 ea1059fef4d9..000000000000 --- 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_file_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_file_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_file_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_file_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 f0fb83cc4f33..000000000000 --- 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 30d74d258442..000000000000 --- 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 5161aff42996..000000000000 --- 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 1fec3fd265bf..000000000000 --- 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 add1af272c68..000000000000 --- 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 fb2af9105142..000000000000 --- 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 af20b99f298d..000000000000 --- 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 == []) diff --git a/test/unittests/test_intent_service.py b/test/unittests/test_intent_service.py index b8f2a38a2700..b5da55b6ccff 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 4f67d073a565..d85fb43bb19a 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_installer.py b/test/unittests/test_skill_installer.py index 08760e7e46d1..a2c8f6c306c0 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) diff --git a/test/unittests/test_skill_manager.py b/test/unittests/test_skill_manager.py index 9bbab883deec..5665fe176f87 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()