Skip to content

Commit e830da3

Browse files
committed
refactor/event_based_converse_service (#32)
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 a dedicated class like adapt/padatious/fallbacks 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 cb03cf6 commit e830da3

File tree

5 files changed

+310
-259
lines changed

5 files changed

+310
-259
lines changed

mycroft/skills/intent_service.py

Lines changed: 38 additions & 162 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,
27-
IntentMatch
26+
ConverseService, 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):
@@ -88,14 +87,13 @@ def __init__(self, bus):
8887

8988
# Dictionary for translating a skill id to a name
9089
self.skill_names = {}
91-
9290
self.adapt_service = AdaptService(config.get('context', {}))
9391
try:
9492
self.padatious_service = PadatiousService(bus, config['padatious'])
9593
except Exception as err:
96-
LOG.exception('Failed to create padatious handlers '
97-
'({})'.format(repr(err)))
94+
LOG.exception(f'Failed to create padatious handlers ({err})')
9895
self.fallback = FallbackService(bus)
96+
self.converse = ConverseService(bus)
9997

10098
self.bus.on('register_vocab', self.handle_register_vocab)
10199
self.bus.on('register_intent', self.handle_register_intent)
@@ -108,7 +106,6 @@ def __init__(self, bus):
108106
self.bus.on('clear_context', self.handle_clear_context)
109107

110108
# Converse method
111-
self.converse_config = config["skills"].get("converse") or {}
112109
self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse)
113110
self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict)
114111

@@ -119,9 +116,6 @@ def __init__(self, bus):
119116
# TODO backwards compat, deprecate
120117
self.bus.on('active_skill_request', self.handle_activate_skill_request)
121118

122-
self.active_skills = [] # [skill_id , timestamp]
123-
self._consecutive_activations = {}
124-
125119
# Intents API
126120
self.registered_vocab = []
127121
self.bus.on('intent.service.intent.get', self.handle_get_intent)
@@ -161,67 +155,18 @@ def get_skill_name(self, skill_id):
161155
return self.skill_names.get(skill_id, skill_id)
162156

163157
# converse handling
164-
def handle_activate_skill_request(self, message):
165-
166-
skill_id = message.data['skill_id']
167-
168-
# cross activation control if skills can activate each other
169-
if not self.converse_config.get("cross_activation"):
170-
# TODO imperfect solution - only a skill can activate itself
171-
# someone can forge this message and emit it raw, but in OpenVoiceOS all
172-
# skill messages should have skill_id in context, so let's make sure
173-
# this doesnt happen accidentally at very least
174-
source_skill = message.context.get("skill_id") or skill_id
175-
if skill_id != source_skill:
176-
# different skill is trying to activate this skill
177-
return
178-
179-
# mode of activation dictates under what conditions a skill is
180-
# allowed to activate itself
181-
acmode = self.converse_config.get("converse_activation") or \
182-
ConverseActivationMode.ACCEPT_ALL
183-
184-
if acmode == ConverseActivationMode.PRIORITY:
185-
prio = self.converse_config.get("converse_priorities") or {}
186-
# only allowed to activate if no skill with higher priority is
187-
# active, currently there is no api for skills to
188-
# define their default priority, this is a user/developer setting
189-
priority = prio.get(skill_id, 50)
190-
if any(p > priority for p in
191-
[prio.get(s[0], 50) for s in self.active_skills]):
192-
return
193-
194-
if acmode == ConverseActivationMode.BLACKLIST:
195-
if skill_id in self.converse_config.get("converse_blacklist", []):
196-
return
197-
198-
if acmode == ConverseActivationMode.WHITELIST:
199-
if skill_id not in self.converse_config.get("converse_whitelist", []):
200-
return
201-
202-
# limit of consecutive activations
203-
default_max = self.converse_config.get("max_activations", -1)
204-
# per skill override limit of consecutive activations
205-
skill_max = self.converse_config.get("skill_activations", {}).get(skill_id)
206-
max_activations = skill_max or default_max
207-
if skill_id not in self._consecutive_activations:
208-
self._consecutive_activations[skill_id] = 0
209-
if max_activations < 0:
210-
pass # no limit (mycroft-core default)
211-
elif max_activations == 0:
212-
return # skill activation disabled
213-
elif self._consecutive_activations.get(skill_id, 0) > max_activations:
214-
return # skill exceeded authorized consecutive number of
215-
# activations
216-
217-
# activate skill
218-
self.add_active_skill(skill_id)
219-
self._consecutive_activations[skill_id] += 1
220-
221-
# converse handling
158+
@property
159+
def active_skills(self):
160+
return self.converse.active_skills # [skill_id , timestamp]
222161

223162
def handle_activate_skill_request(self, message):
224-
self.add_active_skill(message.data['skill_id'])
163+
# TODO imperfect solution - only a skill can activate itself
164+
# someone can forge this message and emit it raw, but in OpenVoiceOS all
165+
# skill messages should have skill_id in context, so let's make sure
166+
# this doesnt happen accidentally at very least
167+
skill_id = message.data['skill_id']
168+
source_skill = message.context.get("skill_id")
169+
self.converse.activate_skill(skill_id, source_skill)
225170

226171
def handle_deactivate_skill_request(self, message):
227172
# TODO imperfect solution - only a skill can deactivate itself
@@ -230,15 +175,13 @@ def handle_deactivate_skill_request(self, message):
230175
# this doesnt happen accidentally
231176
skill_id = message.data['skill_id']
232177
source_skill = message.context.get("skill_id") or skill_id
233-
if skill_id == source_skill:
234-
self.remove_active_skill(message.data['skill_id'])
178+
self.converse.deactivate_skill(skill_id, source_skill)
235179

236180
def reset_converse(self, message):
237181
"""Let skills know there was a problem with speech recognition"""
238182
lang = _get_message_lang(message)
239183
setup_locale(lang) # restore default lang
240-
for skill in copy(self.active_skills):
241-
self.do_converse(None, skill[0], lang, message)
184+
self.converse.converse_with_skills(None, lang, message)
242185

243186
def do_converse(self, utterances, skill_id, lang, message):
244187
"""Call skill and ask if they want to process the utterance.
@@ -250,56 +193,29 @@ def do_converse(self, utterances, skill_id, lang, message):
250193
lang (str): current language
251194
message (Message): message containing interaction info.
252195
"""
253-
opmode = self.converse_config.get("converse_mode",
254-
ConverseMode.ACCEPT_ALL)
255-
256-
if opmode == ConverseMode.BLACKLIST and skill_id in \
257-
self.converse_config.get("converse_blacklist", []):
258-
return False
259-
260-
elif opmode == ConverseMode.WHITELIST and skill_id not in \
261-
self.converse_config.get("converse_whitelist", []):
262-
return False
263-
264-
converse_msg = (message.reply("skill.converse.request", {
265-
"skill_id": skill_id, "utterances": utterances, "lang": lang}))
266-
result = self.bus.wait_for_response(converse_msg,
267-
'skill.converse.response')
268-
if result and 'error' in result.data:
269-
self.handle_converse_error(result)
270-
ret = False
271-
elif result is not None:
272-
ret = result.data.get('result', False)
273-
else:
274-
ret = False
275-
return ret
196+
# NOTE: can not delete method for backwards compat with upstream
197+
LOG.warning("self.do_converse has been deprecated!\n"
198+
"use self.converse.converse instead")
199+
return self.converse.converse(utterances, skill_id, lang, message)
276200

277201
def handle_converse_error(self, message):
278202
"""Handle error in converse system.
279-
280203
Args:
281204
message (Message): info about the error.
282205
"""
283-
skill_id = message.data["skill_id"]
284-
error_msg = message.data['error']
285-
LOG.error("{}: {}".format(skill_id, error_msg))
286-
if message.data["error"] == "skill id does not exist":
287-
self.remove_active_skill(skill_id)
206+
# NOTE: can not delete method for backwards compat with upstream
207+
LOG.warning("handle_converse_error has been deprecated!")
288208

289209
def remove_active_skill(self, skill_id):
290210
"""Remove a skill from being targetable by converse.
291211
292212
Args:
293213
skill_id (str): skill to remove
294214
"""
295-
for skill in self.active_skills:
296-
if skill[0] == skill_id:
297-
self.active_skills.remove(skill)
298-
self.bus.emit(
299-
Message("intent.service.skills.deactivated",
300-
{"skill_id": skill_id}))
301-
if skill_id in self._consecutive_activations:
302-
self._consecutive_activations[skill_id] = 0
215+
# NOTE: can not delete method for backwards compat with upstream
216+
LOG.warning("self.remove_active_skill has been deprecated!\n"
217+
"use self.converse.deactivate_skill instead")
218+
self.converse.deactivate_skill(skill_id)
303219

304220
def add_active_skill(self, skill_id):
305221
"""Add a skill or update the position of an active skill.
@@ -310,21 +226,10 @@ def add_active_skill(self, skill_id):
310226
Args:
311227
skill_id (str): identifier of skill to be added.
312228
"""
313-
# search the list for an existing entry that already contains it
314-
# and remove that reference
315-
if skill_id != '':
316-
# do not call remove method to not send deactivate bus event!
317-
for skill in self.active_skills:
318-
if skill[0] == skill_id:
319-
self.active_skills.remove(skill)
320-
# add skill with timestamp to start of skill_list
321-
self.active_skills.insert(0, [skill_id, time.time()])
322-
self.bus.emit(
323-
Message("intent.service.skills.activated",
324-
{"skill_id": skill_id}))
325-
else:
326-
LOG.warning('Skill ID was empty, won\'t add to list of '
327-
'active skills.')
229+
# NOTE: can not delete method for backwards compat with upstream
230+
LOG.warning("self.add_active_skill has been deprecated!\n"
231+
"use self.converse.activate_skill instead")
232+
self.converse.activate_skill(skill_id)
328233

329234
def send_metrics(self, intent, context, stopwatch):
330235
"""Send timing metrics to the backend.
@@ -392,7 +297,7 @@ def handle_utterance(self, message):
392297
# List of functions to use to match the utterance with intent.
393298
# These are listed in priority order.
394299
match_funcs = [
395-
self._converse, padatious_matcher.match_high,
300+
self.converse.converse_with_skills, padatious_matcher.match_high,
396301
self.adapt_service.match_intent, self.fallback.high_prio,
397302
padatious_matcher.match_medium, self.fallback.medium_prio,
398303
padatious_matcher.match_low, self.fallback.low_prio
@@ -407,7 +312,7 @@ def handle_utterance(self, message):
407312
break
408313
if match:
409314
if match.skill_id:
410-
self.add_active_skill(match.skill_id)
315+
self.converse.activate_skill(match.skill_id)
411316
# If the service didn't report back the skill_id it
412317
# takes on the responsibility of making the skill "active"
413318

@@ -428,35 +333,6 @@ def handle_utterance(self, message):
428333
except Exception as err:
429334
LOG.exception(err)
430335

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

613489
def handle_get_adapt(self, message):
614490
"""handler getting the adapt response for an utterance.

mycroft/skills/intent_services/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from mycroft.skills.intent_services.fallback_service import FallbackService
55
from mycroft.skills.intent_services.padatious_service \
66
import PadatiousService, PadatiousMatcher
7+
from mycroft.skills.intent_services.converse_service import ConverseService

0 commit comments

Comments
 (0)