Skip to content

Commit 0cc12d7

Browse files
committed
refactor/converse
make converse event based, it was a little odd because it was the only code from MycroftSkill called outside of the class itself The event based approach also adds support for external skills, eg, Hivemind and OVOSAbstractApp refactored converse process into smaller methods added a new converse acknowledge bus message, now converse is only called for skills where converse is implemented. skills are removed from active skill list if - skill does not have converse capabilities - skill was unloaded/disconnected - skill does not want to converse at this time
1 parent 12e62a0 commit 0cc12d7

File tree

3 files changed

+179
-159
lines changed

3 files changed

+179
-159
lines changed

mycroft/skills/intent_service.py

Lines changed: 109 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,21 @@
1313
# limitations under the License.
1414
#
1515
"""Mycroft's intent service, providing intent parsing since forever!"""
16-
from copy import copy
1716
import time
17+
from threading import Event
1818

1919
from mycroft.configuration import Configuration, setup_locale
20-
from mycroft.util.log import LOG
21-
from mycroft.util.parse import normalize
20+
from mycroft.messagebus.message import Message
2221
from mycroft.metrics import report_timing, Stopwatch
22+
from mycroft.skills.intent_service_interface import open_intent_envelope
2323
from mycroft.skills.intent_services import (
24-
AdaptService, AdaptIntent,
25-
FallbackService,
24+
AdaptService, FallbackService,
2625
PadatiousService, PadatiousMatcher,
2726
IntentMatch
2827
)
29-
from mycroft.skills.intent_service_interface import open_intent_envelope
3028
from mycroft.skills.permissions import ConverseMode, ConverseActivationMode
31-
from mycroft.messagebus.message import Message
29+
from mycroft.util.log import LOG
30+
from mycroft.util.parse import normalize
3231

3332

3433
def _get_message_lang(message):
@@ -81,13 +80,13 @@ class IntentService:
8180
The intent service also provides the internal API for registering and
8281
querying the intent service.
8382
"""
83+
8484
def __init__(self, bus):
8585
self.bus = bus
8686
config = Configuration.get()
8787

8888
# Dictionary for translating a skill id to a name
8989
self.skill_names = {}
90-
9190
self.adapt_service = AdaptService(config.get('context', {}))
9291
try:
9392
self.padatious_service = PadatiousService(bus, config['padatious'])
@@ -211,17 +210,12 @@ def handle_activate_skill_request(self, message):
211210
return # skill activation disabled
212211
elif self._consecutive_activations.get(skill_id, 0) > max_activations:
213212
return # skill exceeded authorized consecutive number of
214-
# activations
213+
# activations
215214

216215
# activate skill
217216
self.add_active_skill(skill_id)
218217
self._consecutive_activations[skill_id] += 1
219218

220-
# converse handling
221-
222-
def handle_activate_skill_request(self, message):
223-
self.add_active_skill(message.data['skill_id'])
224-
225219
def handle_deactivate_skill_request(self, message):
226220
# TODO imperfect solution - only a skill can deactivate itself
227221
# someone can forge this message and emit it raw, but in ovos-core all
@@ -236,8 +230,8 @@ def reset_converse(self, message):
236230
"""Let skills know there was a problem with speech recognition"""
237231
lang = _get_message_lang(message)
238232
setup_locale(lang) # restore default lang
239-
for skill in copy(self.active_skills):
240-
self.do_converse(None, skill[0], lang, message)
233+
for skill_id in self._collect_converse_skills():
234+
self.do_converse(None, skill_id, lang, message)
241235

242236
def do_converse(self, utterances, skill_id, lang, message):
243237
"""Call skill and ask if they want to process the utterance.
@@ -249,29 +243,93 @@ def do_converse(self, utterances, skill_id, lang, message):
249243
lang (str): current language
250244
message (Message): message containing interaction info.
251245
"""
246+
if self._converse_allowed(skill_id):
247+
converse_msg = message.reply("skill.converse.request",
248+
{"skill_id": skill_id,
249+
"utterances": utterances,
250+
"lang": lang})
251+
result = self.bus.wait_for_response(converse_msg,
252+
'skill.converse.response')
253+
if result and 'error' in result.data:
254+
self.handle_converse_error(result)
255+
return False
256+
elif result is not None:
257+
return result.data.get('result', False)
258+
return False
259+
260+
def _converse_allowed(self, skill_id):
261+
""" check if skill_id is not blacklisted from using converse """
252262
opmode = self.converse_config.get("converse_mode",
253263
ConverseMode.ACCEPT_ALL)
254-
255264
if opmode == ConverseMode.BLACKLIST and skill_id in \
256265
self.converse_config.get("converse_blacklist", []):
257266
return False
258-
259267
elif opmode == ConverseMode.WHITELIST and skill_id not in \
260268
self.converse_config.get("converse_whitelist", []):
261269
return False
270+
return True
271+
272+
def _collect_converse_skills(self):
273+
"""use the messagebus api to determine which skills want to converse
274+
This includes all skills and external applications"""
275+
skill_ids = []
276+
want_converse = []
277+
active_skills = self.get_active_skill_ids()
278+
279+
def handle_ack(message):
280+
skill_id = message.data["skill_id"]
281+
if message.data.get("can_handle", True):
282+
if skill_id in active_skills:
283+
want_converse.append(skill_id)
284+
skill_ids.append(skill_id)
285+
286+
self.bus.on("mycroft.skill.converse.pong", handle_ack)
287+
288+
# wait for all skills to acknowledge they want to converse
289+
self.bus.emit(Message("mycroft.skill.converse.ping"))
290+
start = time.time()
291+
while not all(s in skill_ids for s in active_skills) \
292+
and time.time() - start <= 0.5:
293+
time.sleep(0.02)
294+
295+
# remove any skill that didn't answer the ack from active list
296+
for skill_id in active_skills:
297+
if skill_id not in want_converse:
298+
# - skill does not have converse capabilities
299+
# - skill was unloaded/disconnected
300+
# - skill does not want to converse at this time
301+
self.remove_active_skill(skill_id)
302+
303+
self.bus.remove("mycroft.skill.converse.pong", handle_ack)
304+
return skill_ids
305+
306+
def _check_converse_timeout(self):
307+
""" filter active skill list based on timestamps """
308+
timeouts = self.converse_config.get("skill_timeouts") or {}
309+
def_timeout = self.converse_config.get("timeout", 300)
310+
self.active_skills = [
311+
skill for skill in self.active_skills
312+
if time.time() - skill[1] <= timeouts.get(skill[0], def_timeout)]
262313

263-
converse_msg = (message.reply("skill.converse.request", {
264-
"skill_id": skill_id, "utterances": utterances, "lang": lang}))
265-
result = self.bus.wait_for_response(converse_msg,
266-
'skill.converse.response')
267-
if result and 'error' in result.data:
268-
self.handle_converse_error(result)
269-
ret = False
270-
elif result is not None:
271-
ret = result.data.get('result', False)
272-
else:
273-
ret = False
274-
return ret
314+
def _converse_with_skills(self, utterances, lang, message):
315+
"""Give active skills a chance at the utterance
316+
317+
Args:
318+
utterances (list): list of utterances
319+
lang (string): 4 letter ISO language code
320+
message (Message): message to use to generate reply
321+
322+
Returns:
323+
IntentMatch if handled otherwise None.
324+
"""
325+
utterances = [item for tup in utterances for item in tup]
326+
# filter allowed skills
327+
self._check_converse_timeout()
328+
# check if any skill wants to handle utterance
329+
for skill_id in self._collect_converse_skills():
330+
if self.do_converse(utterances, skill_id, lang, message):
331+
return IntentMatch('Converse', None, None, skill_id)
332+
return None
275333

276334
def handle_converse_error(self, message):
277335
"""Handle error in converse system.
@@ -281,24 +339,26 @@ def handle_converse_error(self, message):
281339
"""
282340
skill_id = message.data["skill_id"]
283341
error_msg = message.data['error']
284-
LOG.error("{}: {}".format(skill_id, error_msg))
285-
if message.data["error"] == "skill id does not exist":
286-
self.remove_active_skill(skill_id)
342+
LOG.error(f"{skill_id}: {error_msg}")
343+
344+
def get_active_skill_ids(self):
345+
return [skill[0] for skill in self.active_skills]
287346

288347
def remove_active_skill(self, skill_id):
289348
"""Remove a skill from being targetable by converse.
290349
291350
Args:
292351
skill_id (str): skill to remove
293352
"""
294-
for skill in self.active_skills:
295-
if skill[0] == skill_id:
296-
self.active_skills.remove(skill)
297-
self.bus.emit(
298-
Message("intent.service.skills.deactivated",
299-
{"skill_id": skill_id}))
300-
if skill_id in self._consecutive_activations:
301-
self._consecutive_activations[skill_id] = 0
353+
active_skills = self.get_active_skill_ids()
354+
if skill_id in active_skills:
355+
idx = active_skills.index(skill_id)
356+
self.active_skills.pop(idx)
357+
self.bus.emit(
358+
Message("intent.service.skills.deactivated",
359+
{"skill_id": skill_id}))
360+
if skill_id in self._consecutive_activations:
361+
self._consecutive_activations[skill_id] = 0
302362

303363
def add_active_skill(self, skill_id):
304364
"""Add a skill or update the position of an active skill.
@@ -312,10 +372,13 @@ def add_active_skill(self, skill_id):
312372
# search the list for an existing entry that already contains it
313373
# and remove that reference
314374
if skill_id != '':
315-
# do not call remove method to not send deactivate bus event!
316-
for skill in self.active_skills:
317-
if skill[0] == skill_id:
318-
self.active_skills.remove(skill)
375+
# NOTE: do not call self.remove_active_skill
376+
# do not want to send the deactivation bus event!
377+
active_skills = self.get_active_skill_ids()
378+
if skill_id in active_skills:
379+
idx = active_skills.index(skill_id)
380+
self.active_skills.pop(idx)
381+
319382
# add skill with timestamp to start of skill_list
320383
self.active_skills.insert(0, [skill_id, time.time()])
321384
self.bus.emit(
@@ -391,7 +454,7 @@ def handle_utterance(self, message):
391454
# List of functions to use to match the utterance with intent.
392455
# These are listed in priority order.
393456
match_funcs = [
394-
self._converse, padatious_matcher.match_high,
457+
self._converse_with_skills, padatious_matcher.match_high,
395458
self.adapt_service.match_intent, self.fallback.high_prio,
396459
padatious_matcher.match_medium, self.fallback.medium_prio,
397460
padatious_matcher.match_low, self.fallback.low_prio
@@ -427,35 +490,6 @@ def handle_utterance(self, message):
427490
except Exception as err:
428491
LOG.exception(err)
429492

430-
def _converse(self, utterances, lang, message):
431-
"""Give active skills a chance at the utterance
432-
433-
Args:
434-
utterances (list): list of utterances
435-
lang (string): 4 letter ISO language code
436-
message (Message): message to use to generate reply
437-
438-
Returns:
439-
IntentMatch if handled otherwise None.
440-
"""
441-
utterances = [item for tup in utterances for item in tup]
442-
443-
# check for conversation time-out
444-
skill_timeouts = self.converse_config.get("skill_timeouts") or {}
445-
default_timeout = self.converse_config.get("timeout", 300)
446-
self.active_skills = [
447-
skill for skill in self.active_skills
448-
if time.time() - skill[1] <= skill_timeouts.get(skill[0],
449-
default_timeout)]
450-
451-
# check if any skill wants to handle utterance
452-
for skill in copy(self.active_skills):
453-
if self.do_converse(utterances, skill[0], lang, message):
454-
# update timestamp, or there will be a timeout where
455-
# intent stops conversing whether its being used or not
456-
return IntentMatch('Converse', None, None, skill[0])
457-
return None
458-
459493
def send_complete_intent_failure(self, message):
460494
"""Send a message that no skill could handle the utterance.
461495

0 commit comments

Comments
 (0)