Skip to content
This repository was archived by the owner on Sep 8, 2024. It is now read-only.

Commit d661b8d

Browse files
JarbasAlSteve Penrod
authored andcommitted
Add conversational support to skill system
The most recently used skills now have an opportunity to preview all utterances before they hit the intent system. ==== Tech Notes ==== Skills get a preview in the order of activation -- most recent first -- and if they can consume the utterance or ignore it. If consumed, processing stops. If ignored, the next most recent skill gets a shot at it. Finally, if no skill consumes it the intent system takes over, running as it always has. Skills remain "active" for 5 minutes after last use. A skill achieves this by implementing the converse() method, e.g. def def converse(self, utterances, lang="en-us"): if .... : return True # handled, consume utterance else: return False # not for this skill, pass it along
1 parent 0dbcef7 commit d661b8d

File tree

3 files changed

+121
-18
lines changed

3 files changed

+121
-18
lines changed

mycroft/skills/core.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,10 @@ def open_intent_envelope(message):
9595
intent_dict.get('optional'))
9696

9797

98-
def load_skill(skill_descriptor, emitter):
98+
def load_skill(skill_descriptor, emitter, skill_id):
9999
try:
100-
logger.info("ATTEMPTING TO LOAD SKILL: " + skill_descriptor["name"])
100+
logger.info("ATTEMPTING TO LOAD SKILL: " + skill_descriptor["name"] +
101+
" with ID " + str(skill_id))
101102
if skill_descriptor['name'] in BLACKLISTED_SKILLS:
102103
logger.info("SKILL IS BLACKLISTED " + skill_descriptor["name"])
103104
return None
@@ -108,6 +109,7 @@ def load_skill(skill_descriptor, emitter):
108109
# v2 skills framework
109110
skill = skill_module.create_skill()
110111
skill.bind(emitter)
112+
skill.skill_id = skill_id
111113
skill._dir = dirname(skill_descriptor['info'][1])
112114
skill.load_data_files(dirname(skill_descriptor['info'][1]))
113115
# Set up intent handlers
@@ -172,12 +174,15 @@ def unload_skills(skills):
172174

173175
def intent_handler(intent_parser):
174176
""" Decorator for adding a method as an intent handler. """
177+
175178
def real_decorator(func):
176179
@wraps(func)
177180
def handler_method(*args, **kwargs):
178181
return func(*args, **kwargs)
182+
179183
_intent_list.append((intent_parser, func))
180184
return handler_method
185+
181186
return real_decorator
182187

183188

@@ -198,6 +203,7 @@ def __init__(self, name=None, emitter=None):
198203
self.log = getLogger(self.name)
199204
self.reload_skill = True
200205
self.events = []
206+
self.skill_id = 0
201207

202208
@property
203209
def location(self):
@@ -249,7 +255,7 @@ def __register_stop(self):
249255

250256
def detach(self):
251257
for (name, intent) in self.registered_intents:
252-
name = self.name + ':' + name
258+
name = str(self.skill_id) + ':' + name
253259
self.emitter.emit(Message("detach_intent", {"intent_name": name}))
254260

255261
def initialize(self):
@@ -260,6 +266,16 @@ def initialize(self):
260266
"""
261267
logger.debug("No initialize function implemented")
262268

269+
def converse(self, utterances, lang="en-us"):
270+
return False
271+
272+
def make_active(self):
273+
# bump skill to active_skill list in intent_service
274+
# this enables converse method to be called even without skill being
275+
# used in last 5 minutes
276+
self.emitter.emit(Message('active_skill_request',
277+
{"skill_id": self.skill_id}))
278+
263279
def _register_decorated(self):
264280
"""
265281
Register all intent handlers that has been decorated with an intent.
@@ -287,7 +303,7 @@ def register_intent(self, intent_parser, handler, need_self=False):
287303
raise ValueError('intent_parser is not an Intent')
288304

289305
name = intent_parser.name
290-
intent_parser.name = self.name + ':' + intent_parser.name
306+
intent_parser.name = str(self.skill_id) + ':' + intent_parser.name
291307
self.emitter.emit(Message("register_intent", intent_parser.__dict__))
292308
self.registered_intents.append((name, intent_parser))
293309

@@ -314,7 +330,7 @@ def receive_handler(message):
314330
def disable_intent(self, intent_name):
315331
"""Disable a registered intent"""
316332
logger.debug('Disabling intent ' + intent_name)
317-
name = self.name + ':' + intent_name
333+
name = str(self.skill_id) + ':' + intent_name
318334
self.emitter.emit(Message("detach_intent", {"intent_name": name}))
319335

320336
def enable_intent(self, intent_name):
@@ -342,8 +358,8 @@ def set_context(self, context, word=''):
342358
raise ValueError('context should be a string')
343359
if not isinstance(word, basestring):
344360
raise ValueError('word should be a string')
345-
self.emitter.emit(Message('add_context', {'context': context, 'word':
346-
word}))
361+
self.emitter.emit(Message('add_context',
362+
{'context': context, 'word': word}))
347363

348364
def remove_context(self, context):
349365
"""
@@ -430,7 +446,7 @@ def shutdown(self):
430446
self.emitter.remove(e, f)
431447

432448
self.emitter.emit(
433-
Message("detach_skill", {"skill_name": self.name + ":"}))
449+
Message("detach_skill", {"skill_id": self.skill_id + ":"}))
434450
try:
435451
self.stop()
436452
except:

mycroft/skills/intent_service.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from adapt.context import ContextManagerFrame
2828
import time
29+
2930
__author__ = 'seanfitz'
3031

3132
logger = getLogger(__name__)
@@ -37,6 +38,7 @@ class ContextManager(object):
3738
Use to track context throughout the course of a conversational session.
3839
How to manage a session's lifecycle is not captured here.
3940
"""
41+
4042
def __init__(self, timeout):
4143
self.frame_stack = []
4244
self.timeout = timeout * 60 # minutes to seconds
@@ -91,7 +93,7 @@ def get_context(self, max_frames=None, missing_entities=[]):
9193
relevant_frames[i].entities]
9294
for entity in frame_entities:
9395
entity['confidence'] = entity.get('confidence', 1.0) \
94-
/ (2.0 + i)
96+
/ (2.0 + i)
9597
context += frame_entities
9698

9799
result = []
@@ -138,6 +140,44 @@ def __init__(self, emitter):
138140
self.emitter.on('add_context', self.handle_add_context)
139141
self.emitter.on('remove_context', self.handle_remove_context)
140142
self.emitter.on('clear_context', self.handle_clear_context)
143+
# Converse method
144+
self.emitter.on('skill.converse.response',
145+
self.handle_converse_response)
146+
self.active_skills = [] # [skill_id , timestamp]
147+
self.converse_timeout = 5 # minutes to prune active_skills
148+
149+
def do_converse(self, utterances, skill_id, lang):
150+
self.emitter.emit(Message("skill.converse.request", {
151+
"skill_id": skill_id, "utterances": utterances, "lang": lang}))
152+
self.waiting = True
153+
self.result = False
154+
start_time = time.time()
155+
t = 0
156+
while self.waiting and t < 5:
157+
t = time.time() - start_time
158+
time.sleep(0.1)
159+
self.waiting = False
160+
return self.result
161+
162+
def handle_converse_response(self, message):
163+
# id = message.data["skill_id"]
164+
# no need to crosscheck id because waiting before new request is made
165+
# no other skill will make this request is safe assumption
166+
result = message.data["result"]
167+
self.result = result
168+
self.waiting = False
169+
170+
def remove_active_skill(self, skill_id):
171+
for skill in self.active_skills:
172+
if skill[0] == skill_id:
173+
self.active_skills.remove(skill)
174+
175+
def add_active_skill(self, skill_id):
176+
# search the list for an existing entry that already contains it
177+
# and remove that reference
178+
self.remove_active_skill(skill_id)
179+
# add skill with timestamp to start of skill_list
180+
self.active_skills.insert(0, [skill_id, time()])
141181

142182
def update_context(self, intent):
143183
for tag in intent['__tags__']:
@@ -155,14 +195,28 @@ def handle_utterance(self, message):
155195

156196
utterances = message.data.get('utterances', '')
157197

198+
# check for conversation time-out
199+
self.active_skills = [skill for skill in self.active_skills
200+
if time.time() - skill[
201+
1] <= self.converse_timeout * 60]
202+
203+
# check if any skill wants to handle utterance
204+
for skill in self.active_skills:
205+
if self.do_converse(utterances, skill[0], lang):
206+
# update timestamp, or there will be a timeout where
207+
# intent stops conversing whether its being used or not
208+
self.add_active_skill(skill[0])
209+
return
210+
211+
# no skill wants to handle utterance
158212
best_intent = None
159213
for utterance in utterances:
160214
try:
161215
# normalize() changes "it's a boy" to "it is boy", etc.
162216
best_intent = next(self.engine.determine_intent(
163-
normalize(utterance, lang), 100,
164-
include_tags=True,
165-
context_manager=self.context_manager))
217+
normalize(utterance, lang), 100,
218+
include_tags=True,
219+
context_manager=self.context_manager))
166220
# TODO - Should Adapt handle this?
167221
best_intent['utterance'] = utterance
168222
except StopIteration, e:
@@ -174,6 +228,10 @@ def handle_utterance(self, message):
174228
reply = message.reply(
175229
best_intent.get('intent_type'), best_intent)
176230
self.emitter.emit(reply)
231+
# update active skills
232+
skill_id = int(best_intent['intent_type'].split(":")[0])
233+
self.add_active_skill(skill_id)
234+
177235
else:
178236
self.emitter.emit(Message("intent_failure", {
179237
"utterance": utterances[0],
@@ -202,10 +260,10 @@ def handle_detach_intent(self, message):
202260
self.engine.intent_parsers = new_parsers
203261

204262
def handle_detach_skill(self, message):
205-
skill_name = message.data.get('skill_name')
263+
skill_id = message.data.get('skill_id')
206264
new_parsers = [
207265
p for p in self.engine.intent_parsers if
208-
not p.name.startswith(skill_name)]
266+
not p.name.startswith(skill_id)]
209267
self.engine.intent_parsers = new_parsers
210268

211269
def handle_add_context(self, message):

mycroft/skills/main.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
skills_directories = []
4848
skill_reload_thread = None
4949
skills_manager_timer = None
50+
id_counter = 0
5051

5152
installer_config = ConfigurationManager.instance().get("SkillInstallerSkill")
5253
MSM_BIN = installer_config.get("path", join(MYCROFT_ROOT_PATH, 'msm', 'msm'))
@@ -81,7 +82,7 @@ def install_default_skills(speak=True):
8182
logger.error('msm failed with error {}: {}'.format(res, output))
8283
ws.emit(Message("speak", {
8384
'utterance': mycroft.dialog.get(
84-
"sorry I couldn't install default skills")}))
85+
"sorry I couldn't install default skills")}))
8586

8687
else:
8788
logger.error("Unable to invoke Mycroft Skill Manager: " + MSM_BIN)
@@ -177,7 +178,8 @@ def _watch_skills():
177178

178179
for skill_folder in list:
179180
if skill_folder not in loaded_skills:
180-
loaded_skills[skill_folder] = {}
181+
id_counter += 1
182+
loaded_skills[skill_folder] = {"id": id_counter}
181183
skill = loaded_skills.get(skill_folder)
182184
skill["path"] = os.path.join(SKILLS_DIR, skill_folder)
183185
# checking if is a skill
@@ -202,7 +204,7 @@ def _watch_skills():
202204
del skill["instance"]
203205
skill["loaded"] = True
204206
skill["instance"] = load_skill(
205-
create_skill_descriptor(skill["path"]), ws)
207+
create_skill_descriptor(skill["path"]), ws, skill["id"])
206208
# get the last modified skill
207209
modified_dates = map(lambda x: x.get("last_modified"),
208210
loaded_skills.values())
@@ -218,6 +220,33 @@ def _starting_up():
218220
_load_skills()
219221

220222

223+
def handle_converse_request(message):
224+
skill_id = int(message.data["skill_id"])
225+
utterances = message.data["utterances"]
226+
lang = message.data["lang"]
227+
global ws, loaded_skills
228+
# loop trough skills list and call converse for skill with skill_id
229+
for skill in loaded_skills:
230+
if loaded_skills[skill]["id"] == skill_id:
231+
try:
232+
instance = loaded_skills[skill]["instance"]
233+
except:
234+
logger.error("converse requested but skill not loaded")
235+
ws.emit(Message("skill.converse.response", {
236+
"skill_id": 0, "result": False}))
237+
return
238+
try:
239+
result = instance.converse(utterances, lang)
240+
ws.emit(Message("skill.converse.response", {
241+
"skill_id": skill_id, "result": result}))
242+
return
243+
except:
244+
logger.error(
245+
"Converse method malformed for skill " + str(skill_id))
246+
ws.emit(Message("skill.converse.response", {
247+
"skill_id": 0, "result": False}))
248+
249+
221250
def main():
222251
global ws
223252
lock = Lock('skills') # prevent multiple instances of this service
@@ -245,7 +274,7 @@ def _echo(message):
245274
logger.debug(message)
246275

247276
ws.on('message', _echo)
248-
277+
ws.on('skill.converse.request', handle_converse_request)
249278
# Startup will be called after websocket is full live
250279
ws.once('open', _starting_up)
251280
ws.run_forever()

0 commit comments

Comments
 (0)