From 308ff2d4430442c10cb077003ba51cf305fb1b67 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 28 Dec 2023 19:59:18 +0000 Subject: [PATCH] refactor/move_from_utils (#153) * refactor/move_from_utils https://github.com/OpenVoiceOS/ovos-utils/issues/205 * update imports * update imports * tests --- ovos_workshop/app.py | 2 +- ovos_workshop/intents.py | 498 ++++++++++++++++++ ovos_workshop/permissions.py | 34 ++ ovos_workshop/settings.py | 58 +- ovos_workshop/skill_launcher.py | 8 +- ovos_workshop/skills/api.py | 82 +++ ovos_workshop/skills/auto_translatable.py | 2 +- ovos_workshop/skills/fallback.py | 3 +- ovos_workshop/skills/idle_display_skill.py | 2 +- ovos_workshop/skills/intent_provider.py | 2 +- ovos_workshop/skills/ovos.py | 21 +- requirements/requirements.txt | 2 +- test/unittests/skills/test_base.py | 6 +- test/unittests/skills/test_fallback_skill.py | 3 +- .../test_mycroft_skill/test_mycroft_skill.py | 2 +- test/unittests/skills/test_ovos.py | 4 +- test/unittests/test_decorators.py | 3 +- 17 files changed, 702 insertions(+), 30 deletions(-) create mode 100644 ovos_workshop/intents.py create mode 100644 ovos_workshop/skills/api.py diff --git a/ovos_workshop/app.py b/ovos_workshop/app.py index 3045adf3..166e38cc 100644 --- a/ovos_workshop/app.py +++ b/ovos_workshop/app.py @@ -3,7 +3,7 @@ from ovos_config.locations import get_xdg_config_save_path from ovos_utils.messagebus import get_mycroft_bus from ovos_utils.log import log_deprecation -from ovos_utils.gui import GUIInterface +from ovos_bus_client.apis.gui import GUIInterface from ovos_bus_client.client.client import MessageBusClient from ovos_workshop.resource_files import locate_lang_directories from ovos_workshop.skills.ovos import OVOSSkill diff --git a/ovos_workshop/intents.py b/ovos_workshop/intents.py new file mode 100644 index 00000000..44a5ca1f --- /dev/null +++ b/ovos_workshop/intents.py @@ -0,0 +1,498 @@ +from os.path import exists +from threading import RLock +from typing import List, Tuple, Optional + +from ovos_utils.log import LOG, log_deprecation +from ovos_utils.messagebus import get_mycroft_bus +from ovos_bus_client.message import Message, dig_for_message + +try: + from adapt.intent import IntentBuilder, Intent +except ImportError: + # adapt is optional + + class Intent: + def __init__(self, name, requires, at_least_one, optional): + """Create Intent object + Args: + name(str): Name for Intent + requires(list): Entities that are required + at_least_one(list): One of these Entities are required + optional(list): Optional Entities used by the intent + """ + self.name = name + self.requires = requires + self.at_least_one = at_least_one + self.optional = optional + + def validate(self, tags, confidence): + """Using this method removes tags from the result of validate_with_tags + Returns: + intent(intent): Results from validate_with_tags + """ + raise NotImplementedError("please install adapt-parser") + + def validate_with_tags(self, tags, confidence): + """Validate whether tags has required entites for this intent to fire + Args: + tags(list): Tags and Entities used for validation + confidence(float): The weight associate to the parse result, + as indicated by the parser. This is influenced by a parser + that uses edit distance or context. + Returns: + intent, tags: Returns intent and tags used by the intent on + failure to meat required entities then returns intent with + confidence + of 0.0 and an empty list for tags. + """ + raise NotImplementedError("please install adapt-parser") + + + class IntentBuilder: + """ + IntentBuilder, used to construct intent parsers. + Attributes: + at_least_one(list): A list of Entities where one is required. + These are separated into lists so you can have one of (A or B) and + then require one of (D or F). + requires(list): A list of Required Entities + optional(list): A list of optional Entities + name(str): Name of intent + Notes: + This is designed to allow construction of intents in one line. + Example: + IntentBuilder("Intent")\ + .requires("A")\ + .one_of("C","D")\ + .optional("G").build() + """ + + def __init__(self, intent_name): + """ + Constructor + Args: + intent_name(str): the name of the intents that this parser + parses/validates + """ + self.at_least_one = [] + self.requires = [] + self.optional = [] + self.name = intent_name + + def one_of(self, *args): + """ + The intent parser should require one of the provided entity types to + validate this clause. + Args: + args(args): *args notation list of entity names + Returns: + self: to continue modifications. + """ + self.at_least_one.append(args) + return self + + def require(self, entity_type, attribute_name=None): + """ + The intent parser should require an entity of the provided type. + Args: + entity_type(str): an entity type + attribute_name(str): the name of the attribute on the parsed intent. + Defaults to match entity_type. + Returns: + self: to continue modifications. + """ + if not attribute_name: + attribute_name = entity_type + self.requires += [(entity_type, attribute_name)] + return self + + def optionally(self, entity_type, attribute_name=None): + """ + Parsed intents from this parser can optionally include an entity of the + provided type. + Args: + entity_type(str): an entity type + attribute_name(str): the name of the attribute on the parsed intent. + Defaults to match entity_type. + Returns: + self: to continue modifications. + """ + if not attribute_name: + attribute_name = entity_type + self.optional += [(entity_type, attribute_name)] + return self + + def build(self): + """ + Constructs an intent from the builder's specifications. + :return: an Intent instance. + """ + return Intent(self.name, self.requires, + self.at_least_one, self.optional) + + +def to_alnum(skill_id: str) -> str: + """ + Convert a skill id to only alphanumeric characters + Non-alphanumeric characters are converted to "_" + + Args: + skill_id (str): identifier to be converted + Returns: + (str) String of letters + """ + return ''.join(c if c.isalnum() else '_' for c in str(skill_id)) + + +def munge_regex(regex: str, skill_id: str) -> str: + """ + Insert skill id as letters into match groups. + + Args: + regex (str): regex string + skill_id (str): skill identifier + Returns: + (str) munged regex + """ + base = '(?P<' + to_alnum(skill_id) + return base.join(regex.split('(?P<')) + + +def munge_intent_parser(intent_parser, name, skill_id): + """ + Rename intent keywords to make them skill exclusive + This gives the intent parser an exclusive name in the + format :. The keywords are given unique + names in the format . + + The function will not munge instances that's already been + munged + + Args: + intent_parser: (IntentParser) object to update + name: (str) Skill name + skill_id: (int) skill identifier + """ + # Munge parser name + if not name.startswith(str(skill_id) + ':'): + intent_parser.name = str(skill_id) + ':' + name + else: + intent_parser.name = name + + # Munge keywords + skill_id = to_alnum(skill_id) + # Munge required keyword + reqs = [] + for i in intent_parser.requires: + if not i[0].startswith(skill_id): + kw = (skill_id + i[0], skill_id + i[0]) + reqs.append(kw) + else: + reqs.append(i) + intent_parser.requires = reqs + + # Munge optional keywords + opts = [] + for i in intent_parser.optional: + if not i[0].startswith(skill_id): + kw = (skill_id + i[0], skill_id + i[0]) + opts.append(kw) + else: + opts.append(i) + intent_parser.optional = opts + + # Munge at_least_one keywords + at_least_one = [] + for i in intent_parser.at_least_one: + element = [skill_id + e.replace(skill_id, '') for e in i] + at_least_one.append(tuple(element)) + intent_parser.at_least_one = at_least_one + + +class IntentServiceInterface: + """ + Interface to communicate with the Mycroft intent service. + + This class wraps the messagebus interface of the intent service allowing + for easier interaction with the service. It wraps both the Adapt and + Padatious parts of the intent services. + """ + + def __init__(self, bus=None): + self._bus = bus + self.skill_id = self.__class__.__name__ + # TODO: Consider using properties with setters to prevent duplicates + self.registered_intents: List[Tuple[str, object]] = [] + self.detached_intents: List[Tuple[str, object]] = [] + self._iterator_lock = RLock() + + @property + def intent_names(self) -> List[str]: + """ + Get a list of intent names (both registered and disabled). + """ + return [a[0] for a in self.registered_intents + self.detached_intents] + + @property + def bus(self): + if not self._bus: + raise RuntimeError("bus not set. call `set_bus()` before trying to" + "interact with the Messagebus") + return self._bus + + @bus.setter + def bus(self, val): + self.set_bus(val) + + def set_bus(self, bus=None): + self._bus = bus or get_mycroft_bus() + + def set_id(self, skill_id: str): + self.skill_id = skill_id + + def register_adapt_keyword(self, vocab_type: str, entity: str, + aliases: Optional[List[str]] = None, + lang: str = None): + """ + Send a message to the intent service to add an Adapt keyword. + @param vocab_type: Keyword reference (file basename) + @param entity: Primary keyword value + @param aliases: List of alternative keyword values + @param lang: BCP-47 language code of entity and aliases + """ + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + + # TODO 22.02: Remove compatibility data + aliases = aliases or [] + entity_data = {'entity_value': entity, + 'entity_type': vocab_type, + 'lang': lang} + compatibility_data = {'start': entity, 'end': vocab_type} + + self.bus.emit(msg.forward("register_vocab", + {**entity_data, **compatibility_data})) + for alias in aliases: + alias_data = { + 'entity_value': alias, + 'entity_type': vocab_type, + 'alias_of': entity, + 'lang': lang} + compatibility_data = {'start': alias, 'end': vocab_type} + self.bus.emit(msg.forward("register_vocab", + {**alias_data, **compatibility_data})) + + def register_adapt_regex(self, regex: str, lang: str = None): + """ + Register a regex string with the intent service. + @param regex: Regex to be registered; Adapt extracts keyword references + from named match group. + @param lang: BCP-47 language code of regex + """ + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward("register_vocab", + {'regex': regex, 'lang': lang})) + + def register_adapt_intent(self, name: str, intent_parser: object): + """ + Register an Adapt intent parser object. Serializes the intent_parser + and sends it over the messagebus to registered. + @param name: string intent name (without skill_id prefix) + @param intent_parser: Adapt Intent object + """ + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward("register_intent", intent_parser.__dict__)) + self.registered_intents.append((name, intent_parser)) + self.detached_intents = [detached for detached in self.detached_intents + if detached[0] != name] + + def detach_intent(self, intent_name: str): + """ + DEPRECATED: Use `remove_intent` instead, all other methods from this + class expect intent_name; this was the weird one expecting the internal + munged intent_name with skill_id. + """ + name = intent_name.split(':')[1] + log_deprecation(f"Update to `self.remove_intent({name})", + "0.1.0") + self.remove_intent(name) + + def remove_intent(self, intent_name: str): + """ + Remove an intent from the intent service. The intent is saved in the + list of detached intents for use when re-enabling an intent. A + `detach_intent` Message is emitted for the intent service to handle. + @param intent_name: Registered intent to remove/detach (no skill_id) + """ + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + if intent_name in self.intent_names: + # TODO: This will create duplicates of already detached intents + LOG.info(f"Detaching intent: {intent_name}") + self.detached_intents.append((intent_name, + self.get_intent(intent_name))) + self.registered_intents = [pair for pair in self.registered_intents + if pair[0] != intent_name] + self.bus.emit(msg.forward("detach_intent", + {"intent_name": + f"{self.skill_id}:{intent_name}"})) + + def intent_is_detached(self, intent_name: str) -> bool: + """ + Determine if an intent is detached. + @param intent_name: String intent reference to check (without skill_id) + @return: True if intent is in detached_intents, else False. + """ + is_detached = False + with self._iterator_lock: + for (name, _) in self.detached_intents: + if name == intent_name: + is_detached = True + break + return is_detached + + def set_adapt_context(self, context: str, word: str, origin: str): + """ + Set an Adapt context. + @param context: context keyword name to add/update + @param word: word to register (context keyword value) + @param origin: original origin of the context (for cross context) + """ + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward('add_context', + {'context': context, 'word': word, + 'origin': origin})) + + def remove_adapt_context(self, context: str): + """ + Remove an Adapt context. + @param context: context keyword name to remove + """ + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward('remove_context', {'context': context})) + + def register_padatious_intent(self, intent_name: str, filename: str, + lang: str): + """ + Register a Padatious intent file with the intent service. + @param intent_name: Unique intent identifier + (usually `skill_id`:`filename`) + @param filename: Absolute file path to entity file + @param lang: BCP-47 language code of registered intent + """ + if not isinstance(filename, str): + raise ValueError('Filename path must be a string') + if not exists(filename): + raise FileNotFoundError(f'Unable to find "{filename}"') + with open(filename) as f: + samples = [_ for _ in f.read().split("\n") if _ + and not _.startswith("#")] + data = {'file_name': filename, + "samples": samples, + 'name': intent_name, + 'lang': lang} + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward("padatious:register_intent", data)) + self.registered_intents.append((intent_name.split(':')[-1], data)) + + def register_padatious_entity(self, entity_name: str, filename: str, + lang: str): + """ + Register a Padatious entity file with the intent service. + @param entity_name: Unique entity identifier + (usually `skill_id`:`filename`) + @param filename: Absolute file path to entity file + @param lang: BCP-47 language code of registered intent + """ + if not isinstance(filename, str): + raise ValueError('Filename path must be a string') + if not exists(filename): + raise FileNotFoundError('Unable to find "{}"'.format(filename)) + with open(filename) as f: + samples = [_ for _ in f.read().split("\n") if _ + and not _.startswith("#")] + msg = dig_for_message() or Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward('padatious:register_entity', + {'file_name': filename, + "samples": samples, + 'name': entity_name, + 'lang': lang})) + + def get_intent_names(self): + log_deprecation("Reference `intent_names` directly", "0.1.0") + return self.intent_names + + def detach_all(self): + """ + Detach all intents associated with this interface and remove all + internal references to intents and handlers. + """ + for name in self.intent_names: + self.remove_intent(name) + if self.registered_intents: + LOG.error(f"Expected an empty list; got: {self.registered_intents}") + self.registered_intents = [] + self.detached_intents = [] # Explicitly remove all intent references + + def get_intent(self, intent_name: str) -> Optional[object]: + """ + Get an intent object by name. This will find both enabled and disabled + intents. + @param intent_name: name of intent to find (without skill_id) + @return: intent object if found, else None + """ + to_return = None + with self._iterator_lock: + for name, intent in self.registered_intents: + if name == intent_name: + to_return = intent + break + if to_return is None: + with self._iterator_lock: + for name, intent in self.detached_intents: + if name == intent_name: + to_return = intent + break + return to_return + + def __iter__(self): + """Iterator over the registered intents. + + Returns an iterator returning name-handler pairs of the registered + intent handlers. + """ + return iter(self.registered_intents) + + def __contains__(self, val): + """ + Checks if an intent name has been registered. + """ + return val in [i[0] for i in self.registered_intents] + + +def open_intent_envelope(message): + """ + Convert dictionary received over messagebus to Intent. + """ + # TODO can this method be fully removed from ovos_utils ? + from adapt.intent import Intent + + intent_dict = message.data + return Intent(intent_dict.get('name'), + intent_dict.get('requires'), + intent_dict.get('at_least_one'), + intent_dict.get('optional')) diff --git a/ovos_workshop/permissions.py b/ovos_workshop/permissions.py index 9325439d..b97f2836 100644 --- a/ovos_workshop/permissions.py +++ b/ovos_workshop/permissions.py @@ -1,5 +1,39 @@ import enum +from ovos_config.config import read_mycroft_config, update_mycroft_config + + +def blacklist_skill(skill, config=None): + config = config or read_mycroft_config() + skills_config = config.get("skills", {}) + blacklisted_skills = skills_config.get("blacklisted_skills", []) + if skill not in blacklisted_skills: + blacklisted_skills.append(skill) + conf = { + "skills": { + "blacklisted_skills": blacklisted_skills + } + } + update_mycroft_config(conf) + return True + return False + + +def whitelist_skill(skill, config=None): + config = config or read_mycroft_config() + skills_config = config.get("skills", {}) + blacklisted_skills = skills_config.get("blacklisted_skills", []) + if skill in blacklisted_skills: + blacklisted_skills.pop(skill) + conf = { + "skills": { + "blacklisted_skills": blacklisted_skills + } + } + update_mycroft_config(conf) + return True + return False + class ConverseMode(str, enum.Enum): """ diff --git a/ovos_workshop/settings.py b/ovos_workshop/settings.py index 1e7a4a39..52988452 100644 --- a/ovos_workshop/settings.py +++ b/ovos_workshop/settings.py @@ -1,9 +1,11 @@ import json -import yaml - from os.path import isfile -from typing import Optional from threading import Timer +from typing import Optional + +import yaml +from json_database import JsonStorageXDG + from ovos_backend_client.api import DeviceApi from ovos_backend_client.pairing import is_paired, requires_backend from ovos_backend_client.settings import RemoteSkillSettings, get_display_name @@ -160,3 +162,53 @@ def handle_upload_local(self, message: Message): def handle_download_remote(self, message: Message): self.download() + + +def settings2meta(settings, section_name="Skill Settings"): + """ generates basic settingsmeta """ + fields = [] + + for k, v in settings.items(): + if k.startswith("_"): + continue + label = k.replace("-", " ").replace("_", " ").title() + if isinstance(v, bool): + fields.append({ + "name": k, + "type": "checkbox", + "label": label, + "value": str(v).lower() + }) + if isinstance(v, str): + fields.append({ + "name": k, + "type": "text", + "label": label, + "value": v + }) + if isinstance(v, int): + fields.append({ + "name": k, + "type": "number", + "label": label, + "value": str(v) + }) + return { + "skillMetadata": { + "sections": [ + { + "name": section_name, + "fields": fields + } + ] + } + } + + +class PrivateSettings(JsonStorageXDG): + def __init__(self, skill_id): + super(PrivateSettings, self).__init__(skill_id) + + @property + def settingsmeta(self): + return settings2meta(self, self.name) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index fadccfb9..23ad3f44 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -15,7 +15,6 @@ from ovos_utils.file_utils import FileWatcher from ovos_utils.log import LOG, deprecated, log_deprecation from ovos_utils.process_utils import RuntimeRequirements -from ovos_utils.skills.locations import get_skill_directories as _get_skill_dirs from ovos_workshop.skills.active import ActiveSkill from ovos_workshop.skills.auto_translatable import UniversalSkill, UniversalFallback @@ -35,15 +34,16 @@ SKILL_MAIN_MODULE = '__init__.py' -@deprecated("This method has moved to `ovos_utils.skills.locations`", "0.1.0") +@deprecated("This method has moved to `ovos_plugin_manager.skills`", "0.1.0") def get_skill_directories(conf=None): conf = conf or Configuration() + from ovos_plugin_manager.skills import get_skill_directories as _get_skill_dirs return _get_skill_dirs(conf) -@deprecated("This method has moved to `ovos_utils.skills.locations`", "0.1.0") +@deprecated("This method has moved to `ovos_plugin_manager.skills`", "0.1.0") def get_default_skills_directory(conf=None): - from ovos_utils.skills.locations import get_default_skills_directory + from ovos_plugin_manager.skills import get_default_skills_directory conf = conf or Configuration() return get_default_skills_directory(conf) diff --git a/ovos_workshop/skills/api.py b/ovos_workshop/skills/api.py new file mode 100644 index 00000000..24958fbd --- /dev/null +++ b/ovos_workshop/skills/api.py @@ -0,0 +1,82 @@ +# Copyright 2020 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. + +from typing import Dict, Optional +from ovos_bus_client.message import Message +from ovos_utils.log import LOG + + +class SkillApi: + """ + SkillApi provides a MessageBus interface to specific registered methods. + Methods decorated with `@skill_api_method` are exposed via the messagebus. + To use a skill's API methods, call `SkillApi.get` with the requested skill's + ID and an object is returned with an interface to all exposed methods. + """ + bus = None + + @classmethod + def connect_bus(cls, mycroft_bus): + """Registers the bus object to use.""" + cls.bus = mycroft_bus + + def __init__(self, method_dict: Dict[str, dict], timeout: int = 3): + """ + Initialize a SkillApi for the given methods + @param method_dict: dict of method name to dict containing: + `help` - method docstring + `type` - string Message type associated with this method + @param timeout: Seconds to wait for a Skill API response + """ + self.method_dict = method_dict + self.timeout = timeout + for key in method_dict: + def get_method(k): + def method(*args, **kwargs): + m = self.method_dict[k] + data = {'args': args, 'kwargs': kwargs} + method_msg = Message(m['type'], data) + response = \ + SkillApi.bus.wait_for_response(method_msg, + timeout=self.timeout) + if not response: + LOG.error(f"Timed out waiting for {method_msg}") + return None + elif 'result' not in response.data: + LOG.error(f"missing `result` in: {response.data}") + else: + return response.data['result'] + + return method + + self.__setattr__(key, get_method(key)) + + @staticmethod + def get(skill: str, api_timeout: int = 3) -> Optional[object]: + """ + Generate a SkillApi object for the requested skill if that skill exposes + and API methods. + @param skill: ID of skill to get an API object for + @param api_timeout: seconds to wait for a skill API response + @return: SkillApi object if available, else None + """ + if not SkillApi.bus: + raise RuntimeError("Requested update before `SkillAPI.bus` is set. " + "Call `SkillAPI.connect_bus` first.") + public_api_msg = f'{skill}.public_api' + api = SkillApi.bus.wait_for_response(Message(public_api_msg)) + if api: + return SkillApi(api.data, api_timeout) + else: + return None \ No newline at end of file diff --git a/ovos_workshop/skills/auto_translatable.py b/ovos_workshop/skills/auto_translatable.py index ad251ef8..2fb3b5cf 100644 --- a/ovos_workshop/skills/auto_translatable.py +++ b/ovos_workshop/skills/auto_translatable.py @@ -2,7 +2,7 @@ from ovos_config import Configuration from ovos_plugin_manager.language import OVOSLangDetectionFactory, OVOSLangTranslationFactory -from ovos_utils import get_handler_name +from ovos_utils.events import get_handler_name from ovos_utils.log import LOG from ovos_workshop.resource_files import SkillResources diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index 56b49056..821b956f 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -19,7 +19,8 @@ from ovos_bus_client import MessageBusClient from ovos_utils.log import LOG -from ovos_utils.messagebus import get_handler_name, Message +from ovos_utils.events import get_handler_name +from ovos_bus_client.message import Message from ovos_utils.metrics import Stopwatch from ovos_utils.skills import get_non_properties from ovos_workshop.decorators.compat import backwards_compat diff --git a/ovos_workshop/skills/idle_display_skill.py b/ovos_workshop/skills/idle_display_skill.py index 7f680482..8d4670ca 100644 --- a/ovos_workshop/skills/idle_display_skill.py +++ b/ovos_workshop/skills/idle_display_skill.py @@ -13,7 +13,7 @@ # limitations under the License. from ovos_utils.log import LOG, log_deprecation -from ovos_utils.messagebus import Message +from ovos_bus_client.message import Message from ovos_workshop.skills.base import BaseSkill diff --git a/ovos_workshop/skills/intent_provider.py b/ovos_workshop/skills/intent_provider.py index f85a389e..8009765a 100644 --- a/ovos_workshop/skills/intent_provider.py +++ b/ovos_workshop/skills/intent_provider.py @@ -1,7 +1,7 @@ from threading import Event from time import time as get_time, sleep from ovos_utils.log import LOG, log_deprecation -from ovos_utils.messagebus import Message +from ovos_bus_client.message import Message from ovos_workshop.skills.fallback import FallbackSkill from ovos_config.config import read_mycroft_config, update_mycroft_config diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 33a3d406..127ca620 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -22,18 +22,17 @@ from ovos_backend_client.api import EmailApi, MetricsApi from ovos_bus_client import MessageBusClient +from ovos_bus_client.apis.enclosure import EnclosureAPI +from ovos_bus_client.apis.gui import GUIInterface +from ovos_bus_client.apis.ocp import OCPInterface from ovos_bus_client.message import Message, dig_for_message from ovos_bus_client.session import SessionManager, Session from ovos_plugin_manager.language import OVOSLangTranslationFactory, OVOSLangDetectionFactory from ovos_utils import camel_case_split, classproperty from ovos_utils.dialog import get_dialog, MustacheDialogRenderer -from ovos_utils.enclosure.api import EnclosureAPI from ovos_utils.events import EventContainer, EventSchedulerInterface from ovos_utils.file_utils import FileWatcher -from ovos_utils.gui import GUIInterface, get_ui_directories -from ovos_utils.intents import ConverseTracker, IntentBuilder, Intent -from ovos_utils.intents.intent_service_interface import munge_regex, \ - munge_intent_parser, IntentServiceInterface +from ovos_utils.gui import get_ui_directories from ovos_utils.json_helper import merge_dict from ovos_utils.log import LOG, log_deprecation, deprecated from ovos_utils.messagebus import get_handler_name, create_wrapper, \ @@ -41,16 +40,17 @@ from ovos_utils.parse import match_one from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties -from ovos_utils.skills.audioservice import OCPInterface -from ovos_utils.skills.settings import PrivateSettings from ovos_utils.sound import play_audio from ovos_workshop.decorators.compat import backwards_compat from ovos_workshop.decorators.killable import AbortEvent, killable_event, \ AbortQuestion from ovos_workshop.decorators.layers import IntentLayers from ovos_workshop.filesystem import FileSystemAccess +from ovos_workshop.intents import IntentBuilder, Intent, munge_regex, \ + munge_intent_parser, IntentServiceInterface from ovos_workshop.resource_files import ResourceFile, \ CoreResources, find_resource, SkillResources +from ovos_workshop.settings import PrivateSettings from ovos_workshop.settings import SkillSettingsManager @@ -868,8 +868,11 @@ def __bind_classic(self, bus): # inject ovos exclusive features in vanilla mycroft-core # if possible # limited support for missing skill deactivated event - # TODO - update ConverseTracker - ConverseTracker.connect_bus(self.bus) # pull/1468 + try: + from ovos_utils.intents.converse import ConverseTracker + ConverseTracker.connect_bus(self.bus) # pull/1468 + except ImportError: + pass # deprecated in utils 0.1.0 self.add_event("converse.skill.deactivated", self._handle_skill_deactivated, speak_errors=False) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ed721d82..22d7d5b8 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ ovos-utils < 0.1.0, >=0.0.35 ovos_config < 0.1.0,>=0.0.10 ovos-lingua-franca~=0.4,>=0.4.6 -ovos-bus-client < 0.1.0, >=0.0.5 +ovos-bus-client < 0.1.0, >=0.0.6a23 ovos_backend_client~=0.0,>=0.0.6 rapidfuzz diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py index b694deed..d3f2944f 100644 --- a/test/unittests/skills/test_base.py +++ b/test/unittests/skills/test_base.py @@ -40,9 +40,9 @@ def test_00_skill_init(self): from ovos_workshop.settings import SkillSettingsManager from ovos_workshop.skills.ovos import SkillGUI from ovos_utils.events import EventContainer, EventSchedulerInterface - from ovos_utils.intents import IntentServiceInterface + from ovos_workshop.intents import IntentServiceInterface from ovos_utils.process_utils import RuntimeRequirements - from ovos_utils.enclosure.api import EnclosureAPI + from ovos_bus_client.apis.enclosure import EnclosureAPI from ovos_workshop.filesystem import FileSystemAccess from ovos_workshop.resource_files import SkillResources @@ -560,7 +560,7 @@ class GuiSkill(Mock): @patch("ovos_workshop.skills.ovos.GUIInterface.__init__") def test_skill_gui(self, interface_init): - from ovos_utils.gui import GUIInterface + from ovos_bus_client.apis.gui import GUIInterface from ovos_workshop.skills.base import SkillGUI # Old skill with `ui` directory in root diff --git a/test/unittests/skills/test_fallback_skill.py b/test/unittests/skills/test_fallback_skill.py index 9b07c35b..7320d791 100644 --- a/test/unittests/skills/test_fallback_skill.py +++ b/test/unittests/skills/test_fallback_skill.py @@ -2,7 +2,8 @@ from unittest.mock import patch, Mock from threading import Event -from ovos_utils.messagebus import FakeBus, Message +from ovos_utils.messagebus import FakeBus +from ovos_bus_client.message import Message from ovos_workshop.decorators import fallback_handler from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.fallback import FallbackSkillV1, FallbackSkillV2, \ diff --git a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py index 12ce8a42..b2b4f18f 100644 --- a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py @@ -20,7 +20,7 @@ from os.path import join, dirname, abspath from unittest.mock import MagicMock, patch -from ovos_utils.intents import IntentBuilder +from ovos_workshop.intents import IntentBuilder from ovos_bus_client import Message from ovos_config.config import Configuration diff --git a/test/unittests/skills/test_ovos.py b/test/unittests/skills/test_ovos.py index d43ab520..fbdcf8d9 100644 --- a/test/unittests/skills/test_ovos.py +++ b/test/unittests/skills/test_ovos.py @@ -43,12 +43,12 @@ class TestOVOSSkill(unittest.TestCase): skill = OVOSSkill(bus=bus, skill_id="test_ovos_skill") def test_00_skill_init(self): - from ovos_utils.skills.audioservice import AudioServiceInterface + from ovos_bus_client.apis.ocp import OCPInterface self.assertIsInstance(self.skill.private_settings, dict) self.assertIsInstance(self.skill._threads, list) self.assertIsNotNone(self.skill._original_converse) self.assertIsInstance(self.skill.intent_layers, IntentLayers) - self.assertIsInstance(self.skill.audio_service, AudioServiceInterface) + self.assertIsInstance(self.skill.audio_service, OCPInterface) self.assertTrue(self.skill.is_fully_initialized) self.assertFalse(self.skill.stop_is_implemented) self.assertFalse(self.skill.converse_is_implemented) diff --git a/test/unittests/test_decorators.py b/test/unittests/test_decorators.py index 62865b52..9405f4a1 100644 --- a/test/unittests/test_decorators.py +++ b/test/unittests/test_decorators.py @@ -5,7 +5,8 @@ from time import sleep from ovos_workshop.skill_launcher import SkillLoader -from ovos_utils.messagebus import FakeBus, Message +from ovos_utils.messagebus import FakeBus +from ovos_bus_client.message import Message class TestDecorators(unittest.TestCase):