24 from inspect
import signature
25 from datetime
import datetime, timedelta
28 from itertools
import chain
29 from adapt.intent
import Intent, IntentBuilder
30 from os.path
import join, abspath, dirname, basename, exists
31 from threading
import Event, Timer
33 from mycroft
import dialog
44 munge_regex, munge_intent_parser,
47 resolve_resource_file,
51 MainModule =
'__init__' 55 stack_trace = stack_trace[:-1]
57 for line
in stack_trace:
65 Dig Through the stack for message. 67 stack = inspect.stack()
69 stack = stack
if len(stack) < 10
else stack[:10]
70 local_vars = [frame[0].f_locals
for frame
in stack]
72 if 'message' in l
and isinstance(l[
'message'], Message):
77 """ Restore message keywords by removing the Letterified skill ID. 80 message (Message): Intent result message 81 skill_id (str): skill identifier 84 Message without clear keywords 86 if isinstance(message, Message)
and isinstance(message.data, dict):
88 for key
in list(message.data.keys()):
89 if key.startswith(skill_id):
91 new_key = key[len(skill_id):]
92 message.data[new_key] = message.data.pop(key)
98 """ Convert dictionary received over messagebus to Intent. """ 99 intent_dict = message.data
100 return Intent(intent_dict.get(
'name'),
101 intent_dict.get(
'requires'),
102 intent_dict.get(
'at_least_one'),
103 intent_dict.get(
'optional'))
106 def load_skill(skill_descriptor, bus, skill_id, BLACKLISTED_SKILLS=None):
107 """ Load skill from skill descriptor. 110 skill_descriptor: descriptor of skill to load 111 bus: Mycroft messagebus connection 112 skill_id: id number for skill 113 use_settings: (default True) selects if the skill should create 117 MycroftSkill: the loaded skill or None on failure 119 BLACKLISTED_SKILLS = BLACKLISTED_SKILLS
or []
120 path = skill_descriptor[
"path"]
121 name = basename(path)
122 LOG.info(
"ATTEMPTING TO LOAD SKILL: {} with ID {}".format(name, skill_id))
123 if name
in BLACKLISTED_SKILLS:
124 LOG.info(
"SKILL IS BLACKLISTED " + name)
126 main_file = join(path, MainModule +
'.py')
128 with open(main_file,
'rb')
as fp:
129 skill_module = imp.load_module(name.replace(
'.',
'_'), fp,
130 main_file, (
'.py',
'rb',
132 if (hasattr(skill_module,
'create_skill')
and 133 callable(skill_module.create_skill)):
135 skill = skill_module.create_skill()
136 skill.skill_id = skill_id
137 skill.settings.allow_overwrite =
True 138 skill.settings.load_skill_settings_from_file()
141 skill.load_data_files(path)
143 skill._register_decorated()
144 skill.register_resting_screen()
146 except Exception
as e:
148 skill.default_shutdown()
151 LOG.info(
"Loaded " + name)
153 first_run = skill.settings.get(
"__mycroft_skill_firstrun",
True)
155 LOG.info(
"First run of " + name)
156 skill.settings[
"__mycroft_skill_firstrun"] =
False 157 skill.settings.store()
158 intro = skill.get_intro_message()
163 LOG.warning(
"Module {} does not appear to be skill".format(name))
164 except FileNotFoundError
as e:
166 'Failed to load {} due to a missing file: {}'.format(name, str(e))
169 LOG.exception(
"Failed to load skill: " + name)
174 return {
"path": skill_path}
178 """ Name (including class if available) of handler function. 181 handler (function): Function to be named 184 string: handler name as string 186 if '__self__' in dir(handler)
and 'name' in dir(handler.__self__):
187 return handler.__self__.name +
'.' + handler.__name__
189 return handler.__name__
193 """ Decorator for adding a method as an intent handler. """ 195 def real_decorator(func):
198 if not hasattr(func,
'intents'):
200 func.intents.append(intent_parser)
203 return real_decorator
207 """ Decorator for adding a method as an intent file handler. """ 209 def real_decorator(func):
212 if not hasattr(func,
'intent_files'):
213 func.intent_files = []
214 func.intent_files.append(intent_file)
217 return real_decorator
222 SkillGUI - Interface to the Graphical User Interface 224 Values set in this class are synced to the GUI, accessible within QML 225 via the built-in sessionData mechanism. For example, in Python you can 227 self.gui['temp'] = 33 228 self.gui.show_page('Weather.qml') 229 Then in the Weather.qml you'd access the temp via code such as: 230 text: sessionData.time 242 """ Returns configuration value for url of remote-server. """ 243 return self.config.get(
'remote-server')
246 """ Builds a message matching the output from the enclosure. """ 247 return '{}.{}'.format(self.skill.skill_id, event)
250 """ Sets the handlers for the default messages. """ 252 self.skill.add_event(msg_type, self.
gui_set)
255 """ Register a handler for GUI events. 257 when using the triggerEvent method from Qt 258 triggerEvent("event", {"data": "cool"}) 261 event (str): event to catch 262 handler: function to handle the event 265 self.skill.add_event(msg_type, handler)
268 """ Registers a callback function to run when a value is 269 changed from the GUI. 272 callback: Function to call when a value is changed 277 for key
in message.data:
278 self[key] = message.data[key]
287 data = self.__session_data.copy()
288 data.update({
'__from': self.skill.skill_id})
289 self.skill.bus.emit(
Message(
"gui.value.set", data))
295 return self.__session_data.__contains__(key)
298 """ Reset the value dictionary, and remove namespace from GUI """ 301 self.skill.bus.emit(
Message(
"gui.clear.namespace",
302 {
"__from": self.skill.skill_id}))
305 """ Trigger a gui event. 308 event_name (str): name of event to be triggered 309 params: json serializable object containing any parameters that 310 should be sent along with the request. 312 self.skill.bus.emit(
Message(
"gui.event.send",
313 {
"__from": self.skill.skill_id,
314 "event_name": event_name,
319 Begin showing the page in the GUI 322 name (str): Name of page (e.g "mypage.qml") to display 323 override_idle (boolean, int): 324 True: Takes over the resting page indefinitely 325 (int): Delays resting page for the specified number of 330 def show_pages(self, page_names, index=0, override_idle=None):
332 Begin showing the list of pages in the GUI 335 page_names (list): List of page names (str) to display, such as 336 ["Weather.qml", "Forecast.qml", "Details.qml"] 337 index (int): Page number (0-based) to show initially. For the 338 above list a value of 1 would start on "Forecast.qml" 339 override_idle (boolean, int): 340 True: Takes over the resting page indefinitely 341 (int): Delays resting page for the specified number of 344 if not isinstance(page_names, list):
345 raise ValueError(
'page_names must be a list')
347 if index > len(page_names):
348 raise ValueError(
'Default index is larger than page list length')
350 self.
page = page_names[index]
353 data = self.__session_data.copy()
354 data.update({
'__from': self.skill.skill_id})
355 self.skill.bus.emit(
Message(
"gui.value.set", data))
359 for name
in page_names:
360 if name.startswith(
"SYSTEM"):
363 page = self.skill.find_resource(name,
'ui')
365 if self.config.get(
'remote')
is True:
366 page_urls.append(self.
remote_url +
"/" + page)
368 page_urls.append(
"file://" + page)
370 raise FileNotFoundError(
"Unable to find page: {}".format(name))
372 self.skill.bus.emit(
Message(
"gui.page.show",
375 "__from": self.skill.skill_id,
376 "__idle": override_idle}))
379 """ Remove a single page from the GUI. 382 page (str): Page to remove from the GUI 387 """ Remove a list of pages in the GUI. 390 page_names (list): List of page names (str) to display, such as 391 ["Weather.qml", "Forecast.qml", "Other.qml"] 393 if not isinstance(page_names, list):
394 raise ValueError(
'page_names must be a list')
398 for name
in page_names:
399 page = self.skill.find_resource(name,
'ui')
401 page_urls.append(
"file://" + page)
403 raise FileNotFoundError(
"Unable to find page: {}".format(name))
405 self.skill.bus.emit(
Message(
"gui.page.delete",
407 "__from": self.skill.skill_id}))
409 def show_text(self, text, title=None, override_idle=None):
410 """ Display a GUI page for viewing simple text 413 text (str): Main text content. It will auto-paginate 414 title (str): A title to display above the text content. 418 self[
"title"] = title
419 self.
show_page(
"SYSTEM_TextFrame.qml", override_idle)
422 title=
None, fill=
None,
424 """ Display a GUI page for viewing an image 427 url (str): Pointer to the image 428 caption (str): A caption to show under the image 429 title (str): A title to display above the image content 430 fill (str): Fill type supports 'PreserveAspectFit', 431 'PreserveAspectCrop', 'Stretch' 435 self[
"title"] = title
436 self[
"caption"] = caption
438 self.
show_page(
"SYSTEM_ImageFrame.qml", override_idle)
440 def show_html(self, html, resource_url=None, override_idle=None):
441 """ Display an HTML page in the GUI 444 html (str): HTML text to display 445 resource_url (str): Pointer to HTML resources 449 self[
"resourceLocation"] = resource_url
450 self.
show_page(
"SYSTEM_HtmlFrame.qml", override_idle)
453 """ Display an HTML page in the GUI 456 url (str): URL to render 460 self.
show_page(
"SYSTEM_UrlFrame.qml", override_idle)
464 """ Decorator for adding a method as an resting screen handler. 466 If selected will be shown on screen when device enters idle mode 468 name = name
or func.__self__.name
470 def real_decorator(func):
473 if not hasattr(func,
'resting_handler'):
474 func.resting_handler = name
477 return real_decorator
485 Abstract base class which provides common behaviour and parameters to all 486 Skills implementation. 489 def __init__(self, name=None, bus=None, use_settings=True):
490 self.
name = name
or self.__class__.__name__
493 self.
_dir = dirname(abspath(sys.modules[self.__module__].__file__))
527 LOG.error(
"Skill not fully initialized. Move code " +
528 "from __init__() to initialize() to correct this.")
530 raise Exception(
"Accessed MycroftSkill.enclosure in __init__")
537 LOG.error(
"Skill not fully initialized. Move code " +
538 "from __init__() to initialize() to correct this.")
540 raise Exception(
"Accessed MycroftSkill.bus in __init__")
544 """ Provide deprecation warning when accessing config. 545 TODO: Remove in 19.08 548 if (
" _register_decorated" not in stack
and 549 "register_resting_screen" not in stack):
550 LOG.warning(
'self.config is deprecated. Switch to using ' 551 'self.setting["whatever"] within your skill.')
557 """ Get the JSON data struction holding location information. """ 560 return self.config_core.get(
'location')
564 """ Get a more 'human' version of the location as a string. """ 566 if type(loc)
is dict
and loc[
"city"]:
567 return loc[
"city"][
"name"]
572 """ Get the timezone code, such as 'America/Los_Angeles' """ 574 if type(loc)
is dict
and loc[
"timezone"]:
575 return loc[
"timezone"][
"code"]
580 return self.config_core.get(
'lang')
583 """ Register messagebus emitter with skill. 586 bus: Mycroft messagebus connection 592 self.
add_event(
'mycroft.skill.enable_intent',
594 self.
add_event(
'mycroft.skill.disable_intent',
596 self.
add_event(
"mycroft.skill.set_cross_context",
598 self.
add_event(
"mycroft.skill.remove_cross_context",
600 name =
'mycroft.skills.settings.update' 601 func = self.settings.run_poll
603 self.events.append((name, func))
606 self.gui.setup_default_handlers()
610 name = str(self.
skill_id) +
':' + name
611 self.bus.emit(
Message(
"detach_intent", {
"intent_name": name}))
614 """ Perform any final setup needed for the skill. 616 Invoked after the skill is fully constructed and registered with the 622 """ Get a message to speak on first load of the skill. 624 Useful for post-install setup instructions. 627 str: message that will be spoken to the user 632 """ Handle conversation. 634 This method gets a peek at utterances before the normal intent 635 handling process after a skill has been invoked once. 637 To use, override the converse() method and return True to 638 indicate that the utterance has been handled. 641 utterances (list): The utterances from the user. If there are 642 multiple utterances, consider them all to be 643 transcription possibilities. Commonly, the 644 first entry is the user utt and the second 645 is normalized() version of the first utterance 646 lang: language the utterance is in, None for default 649 bool: True if an utterance was handled, otherwise False 654 """ Helper to get a reponse from the user 657 str: user's response or None on a timeout 661 def converse(utterances, lang=None):
662 converse.response = utterances[0]
if utterances
else None 668 converse.response =
None 673 return converse.response
675 def get_response(self, dialog='', data=None, validator=None,
676 on_fail=
None, num_retries=-1):
678 Prompt user and wait for response 680 The given dialog is spoken, followed immediately by listening 681 for a user response. The response can optionally be 682 validated before returning. 685 color = self.get_response('ask.favorite.color') 688 dialog (str): Announcement dialog to speak to the user 689 data (dict): Data used to render the dialog 690 validator (any): Function with following signature 691 def validator(utterance): 692 return utterance != "red" 693 on_fail (any): Dialog or function returning literal string 694 to speak on invalid input. For example: 695 def on_fail(utterance): 696 return "nobody likes the color red, pick another" 697 num_retries (int): Times to ask user for input, -1 for infinite 698 NOTE: User can not respond and timeout or say "cancel" to stop 701 str: User's reply or None if timed out or canceled 705 def get_announcement():
706 return self.dialog_renderer.render(dialog, data)
708 if not get_announcement():
709 raise ValueError(
'dialog message required')
711 def on_fail_default(utterance):
712 fail_data = data.copy()
713 fail_data[
'utterance'] = utterance
715 return self.dialog_renderer.render(on_fail, fail_data)
717 return get_announcement()
719 def is_cancel(utterance):
720 return self.
voc_match(utterance,
'cancel')
722 def validator_default(utterance):
724 return not is_cancel(utterance)
726 validator = validator
or validator_default
727 on_fail_fn = on_fail
if callable(on_fail)
else on_fail_default
729 self.
speak(get_announcement(), expect_response=
True, wait=
True)
736 num_none_fails = 1
if num_retries < 0
else num_retries
737 if num_fails >= num_none_fails:
740 if validator(response):
744 if is_cancel(response):
748 if 0 < num_retries < num_fails:
751 line = on_fail_fn(response)
752 self.
speak(line, expect_response=
True)
755 """ Read prompt and wait for a yes/no answer 757 This automatically deals with translation and common variants, 758 such as 'yeah', 'sure', etc. 761 prompt (str): a dialog id or string to read 763 string: 'yes', 'no' or whatever the user response if not 764 one of those, including None 776 """ Determine if the given utterance contains the vocabulary provided 778 Checks for vocabulary match in the utterance instead of the other 779 way around to allow the user to say things like "yes, please" and 780 still match against "Yes.voc" containing only "yes". The method first 781 checks in the current skill's .voc files and secondly the "res/text" 782 folder of mycroft-core. The result is cached to avoid hitting the 783 disk each time the method is called. 786 utt (str): Utterance to be tested 787 voc_filename (str): Name of vocabulary file (e.g. 'yes' for 788 'res/text/en-us/yes.voc') 789 lang (str): Language code, defaults to self.long 792 bool: True if the utterance has the given vocabulary it 794 lang = lang
or self.
lang 795 cache_key = lang + voc_filename
801 voc_filename +
'.voc'))
803 if not voc
or not exists(voc):
804 raise FileNotFoundError(
805 'Could not find {}.voc file'.format(voc_filename))
811 return any([re.match(
r'.*\b' + i +
r'\b.*', utt)
817 """ Report a skill metric to the Mycroft servers 820 name (str): Name of metric. Must use only letters and hyphens 821 data (dict): JSON dictionary to report. Must be valid JSON 826 """ Send an email to the registered user's email 829 title (str): Title of email 830 body (str): HTML body of email. This supports 831 simple HTML like bold and italics 836 """ Bump skill to active_skill list in intent_service 838 This enables converse method to be called even without skill being 839 used in last 5 minutes. 841 self.bus.emit(
Message(
'active_skill_request',
845 """ Handler for collect resting screen messages. 847 Sends info on how to trigger this skills resting page. 849 self.log.info(
'Registering resting screen')
850 self.bus.emit(
Message(
'mycroft.mark2.register_idle',
855 """ Registers resting screen from the resting_screen_handler decorator. 857 This only allows one screen and if two is registered only one 860 attributes = [a
for a
in dir(self)]
861 for attr_name
in attributes:
862 method = getattr(self, attr_name)
864 if hasattr(method,
'resting_handler'):
866 self.log.info(
'Registering resting screen {} for {}.'.format(
870 msg_type =
'{}.{}'.format(self.
skill_id,
'idle')
873 self.
add_event(
'mycroft.mark2.collect_idle',
882 """ Register all intent handlers that are decorated with an intent. 884 Looks for all functions that have been marked by a decorator 885 and read the intent data from them 887 attributes = [a
for a
in dir(self)]
888 for attr_name
in attributes:
889 method = getattr(self, attr_name)
891 if hasattr(method,
'intents'):
892 for intent
in getattr(method,
'intents'):
895 if hasattr(method,
'intent_files'):
896 for intent_file
in getattr(method,
'intent_files'):
900 """ Load a translatable single string resource 902 The string is loaded from a file in the skill's dialog subdirectory 903 'dialog/<lang>/<text>.dialog' 904 The string is randomly chosen from the file and rendered, replacing 905 mustache placeholders with values found in the data dictionary. 908 text (str): The base filename (no extension needed) 909 data (dict, optional): a JSON dictionary 912 str: A randomly chosen string from the file 914 return self.dialog_renderer.render(text, data
or {})
917 """ Find a resource file 919 Searches for the given filename using this scheme: 920 1) Search the resource lang directory: 921 <skill>/<res_dirname>/<lang>/<res_name> 922 2) Search the resource directory: 923 <skill>/<res_dirname>/<res_name> 924 3) Search the locale lang directory or other subdirectory: 925 <skill>/locale/<lang>/<res_name> or 926 <skill>/locale/<lang>/.../<res_name> 929 res_name (string): The resource name to be found 930 res_dirname (string, optional): A skill resource directory, such 931 'dialog', 'vocab', 'regex' or 'ui'. 935 string: The full path to the resource file or None if not found 939 path = join(self.
root_dir, res_dirname, self.
lang, res_name)
944 path = join(self.
root_dir, res_dirname, res_name)
950 for path, _, files
in os.walk(root_path):
951 if res_name
in files:
952 return join(path, res_name)
958 """ Load translation dict containing names and values. 960 This loads a simple CSV from the 'dialog' folders. 961 The name is the first list item, the value is the 962 second. Lines prefixed with # or // get ignored 965 name (str): name of the .value file, no extension needed 966 delim (char): delimiter character used, default is ',' 969 dict: name and value dictionary, or empty dict if load fails 973 result = collections.OrderedDict()
974 if not name.endswith(
".value"):
980 with open(filename)
as f:
981 reader = csv.reader(f, delimiter=delim)
984 if not row
or row[0].startswith(
"#"):
989 result[row[0]] = row[1]
996 """ Load a translatable template 998 The strings are loaded from a template file in the skill's dialog 1000 'dialog/<lang>/<template_name>.template' 1001 The strings are loaded and rendered, replacing mustache placeholders 1002 with values found in the data dictionary. 1005 template_name (str): The base filename (no extension needed) 1006 data (dict, optional): a JSON dictionary 1009 list of str: The loaded template file 1014 """ Load a list of translatable string resources 1016 The strings are loaded from a list file in the skill's dialog 1018 'dialog/<lang>/<list_name>.list' 1019 The strings are loaded and rendered, replacing mustache placeholders 1020 with values found in the data dictionary. 1023 list_name (str): The base filename (no extension needed) 1024 data (dict, optional): a JSON dictionary 1027 list of str: The loaded list of strings with items in consistent 1028 positions regardless of the language. 1033 """Load and render lines from dialog/<lang>/<name>""" 1036 with open(filename)
as f:
1037 text = f.read().replace(
'{{',
'{').replace(
'}}',
'}')
1038 return text.format(**data
or {}).rstrip(
'\n').split(
'\n')
1042 def add_event(self, name, handler, handler_info=None, once=False):
1043 """ Create event handler for executing intent 1046 name (string): IntentParser name 1047 handler (func): Method to call 1048 handler_info (string): Base message when reporting skill event 1049 handler status on messagebus. 1050 once (bool, optional): Event handler will be removed after it has 1054 def wrapper(message):
1062 msg_type = handler_info +
'.start' 1063 self.bus.emit(message.reply(msg_type, skill_data))
1071 if len(signature(handler).parameters) == 0:
1075 self.settings.store()
1077 except Exception
as e:
1080 msg_data = {
'skill': handler_name}
1081 msg = dialog.get(
'skill.error', self.
lang, msg_data)
1085 skill_data[
'exception'] = repr(e)
1089 msg_type = handler_info +
'.complete' 1090 self.bus.emit(message.reply(msg_type, skill_data))
1093 context = message.context
1094 if context
and 'ident' in context:
1096 {
'handler': handler.__name__})
1100 self.bus.once(name, wrapper)
1102 self.bus.on(name, wrapper)
1103 self.events.append((name, wrapper))
1106 """ Removes an event from bus emitter and events list 1109 name (string): Name of Intent or Scheduler Event 1111 bool: True if found and removed, False if not found 1114 for _name, _handler
in list(self.
events):
1117 self.events.remove((_name, _handler))
1130 self.bus.remove_all_listeners(name)
1134 """ Register an Intent with the intent service. 1137 intent_parser: Intent or IntentBuilder object to parse 1138 utterance for the handler. 1139 handler (func): function to register with intent 1141 if isinstance(intent_parser, IntentBuilder):
1142 intent_parser = intent_parser.build()
1143 elif not isinstance(intent_parser, Intent):
1144 raise ValueError(
'"' + str(intent_parser) +
'" is not an Intent')
1147 name = intent_parser.name
or handler.__name__
1149 self.bus.emit(
Message(
"register_intent", intent_parser.__dict__))
1150 self.registered_intents.append((name, intent_parser))
1151 self.
add_event(intent_parser.name, handler,
'mycroft.skill.handler')
1155 Register an Intent file with the intent service. 1158 === food.order.intent === 1160 Order some {food} from {place}. 1162 Grab some {food} from {place}. 1164 Optionally, you can also use <register_entity_file> 1165 to specify some examples of {food} and {place} 1167 In addition, instead of writing out multiple variations 1168 of the same sentence you can write: 1170 === food.order.intent === 1171 (Order | Grab) some {food} (from {place} | ). 1175 intent_file: name of file that contains example queries 1176 that should activate the intent. Must end with 1178 handler: function to register with intent 1180 name = str(self.
skill_id) +
':' + intent_file
1184 raise FileNotFoundError(
1185 'Unable to find "' + str(intent_file) +
'"' 1189 "file_name": filename,
1192 self.bus.emit(
Message(
"padatious:register_intent", data))
1193 self.registered_intents.append((intent_file, data))
1194 self.
add_event(name, handler,
'mycroft.skill.handler')
1197 """ Register an Entity file with the intent service. 1199 An Entity file lists the exact values that an entity can hold. 1202 === ask.day.intent === 1205 === weekend.entity === 1210 entity_file (string): name of file that contains examples of an 1211 entity. Must end with '.entity' 1213 if entity_file.endswith(
'.entity'):
1214 entity_file = entity_file.replace(
'.entity',
'')
1216 filename = self.
find_resource(entity_file +
".entity",
'vocab')
1218 raise FileNotFoundError(
1219 'Unable to find "' + entity_file +
'.entity"' 1221 name = str(self.
skill_id) +
':' + entity_file
1223 self.bus.emit(
Message(
"padatious:register_entity", {
1224 "file_name": filename,
1230 Listener to enable a registered intent if it belongs to this skill 1232 intent_name = message.data[
"intent_name"]
1234 if name == intent_name:
1239 Listener to disable a registered intent if it belongs to this skill 1241 intent_name = message.data[
"intent_name"]
1243 if name == intent_name:
1248 Disable a registered intent if it belongs to this skill 1251 intent_name (string): name of the intent to be disabled 1254 bool: True if disabled, False if it wasn't registered 1257 if intent_name
in names:
1258 LOG.debug(
'Disabling intent ' + intent_name)
1259 name = str(self.
skill_id) +
':' + intent_name
1260 self.bus.emit(
Message(
"detach_intent", {
"intent_name": name}))
1263 LOG.error(
'Could not disable ' + intent_name +
1264 ', it hasn\'t been registered.')
1269 (Re)Enable a registered intent if it belongs to this skill 1272 intent_name: name of the intent to be enabled 1275 bool: True if enabled, False if it wasn't registered 1279 if intent_name
in names:
1280 intent = intents[names.index(intent_name)]
1281 self.registered_intents.remove((intent_name, intent))
1282 if ".intent" in intent_name:
1285 intent.name = intent_name
1287 LOG.debug(
'Enabling intent ' + intent_name)
1290 LOG.error(
'Could not enable ' + intent_name +
', it hasn\'t been ' 1296 Add context to intent service 1300 word: word connected to keyword 1302 if not isinstance(context, str):
1303 raise ValueError(
'context should be a string')
1304 if not isinstance(word, str):
1305 raise ValueError(
'word should be a string')
1307 origin = origin
or '' 1309 self.bus.emit(
Message(
'add_context',
1310 {
'context': context,
'word': word,
1315 Add global context to intent service 1318 context = message.data.get(
"context")
1319 word = message.data.get(
"word")
1320 origin = message.data.get(
"origin")
1326 Remove global context from intent service 1329 context = message.data.get(
"context")
1334 Tell all skills to add a context to intent service 1338 word: word connected to keyword 1340 self.bus.emit(
Message(
"mycroft.skill.set_cross_context",
1341 {
"context": context,
"word": word,
1346 tell all skills to remove a keyword from the context manager. 1348 if not isinstance(context, str):
1349 raise ValueError(
'context should be a string')
1350 self.bus.emit(
Message(
"mycroft.skill.remove_cross_context",
1351 {
"context": context}))
1355 remove a keyword from the context manager. 1357 if not isinstance(context, str):
1358 raise ValueError(
'context should be a string')
1360 self.bus.emit(
Message(
'remove_context', {
'context': context}))
1363 """ Register a word to a keyword 1366 entity: word to register 1367 entity_type: Intent handler entity to tie the word to 1369 self.bus.emit(
Message(
'register_vocab', {
1374 """ Register a new regex. 1376 regex_str: Regex string 1380 self.bus.emit(
Message(
'register_vocab', {
'regex': regex}))
1382 def speak(self, utterance, expect_response=False, wait=False):
1383 """ Speak a sentence. 1386 utterance (str): sentence mycroft should speak 1387 expect_response (bool): set to True if Mycroft should listen 1388 for a response immediately after 1389 speaking the utterance. 1390 wait (bool): set to True to block while the text 1394 self.enclosure.register(self.
name)
1395 data = {
'utterance': utterance,
1396 'expect_response': expect_response}
1399 self.bus.emit(message.reply(
"speak", data))
1401 self.bus.emit(
Message(
"speak", data))
1406 """ Speak a random sentence from a dialog file. 1409 key (str): dialog file key (e.g. "hello" to speak from the file 1410 "locale/en-us/hello.dialog") 1411 data (dict): information used to populate sentence 1412 expect_response (bool): set to True if Mycroft should listen 1413 for a response immediately after 1414 speaking the utterance. 1415 wait (bool): set to True to block while the text 1419 self.
speak(self.dialog_renderer.render(key, data),
1420 expect_response, wait)
1425 dialog_dir = join(root_directory,
'dialog', self.
lang)
1426 if exists(dialog_dir):
1428 elif exists(join(root_directory,
'locale', self.
lang)):
1429 locale_path = join(root_directory,
'locale', self.
lang)
1432 LOG.debug(
'No dialog loaded')
1441 vocab_dir = join(root_directory,
'vocab', self.
lang)
1442 if exists(vocab_dir):
1444 elif exists(join(root_directory,
'locale', self.
lang)):
1448 LOG.debug(
'No vocab loaded')
1451 regex_dir = join(root_directory,
'regex', self.
lang)
1452 if exists(regex_dir):
1454 elif exists(join(root_directory,
'locale', self.
lang)):
1460 Handler for the "mycroft.stop" signal. Runs the user defined 1464 def __stop_timeout():
1466 self.bus.emit(
Message(
"mycroft.stop.handled",
1467 {
"skill_id": str(self.
skill_id) +
":"}))
1469 timer = Timer(0.1, __stop_timeout)
1472 self.bus.emit(
Message(
"mycroft.stop.handled",
1473 {
"by":
"skill:"+str(self.
skill_id)}))
1477 LOG.error(
"Failed to stop skill: {}".format(self.
name),
1485 This method is intended to be called during the skill 1486 process termination. The skill implementation must 1487 shutdown all processes and operations in execution. 1492 """Parent function called internally to shut down everything. 1494 Shuts down known entities and calls skill specific shutdown method. 1498 except Exception
as e:
1499 LOG.error(
'Skill specific shutdown function encountered ' 1500 'an error: {}'.format(repr(e)))
1502 if exists(self._dir):
1503 self.settings.store()
1504 self.settings.stop_polling()
1510 self.cancel_all_repeating_events()
1511 for e, f
in self.events:
1512 self.bus.remove(e, f)
1516 Message(
"detach_skill", {
"skill_id": str(self.skill_id) +
":"}))
1520 LOG.error(
"Failed to stop skill: {}".format(self.name),
1525 Return a name unique to this skill using the format 1529 name: Name to use internally 1532 str: name unique to this skill 1534 return str(self.
skill_id) +
':' + (name
or '')
1539 Underlying method for schedule_event and schedule_repeating_event. 1540 Takes scheduling information and sends it off on the message bus. 1543 name = self.
name + handler.__name__
1546 self.scheduled_repeats.append(name)
1549 self.
add_event(unique_name, handler, once=
not repeat)
1551 event_data[
'time'] = time.mktime(when.timetuple())
1552 event_data[
'event'] = unique_name
1553 event_data[
'repeat'] = repeat
1554 event_data[
'data'] = data
1555 self.bus.emit(
Message(
'mycroft.scheduler.schedule_event',
1560 Schedule a single-shot event. 1563 handler: method to be called 1564 when (datetime/int/float): datetime (in system timezone) or 1565 number of seconds in the future when the 1566 handler should be called 1567 data (dict, optional): data to send when the handler is called 1568 name (str, optional): reference name 1569 NOTE: This will not warn or replace a 1570 previously scheduled event of the same 1574 if isinstance(when, (int, float)):
1575 when = datetime.now() + timedelta(seconds=when)
1579 data=
None, name=
None):
1581 Schedule a repeating event. 1584 handler: method to be called 1585 when (datetime): time (in system timezone) for first 1586 calling the handler, or None to 1587 initially trigger <frequency> seconds 1589 frequency (float/int): time in seconds between calls 1590 data (dict, optional): data to send when the handler is called 1591 name (str, optional): reference name, must be unique 1597 when = datetime.now() + timedelta(seconds=frequency)
1600 LOG.debug(
'The event is already scheduled, cancel previous ' 1601 'event if this scheduling should replace the last.')
1605 Change data of event. 1608 name (str): reference name of event (from original scheduling) 1615 self.bus.emit(
Message(
'mycroft.schedule.update_event', data=data))
1619 Cancel a pending event. The event will no longer be scheduled 1623 name (str): reference name of event (from original scheduling) 1626 data = {
'event': unique_name}
1628 self.scheduled_repeats.remove(name)
1630 self.bus.emit(
Message(
'mycroft.scheduler.remove_event',
1635 Get scheduled event data and return the amount of time left 1638 name (str): reference name of event (from original scheduling) 1641 int: the time left in seconds 1644 Exception: Raised if event is not found 1647 data = {
'name': event_name}
1651 finished_callback =
False 1653 def callback(message):
1654 nonlocal event_status
1655 nonlocal finished_callback
1656 if message.data
is not None:
1657 event_time = int(message.data[0][0])
1658 current_time = int(time.time())
1659 time_left_in_seconds = event_time - current_time
1660 event_status = time_left_in_seconds
1661 finished_callback =
True 1663 emitter_name =
'mycroft.event_status.callback.{}'.format(event_name)
1664 self.bus.once(emitter_name, callback)
1665 self.bus.emit(
Message(
'mycroft.scheduler.get_event', data=data))
1667 start_wait = time.time()
1668 while finished_callback
is False and time.time() - start_wait < 3.0:
1670 if time.time() - start_wait > 3.0:
1671 raise Exception(
"Event Status Messagebus Timeout")
1675 """ Cancel any repeating events started by the skill. """ 1682 """ Acknowledge a successful request. 1684 This method plays a sound to acknowledge a request that does not 1685 require a verbal response. This is intended to provide simple feedback 1686 to the user that their request was handled successfully. 1689 self.config_core.get(
'sounds').
get(
'acknowledge'))
1692 LOG.warning(
"Could not find 'acknowledge' audio file!")
1697 LOG.warning(
"Unable to play 'acknowledge' audio file!")
1705 Fallbacks come into play when no skill matches an Adapt or closely with 1706 a Padatious intent. All Fallback skills work together to give them a 1707 view of the user's utterance. Fallback handlers are called in an order 1708 determined the priority provided when the the handler is registered. 1710 ======== ======== ================================================ 1711 Priority Who? Purpose 1712 ======== ======== ================================================ 1713 1-4 RESERVED Unused for now, slot for pre-Padatious if needed 1714 5 MYCROFT Padatious near match (conf > 0.8) 1716 89 MYCROFT Padatious loose match (conf > 0.5) 1717 90-99 USER Uncaught intents 1718 100+ MYCROFT Fallback Unknown or other future use 1719 ======== ======== ================================================ 1721 Handlers with the numerically lowest priority are invoked first. 1722 Multiple fallbacks can exist at the same priority, but no order is 1725 A Fallback can either observe or consume an utterance. A consumed 1726 utterance will not be see by any other Fallback handlers. 1728 fallback_handlers = {}
1730 def __init__(self, name=None, bus=None, use_settings=True):
1731 MycroftSkill.__init__(self, name, bus, use_settings)
1738 """Goes through all fallback handlers until one returns True""" 1740 def handler(message):
1742 bus.emit(message.reply(
"mycroft.skill.handler.start",
1743 data={
'handler':
"fallback"}))
1748 for _, handler
in sorted(cls.fallback_handlers.items(),
1749 key=operator.itemgetter(0)):
1751 if handler(message):
1754 bus.emit(message.reply(
1755 'mycroft.skill.handler.complete',
1756 data={
'handler':
"fallback",
1757 "fallback_handler": handler_name}))
1760 LOG.exception(
'Exception in fallback.')
1762 bus.emit(message.reply(
'complete_intent_failure'))
1763 warning =
"No fallback could handle intent." 1764 LOG.warning(warning)
1766 bus.emit(message.reply(
'mycroft.skill.handler.complete',
1767 data={
'handler':
"fallback",
1768 'exception': warning}))
1771 if message.context.get(
'ident'):
1772 ident = message.context[
'ident']
1774 {
'handler': handler_name})
1781 Register a function to be called as a general info fallback 1782 Fallback should receive message and return 1783 a boolean (True if succeeded or False if failed) 1785 Lower priority gets run first 1786 0 for high priority 100 for low priority 1795 register a fallback with the list of fallback handlers 1796 and with the list of handlers registered by this instance 1799 def wrapper(*args, **kwargs):
1800 if handler(*args, **kwargs):
1805 self.instance_fallback_handlers.append(wrapper)
1811 Remove a fallback handler 1814 handler_to_del: reference to handler 1816 for priority, handler
in cls.fallback_handlers.items():
1817 if handler == handler_to_del:
1820 LOG.warning(
'Could not remove fallback!')
1824 Remove all fallback handlers registered by the fallback skill. 1827 handler = self.instance_fallback_handlers.pop()
1832 Remove all registered handlers and perform skill shutdown. def register_handler(self, event, handler)
def load_regex(basedir, bus, skill_id)
def report_timing(ident, system, timing, additional_data=None)
def remove_pages(self, page_names)
def register_fallback(self, handler, priority)
def handle_disable_intent(self, message)
def read_vocab_file(path)
instance_fallback_handlers
def resolve_resource_file(res_name)
def default_shutdown(self)
def disable_intent(self, intent_name)
def location_timezone(self)
def translate_list(self, list_name, data=None)
def load_vocabulary(basedir, bus, skill_id)
def schedule_event(self, handler, when, data=None, name=None)
def register_entity_file(self, entity_file)
def set_context(self, context, word='', origin=None)
def __handle_stop(self, event)
def show_text(self, text, title=None, override_idle=None)
def show_page(self, name, override_idle=None)
def set_cross_skill_context(self, context, word='')
def send_event(self, event_name, params={})
def get_handler_name(handler)
def munge_intent_parser(intent_parser, name, skill_id)
def __contains__(self, key)
def init_dialog(self, root_directory)
def wait_while_speaking()
def open_intent_envelope(message)
def resting_screen_handler(name=None)
def __setitem__(self, key, value)
def location_pretty(self)
def handle_enable_intent(self, message)
def remove_fallback(cls, handler_to_del)
def get_scheduled_event_status(self, name)
def load_skill(skill_descriptor, bus, skill_id, BLACKLISTED_SKILLS=None)
def set_on_gui_changed(self, callback)
def get_intro_message(self)
def register_intent(self, intent_parser, handler)
def _register_decorated(self)
def unmunge_message(message, skill_id)
def __init__(self, name=None, bus=None, use_settings=True)
def remove_instance_handlers(self)
def handle_remove_cross_context(self, message)
def load_data_files(self, root_directory)
def default_shutdown(self)
def register_resting_screen(self)
def remove_context(self, context)
def __init__(self, name=None, bus=None, use_settings=True)
def remove_cross_skill_context(self, context)
def ask_yesno(self, prompt, data=None)
def register_vocabulary(self, entity, entity_type)
def gui_set(self, message)
def speak(self, utterance, expect_response=False, wait=False)
def build_message_type(self, event)
def setup_default_handlers(self)
def translate(self, text, data=None)
def load_regex_files(self, root_directory)
def handle_set_cross_context(self, message)
def speak_dialog(self, key, data=None, expect_response=False, wait=False)
def send_email(self, title, body)
def munge_regex(regex, skill_id)
def voc_match(self, utt, voc_filename, lang=None)
def register_regex(self, regex_str)
def __translate_file(self, name, data)
def remove_page(self, page)
def translate_template(self, template_name, data=None)
def get_response(self, dialog='', data=None, validator=None, on_fail=None, num_retries=-1)
def _schedule_event(self, handler, when, data=None, name=None, repeat=None)
def simple_trace(stack_trace)
def _register_fallback(cls, handler, priority)
def add_event(self, name, handler, handler_info=None, once=False)
def _unique_name(self, name)
def remove_event(self, name)
def _handle_collect_resting(self, message=None)
def __getitem__(self, key)
def show_pages(self, page_names, index=0, override_idle=None)
def create_skill_descriptor(skill_path)
def __init__(self, skill)
def find_resource(self, res_name, res_dirname=None)
def enable_intent(self, intent_name)
def intent_file_handler(intent_file)
def cancel_scheduled_event(self, name)
def load_vocab_files(self, root_directory)
def report_metric(self, name, data)
def register_intent_file(self, intent_file, handler)
def intent_handler(intent_parser)
dictionary fallback_handlers
def translate_namedvalues(self, name, delim=None)
def schedule_repeating_event(self, handler, when, frequency, data=None, name=None)
def show_html(self, html, resource_url=None, override_idle=None)
def get(phrase, lang=None, context=None)
def update_scheduled_event(self, name, data=None)
def make_intent_failure_handler(cls, bus)
def show_url(self, url, override_idle=None)
def show_image(self, url, caption=None, title=None, fill=None, override_idle=None)
def cancel_all_repeating_events(self)