1313# limitations under the License.
1414#
1515"""Mycroft's intent service, providing intent parsing since forever!"""
16- from copy import copy
1716import time
17+ from threading import Event
1818
1919from 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
2221from mycroft .metrics import report_timing , Stopwatch
22+ from mycroft .skills .intent_service_interface import open_intent_envelope
2323from 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
3028from 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
3433def _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