diff --git a/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.hash b/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.hash index 00a57cbb..919444b2 100644 --- a/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.hash +++ b/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.hash @@ -1,2 +1,2 @@ # Locally computed -sha256 51c99fbb8b74941811f89bdc815f25018a5b714a243f12757ef0b50bca6a2910 mycroft-embedded-shell-c2855bffe17cd346397909bf84842eca2d15bd39.tar.gz +sha256 9ca71d8feaa126ef7a50c9112a49282a9e3840dfcfbf6d83ef7b317103f98234 mycroft-embedded-shell-be59e63342e25071be789830f70829b1b9d4755e.tar.gz diff --git a/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.mk b/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.mk index bedefa4d..90e319ef 100644 --- a/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.mk +++ b/buildroot-external/package/mycroft-embedded-shell/mycroft-embedded-shell.mk @@ -4,7 +4,7 @@ # ################################################################################ -MYCROFT_EMBEDDED_SHELL_VERSION = c2855bffe17cd346397909bf84842eca2d15bd39 +MYCROFT_EMBEDDED_SHELL_VERSION = be59e63342e25071be789830f70829b1b9d4755e MYCROFT_EMBEDDED_SHELL_SITE = $(call github,OpenVoiceOS,mycroft-embedded-shell,$(MYCROFT_EMBEDDED_SHELL_VERSION)) MYCROFT_EMBEDDED_SHELL_LICENSE = Apache License 2.0 diff --git a/buildroot-external/package/mycroft-skill-ovos-volume/mycroft-skill-ovos-volume.mk b/buildroot-external/package/mycroft-skill-ovos-volume/mycroft-skill-ovos-volume.mk index d99b0178..b1d56112 100644 --- a/buildroot-external/package/mycroft-skill-ovos-volume/mycroft-skill-ovos-volume.mk +++ b/buildroot-external/package/mycroft-skill-ovos-volume/mycroft-skill-ovos-volume.mk @@ -4,7 +4,7 @@ # ################################################################################ -MYCROFT_SKILL_OVOS_VOLUME_VERSION = ecef563f77dab21605b4e71bb207692c2cbede1f +MYCROFT_SKILL_OVOS_VOLUME_VERSION = 1967d13df549fe9e325fe78221ef46413cf8e5b2 MYCROFT_SKILL_OVOS_VOLUME_SITE = git://github.com/OpenVoiceOS/skill-ovos-volume MYCROFT_SKILL_OVOS_VOLUME_SITE_METHOD = git MYCROFT_SKILL_OVOS_VOLUME_DIRLOCATION = home/mycroft/.local/share/mycroft/skills diff --git a/buildroot-external/package/mycroft-skill-simple-youtube/mycroft-skill-simple-youtube.mk b/buildroot-external/package/mycroft-skill-simple-youtube/mycroft-skill-simple-youtube.mk index 5fe5c671..a23f87de 100644 --- a/buildroot-external/package/mycroft-skill-simple-youtube/mycroft-skill-simple-youtube.mk +++ b/buildroot-external/package/mycroft-skill-simple-youtube/mycroft-skill-simple-youtube.mk @@ -4,7 +4,7 @@ # ################################################################################ -MYCROFT_SKILL_SIMPLE_YOUTUBE_VERSION = 808e3b3f973ce5b0ee129b3e1532dd9e872345e0 +MYCROFT_SKILL_SIMPLE_YOUTUBE_VERSION = 9c01487e8094a3f575e9d9ee212c6ee55335d7dc MYCROFT_SKILL_SIMPLE_YOUTUBE_SITE = git://github.com/JarbasSkills/skill-simple-youtube MYCROFT_SKILL_SIMPLE_YOUTUBE_SITE_METHOD = git MYCROFT_SKILL_SIMPLE_YOUTUBE_DIRLOCATION = home/mycroft/.local/share/mycroft/skills diff --git a/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.hash b/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.hash index d4db3c82..db7aad0c 100644 --- a/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.hash +++ b/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.hash @@ -1,2 +1,2 @@ # sha256 locally computed -sha256 d2c1f3bea5ae9cde074d6c6d069211a07691612523f452e6e298f80f2784ed02 python-ovos-local-backend-61bb2cdb7289c66936a7f2aa72e4f5e5ebac435f.tar.gz +sha256 aa253d7adc7e364a059bd55a25018b07860fa1c7e31a93a050dcbdc01f3e18c8 python-ovos-local-backend-308c51c7c9b662543ad4f18f4da71e4c3b48c945.tar.gz diff --git a/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.mk b/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.mk index 804724ed..68512bb4 100644 --- a/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.mk +++ b/buildroot-external/package/python-ovos-local-backend/python-ovos-local-backend.mk @@ -4,7 +4,7 @@ # ################################################################################ -PYTHON_OVOS_LOCAL_BACKEND_VERSION = 61bb2cdb7289c66936a7f2aa72e4f5e5ebac435f +PYTHON_OVOS_LOCAL_BACKEND_VERSION = 308c51c7c9b662543ad4f18f4da71e4c3b48c945 PYTHON_OVOS_LOCAL_BACKEND_SITE = $(call github,OpenVoiceOS,OVOS-local-backend,$(PYTHON_OVOS_LOCAL_BACKEND_VERSION)) PYTHON_OVOS_LOCAL_BACKEND_SETUP_TYPE = setuptools diff --git a/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.hash b/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.hash index 07d59235..34caa537 100644 --- a/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.hash +++ b/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.hash @@ -1 +1 @@ -sha256 ee1b692216fafdf17ecc352222a23b96e7f1dc8e5f543bf584877b1e16f8a0ef python-ovos-ocp-audio-plugin-16f1bb53a0f7fcd97aab27e6367e80111876958d.tar.gz +sha256 3f8ce185334ddcc098598176f45f8e251660b0090088e433ce6fc89267e8ff0c python-ovos-ocp-audio-plugin-a521dd56c9ab4f995080b6da860ef8304c931f8e.tar.gz diff --git a/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.mk b/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.mk index 8105a2fd..09f8fc5a 100644 --- a/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.mk +++ b/buildroot-external/package/python-ovos-ocp-audio-plugin/python-ovos-ocp-audio-plugin.mk @@ -4,7 +4,7 @@ # ################################################################################ -PYTHON_OVOS_OCP_AUDIO_PLUGIN_VERSION = 16f1bb53a0f7fcd97aab27e6367e80111876958d +PYTHON_OVOS_OCP_AUDIO_PLUGIN_VERSION = a521dd56c9ab4f995080b6da860ef8304c931f8e PYTHON_OVOS_OCP_AUDIO_PLUGIN_SITE = $(call github,OpenVoiceOS,ovos-ocp-audio-plugin,$(PYTHON_OVOS_OCP_AUDIO_PLUGIN_VERSION)) PYTHON_OVOS_OCP_AUDIO_PLUGIN_SETUP_TYPE = setuptools PYTHON_OVOS_OCP_AUDIO_PLUGIN_LICENSE_FILES = LICENSE diff --git a/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.hash b/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.hash index deb841f4..69fc8eb7 100644 --- a/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.hash +++ b/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.hash @@ -1 +1 @@ -sha256 f7a98b8d3e29f68e10c7ca2695e0eb4880675f131f3ae0eb6f7dde04bfbc64da python-ovos-plugin-manager-9c7512f5e4d1aedaea451f944a0445f293517d26.tar.gz +sha256 7a388819567a9be7d4ac209188512c4ed9c77e8acf699379429b23ab4b38ba86 python-ovos-plugin-manager-59ceffee7cbb9ad88526382b42e748d81fbd9b20.tar.gz diff --git a/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.mk b/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.mk index eea566b4..6b615299 100644 --- a/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.mk +++ b/buildroot-external/package/python-ovos-plugin-manager/python-ovos-plugin-manager.mk @@ -4,7 +4,7 @@ # ################################################################################ -PYTHON_OVOS_PLUGIN_MANAGER_VERSION = 9c7512f5e4d1aedaea451f944a0445f293517d26 +PYTHON_OVOS_PLUGIN_MANAGER_VERSION = 59ceffee7cbb9ad88526382b42e748d81fbd9b20 PYTHON_OVOS_PLUGIN_MANAGER_SITE = $(call github,OpenVoiceOS,OVOS-plugin-manager,$(PYTHON_OVOS_PLUGIN_MANAGER_VERSION)) PYTHON_OVOS_PLUGIN_MANAGER_SETUP_TYPE = setuptools PYTHON_OVOS_PLUGIN_MANAGER_LICENSE_FILES = LICENSE diff --git a/buildroot-external/package/python-ovos-utils/0001-initent-utils.patch b/buildroot-external/package/python-ovos-utils/0001-initent-utils.patch deleted file mode 100644 index 84a7424a..00000000 --- a/buildroot-external/package/python-ovos-utils/0001-initent-utils.patch +++ /dev/null @@ -1,2312 +0,0 @@ -From 1f7076cb5c3b1ff17e76947c080a6072b13124c5 Mon Sep 17 00:00:00 2001 -From: jarbasal -Date: Thu, 30 Sep 2021 00:21:18 +0100 -Subject: [PATCH 1/5] intent utils - ---- - ovos_utils/__init__.py | 96 +--- - ovos_utils/bracket_expansion.py | 196 +++++++ - ovos_utils/events.py | 80 ++- - ovos_utils/file_utils.py | 237 +++++++++ - ovos_utils/intents/__init__.py | 421 +-------------- - ovos_utils/intents/converse.py | 201 +++++++ - .../intents/intent_service_interface.py | 495 ++++++++++++++++++ - 7 files changed, 1213 insertions(+), 513 deletions(-) - create mode 100644 ovos_utils/bracket_expansion.py - create mode 100644 ovos_utils/file_utils.py - create mode 100644 ovos_utils/intents/converse.py - create mode 100644 ovos_utils/intents/intent_service_interface.py - -diff --git a/ovos_utils/__init__.py b/ovos_utils/__init__.py -index d8b9134..38c66c7 100644 ---- a/ovos_utils/__init__.py -+++ b/ovos_utils/__init__.py -@@ -12,14 +12,14 @@ - # - from threading import Thread - from time import sleep --import os --from os.path import isdir, join, dirname -+from os.path import isdir, join - import re - import datetime - import kthread - from ovos_utils.network_utils import * - from inflection import camelize, titleize, transliterate, parameterize, \ - ordinalize -+from ovos_utils.file_utils import resolve_ovos_resource_file, resolve_resource_file - - - def ensure_mycroft_import(): -@@ -35,33 +35,6 @@ def ensure_mycroft_import(): - raise - - --def resolve_ovos_resource_file(res_name): -- """Convert a resource into an absolute filename. -- used internally for ovos resources -- """ -- # First look for fully qualified file (e.g. a user setting) -- if os.path.isfile(res_name): -- return res_name -- -- # now look in bundled ovos resources -- filename = join(dirname(__file__), "res", res_name) -- if os.path.isfile(filename): -- return filename -- -- # let's look in ovos_workshop if it's installed -- try: -- import ovos_workshop -- pkg_dir = dirname(ovos_workshop.__file__) -- filename = join(pkg_dir, "res", res_name) -- if os.path.isfile(filename): -- return filename -- filename = join(pkg_dir, "res", "ui", res_name) -- if os.path.isfile(filename): -- return filename -- except: -- pass -- return None # Resource cannot be resolved -- - - def get_mycroft_root(): - paths = [ -@@ -75,71 +48,6 @@ def get_mycroft_root(): - return None - - --def resolve_resource_file(res_name, root_path=None, config=None): -- """Convert a resource into an absolute filename. -- -- Resource names are in the form: 'filename.ext' -- or 'path/filename.ext' -- -- The system wil look for ~/.mycroft/res_name first, and -- if not found will look at /opt/mycroft/res_name, -- then finally it will look for res_name in the 'mycroft/res' -- folder of the source code package. -- -- Example: -- With mycroft running as the user 'bob', if you called -- resolve_resource_file('snd/beep.wav') -- it would return either '/home/bob/.mycroft/snd/beep.wav' or -- '/opt/mycroft/snd/beep.wav' or '.../mycroft/res/snd/beep.wav', -- where the '...' is replaced by the path where the package has -- been installed. -- -- Args: -- res_name (str): a resource path/name -- config (dict): mycroft.conf, to read data directory from -- Returns: -- str: path to resource or None if no resource found -- """ -- if config is None: -- from ovos_utils.configuration import read_mycroft_config -- config = read_mycroft_config() -- -- # First look for fully qualified file (e.g. a user setting) -- if os.path.isfile(res_name): -- return res_name -- -- # Now look for ~/.mycroft/res_name (in user folder) -- filename = os.path.expanduser("~/.mycroft/" + res_name) -- if os.path.isfile(filename): -- return filename -- -- # Next look for /opt/mycroft/res/res_name -- data_dir = os.path.expanduser(config.get('data_dir', "/opt/mycroft")) -- filename = os.path.expanduser(os.path.join(data_dir, res_name)) -- if os.path.isfile(filename): -- return filename -- -- # look in ovos_utils package itself -- found = resolve_ovos_resource_file(res_name) -- if found: -- return found -- -- # Finally look for it in the source package -- paths = [ -- "/opt/venvs/mycroft-core/lib/python3.7/site-packages/", # mark1/2 -- "/opt/venvs/mycroft-core/lib/python3.4/site-packages/ ", # old mark1 installs -- "/home/pi/mycroft-core" # picroft -- ] -- if root_path: -- paths += [root_path] -- for p in paths: -- filename = os.path.join(p, 'mycroft', 'res', res_name) -- filename = os.path.abspath(os.path.normpath(filename)) -- if os.path.isfile(filename): -- return filename -- -- return None # Resource cannot be resolved -- - - def create_killable_daemon(target, args=(), kwargs=None, autostart=True): - """Helper to quickly create and start a thread with daemon = True""" -diff --git a/ovos_utils/bracket_expansion.py b/ovos_utils/bracket_expansion.py -new file mode 100644 -index 0000000..1c69a6f ---- /dev/null -+++ b/ovos_utils/bracket_expansion.py -@@ -0,0 +1,196 @@ -+import re -+ -+ -+def expand_parentheses(sent): -+ """ -+ ['1', '(', '2', '|', '3, ')'] -> [['1', '2'], ['1', '3']] -+ For example: -+ Will it (rain|pour) (today|tomorrow|)? -+ ----> -+ Will it rain today? -+ Will it rain tomorrow? -+ Will it rain? -+ Will it pour today? -+ Will it pour tomorrow? -+ Will it pour? -+ Args: -+ sent (list): List of tokens in sentence -+ Returns: -+ list>: Multiple possible sentences from original -+ """ -+ return SentenceTreeParser(sent).expand_parentheses() -+ -+ -+def expand_options(parentheses_line: str) -> list: -+ """ -+ Convert 'test (a|b)' -> ['test a', 'test b'] -+ Args: -+ parentheses_line: Input line to expand -+ Returns: -+ List of expanded possibilities -+ """ -+ # 'a(this|that)b' -> [['a', 'this', 'b'], ['a', 'that', 'b']] -+ options = expand_parentheses(re.split(r'([(|)])', parentheses_line)) -+ return [re.sub(r'\s+', ' ', ' '.join(i)).strip() for i in options] -+ -+ -+class Fragment: -+ """(Abstract) empty sentence fragment""" -+ -+ def __init__(self, tree): -+ """ -+ Construct a sentence tree fragment which is merely a wrapper for -+ a list of Strings -+ Args: -+ tree (?): Base tree for the sentence fragment, type depends on -+ subclass, refer to those subclasses -+ """ -+ self._tree = tree -+ -+ def tree(self): -+ """Return the represented sentence tree as raw data.""" -+ return self._tree -+ -+ def expand(self): -+ """ -+ Expanded version of the fragment. In this case an empty sentence. -+ Returns: -+ List>: A list with an empty sentence (= token/string list) -+ """ -+ return [[]] -+ -+ def __str__(self): -+ return self._tree.__str__() -+ -+ def __repr__(self): -+ return self._tree.__repr__() -+ -+ -+class Word(Fragment): -+ """ -+ Single word in the sentence tree. -+ Construct with a string as argument. -+ """ -+ -+ def expand(self): -+ """ -+ Creates one sentence that contains exactly that word. -+ Returns: -+ List>: A list with the given string as sentence -+ (= token/string list) -+ """ -+ return [[self._tree]] -+ -+ -+class Sentence(Fragment): -+ """ -+ A Sentence made of several concatenations/words. -+ Construct with a List as argument. -+ """ -+ -+ def expand(self): -+ """ -+ Creates a combination of all sub-sentences. -+ Returns: -+ List>: A list with all subsentence expansions combined in -+ every possible way -+ """ -+ old_expanded = [[]] -+ for sub in self._tree: -+ sub_expanded = sub.expand() -+ new_expanded = [] -+ while len(old_expanded) > 0: -+ sentence = old_expanded.pop() -+ for new in sub_expanded: -+ new_expanded.append(sentence + new) -+ old_expanded = new_expanded -+ return old_expanded -+ -+ -+class Options(Fragment): -+ """ -+ A Combination of possible sub-sentences. -+ Construct with List as argument. -+ """ -+ -+ def expand(self): -+ """ -+ Returns all of its options as seperated sub-sentences. -+ Returns: -+ List>: A list containing the sentences created by all -+ expansions of its sub-sentences -+ """ -+ options = [] -+ for option in self._tree: -+ options.extend(option.expand()) -+ return options -+ -+ -+class SentenceTreeParser: -+ """ -+ Generate sentence token trees from a list of tokens -+ ['1', '(', '2', '|', '3, ')'] -> [['1', '2'], ['1', '3']] -+ """ -+ -+ def __init__(self, tokens): -+ self.tokens = tokens -+ -+ def _parse(self): -+ """ -+ Generate sentence token trees -+ ['1', '(', '2', '|', '3, ')'] -> ['1', ['2', '3']] -+ """ -+ self._current_position = 0 -+ return self._parse_expr() -+ -+ def _parse_expr(self): -+ """ -+ Generate sentence token trees from the current position to -+ the next closing parentheses / end of the list and return it -+ ['1', '(', '2', '|', '3, ')'] -> ['1', [['2'], ['3']]] -+ ['2', '|', '3'] -> [['2'], ['3']] -+ """ -+ # List of all generated sentences -+ sentence_list = [] -+ # Currently active sentence -+ cur_sentence = [] -+ sentence_list.append(Sentence(cur_sentence)) -+ # Determine which form the current expression has -+ while self._current_position < len(self.tokens): -+ cur = self.tokens[self._current_position] -+ self._current_position += 1 -+ if cur == '(': -+ # Parse the subexpression -+ subexpr = self._parse_expr() -+ # Check if the subexpression only has one branch -+ # -> If so, append "(" and ")" and add it as is -+ normal_brackets = False -+ if len(subexpr.tree()) == 1: -+ normal_brackets = True -+ cur_sentence.append(Word('(')) -+ # add it to the sentence -+ cur_sentence.append(subexpr) -+ if normal_brackets: -+ cur_sentence.append(Word(')')) -+ elif cur == '|': -+ # Begin parsing a new sentence -+ cur_sentence = [] -+ sentence_list.append(Sentence(cur_sentence)) -+ elif cur == ')': -+ # End parsing the current subexpression -+ break -+ # TODO anything special about {sth}? -+ else: -+ cur_sentence.append(Word(cur)) -+ return Options(sentence_list) -+ -+ def _expand_tree(self, tree): -+ """ -+ Expand a list of sub sentences to all combinated sentences. -+ ['1', ['2', '3']] -> [['1', '2'], ['1', '3']] -+ """ -+ return tree.expand() -+ -+ def expand_parentheses(self): -+ tree = self._parse() -+ return self._expand_tree(tree) -diff --git a/ovos_utils/events.py b/ovos_utils/events.py -index fddd2a5..a8b6421 100644 ---- a/ovos_utils/events.py -+++ b/ovos_utils/events.py -@@ -1,9 +1,81 @@ --from ovos_utils.log import LOG --from ovos_utils.messagebus import Message, FakeBus - import time - from datetime import datetime, timedelta - from inspect import signature - -+from ovos_utils.intents.intent_service_interface import to_alnum -+from ovos_utils.log import LOG -+from ovos_utils.messagebus import Message, FakeBus -+ -+ -+def unmunge_message(message, skill_id): -+ """Restore message keywords by removing the Letterified skill ID. -+ Args: -+ message (Message): Intent result message -+ skill_id (str): skill identifier -+ Returns: -+ Message without clear keywords -+ """ -+ if isinstance(message, Message) and isinstance(message.data, dict): -+ skill_id = to_alnum(skill_id) -+ for key in list(message.data.keys()): -+ if key.startswith(skill_id): -+ # replace the munged key with the real one -+ new_key = key[len(skill_id):] -+ message.data[new_key] = message.data.pop(key) -+ -+ return message -+ -+ -+def get_handler_name(handler): -+ """Name (including class if available) of handler function. -+ -+ Args: -+ handler (function): Function to be named -+ -+ Returns: -+ string: handler name as string -+ """ -+ if '__self__' in dir(handler) and 'name' in dir(handler.__self__): -+ return handler.__self__.name + '.' + handler.__name__ -+ else: -+ return handler.__name__ -+ -+ -+def create_wrapper(handler, skill_id, on_start, on_end, on_error): -+ """Create the default skill handler wrapper. -+ -+ This wrapper handles things like metrics, reporting handler start/stop -+ and errors. -+ handler (callable): method/function to call -+ skill_id: skill_id for associated skill -+ on_start (function): function to call before executing the handler -+ on_end (function): function to call after executing the handler -+ on_error (function): function to call for error reporting -+ """ -+ -+ def wrapper(message): -+ try: -+ message = unmunge_message(message, skill_id) -+ if on_start: -+ on_start(message) -+ -+ if len(signature(handler).parameters) == 0: -+ handler() -+ else: -+ handler(message) -+ -+ except Exception as e: -+ if on_error: -+ if len(signature(on_error).parameters) == 2: -+ on_error(e, message) -+ else: -+ on_error(e) -+ finally: -+ if on_end: -+ on_end(message) -+ -+ return wrapper -+ - - def create_basic_wrapper(handler, on_error=None): - """Create the default skill handler wrapper. -@@ -18,6 +90,7 @@ def create_basic_wrapper(handler, on_error=None): - Returns: - Wrapped callable - """ -+ - def wrapper(message): - try: - if len(signature(handler).parameters) == 0: -@@ -37,6 +110,7 @@ class EventContainer: - This container tracks events added by a skill, allowing unregistering - all events on shutdown. - """ -+ - def __init__(self, bus=None): - self.bus = bus or FakeBus() - self.events = [] -@@ -53,6 +127,7 @@ def add(self, name, handler, once=False): - once (bool, optional): Event handler will be removed after it has - been run once. - """ -+ - def once_wrapper(message): - # Remove registered one-time handler before invoking, - # allowing them to re-schedule themselves. -@@ -113,6 +188,7 @@ def clear(self): - - class EventSchedulerInterface: - """Interface for accessing the event scheduler over the message bus.""" -+ - def __init__(self, name, sched_id=None, bus=None): - self.name = name - self.sched_id = sched_id -diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py -new file mode 100644 -index 0000000..de9122a ---- /dev/null -+++ b/ovos_utils/file_utils.py -@@ -0,0 +1,237 @@ -+import collections -+import csv -+import re -+import os -+from os import walk -+from os.path import splitext, join, dirname -+ -+from ovos_utils.bracket_expansion import expand_options -+from ovos_utils.intents.intent_service_interface import to_alnum, munge_regex -+from ovos_utils.log import LOG -+ -+ -+def resolve_ovos_resource_file(res_name): -+ """Convert a resource into an absolute filename. -+ used internally for ovos resources -+ """ -+ # First look for fully qualified file (e.g. a user setting) -+ if os.path.isfile(res_name): -+ return res_name -+ -+ # now look in bundled ovos resources -+ filename = join(dirname(__file__), "res", res_name) -+ if os.path.isfile(filename): -+ return filename -+ -+ # let's look in ovos_workshop if it's installed -+ try: -+ import ovos_workshop -+ pkg_dir = dirname(ovos_workshop.__file__) -+ filename = join(pkg_dir, "res", res_name) -+ if os.path.isfile(filename): -+ return filename -+ filename = join(pkg_dir, "res", "ui", res_name) -+ if os.path.isfile(filename): -+ return filename -+ except: -+ pass -+ return None # Resource cannot be resolved -+ -+ -+def resolve_resource_file(res_name, root_path=None, config=None): -+ """Convert a resource into an absolute filename. -+ -+ Resource names are in the form: 'filename.ext' -+ or 'path/filename.ext' -+ -+ The system wil look for ~/.mycroft/res_name first, and -+ if not found will look at /opt/mycroft/res_name, -+ then finally it will look for res_name in the 'mycroft/res' -+ folder of the source code package. -+ -+ Example: -+ With mycroft running as the user 'bob', if you called -+ resolve_resource_file('snd/beep.wav') -+ it would return either '/home/bob/.mycroft/snd/beep.wav' or -+ '/opt/mycroft/snd/beep.wav' or '.../mycroft/res/snd/beep.wav', -+ where the '...' is replaced by the path where the package has -+ been installed. -+ -+ Args: -+ res_name (str): a resource path/name -+ config (dict): mycroft.conf, to read data directory from -+ Returns: -+ str: path to resource or None if no resource found -+ """ -+ if config is None: -+ from ovos_utils.configuration import read_mycroft_config -+ config = read_mycroft_config() -+ -+ # First look for fully qualified file (e.g. a user setting) -+ if os.path.isfile(res_name): -+ return res_name -+ -+ # Now look for ~/.mycroft/res_name (in user folder) -+ filename = os.path.expanduser("~/.mycroft/" + res_name) -+ if os.path.isfile(filename): -+ return filename -+ -+ # Next look for /opt/mycroft/res/res_name -+ data_dir = os.path.expanduser(config.get('data_dir', "/opt/mycroft")) -+ filename = os.path.expanduser(os.path.join(data_dir, res_name)) -+ if os.path.isfile(filename): -+ return filename -+ -+ # look in ovos_utils package itself -+ found = resolve_ovos_resource_file(res_name) -+ if found: -+ return found -+ -+ # Finally look for it in the source package -+ paths = [ -+ "/opt/venvs/mycroft-core/lib/python3.7/site-packages/", # mark1/2 -+ "/opt/venvs/mycroft-core/lib/python3.4/site-packages/ ", -+ # old mark1 installs -+ "/home/pi/mycroft-core" # picroft -+ ] -+ if root_path: -+ paths += [root_path] -+ for p in paths: -+ filename = os.path.join(p, 'mycroft', 'res', res_name) -+ filename = os.path.abspath(os.path.normpath(filename)) -+ if os.path.isfile(filename): -+ return filename -+ -+ return None # Resource cannot be resolved -+ -+ -+def read_vocab_file(path): -+ """ Read voc file. -+ -+ This reads a .voc file, stripping out empty lines comments and expand -+ parentheses. It returns each line as a list of all expanded -+ alternatives. -+ -+ Args: -+ path (str): path to vocab file. -+ -+ Returns: -+ List of Lists of strings. -+ """ -+ vocab = [] -+ with open(path, 'r', encoding='utf8') as voc_file: -+ for line in voc_file.readlines(): -+ if line.startswith('#') or line.strip() == '': -+ continue -+ vocab.append(expand_options(line.lower())) -+ return vocab -+ -+ -+def load_regex_from_file(path, skill_id): -+ """Load regex from file -+ The regex is sent to the intent handler using the message bus -+ -+ Args: -+ path: path to vocabulary file (*.voc) -+ skill_id: skill_id to the regex is tied to -+ """ -+ regexes = [] -+ if path.endswith('.rx'): -+ with open(path, 'r', encoding='utf8') as reg_file: -+ for line in reg_file.readlines(): -+ if line.startswith("#"): -+ continue -+ LOG.debug('regex pre-munge: ' + line.strip()) -+ regex = munge_regex(line.strip(), skill_id) -+ LOG.debug('regex post-munge: ' + regex) -+ # Raise error if regex can't be compiled -+ try: -+ re.compile(regex) -+ regexes.append(regex) -+ except Exception as e: -+ LOG.warning(f'Failed to compile regex {regex}: {e}') -+ -+ return regexes -+ -+ -+def load_vocabulary(basedir, skill_id): -+ """Load vocabulary from all files in the specified directory. -+ -+ Args: -+ basedir (str): path of directory to load from (will recurse) -+ skill_id: skill the data belongs to -+ Returns: -+ dict with intent_type as keys and list of list of lists as value. -+ """ -+ vocabs = {} -+ for path, _, files in walk(basedir): -+ for f in files: -+ if f.endswith(".voc"): -+ vocab_type = to_alnum(skill_id) + splitext(f)[0] -+ vocs = read_vocab_file(join(path, f)) -+ if vocs: -+ vocabs[vocab_type] = vocs -+ return vocabs -+ -+ -+def load_regex(basedir, skill_id): -+ """Load regex from all files in the specified directory. -+ -+ Args: -+ basedir (str): path of directory to load from -+ bus (messagebus emitter): messagebus instance used to send the vocab to -+ the intent service -+ skill_id (str): skill identifier -+ """ -+ regexes = [] -+ for path, _, files in walk(basedir): -+ for f in files: -+ if f.endswith(".rx"): -+ regexes += load_regex_from_file(join(path, f), skill_id) -+ return regexes -+ -+ -+def read_value_file(filename, delim): -+ """Read value file. -+ -+ The value file is a simple csv structure with a key and value. -+ -+ Args: -+ filename (str): file to read -+ delim (str): csv delimiter -+ -+ Returns: -+ OrderedDict with results. -+ """ -+ result = collections.OrderedDict() -+ -+ if filename: -+ with open(filename) as f: -+ reader = csv.reader(f, delimiter=delim) -+ for row in reader: -+ # skip blank or comment lines -+ if not row or row[0].startswith("#"): -+ continue -+ if len(row) != 2: -+ continue -+ -+ result[row[0]] = row[1] -+ return result -+ -+ -+def read_translated_file(filename, data): -+ """Read a file inserting data. -+ -+ Args: -+ filename (str): file to read -+ data (dict): dictionary with data to insert into file -+ -+ Returns: -+ list of lines. -+ """ -+ if filename: -+ with open(filename) as f: -+ text = f.read().replace('{{', '{').replace('}}', '}') -+ return text.format(**data or {}).rstrip('\n').split('\n') -+ else: -+ return None -diff --git a/ovos_utils/intents/__init__.py b/ovos_utils/intents/__init__.py -index 6165119..ff9e985 100644 ---- a/ovos_utils/intents/__init__.py -+++ b/ovos_utils/intents/__init__.py -@@ -1,417 +1,4 @@ --from ovos_utils.log import LOG --from ovos_utils.messagebus import get_mycroft_bus, Message --import time --from os.path import isfile -- -- --class IntentQueryApi: -- """ -- Query Intent Service at runtime -- """ -- -- def __init__(self, bus=None, timeout=5): -- self.bus = bus or get_mycroft_bus() -- self.timeout = timeout -- -- def get_adapt_intent(self, utterance, lang="en-us"): -- """ get best adapt intent for utterance """ -- msg = Message("intent.service.adapt.get", -- {"utterance": utterance, "lang": lang}, -- context={"destination": "intent_service", -- "source": "intent_api"}) -- -- resp = self.bus.wait_for_response(msg, -- 'intent.service.adapt.reply', -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- return data["intent"] -- -- def get_padatious_intent(self, utterance, lang="en-us"): -- """ get best padatious intent for utterance """ -- msg = Message("intent.service.padatious.get", -- {"utterance": utterance, "lang": lang}, -- context={"destination": "intent_service", -- "source": "intent_api"}) -- resp = self.bus.wait_for_response(msg, -- 'intent.service.padatious.reply', -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- return data["intent"] -- -- def get_intent(self, utterance, lang="en-us"): -- """ get best intent for utterance """ -- msg = Message("intent.service.intent.get", -- {"utterance": utterance, "lang": lang}, -- context={"destination": "intent_service", -- "source": "intent_api"}) -- resp = self.bus.wait_for_response(msg, -- 'intent.service.intent.reply', -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- return data["intent"] -- -- def get_skill(self, utterance, lang="en-us"): -- """ get skill that utterance will trigger """ -- intent = self.get_intent(utterance, lang) -- if not intent: -- return None -- # theoretically skill_id might be missing -- if intent.get("skill_id"): -- return intent["skill_id"] -- # retrieve skill from munged intent name -- if intent.get("intent_name"): # padatious + adapt -- return intent["name"].split(":")[0] -- if intent.get("intent_type"): # adapt -- return intent["intent_type"].split(":")[0] -- return None # raise some error here maybe? this should never happen -- -- def get_skills_manifest(self): -- msg = Message("intent.service.skills.get", -- context={"destination": "intent_service", -- "source": "intent_api"}) -- resp = self.bus.wait_for_response(msg, -- 'intent.service.skills.reply', -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- return data["skills"] -- -- def get_active_skills(self, include_timestamps=False): -- msg = Message("intent.service.active_skills.get", -- context={"destination": "intent_service", -- "source": "intent_api"}) -- resp = self.bus.wait_for_response(msg, -- 'intent.service.active_skills.reply', -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- if include_timestamps: -- return data["skills"] -- # HACK waiting for https://github.com/MycroftAI/mycroft-core/pull/2786 -- if len(data["skills"]) and not len(data["skills"][0]) == 2: -- return data["skills"] -- return [s[0] for s in data["skills"]] -- -- def get_adapt_manifest(self): -- msg = Message("intent.service.adapt.manifest.get", -- context={"destination": "intent_service", -- "source": "intent_api"}) -- resp = self.bus.wait_for_response(msg, -- 'intent.service.adapt.manifest', -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- return data["intents"] -- -- def get_padatious_manifest(self): -- msg = Message("intent.service.padatious.manifest.get", -- context={"destination": "intent_service", -- "source": "intent_api"}) -- resp = self.bus.wait_for_response(msg, -- 'intent.service.padatious.manifest', -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- return data["intents"] -- -- def get_intent_manifest(self): -- padatious = self.get_padatious_manifest() -- adapt = self.get_adapt_manifest() -- return {"adapt": adapt, -- "padatious": padatious} -- -- def get_vocab_manifest(self): -- msg = Message("intent.service.adapt.vocab.manifest.get", -- context={"destination": "intent_service", -- "source": "intent_api"}) -- reply_msg_type = 'intent.service.adapt.vocab.manifest' -- resp = self.bus.wait_for_response(msg, -- reply_msg_type, -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- -- vocab = {} -- for voc in data["vocab"]: -- if voc.get("regex"): -- continue -- if voc["end"] not in vocab: -- vocab[voc["end"]] = {"samples": []} -- vocab[voc["end"]]["samples"].append(voc["start"]) -- return [{"name": voc, "samples": vocab[voc]["samples"]} -- for voc in vocab] -- -- def get_regex_manifest(self): -- msg = Message("intent.service.adapt.vocab.manifest.get", -- context={"destination": "intent_service", -- "source": "intent_api"}) -- reply_msg_type = 'intent.service.adapt.vocab.manifest' -- resp = self.bus.wait_for_response(msg, -- reply_msg_type, -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- -- vocab = {} -- for voc in data["vocab"]: -- if not voc.get("regex"): -- continue -- name = voc["regex"].split("(?P<")[-1].split(">")[0] -- if name not in vocab: -- vocab[name] = {"samples": []} -- vocab[name]["samples"].append(voc["regex"]) -- return [{"name": voc, "regexes": vocab[voc]["samples"]} -- for voc in vocab] -- -- def get_entities_manifest(self): -- msg = Message("intent.service.padatious.entities.manifest.get", -- context={"destination": "intent_service", -- "source": "intent_api"}) -- reply_msg_type = 'intent.service.padatious.entities.manifest' -- resp = self.bus.wait_for_response(msg, -- reply_msg_type, -- timeout=self.timeout) -- data = resp.data if resp is not None else {} -- if not data: -- LOG.error("Intent Service timed out!") -- return None -- -- entities = [] -- # read files -- for ent in data["entities"]: -- if isfile(ent["file_name"]): -- with open(ent["file_name"]) as f: -- lines = f.read().replace("(", "").replace(")", "").split( -- "\n") -- samples = [] -- for l in lines: -- samples += [a.strip() for a in l.split("|") if a.strip()] -- entities.append({"name": ent["name"], "samples": samples}) -- return entities -- -- def get_keywords_manifest(self): -- padatious = self.get_entities_manifest() -- adapt = self.get_vocab_manifest() -- regex = self.get_regex_manifest() -- return {"adapt": adapt, -- "padatious": padatious, -- "regex": regex} -- -- --class ConverseTracker: -- """ Using the messagebus this class recreates/keeps track of the state -- of the converse system, it uses both passive listening and active -- queries to sync it's state, it also emits 2 new bus events -- -- Implements https://github.com/MycroftAI/mycroft-core/pull/1468 -- """ -- bus = None -- active_skills = [] -- converse_timeout = 5 # MAGIC NUMBER hard coded in mycroft-core -- last_conversed = None -- intent_api = None -- -- @classmethod -- def connect_bus(cls, mycroft_bus): -- """Registers the bus object to use.""" -- # PATCH - in mycroft-core this would be handled in intent_service -- # in here it is done in MycroftSkill.bind so i added this -- # conditional check -- if cls.bus is None and mycroft_bus is not None: -- cls.bus = mycroft_bus -- cls.intent_api = IntentQueryApi(cls.bus) -- cls.register_bus_events() -- -- @classmethod -- def register_bus_events(cls): -- cls.bus.on('active_skill_request', cls.handle_activate_request) -- cls.bus.on('skill.converse.response', cls.handle_converse_response) -- cls.bus.on("mycroft.skill.handler.start", cls.handle_intent_start) -- cls.bus.on("recognizer_loop:utterance", cls.handle_utterance) -- -- # public methods -- @classmethod -- def check_skill(cls, skill_id): -- """ Check if a skill is active """ -- cls.filter_active_skills() -- for skill in list(cls.active_skills): -- if skill[0] == skill_id: -- return True -- return False -- -- @classmethod -- def filter_active_skills(cls): -- """ Removes expired skills from active skill list """ -- # filter timestamps -- for skill in list(cls.active_skills): -- if time.time() - skill[1] <= cls.converse_timeout * 60: -- cls.remove_active_skill(skill[0]) -- -- @classmethod -- def sync_with_intent_service(cls): -- """sync active skill list using intent api -- -- WARNING -- we don't have the timestamps so order might be messed up!! -- avoid calling this until -- """ -- skill_ids = cls.intent_api.get_active_skills(include_timestamps=True) -- if skill_ids: -- if len(skill_ids[0]) == 2: -- # PR was merged! hurray! -- cls.active_skills = skill_ids -- else: -- # hoping they come sorted by timestamp.... -- # older to newer (most recently used) -- for skill_id in reversed(skill_ids): -- # are we tracking this skill ? -- if not cls.check_skill(skill_id): -- # we missed adding this skill in our tracking -- cls.add_active_skill(skill_id) -- for skill in cls.active_skills: -- if skill[0] not in skill_ids: -- # we missed removing this skill in our tracking -- cls.remove_active_skill(skill[0]) -- -- # https://github.com/MycroftAI/mycroft-core/pull/1468 -- @classmethod -- def remove_active_skill(cls, skill_id, silent=False): -- """ -- Emits "converse.skill.deactivated" event, improvement of #1468 -- """ -- for skill in list(cls.active_skills): -- if skill[0] == skill_id: -- cls.active_skills.remove(skill) -- if not silent: -- cls.bus.emit(Message("converse.skill.deactivated", -- {"skill_id": skill[0]})) -- -- @classmethod -- def add_active_skill(cls, skill_id): -- """ -- Emits "converse.skill.activated" event, improvement of #1468 -- """ -- # search the list for an existing entry that already contains it -- # and remove that reference -- if skill_id != '': -- cls.remove_active_skill(skill_id, silent=True) -- # add skill with timestamp to start of skill_list -- cls.active_skills.insert(0, [skill_id, time.time()]) -- # this might be sent more than once and it's perfectly fine -- # it's just a new info message not consumed anywhere by default -- cls.bus.emit(Message("converse.skill.activated", -- {"skill_id": skill_id})) -- else: -- LOG.warning('Skill ID was empty, won\'t add to list of ' -- 'active skills.') -- -- # status tracking -- @classmethod -- def handle_activate_request(cls, message): -- """ -- a skill bumped itself to the top of active skills list -- duplicate functionality from mycroft-core, keeping list in sync -- """ -- skill_id = message.data["skill_id"] -- cls.add_active_skill(skill_id) -- -- @classmethod -- def handle_converse_error(cls, message): -- """ -- a skill was removed from active skill list due to converse error -- duplicate functionality from mycroft-core, keeping list in sync -- """ -- skill_id = message.data["skill_id"] -- if message.data["error"] == "skill id does not exist": -- cls.remove_active_skill(skill_id) -- -- @classmethod -- def handle_intent_start(cls, message): -- """ -- duplicate functionality from mycroft-core, keeping list in sync -- -- TODO skill_id from message, core is not passing it along... it used -- to be possible to retrieve it from munged message but that changed. -- send a PR (if those got merged this code wouldn't exist) -- -- handle_utterance will take over this functionality for now -- handle_converse_response will take corrective action -- """ -- # skill_id = message.data["skill_id"] -- # bump skill to top of active list -- # cls.add_active_skill(skill_id) -- -- @classmethod -- def handle_utterance(cls, message): -- """ -- duplicate functionality from mycroft-core, keeping list in sync -- -- WORKAROUND - skill_id missing in handle_intent_start, will keep list -- in sync by using the IntentAPI to check what skill the utterance -- should trigger -- -- handle_converse_response will take corrective action -- """ -- # NOTE borked in mycroft-core -- # needs https://github.com/MycroftAI/mycroft-core/pull/2786 -- skill_id = cls.intent_api.get_skill(message.data["utterances"][0]) -- if skill_id: -- # this skill will trigger and therefore is the last active skill -- cls.add_active_skill(skill_id) -- # will remove expired intents from list -- cls.filter_active_skills() -- -- @classmethod -- def handle_converse_response(cls, message): -- """ -- tracks last_conversed skill -- -- FAILSAFE - additional checks to correct active skills list, -- but that should never happen, accounts for mistakes in -- handle_utterance / intent_api -- -- accounts for https://github.com/MycroftAI/mycroft-core/pull/2786 -- not yet being merged -- """ -- skill_id = message.data["skill_id"] -- if 'error' in message.data: -- cls.handle_converse_error(message) -- elif message.data.get('result') is True: -- cls.last_conversed = skill_id -- if not cls.check_skill(skill_id): -- # seems like we missed adding this skill to active list -- # NOTE this is a failsafe and should never trigger -- # since this answered True we have the real timestamp -- cls.add_active_skill(skill_id) -- elif not cls.check_skill(skill_id): -- # seems like we missed adding this skill to active list -- # NOTE this is a failsafe and should never trigger -- # since this answered false and we don't have the real timestamp -- # let's add it to the end of the active_skills list -- ts = time.time() -- if len(cls.active_skills): -- ts = cls.active_skills[-1][1] -- cls.active_skills.append([skill_id, ts]) -- -- -+from ovos_utils.intents.intent_service_interface import IntentQueryApi, \ -+ IntentServiceInterface -+from ovos_utils.intents.converse import ConverseTracker -+from ovos_utils.intents.layers import IntentLayers -diff --git a/ovos_utils/intents/converse.py b/ovos_utils/intents/converse.py -new file mode 100644 -index 0000000..7970585 ---- /dev/null -+++ b/ovos_utils/intents/converse.py -@@ -0,0 +1,201 @@ -+import time -+ -+from ovos_utils.intents.intent_service_interface import IntentQueryApi -+from ovos_utils.log import LOG -+from ovos_utils.messagebus import Message -+ -+ -+class ConverseTracker: -+ """ Using the messagebus this class recreates/keeps track of the state -+ of the converse system, it uses both passive listening and active -+ queries to sync it's state, it also emits 2 new bus events -+ -+ Implements https://github.com/MycroftAI/mycroft-core/pull/1468 -+ """ -+ bus = None -+ active_skills = [] -+ converse_timeout = 5 # MAGIC NUMBER hard coded in mycroft-core -+ last_conversed = None -+ intent_api = None -+ -+ @classmethod -+ def connect_bus(cls, mycroft_bus): -+ """Registers the bus object to use.""" -+ # PATCH - in mycroft-core this would be handled in intent_service -+ # in here it is done in MycroftSkill.bind so i added this -+ # conditional check -+ if cls.bus is None and mycroft_bus is not None: -+ cls.bus = mycroft_bus -+ cls.intent_api = IntentQueryApi(cls.bus) -+ cls.register_bus_events() -+ -+ @classmethod -+ def register_bus_events(cls): -+ cls.bus.on('active_skill_request', cls.handle_activate_request) -+ cls.bus.on('skill.converse.response', cls.handle_converse_response) -+ cls.bus.on("mycroft.skill.handler.start", cls.handle_intent_start) -+ cls.bus.on("recognizer_loop:utterance", cls.handle_utterance) -+ -+ # public methods -+ @classmethod -+ def check_skill(cls, skill_id): -+ """ Check if a skill is active """ -+ cls.filter_active_skills() -+ for skill in list(cls.active_skills): -+ if skill[0] == skill_id: -+ return True -+ return False -+ -+ @classmethod -+ def filter_active_skills(cls): -+ """ Removes expired skills from active skill list """ -+ # filter timestamps -+ for skill in list(cls.active_skills): -+ if time.time() - skill[1] <= cls.converse_timeout * 60: -+ cls.remove_active_skill(skill[0]) -+ -+ @classmethod -+ def sync_with_intent_service(cls): -+ """sync active skill list using intent api -+ -+ WARNING -+ we don't have the timestamps so order might be messed up!! -+ avoid calling this until -+ """ -+ skill_ids = cls.intent_api.get_active_skills(include_timestamps=True) -+ if skill_ids: -+ if len(skill_ids[0]) == 2: -+ # PR was merged! hurray! -+ cls.active_skills = skill_ids -+ else: -+ # hoping they come sorted by timestamp.... -+ # older to newer (most recently used) -+ for skill_id in reversed(skill_ids): -+ # are we tracking this skill ? -+ if not cls.check_skill(skill_id): -+ # we missed adding this skill in our tracking -+ cls.add_active_skill(skill_id) -+ for skill in cls.active_skills: -+ if skill[0] not in skill_ids: -+ # we missed removing this skill in our tracking -+ cls.remove_active_skill(skill[0]) -+ -+ # https://github.com/MycroftAI/mycroft-core/pull/1468 -+ @classmethod -+ def remove_active_skill(cls, skill_id, silent=False): -+ """ -+ Emits "converse.skill.deactivated" event, improvement of #1468 -+ """ -+ for skill in list(cls.active_skills): -+ if skill[0] == skill_id: -+ cls.active_skills.remove(skill) -+ if not silent: -+ cls.bus.emit(Message("converse.skill.deactivated", -+ {"skill_id": skill[0]})) -+ -+ @classmethod -+ def add_active_skill(cls, skill_id): -+ """ -+ Emits "converse.skill.activated" event, improvement of #1468 -+ """ -+ # search the list for an existing entry that already contains it -+ # and remove that reference -+ if skill_id != '': -+ cls.remove_active_skill(skill_id, silent=True) -+ # add skill with timestamp to start of skill_list -+ cls.active_skills.insert(0, [skill_id, time.time()]) -+ # this might be sent more than once and it's perfectly fine -+ # it's just a new info message not consumed anywhere by default -+ cls.bus.emit(Message("converse.skill.activated", -+ {"skill_id": skill_id})) -+ else: -+ LOG.warning('Skill ID was empty, won\'t add to list of ' -+ 'active skills.') -+ -+ # status tracking -+ @classmethod -+ def handle_activate_request(cls, message): -+ """ -+ a skill bumped itself to the top of active skills list -+ duplicate functionality from mycroft-core, keeping list in sync -+ """ -+ skill_id = message.data["skill_id"] -+ cls.add_active_skill(skill_id) -+ -+ @classmethod -+ def handle_converse_error(cls, message): -+ """ -+ a skill was removed from active skill list due to converse error -+ duplicate functionality from mycroft-core, keeping list in sync -+ """ -+ skill_id = message.data["skill_id"] -+ if message.data["error"] == "skill id does not exist": -+ cls.remove_active_skill(skill_id) -+ -+ @classmethod -+ def handle_intent_start(cls, message): -+ """ -+ duplicate functionality from mycroft-core, keeping list in sync -+ -+ TODO skill_id from message, core is not passing it along... it used -+ to be possible to retrieve it from munged message but that changed. -+ send a PR (if those got merged this code wouldn't exist) -+ -+ handle_utterance will take over this functionality for now -+ handle_converse_response will take corrective action -+ """ -+ # skill_id = message.data["skill_id"] -+ # bump skill to top of active list -+ # cls.add_active_skill(skill_id) -+ -+ @classmethod -+ def handle_utterance(cls, message): -+ """ -+ duplicate functionality from mycroft-core, keeping list in sync -+ -+ WORKAROUND - skill_id missing in handle_intent_start, will keep list -+ in sync by using the IntentAPI to check what skill the utterance -+ should trigger -+ -+ handle_converse_response will take corrective action -+ """ -+ # NOTE borked in mycroft-core -+ # needs https://github.com/MycroftAI/mycroft-core/pull/2786 -+ skill_id = cls.intent_api.get_skill(message.data["utterances"][0]) -+ if skill_id: -+ # this skill will trigger and therefore is the last active skill -+ cls.add_active_skill(skill_id) -+ # will remove expired intents from list -+ cls.filter_active_skills() -+ -+ @classmethod -+ def handle_converse_response(cls, message): -+ """ -+ tracks last_conversed skill -+ -+ FAILSAFE - additional checks to correct active skills list, -+ but that should never happen, accounts for mistakes in -+ handle_utterance / intent_api -+ -+ accounts for https://github.com/MycroftAI/mycroft-core/pull/2786 -+ not yet being merged -+ """ -+ skill_id = message.data["skill_id"] -+ if 'error' in message.data: -+ cls.handle_converse_error(message) -+ elif message.data.get('result') is True: -+ cls.last_conversed = skill_id -+ if not cls.check_skill(skill_id): -+ # seems like we missed adding this skill to active list -+ # NOTE this is a failsafe and should never trigger -+ # since this answered True we have the real timestamp -+ cls.add_active_skill(skill_id) -+ elif not cls.check_skill(skill_id): -+ # seems like we missed adding this skill to active list -+ # NOTE this is a failsafe and should never trigger -+ # since this answered false and we don't have the real timestamp -+ # let's add it to the end of the active_skills list -+ ts = time.time() -+ if len(cls.active_skills): -+ ts = cls.active_skills[-1][1] -+ cls.active_skills.append([skill_id, ts]) -diff --git a/ovos_utils/intents/intent_service_interface.py b/ovos_utils/intents/intent_service_interface.py -new file mode 100644 -index 0000000..25eaceb ---- /dev/null -+++ b/ovos_utils/intents/intent_service_interface.py -@@ -0,0 +1,495 @@ -+from os.path import exists, isfile -+ -+from adapt.intent import Intent -+from mycroft_bus_client import MessageBusClient -+from mycroft_bus_client.message import Message, dig_for_message -+from ovos_utils import create_daemon -+from ovos_utils.log import LOG -+ -+ -+def to_alnum(skill_id): -+ """Convert a skill id to only alphanumeric characters -+ -+ Non alpha-numeric 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, skill_id): -+ """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__ -+ self.registered_intents = [] -+ self.detached_intents = [] -+ -+ def set_bus(self, bus): -+ self.bus = bus -+ -+ def set_id(self, skill_id): -+ self.skill_id = skill_id -+ -+ def register_adapt_keyword(self, vocab_type, entity, aliases=None, -+ lang=None): -+ """Send a message to the intent service to add an Adapt keyword. -+ -+ vocab_type(str): Keyword reference -+ entity (str): Primary keyword -+ aliases (list): List of alternative kewords -+ """ -+ aliases = aliases or [] -+ 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", -+ {'start': entity, -+ 'end': vocab_type, -+ 'lang': lang})) -+ for alias in aliases: -+ self.bus.emit(msg.forward("register_vocab", -+ {'start': alias, -+ 'end': vocab_type, -+ 'alias_of': entity, -+ 'lang': lang})) -+ -+ def register_adapt_regex(self, regex, lang=None): -+ """Register a regex with the intent service. -+ -+ Args: -+ regex (str): Regex to be registered, (Adapt extracts keyword -+ reference from named match group. -+ """ -+ 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, intent_parser): -+ """Register an Adapt intent parser object. -+ -+ Serializes the intent_parser and sends it over the messagebus to -+ registered. -+ """ -+ 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): -+ """Remove an intent from the intent service. -+ -+ The intent is saved in the list of detached intents for use when -+ re-enabling an intent. -+ -+ Args: -+ intent_name(str): Intent reference -+ """ -+ name = intent_name.split(':')[1] -+ if name in self: -+ 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("detach_intent", -+ {"intent_name": intent_name})) -+ self.detached_intents.append((name, self.get_intent(name))) -+ self.registered_intents = [pair for pair in self.registered_intents -+ if pair[0] != name] -+ -+ def set_adapt_context(self, context, word, origin): -+ """Set an Adapt context. -+ -+ Args: -+ context (str): context keyword name -+ word (str): word to register -+ origin (str): 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): -+ """Remove an active Adapt context. -+ -+ Args: -+ context(str): name of context 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, filename, lang): -+ """Register a padatious intent file with Padatious. -+ -+ Args: -+ intent_name(str): intent identifier -+ filename(str): complete file path for entity file -+ """ -+ if not isinstance(filename, str): -+ raise ValueError('Filename path must be a string') -+ if not exists(filename): -+ raise FileNotFoundError('Unable to find "{}"'.format(filename)) -+ -+ data = {'file_name': filename, -+ '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, filename, lang): -+ """Register a padatious entity file with Padatious. -+ -+ Args: -+ entity_name(str): entity name -+ filename(str): complete file path for entity file -+ """ -+ if not isinstance(filename, str): -+ raise ValueError('Filename path must be a string') -+ if not exists(filename): -+ raise FileNotFoundError('Unable to find "{}"'.format(filename)) -+ 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, -+ 'name': entity_name, -+ 'lang': lang})) -+ -+ 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 get_intent(self, intent_name): -+ """Get intent from intent_name. -+ -+ This will find both enabled and disabled intents. -+ -+ Args: -+ intent_name (str): name to find. -+ -+ Returns: -+ Found intent or None if none were found. -+ """ -+ for name, intent in self: -+ if name == intent_name: -+ return intent -+ for name, intent in self.detached_intents: -+ if name == intent_name: -+ return intent -+ return None -+ -+ -+class IntentQueryApi: -+ """ -+ Query Intent Service at runtime -+ """ -+ -+ def __init__(self, bus=None, timeout=5): -+ if bus is None: -+ bus = MessageBusClient() -+ create_daemon(bus.run_forever) -+ self.bus = bus -+ self.timeout = timeout -+ -+ def get_adapt_intent(self, utterance, lang="en-us"): -+ """ get best adapt intent for utterance """ -+ msg = Message("intent.service.adapt.get", -+ {"utterance": utterance, "lang": lang}, -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ -+ resp = self.bus.wait_for_response(msg, -+ 'intent.service.adapt.reply', -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ return data["intent"] -+ -+ def get_padatious_intent(self, utterance, lang="en-us"): -+ """ get best padatious intent for utterance """ -+ msg = Message("intent.service.padatious.get", -+ {"utterance": utterance, "lang": lang}, -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ resp = self.bus.wait_for_response(msg, -+ 'intent.service.padatious.reply', -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ return data["intent"] -+ -+ def get_intent(self, utterance, lang="en-us"): -+ """ get best intent for utterance """ -+ msg = Message("intent.service.intent.get", -+ {"utterance": utterance, "lang": lang}, -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ resp = self.bus.wait_for_response(msg, -+ 'intent.service.intent.reply', -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ return data["intent"] -+ -+ def get_skill(self, utterance, lang="en-us"): -+ """ get skill that utterance will trigger """ -+ intent = self.get_intent(utterance, lang) -+ if not intent: -+ return None -+ # theoretically skill_id might be missing -+ if intent.get("skill_id"): -+ return intent["skill_id"] -+ # retrieve skill from munged intent name -+ if intent.get("intent_name"): # padatious + adapt -+ return intent["name"].split(":")[0] -+ if intent.get("intent_type"): # adapt -+ return intent["intent_type"].split(":")[0] -+ return None # raise some error here maybe? this should never happen -+ -+ def get_skills_manifest(self): -+ msg = Message("intent.service.skills.get", -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ resp = self.bus.wait_for_response(msg, -+ 'intent.service.skills.reply', -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ return data["skills"] -+ -+ def get_active_skills(self, include_timestamps=False): -+ msg = Message("intent.service.active_skills.get", -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ resp = self.bus.wait_for_response(msg, -+ 'intent.service.active_skills.reply', -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ if include_timestamps: -+ return data["skills"] -+ return [s[0] for s in data["skills"]] -+ -+ def get_adapt_manifest(self): -+ msg = Message("intent.service.adapt.manifest.get", -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ resp = self.bus.wait_for_response(msg, -+ 'intent.service.adapt.manifest', -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ return data["intents"] -+ -+ def get_padatious_manifest(self): -+ msg = Message("intent.service.padatious.manifest.get", -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ resp = self.bus.wait_for_response(msg, -+ 'intent.service.padatious.manifest', -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ return data["intents"] -+ -+ def get_intent_manifest(self): -+ padatious = self.get_padatious_manifest() -+ adapt = self.get_adapt_manifest() -+ return {"adapt": adapt, -+ "padatious": padatious} -+ -+ def get_vocab_manifest(self): -+ msg = Message("intent.service.adapt.vocab.manifest.get", -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ reply_msg_type = 'intent.service.adapt.vocab.manifest' -+ resp = self.bus.wait_for_response(msg, -+ reply_msg_type, -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ -+ vocab = {} -+ for voc in data["vocab"]: -+ if voc.get("regex"): -+ continue -+ if voc["end"] not in vocab: -+ vocab[voc["end"]] = {"samples": []} -+ vocab[voc["end"]]["samples"].append(voc["start"]) -+ return [{"name": voc, "samples": vocab[voc]["samples"]} -+ for voc in vocab] -+ -+ def get_regex_manifest(self): -+ msg = Message("intent.service.adapt.vocab.manifest.get", -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ reply_msg_type = 'intent.service.adapt.vocab.manifest' -+ resp = self.bus.wait_for_response(msg, -+ reply_msg_type, -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ -+ vocab = {} -+ for voc in data["vocab"]: -+ if not voc.get("regex"): -+ continue -+ name = voc["regex"].split("(?P<")[-1].split(">")[0] -+ if name not in vocab: -+ vocab[name] = {"samples": []} -+ vocab[name]["samples"].append(voc["regex"]) -+ return [{"name": voc, "regexes": vocab[voc]["samples"]} -+ for voc in vocab] -+ -+ def get_entities_manifest(self): -+ msg = Message("intent.service.padatious.entities.manifest.get", -+ context={"destination": "intent_service", -+ "source": "intent_api"}) -+ reply_msg_type = 'intent.service.padatious.entities.manifest' -+ resp = self.bus.wait_for_response(msg, -+ reply_msg_type, -+ timeout=self.timeout) -+ data = resp.data if resp is not None else {} -+ if not data: -+ LOG.error("Intent Service timed out!") -+ return None -+ -+ entities = [] -+ # read files -+ for ent in data["entities"]: -+ if isfile(ent["file_name"]): -+ with open(ent["file_name"]) as f: -+ lines = f.read().replace("(", "").replace(")", "").split( -+ "\n") -+ samples = [] -+ for l in lines: -+ samples += [a.strip() for a in l.split("|") if a.strip()] -+ entities.append({"name": ent["name"], "samples": samples}) -+ return entities -+ -+ def get_keywords_manifest(self): -+ padatious = self.get_entities_manifest() -+ adapt = self.get_vocab_manifest() -+ regex = self.get_regex_manifest() -+ return {"adapt": adapt, -+ "padatious": padatious, -+ "regex": regex} -+ -+ -+def open_intent_envelope(message): -+ """Convert dictionary received over messagebus to 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')) - -From c3b7ce30d1060251d1f3db894adcc780f10a7a1f Mon Sep 17 00:00:00 2001 -From: jarbasal -Date: Thu, 30 Sep 2021 00:55:13 +0100 -Subject: [PATCH 2/5] dialog utils - ---- - ovos_utils/dialog.py | 193 ++++++++++++++++++++++++++++++++++++ - ovos_utils/lang/__init__.py | 26 +++++ - 2 files changed, 219 insertions(+) - create mode 100644 ovos_utils/dialog.py - -diff --git a/ovos_utils/dialog.py b/ovos_utils/dialog.py -new file mode 100644 -index 0000000..7b51900 ---- /dev/null -+++ b/ovos_utils/dialog.py -@@ -0,0 +1,193 @@ -+import os -+import random -+import re -+from os.path import join -+from pathlib import Path -+ -+from ovos_utils.bracket_expansion import expand_options -+from ovos_utils.configuration import read_mycroft_config -+from ovos_utils.file_utils import resolve_resource_file -+from ovos_utils.lang import translate_word -+from ovos_utils.log import LOG -+ -+ -+class MustacheDialogRenderer: -+ """A dialog template renderer based on the mustache templating language.""" -+ -+ def __init__(self): -+ self.templates = {} -+ self.recent_phrases = [] -+ -+ # TODO magic numbers are bad! -+ self.max_recent_phrases = 3 -+ # We cycle through lines in .dialog files to keep Mycroft from -+ # repeating the same phrase over and over. However, if a .dialog -+ # file only contains a few entries, this can cause it to loop. -+ # -+ # This offset will override max_recent_phrases on very short .dialog -+ # files. With the offset at 2, .dialog files with 3 or more lines will -+ # be managed to avoid repetition, but .dialog files with only 1 or 2 -+ # lines will be unaffected. Dialog should never get stuck in a loop. -+ self.loop_prevention_offset = 2 -+ -+ def load_template_file(self, template_name, filename): -+ """Load a template by file name into the templates cache. -+ -+ Args: -+ template_name (str): a unique identifier for a group of templates -+ filename (str): a fully qualified filename of a mustache template. -+ """ -+ with open(filename, 'r', encoding='utf8') as f: -+ for line in f: -+ template_text = line.strip() -+ # Skip all lines starting with '#' and all empty lines -+ if (not template_text.startswith('#') and -+ template_text != ''): -+ if template_name not in self.templates: -+ self.templates[template_name] = [] -+ -+ # convert to standard python format string syntax. From -+ # double (or more) '{' followed by any number of -+ # whitespace followed by actual key followed by any number -+ # of whitespace followed by double (or more) '}' -+ template_text = re.sub(r'\{\{+\s*(.*?)\s*\}\}+', r'{\1}', -+ template_text) -+ -+ self.templates[template_name].append(template_text) -+ -+ def render(self, template_name, context=None, index=None): -+ """ -+ Given a template name, pick a template and render it using the context. -+ If no matching template exists use template_name as template. -+ -+ Tries not to let Mycroft say exactly the same thing twice in a row. -+ -+ Args: -+ template_name (str): the name of a template group. -+ context (dict): dictionary representing values to be rendered -+ index (int): optional, the specific index in the collection of -+ templates -+ -+ Returns: -+ str: the rendered string -+ """ -+ context = context or {} -+ if template_name not in self.templates: -+ # When not found, return the name itself as the dialog -+ # This allows things like render("record.not.found") to either -+ # find a translation file "record.not.found.dialog" or return -+ # "record not found" literal. -+ return template_name.replace('.', ' ') -+ -+ # Get the .dialog file's contents, minus any which have been spoken -+ # recently. -+ template_functions = self.templates.get(template_name) -+ -+ if index is None: -+ template_functions = ([t for t in template_functions -+ if t not in self.recent_phrases] or -+ template_functions) -+ line = random.choice(template_functions) -+ else: -+ line = template_functions[index % len(template_functions)] -+ # Replace {key} in line with matching values from context -+ line = line.format(**context) -+ line = random.choice(expand_options(line)) -+ -+ # Here's where we keep track of what we've said recently. Remember, -+ # this is by line in the .dialog file, not by exact phrase -+ self.recent_phrases.append(line) -+ if (len(self.recent_phrases) > -+ min(self.max_recent_phrases, len(self.templates.get( -+ template_name)) - self.loop_prevention_offset)): -+ self.recent_phrases.pop(0) -+ return line -+ -+ -+def load_dialogs(dialog_dir, renderer=None): -+ """Load all dialog files within the specified directory. -+ -+ Args: -+ dialog_dir (str): directory that contains dialog files -+ -+ Returns: -+ a loaded instance of a dialog renderer -+ """ -+ if renderer is None: -+ renderer = MustacheDialogRenderer() -+ -+ directory = Path(dialog_dir) -+ if not directory.exists() or not directory.is_dir(): -+ LOG.warning('No dialog files found: {}'.format(dialog_dir)) -+ return renderer -+ -+ for path, _, files in os.walk(str(directory)): -+ for f in files: -+ if f.endswith('.dialog'): -+ renderer.load_template_file(f.replace('.dialog', ''), -+ join(path, f)) -+ return renderer -+ -+ -+def get_dialog(phrase, lang=None, context=None): -+ """Looks up a resource file for the given phrase. -+ -+ If no file is found, the requested phrase is returned as the string. This -+ will use the default language for translations. -+ -+ Args: -+ phrase (str): resource phrase to retrieve/translate -+ lang (str): the language to use -+ context (dict): values to be inserted into the string -+ -+ Returns: -+ str: a randomized and/or translated version of the phrase -+ """ -+ -+ if not lang: -+ try: -+ conf = read_mycroft_config() -+ lang = conf.get('lang') -+ except FileNotFoundError: -+ lang = "en-us" -+ -+ filename = join('text', lang.lower(), phrase + '.dialog') -+ template = resolve_resource_file(filename) -+ if not template: -+ LOG.debug('Resource file not found: {}'.format(filename)) -+ return phrase -+ -+ stache = MustacheDialogRenderer() -+ stache.load_template_file('template', template) -+ if not context: -+ context = {} -+ return stache.render('template', context) -+ -+ -+def join_list(items, connector, sep=None, lang=''): -+ """ Join a list into a phrase using the given connector word -+ Examples: -+ join_list([1,2,3], "and") -> "1, 2 and 3" -+ join_list([1,2,3], "and", ";") -> "1; 2 and 3" -+ Args: -+ items (array): items to be joined -+ connector (str): connecting word (resource name), like "and" or "or" -+ sep (str, optional): separator character, default = "," -+ lang (str, optional): an optional BCP-47 language code, if omitted -+ the default language will be used. -+ Returns: -+ str: the connected list phrase -+ """ -+ -+ if not items: -+ return "" -+ if len(items) == 1: -+ return str(items[0]) -+ -+ if not sep: -+ sep = ", " -+ else: -+ sep += " " -+ return (sep.join(str(item) for item in items[:-1]) + -+ " " + translate_word(connector, lang) + -+ " " + items[-1]) -diff --git a/ovos_utils/lang/__init__.py b/ovos_utils/lang/__init__.py -index 28c8235..b43ccae 100644 ---- a/ovos_utils/lang/__init__.py -+++ b/ovos_utils/lang/__init__.py -@@ -2,6 +2,7 @@ - from os import system, makedirs, listdir - from ovos_utils.lang.detect import detect_lang - from ovos_utils.lang.translate import translate_text -+from ovos_utils.file_utils import resolve_resource_file - - - def get_tts(sentence, lang="en-us", mp3_file="/tmp/google_tx_tts.mp3"): -@@ -42,3 +43,28 @@ def get_language_dir(base_path, lang="en-us"): - if len(paths): - return paths[0] - return join(base_path, lang) -+ -+ -+def translate_word(name, lang='en-us'): -+ """ Helper to get word translations -+ Args: -+ name (str): Word name. Returned as the default value if not translated -+ lang (str, optional): an optional BCP-47 language code, if omitted -+ the default language will be used. -+ Returns: -+ str: translated version of resource name -+ """ -+ filename = resolve_resource_file(join("text", lang, name + ".word")) -+ if filename: -+ # open the file -+ try: -+ with open(filename, 'r', encoding='utf8') as f: -+ for line in f: -+ word = line.strip() -+ if word.startswith("#"): -+ continue # skip comment lines -+ return word -+ except Exception: -+ pass -+ return name # use resource name as the word -+ - -From 3b271d6fa3c95b311b11438ab63be7ac1371e6ed Mon Sep 17 00:00:00 2001 -From: jarbasal -Date: Thu, 30 Sep 2021 06:17:20 +0100 -Subject: [PATCH 3/5] circular import - ---- - ovos_utils/file_utils.py | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py -index de9122a..9b3c44b 100644 ---- a/ovos_utils/file_utils.py -+++ b/ovos_utils/file_utils.py -@@ -6,7 +6,6 @@ - from os.path import splitext, join, dirname - - from ovos_utils.bracket_expansion import expand_options --from ovos_utils.intents.intent_service_interface import to_alnum, munge_regex - from ovos_utils.log import LOG - - -@@ -135,6 +134,8 @@ def load_regex_from_file(path, skill_id): - path: path to vocabulary file (*.voc) - skill_id: skill_id to the regex is tied to - """ -+ from ovos_utils.intents.intent_service_interface import munge_regex -+ - regexes = [] - if path.endswith('.rx'): - with open(path, 'r', encoding='utf8') as reg_file: -@@ -163,6 +164,8 @@ def load_vocabulary(basedir, skill_id): - Returns: - dict with intent_type as keys and list of list of lists as value. - """ -+ from ovos_utils.intents.intent_service_interface import to_alnum -+ - vocabs = {} - for path, _, files in walk(basedir): - for f in files: - -From 1771ed406432ae85cae172c19e255f915c730a7d Mon Sep 17 00:00:00 2001 -From: jarbasal -Date: Thu, 30 Sep 2021 16:43:59 +0100 -Subject: [PATCH 4/5] delayed bus init - ---- - ovos_utils/gui.py | 32 ++++++++++++++++++++++++++++---- - 1 file changed, 28 insertions(+), 4 deletions(-) - -diff --git a/ovos_utils/gui.py b/ovos_utils/gui.py -index 9b4a472..ea62f9c 100644 ---- a/ovos_utils/gui.py -+++ b/ovos_utils/gui.py -@@ -429,7 +429,7 @@ class GUIInterface: - """ - - def __init__(self, skill_id, bus=None, remote_server=None, config=None): -- self.bus = bus or get_mycroft_bus() -+ self.bus = bus - self.__session_data = {} # synced to GUI for use by this skill's pages - self.pages = [] - self.current_page_idx = -1 -@@ -437,6 +437,11 @@ def __init__(self, skill_id, bus=None, remote_server=None, config=None): - self.on_gui_changed_callback = None - self.remote_url = remote_server - self._events = [] -+ if bus: -+ self.set_bus(bus) -+ -+ def set_bus(self, bus=None): -+ self.bus = bus or get_mycroft_bus() - self.setup_default_handlers() - - @property -@@ -448,6 +453,8 @@ def page(self): - def connected(self): - """Returns True if at least 1 remote gui is connected or if gui is - installed and running locally, else False""" -+ if not self.bus: -+ return False - return can_use_gui(self.bus) - - def build_message_type(self, event): -@@ -475,6 +482,8 @@ def register_handler(self, event, handler): - event (str): event to catch - handler: function to handle the event - """ -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - event = self.build_message_type(event) - self._events.append((event, handler)) - self.bus.on(event, handler) -@@ -501,6 +510,8 @@ def gui_set(self, message): - self.on_gui_changed_callback() - - def _sync_data(self): -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - data = self.__session_data.copy() - data.update({'__from': self.skill_id}) - self.bus.emit(Message("gui.value.set", data)) -@@ -539,6 +550,8 @@ def clear(self): - self.__session_data = {} - self.pages = [] - self.current_page_idx = -1 -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - self.bus.emit(Message("gui.clear.namespace", - {"__from": self.skill_id})) - -@@ -551,6 +564,8 @@ def send_event(self, event_name, params=None): - should be sent along with the request. - """ - params = params or {} -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - self.bus.emit(Message("gui.event.send", - {"__from": self.skill_id, - "event_name": event_name, -@@ -610,6 +625,8 @@ def show_pages(self, page_names, index=0, override_idle=None, - True: Disables showing all platform skill animations. - False: 'Default' always show animations. - """ -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - if isinstance(page_names, str): - page_names = [page_names] - if not isinstance(page_names, list): -@@ -649,6 +666,8 @@ def remove_pages(self, page_names): - page_names (list): List of page names (str) to display, such as - ["Weather.qml", "Forecast.qml", "Other.qml"] - """ -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - if not isinstance(page_names, list): - page_names = [page_names] - page_urls = self._pages2uri(page_names) -@@ -671,6 +690,8 @@ def show_notification(self, content, action=None, - transient: 'Default' displays a notification with a timeout. - sticky: displays a notification that sticks to the screen. - """ -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - self.bus.emit(Message("homescreen.notification.set", - data={ - "sender": self.skill_id, -@@ -794,6 +815,8 @@ def release(self): - allow different platforms to properly handle this event. - Also calls self.clear() to reset the state variables - Platforms can close the window or go back to previous page""" -+ if not self.bus: -+ raise RuntimeError("bus not set, did you call self.bind() ?") - self.clear() - self.bus.emit(Message("mycroft.gui.screen.close", - {"skill_id": self.skill_id})) -@@ -803,9 +826,10 @@ def shutdown(self): - - Clear pages loaded through this interface and remove the bus events - """ -- self.release() -- for event, handler in self._events: -- self.bus.remove(event, handler) -+ if self.bus: -+ self.release() -+ for event, handler in self._events: -+ self.bus.remove(event, handler) - - - if __name__ == "__main__": - -From 6db561b684ee88fbf3bb9bd9906bf25d95432873 Mon Sep 17 00:00:00 2001 -From: jarbasal -Date: Fri, 1 Oct 2021 15:46:26 +0100 -Subject: [PATCH 5/5] added some helper functions for intent handling / - disabling - ---- - ovos_utils/intents/intent_service_interface.py | 16 +++++++++++++++- - 1 file changed, 15 insertions(+), 1 deletion(-) - -diff --git a/ovos_utils/intents/intent_service_interface.py b/ovos_utils/intents/intent_service_interface.py -index 25eaceb..a014880 100644 ---- a/ovos_utils/intents/intent_service_interface.py -+++ b/ovos_utils/intents/intent_service_interface.py -@@ -162,7 +162,12 @@ def detach_intent(self, intent_name): - Args: - intent_name(str): Intent reference - """ -- name = intent_name.split(':')[1] -+ # split skill_id if needed -+ if intent_name not in self and ":" in intent_name: -+ name = intent_name.split(':')[1] -+ else: -+ name = intent_name -+ - if name in self: - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: -@@ -251,6 +256,15 @@ def __contains__(self, val): - """Checks if an intent name has been registered.""" - return val in [i[0] for i in self.registered_intents] - -+ def get_intent_names(self): -+ return [i[0] for i in self.registered_intents] -+ -+ def detach_all(self): -+ for name in self.get_intent_names(): -+ self.detach_intent(name) -+ self.registered_intents = [] -+ self.detached_intents = [] -+ - def get_intent(self, intent_name): - """Get intent from intent_name. - diff --git a/buildroot-external/package/python-ovos-utils/python-ovos-utils.hash b/buildroot-external/package/python-ovos-utils/python-ovos-utils.hash index 6a6f7d8b..69b79dac 100644 --- a/buildroot-external/package/python-ovos-utils/python-ovos-utils.hash +++ b/buildroot-external/package/python-ovos-utils/python-ovos-utils.hash @@ -1 +1 @@ -sha256 00a37f169da8c1712eaafae9b4cd3735bd91d4eb5ca2ad813d46bc5b2513f3d1 python-ovos-utils-b4fba3732e99b5e41a5e0f0043fed24f6c8d9f84.tar.gz +sha256 bea167dfdd2f22b903156243421e7dd706de1ec4647dfeb613982a1ee8121447 python-ovos-utils-de67098268b1994cc6ff712ad3160a5e5ae67be6.tar.gz diff --git a/buildroot-external/package/python-ovos-utils/python-ovos-utils.mk b/buildroot-external/package/python-ovos-utils/python-ovos-utils.mk index 327e1c57..ccc805bc 100644 --- a/buildroot-external/package/python-ovos-utils/python-ovos-utils.mk +++ b/buildroot-external/package/python-ovos-utils/python-ovos-utils.mk @@ -4,7 +4,7 @@ # ################################################################################ -PYTHON_OVOS_UTILS_VERSION = b4fba3732e99b5e41a5e0f0043fed24f6c8d9f84 +PYTHON_OVOS_UTILS_VERSION = de67098268b1994cc6ff712ad3160a5e5ae67be6 PYTHON_OVOS_UTILS_SITE = $(call github,OpenVoiceOS,ovos_utils,$(PYTHON_OVOS_UTILS_VERSION)) PYTHON_OVOS_UTILS_SETUP_TYPE = setuptools PYTHON_OVOS_UTILS_LICENSE_FILES = LICENSE diff --git a/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.hash b/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.hash index 7f925d98..65f03514 100644 --- a/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.hash +++ b/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.hash @@ -1 +1 @@ -sha256 8dd682c6677d35d1150d58d39e044c38cb86f879f363c0079aef8a86d1beefcb python-ovos-vlc-plugin-604c71e3b3a66da25d2ee76ef1c9603eeaf762b7.tar.gz +sha256 b0112fdc4f76625465018a43ccc4342f9cf0fc39990c747aab8fe5dc0b7fc0d7 python-ovos-vlc-plugin-8750fa5107773b16e59a71820154cbe2c18a83d5.tar.gz diff --git a/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.mk b/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.mk index ee990f60..178bb198 100644 --- a/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.mk +++ b/buildroot-external/package/python-ovos-vlc-plugin/python-ovos-vlc-plugin.mk @@ -4,7 +4,7 @@ # ################################################################################ -PYTHON_OVOS_VLC_PLUGIN_VERSION = 604c71e3b3a66da25d2ee76ef1c9603eeaf762b7 +PYTHON_OVOS_VLC_PLUGIN_VERSION = 8750fa5107773b16e59a71820154cbe2c18a83d5 PYTHON_OVOS_VLC_PLUGIN_SITE = $(call github,OpenVoiceOS,ovos-vlc-plugin,$(PYTHON_OVOS_VLC_PLUGIN_VERSION)) PYTHON_OVOS_VLC_PLUGIN_SETUP_TYPE = setuptools PYTHON_OVOS_VLC_PLUGIN_LICENSE_FILES = LICENSE diff --git a/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.hash b/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.hash index cea231cd..239830d1 100644 --- a/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.hash +++ b/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.hash @@ -1 +1 @@ -sha256 ffcac3069f5299719557204af939acf4117b11e89b3043296e0d0614a61658b3 python-ovos-workshop-279d0c33078e143c00e0261600265a76cd0d4e7c.tar.gz +sha256 ef895892075d20a9557017ec141de3639a22be72d35479775cefac989c4c8c2f python-ovos-workshop-f309a4ae30cebf5376e92af2b59f65e49187bf8e.tar.gz diff --git a/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.mk b/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.mk index ec370eaf..8b29ea8d 100644 --- a/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.mk +++ b/buildroot-external/package/python-ovos-workshop/python-ovos-workshop.mk @@ -4,7 +4,7 @@ # ################################################################################ -PYTHON_OVOS_WORKSHOP_VERSION = 279d0c33078e143c00e0261600265a76cd0d4e7c +PYTHON_OVOS_WORKSHOP_VERSION = f309a4ae30cebf5376e92af2b59f65e49187bf8e PYTHON_OVOS_WORKSHOP_SITE = $(call github,OpenVoiceOS,OVOS-workshop,$(PYTHON_OVOS_WORKSHOP_VERSION)) PYTHON_OVOS_WORKSHOP_SETUP_TYPE = setuptools PYTHON_OVOS_WORKSHOP_LICENSE_FILES = LICENSE diff --git a/buildroot-external/package/spotifyd/spotifyd.conf b/buildroot-external/package/spotifyd/spotifyd.conf index 87e0a3df..390a3dd1 100644 --- a/buildroot-external/package/spotifyd/spotifyd.conf +++ b/buildroot-external/package/spotifyd/spotifyd.conf @@ -25,7 +25,7 @@ # and expose MPRIS controls. When running headless, without a dbus session, # then set this to false to avoid binding errors # -#use_mpris = true +use_mpris = true # The audio backend used to play the your music. To get # a list of possible backends, run `spotifyd --help`. diff --git a/buildroot-external/package/spotifyd/spotifyd.mk b/buildroot-external/package/spotifyd/spotifyd.mk index a1dd9182..a57afa7b 100644 --- a/buildroot-external/package/spotifyd/spotifyd.mk +++ b/buildroot-external/package/spotifyd/spotifyd.mk @@ -4,8 +4,8 @@ # ################################################################################ -SPOTIFYD_VERSION = v0.3.2 -SPOTIFYD_SITE = $(call github,Spotifyd,spotifyd,$(SPOTIFYD_VERSION)) +SPOTIFYD_VERSION = 4dd5a6010a5dbff5aac5e06f236f84e83d71c597 +SPOTIFYD_SITE = $(call github,capnfabs,spotifyd,$(SPOTIFYD_VERSION)) SPOTIFYD_LICENSE = GPL-3.0 SPOTIFYD_LICENSE_FILES = LICENSE diff --git a/buildroot-external/package/spotifyd/spotifyd.service b/buildroot-external/package/spotifyd/spotifyd.service index 699a295a..88cda618 100644 --- a/buildroot-external/package/spotifyd/spotifyd.service +++ b/buildroot-external/package/spotifyd/spotifyd.service @@ -8,6 +8,7 @@ After=network-online.target After=pulseaudio.service [Service] +User=mycroft Type=simple ExecStart=/usr/bin/spotifyd --no-daemon Restart=always