Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/stop_per_session #391

Merged
merged 2 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ jobs:
pip install -e .[mycroft,deprecated]
- name: Install test dependencies
run: |
pip install -r requirements/tests.txt
pip install ./test/unittests/common_query/ovos_tskill_fakewiki
pip install ./test/end2end/skill-ovos-hello-world
pip install ./test/end2end/skill-ovos-schedule
pip install ./test/end2end/skill-ovos-fallback-unknown
pip install ./test/end2end/skill-ovos-fallback-unknownv1
pip install ./test/end2end/skill-converse_test
pip install -r requirements/tests.txt
pip install ./test/unittests/common_query/ovos_tskill_fakewiki
pip install ./test/end2end/skill-ovos-hello-world
pip install ./test/end2end/skill-ovos-fallback-unknown
pip install ./test/end2end/skill-ovos-fallback-unknownv1
pip install ./test/end2end/skill-converse_test
pip install ./test/end2end/skill-ovos-schedule
pip install ./test/end2end/skill-new-stop
pip install ./test/end2end/skill-old-stop
- name: Generate coverage report
run: |
pytest --cov=ovos_core --cov-report xml test/unittests
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ jobs:
pip install ./test/end2end/skill-ovos-fallback-unknownv1
pip install ./test/end2end/skill-converse_test
pip install ./test/end2end/skill-ovos-schedule
pip install ./test/end2end/skill-new-stop
pip install ./test/end2end/skill-old-stop
- name: Install core repo
run: |
pip install -e .[mycroft,deprecated]
Expand Down
5 changes: 5 additions & 0 deletions ovos_core/intent_services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ovos_core.intent_services.adapt_service import AdaptService
from ovos_core.intent_services.commonqa_service import CommonQAService
from ovos_core.intent_services.converse_service import ConverseService
from ovos_core.intent_services.stop_service import StopService
from ovos_core.intent_services.fallback_service import FallbackService
from ovos_core.intent_services.padacioso_service import PadaciosoService
from ovos_core.transformers import MetadataTransformersService, UtteranceTransformersService
Expand Down Expand Up @@ -71,6 +72,7 @@ def __init__(self, bus):
self.fallback = FallbackService(bus)
self.converse = ConverseService(bus)
self.common_qa = CommonQAService(bus)
self.stop = StopService(bus)
self.utterance_plugins = UtteranceTransformersService(bus, config=config)
self.metadata_plugins = MetadataTransformersService(bus, config=config)
# connection SessionManager to the bus,
Expand Down Expand Up @@ -216,6 +218,9 @@ def get_pipeline(self, skips=None, session=None):

matchers = {
"converse": self.converse.converse_with_skills,
"stop_high": self.stop.match_stop_high,
"stop_medium": self.stop.match_stop_medium,
"stop_low": self.stop.match_stop_low,
"padatious_high": padatious_matcher.match_high,
"padacioso_high": self.padacioso_service.match_high,
"adapt": self.adapt_service.match_intent,
Expand Down
260 changes: 260 additions & 0 deletions ovos_core/intent_services/stop_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import os
import re
from os.path import dirname
from threading import Event

import ovos_core.intent_services
from ovos_bus_client.message import Message
from ovos_bus_client.session import SessionManager
from ovos_config.config import Configuration
from ovos_utils import flatten_list
from ovos_utils.bracket_expansion import expand_options
from ovos_utils.log import LOG
from ovos_utils.parse import match_one


class StopService:
"""Intent Service thats handles stopping skills."""

def __init__(self, bus):
self.bus = bus
self._voc_cache = {}
self.load_resource_files()

def load_resource_files(self):
base = f"{dirname(dirname(__file__))}/locale"
for lang in os.listdir(base):
lang2 = lang.split("-")[0].lower()
self._voc_cache[lang2] = {}
for f in os.listdir(f"{base}/{lang}"):
with open(f"{base}/{lang}/{f}") as fi:
lines = [expand_options(l) for l in fi.read().split("\n")
if l.strip() and not l.startswith("#")]
n = f.split(".", 1)[0]
self._voc_cache[lang2][n] = flatten_list(lines)

@property
def config(self):
"""
Returns:
stop_config (dict): config for stop handling options
"""
return Configuration().get("skills", {}).get("stop") or {}

def get_active_skills(self, message=None):
"""Active skill ids ordered by converse priority
this represents the order in which stop will be called

Returns:
active_skills (list): ordered list of skill_ids
"""
session = SessionManager.get(message)
return [skill[0] for skill in session.active_skills]

def _collect_stop_skills(self, message):
"""use the messagebus api to determine which skills can stop
This includes all skills and external applications"""

want_stop = []
skill_ids = []

active_skills = self.get_active_skills(message)

if not active_skills:
return want_stop

event = Event()

def handle_ack(msg):
nonlocal event
skill_id = msg.data["skill_id"]

# validate the stop pong
if all((skill_id not in want_stop,
msg.data.get("can_handle", True),
skill_id in active_skills)):
want_stop.append(skill_id)

if skill_id not in skill_ids: # track which answer we got
skill_ids.append(skill_id)

if all(s in skill_ids for s in active_skills):
# all skills answered the ping!
event.set()

self.bus.on("skill.stop.pong", handle_ack)

# ask skills if they can stop
for skill_id in active_skills:
self.bus.emit(message.forward(f"{skill_id}.stop.ping",
{"skill_id": skill_id}))

# wait for all skills to acknowledge they can stop
event.wait(timeout=0.5)

self.bus.remove("skill.stop.pong", handle_ack)
return want_stop or active_skills

def stop_skill(self, skill_id, message):
"""Tell a skill to stop anything it's doing,
taking into account the message Session

Args:
skill_id: skill to query.
message (Message): message containing interaction info.

Returns:
handled (bool): True if handled otherwise False.
"""
stop_msg = message.reply(f"{skill_id}.stop")
result = self.bus.wait_for_response(stop_msg, f"{skill_id}.stop.response")
if result and 'error' in result.data:
error_msg = result.data['error']
LOG.error(f"{skill_id}: {error_msg}")
return False
elif result is not None:
return result.data.get('result', False)

def match_stop_high(self, utterances, lang, message):
"""If utterance is an exact match for "stop" , run before intent stage

Args:
utterances (list): list of utterances
lang (string): 4 letter ISO language code
message (Message): message to use to generate reply

Returns:
IntentMatch if handled otherwise None.
"""
lang = lang.split("-")[0]
if lang not in self._voc_cache:
return None

# we call flatten in case someone is sending the old style list of tuples
utterance = flatten_list(utterances)[0]

is_stop = self.voc_match(utterance, 'stop', exact=True, lang=lang)
is_global_stop = self.voc_match(utterance, 'global_stop', exact=True, lang=lang) or \
(is_stop and not len(self.get_active_skills(message)))

conf = 1.0

if is_global_stop:
# emit a global stop, full stop anything OVOS is doing
self.bus.emit(message.reply("mycroft.stop", {}))
return ovos_core.intent_services.IntentMatch('Stop', None, {"conf": conf},
None, utterance)

if is_stop:
# check if any skill can stop
for skill_id in self._collect_stop_skills(message):
if self.stop_skill(skill_id, message):
return ovos_core.intent_services.IntentMatch('Stop', None, {"conf": conf},
skill_id, utterance)
return None

def match_stop_medium(self, utterances, lang, message):
""" if "stop" intent is in the utterance,
but it contains additional words not in .intent files

Args:
utterances (list): list of utterances
lang (string): 4 letter ISO language code
message (Message): message to use to generate reply

Returns:
IntentMatch if handled otherwise None.
"""
lang = lang.split("-")[0]
if lang not in self._voc_cache:
return None

# we call flatten in case someone is sending the old style list of tuples
utterance = flatten_list(utterances)[0]

is_stop = self.voc_match(utterance, 'stop', exact=False, lang=lang)
if not is_stop:
is_global_stop = self.voc_match(utterance, 'global_stop', exact=False, lang=lang) or \
(is_stop and not len(self.get_active_skills(message)))
if not is_global_stop:
return None

return self.match_stop_low(utterances, lang, message)

def match_stop_low(self, utterances, lang, message):
""" before fallback_low , fuzzy match stop intent

Args:
utterances (list): list of utterances
lang (string): 4 letter ISO language code
message (Message): message to use to generate reply

Returns:
IntentMatch if handled otherwise None.
"""
lang = lang.split("-")[0]
if lang not in self._voc_cache:
return None

# we call flatten in case someone is sending the old style list of tuples
utterance = flatten_list(utterances)[0]

conf = match_one(utterance, self._voc_cache[lang]['stop'])[1]
if len(self.get_active_skills(message)) > 0:
conf += 0.1
conf = round(min(conf, 1.0), 3)

if conf < self.config.get("min_conf", 0.5):
return None

# check if any skill can stop
for skill_id in self._collect_stop_skills(message):
if self.stop_skill(skill_id, message):
return ovos_core.intent_services.IntentMatch('Stop', None, {"conf": conf},
skill_id, utterance)

# emit a global stop, full stop anything OVOS is doing
self.bus.emit(message.reply("mycroft.stop", {}))
return ovos_core.intent_services.IntentMatch('Stop', None, {"conf": conf},
None, utterance)

def voc_match(self, utt: str, voc_filename: str, lang: str,
exact: bool = False):
"""
Determine if the given utterance contains the vocabulary provided.

By default the method checks if the utterance contains the given vocab
thereby allowing the user to say things like "yes, please" and still
match against "Yes.voc" containing only "yes". An exact match can be
requested.

The method first checks in the current Skill's .voc files and secondly
in the "res/text" folder of mycroft-core. The result is cached to
avoid hitting the disk each time the method is called.

Args:
utt (str): Utterance to be tested
voc_filename (str): Name of vocabulary file (e.g. 'yes' for
'res/text/en-us/yes.voc')
lang (str): Language code, defaults to self.lang
exact (bool): Whether the vocab must exactly match the utterance

Returns:
bool: True if the utterance has the given vocabulary it
"""
lang = lang.split("-")[0].lower()
if lang not in self._voc_cache:
return False

_vocs = self._voc_cache[lang].get(voc_filename) or []

if utt and _vocs:
if exact:
# Check for exact match
return any(i.strip() == utt
for i in _vocs)
else:
# Check for matches against complete words
return any([re.match(r'.*\b' + i + r'\b.*', utt)
for i in _vocs])
return False
5 changes: 5 additions & 0 deletions ovos_core/locale/de-de/global_stop.intent
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
alles (stoppen|schließen|schliessen|beenden|abbrechen)
(schließe|schließ|schliess|stop|stoppe|beende) alles
(schließe|schließ|schliess|stop|stoppe|beende) alle (programme|anwendungen|prozesse|fenster|skills)
(breche|brech) alles ab
(breche|brech) (alle|) (programme|anwendungen|prozesse|fenster|skills) ab
13 changes: 13 additions & 0 deletions ovos_core/locale/de-de/stop.intent
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
stop
stopp
genug (davon|)
schluss
schluß
schließen
ende
beenden
aufhören
hör auf (damit|)
abbrechen
(brech|breche) ab
abbruch
31 changes: 31 additions & 0 deletions ovos_core/locale/en-us/global_stop.intent
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
stop all
end all
terminate all
cancel all
finish all
halt all
abort all
cease all
stop everything
end everything
terminate everything
cancel everything
finish everything
halt everything
abort everything
cease everything
Stop everything now
End all processes
Terminate all operations
Cancel all tasks
Finish all activities
Halt all activities immediately
Abort all ongoing processes
Cease all actions
Stop all current tasks
Terminate all running activities
Cancel all pending operations
Finish all open tasks
Halt all ongoing processes
Abort all running actions
Cease all active activities
17 changes: 17 additions & 0 deletions ovos_core/locale/en-us/stop.intent
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
stop
stop doing that
stop that
Stop what you're doing
Please stop that
Can you stop now
Stop performing that task
Please halt the current action
Stop the ongoing process
Cease the current activity
Please put an end to it
Stop working on that
Stop executing the current command
Please terminate the current task
Stop the current operation
Cease the current action
Please cancel the current task
4 changes: 4 additions & 0 deletions ovos_core/locale/es-es/stop.intent
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# auto translated from en-us to es-es
Para
para de hacer eso
detener
Loading
Loading