Skip to content

Commit 184068a

Browse files
committed
refactor/event_based_converse_service
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 12e62a0 commit 184068a

File tree

5 files changed

+322
-267
lines changed

5 files changed

+322
-267
lines changed

mycroft/skills/intent_service.py

Lines changed: 39 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):
@@ -81,20 +80,20 @@ 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'])
9493
except Exception as err:
95-
LOG.exception('Failed to create padatious handlers '
96-
'({})'.format(repr(err)))
94+
LOG.exception(f'Failed to create padatious handlers ({err})')
9795
self.fallback = FallbackService(bus)
96+
self.converse = ConverseService(bus)
9897

9998
self.bus.on('register_vocab', self.handle_register_vocab)
10099
self.bus.on('register_intent', self.handle_register_intent)
@@ -107,7 +106,6 @@ def __init__(self, bus):
107106
self.bus.on('clear_context', self.handle_clear_context)
108107

109108
# Converse method
110-
self.converse_config = config["skills"].get("converse") or {}
111109
self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse)
112110
self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict)
113111

@@ -118,9 +116,6 @@ def __init__(self, bus):
118116
# TODO backwards compat, deprecate
119117
self.bus.on('active_skill_request', self.handle_activate_skill_request)
120118

121-
self.active_skills = [] # [skill_id , timestamp]
122-
self._consecutive_activations = {}
123-
124119
# Intents API
125120
self.registered_vocab = []
126121
self.bus.on('intent.service.intent.get', self.handle_get_intent)
@@ -160,67 +155,18 @@ def get_skill_name(self, skill_id):
160155
return self.skill_names.get(skill_id, skill_id)
161156

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

222162
def handle_activate_skill_request(self, message):
223-
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)
224170

225171
def handle_deactivate_skill_request(self, message):
226172
# TODO imperfect solution - only a skill can deactivate itself
@@ -229,15 +175,13 @@ def handle_deactivate_skill_request(self, message):
229175
# this doesnt happen accidentally
230176
skill_id = message.data['skill_id']
231177
source_skill = message.context.get("skill_id") or skill_id
232-
if skill_id == source_skill:
233-
self.remove_active_skill(message.data['skill_id'])
178+
self.converse.deactivate_skill(skill_id, source_skill)
234179

235180
def reset_converse(self, message):
236181
"""Let skills know there was a problem with speech recognition"""
237182
lang = _get_message_lang(message)
238183
setup_locale(lang) # restore default lang
239-
for skill in copy(self.active_skills):
240-
self.do_converse(None, skill[0], lang, message)
184+
self.converse.converse_with_skills(None, lang, message)
241185

242186
def do_converse(self, utterances, skill_id, lang, message):
243187
"""Call skill and ask if they want to process the utterance.
@@ -249,56 +193,29 @@ def do_converse(self, utterances, skill_id, lang, message):
249193
lang (str): current language
250194
message (Message): message containing interaction info.
251195
"""
252-
opmode = self.converse_config.get("converse_mode",
253-
ConverseMode.ACCEPT_ALL)
254-
255-
if opmode == ConverseMode.BLACKLIST and skill_id in \
256-
self.converse_config.get("converse_blacklist", []):
257-
return False
258-
259-
elif opmode == ConverseMode.WHITELIST and skill_id not in \
260-
self.converse_config.get("converse_whitelist", []):
261-
return False
262-
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
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)
275200

276201
def handle_converse_error(self, message):
277202
"""Handle error in converse system.
278-
279203
Args:
280204
message (Message): info about the error.
281205
"""
282-
skill_id = message.data["skill_id"]
283-
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)
206+
# NOTE: can not delete method for backwards compat with upstream
207+
LOG.warning("handle_converse_error has been deprecated!")
287208

288209
def remove_active_skill(self, skill_id):
289210
"""Remove a skill from being targetable by converse.
290211
291212
Args:
292213
skill_id (str): skill to remove
293214
"""
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
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)
302219

303220
def add_active_skill(self, skill_id):
304221
"""Add a skill or update the position of an active skill.
@@ -309,21 +226,10 @@ def add_active_skill(self, skill_id):
309226
Args:
310227
skill_id (str): identifier of skill to be added.
311228
"""
312-
# search the list for an existing entry that already contains it
313-
# and remove that reference
314-
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)
319-
# add skill with timestamp to start of skill_list
320-
self.active_skills.insert(0, [skill_id, time.time()])
321-
self.bus.emit(
322-
Message("intent.service.skills.activated",
323-
{"skill_id": skill_id}))
324-
else:
325-
LOG.warning('Skill ID was empty, won\'t add to list of '
326-
'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)
327233

328234
def send_metrics(self, intent, context, stopwatch):
329235
"""Send timing metrics to the backend.
@@ -391,7 +297,7 @@ def handle_utterance(self, message):
391297
# List of functions to use to match the utterance with intent.
392298
# These are listed in priority order.
393299
match_funcs = [
394-
self._converse, padatious_matcher.match_high,
300+
self.converse.converse_with_skills, padatious_matcher.match_high,
395301
self.adapt_service.match_intent, self.fallback.high_prio,
396302
padatious_matcher.match_medium, self.fallback.medium_prio,
397303
padatious_matcher.match_low, self.fallback.low_prio
@@ -406,7 +312,7 @@ def handle_utterance(self, message):
406312
break
407313
if match:
408314
if match.skill_id:
409-
self.add_active_skill(match.skill_id)
315+
self.converse.activate_skill(match.skill_id)
410316
# If the service didn't report back the skill_id it
411317
# takes on the responsibility of making the skill "active"
412318

@@ -427,35 +333,6 @@ def handle_utterance(self, message):
427333
except Exception as err:
428334
LOG.exception(err)
429335

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-
459336
def send_complete_intent_failure(self, message):
460337
"""Send a message that no skill could handle the utterance.
461338
@@ -607,7 +484,7 @@ def handle_get_active_skills(self, message):
607484
message: query message to reply to.
608485
"""
609486
self.bus.emit(message.reply("intent.service.active_skills.reply",
610-
{"skills": self.active_skills}))
487+
{"skills": self.converse.active_skills}))
611488

612489
def handle_get_adapt(self, message):
613490
"""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)