197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
|
import imp
|
||
|
import time
|
||
|
|
||
|
import abc
|
||
|
import os.path
|
||
|
import re
|
||
|
from adapt.intent import Intent
|
||
|
from os.path import join, dirname, splitext, isdir
|
||
|
|
||
|
from mycroft.client.enclosure.api import EnclosureAPI
|
||
|
from mycroft.configuration.config import ConfigurationManager
|
||
|
from mycroft.dialog import DialogLoader
|
||
|
from mycroft.filesystem import FileSystemAccess
|
||
|
from mycroft.messagebus.message import Message
|
||
|
from mycroft.util.log import getLogger
|
||
|
|
||
|
__author__ = 'seanfitz'
|
||
|
|
||
|
PRIMARY_SKILLS = ['intent', 'wake']
|
||
|
BLACKLISTED_SKILLS = ["send_sms"]
|
||
|
SKILLS_BASEDIR = dirname(__file__)
|
||
|
|
||
|
MainModule = '__init__'
|
||
|
|
||
|
logger = getLogger(__name__)
|
||
|
|
||
|
|
||
|
def load_vocab_from_file(path, vocab_type, emitter):
|
||
|
with open(path, 'r') as voc_file:
|
||
|
for line in voc_file.readlines():
|
||
|
parts = line.strip().split("|")
|
||
|
entity = parts[0]
|
||
|
|
||
|
emitter.emit(Message("register_vocab", metadata={'start': entity, 'end': vocab_type}))
|
||
|
for alias in parts[1:]:
|
||
|
emitter.emit(
|
||
|
Message("register_vocab", metadata={'start': alias, 'end': vocab_type, 'alias_of': entity}))
|
||
|
|
||
|
|
||
|
def load_vocabulary(basedir, emitter):
|
||
|
for vocab_type in os.listdir(basedir):
|
||
|
load_vocab_from_file(join(basedir, vocab_type), splitext(vocab_type)[0], emitter)
|
||
|
|
||
|
|
||
|
def create_intent_envelope(intent):
|
||
|
return Message(None, metadata=intent.__dict__, context={})
|
||
|
|
||
|
|
||
|
def open_intent_envelope(message):
|
||
|
intent_dict = message.metadata
|
||
|
return Intent(intent_dict.get('name'),
|
||
|
intent_dict.get('requires'),
|
||
|
intent_dict.get('at_least_one'),
|
||
|
intent_dict.get('optional'))
|
||
|
|
||
|
|
||
|
def load_skill(skill_descriptor, emitter):
|
||
|
try:
|
||
|
skill_module = imp.load_module(skill_descriptor["name"] + MainModule, *skill_descriptor["info"])
|
||
|
if hasattr(skill_module, 'create_skill') and callable(skill_module.create_skill): # v2 skills framework
|
||
|
skill = skill_module.create_skill()
|
||
|
skill.bind(emitter)
|
||
|
skill.initialize()
|
||
|
return skill
|
||
|
else:
|
||
|
logger.warn("Module %s does not appear to be skill" % (skill_descriptor["name"]))
|
||
|
except:
|
||
|
logger.error("Failed to load skill: " + skill_descriptor["name"], exc_info=True)
|
||
|
return None
|
||
|
|
||
|
|
||
|
def get_skills(skills_folder):
|
||
|
skills = []
|
||
|
possible_skills = os.listdir(skills_folder)
|
||
|
for i in possible_skills:
|
||
|
location = join(skills_folder, i)
|
||
|
if not isdir(location) or not MainModule + ".py" in os.listdir(location):
|
||
|
continue
|
||
|
|
||
|
skills.append(create_skill_descriptor(location))
|
||
|
skills = sorted(skills, key=lambda p: p.get('name'))
|
||
|
return skills
|
||
|
|
||
|
|
||
|
def create_skill_descriptor(skill_folder):
|
||
|
info = imp.find_module(MainModule, [skill_folder])
|
||
|
return {"name": os.path.basename(skill_folder), "info": info}
|
||
|
|
||
|
|
||
|
def load_skills(emitter, skills_root=SKILLS_BASEDIR):
|
||
|
skills = get_skills(skills_root)
|
||
|
for skill in skills:
|
||
|
if skill['name'] in PRIMARY_SKILLS:
|
||
|
load_skill(skill, emitter)
|
||
|
|
||
|
for skill in skills:
|
||
|
if skill['name'] not in PRIMARY_SKILLS and skill['name'] not in BLACKLISTED_SKILLS:
|
||
|
load_skill(skill, emitter)
|
||
|
|
||
|
|
||
|
class MycroftSkill(object):
|
||
|
"""
|
||
|
Abstract base class which provides common behaviour and parameters to all Skills implementation.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, name, emitter=None):
|
||
|
self.name = name
|
||
|
self.bind(emitter)
|
||
|
config = ConfigurationManager.get_config()
|
||
|
self.config = config.get(name)
|
||
|
self.config_core = config.get('core')
|
||
|
self.dialog_renderer = None
|
||
|
self.file_system = FileSystemAccess(join('skills', name))
|
||
|
self.registered_intents = []
|
||
|
|
||
|
@property
|
||
|
def location(self):
|
||
|
return self.config_core.get('location')
|
||
|
|
||
|
@property
|
||
|
def lang(self):
|
||
|
return self.config_core.get('lang')
|
||
|
|
||
|
def bind(self, emitter):
|
||
|
if emitter:
|
||
|
self.emitter = emitter
|
||
|
self.enclosure = EnclosureAPI(emitter)
|
||
|
self.__register_stop()
|
||
|
|
||
|
def __register_stop(self):
|
||
|
self.stop_time = time.time()
|
||
|
self.stop_threshold = self.config_core.get('stop_threshold')
|
||
|
self.emitter.on('mycroft.stop', self.__handle_stop)
|
||
|
|
||
|
def detach(self):
|
||
|
for name in self.registered_intents:
|
||
|
self.emitter.emit(Message("detach_intent", metadata={"intent_name": name}))
|
||
|
|
||
|
def initialize(self):
|
||
|
"""
|
||
|
Initialization function to be implemented by all Skills.
|
||
|
|
||
|
Usually used to create intents rules and register them.
|
||
|
"""
|
||
|
raise Exception("Initialize not implemented for skill: " + self.name)
|
||
|
|
||
|
def register_intent(self, intent_parser, handler):
|
||
|
intent_message = create_intent_envelope(intent_parser)
|
||
|
intent_message.message_type = "register_intent"
|
||
|
self.emitter.emit(intent_message)
|
||
|
self.registered_intents.append(intent_parser.name)
|
||
|
|
||
|
def receive_handler(message):
|
||
|
try:
|
||
|
handler(message)
|
||
|
except:
|
||
|
# TODO: Localize
|
||
|
self.speak("An error occurred while processing a request in " + self.name)
|
||
|
logger.error("An error occurred while processing a request in " + self.name, exc_info=True)
|
||
|
|
||
|
self.emitter.on(intent_parser.name, receive_handler)
|
||
|
|
||
|
def register_vocabulary(self, entity, entity_type):
|
||
|
self.emitter.emit(Message('register_vocab', metadata={'start': entity, 'end': entity_type}))
|
||
|
|
||
|
def register_regex(self, regex_str):
|
||
|
re.compile(regex_str) # validate regex
|
||
|
self.emitter.emit(Message('register_vocab', metadata={'regex': regex_str}))
|
||
|
|
||
|
def speak(self, utterance):
|
||
|
self.emitter.emit(Message("speak", metadata={'utterance': utterance}))
|
||
|
|
||
|
def speak_dialog(self, key, data={}):
|
||
|
self.speak(self.dialog_renderer.render(key, data))
|
||
|
|
||
|
def init_dialog(self, root_directory):
|
||
|
self.dialog_renderer = DialogLoader().load(join(root_directory, 'dialog', self.lang))
|
||
|
|
||
|
def load_data_files(self, root_directory):
|
||
|
self.init_dialog(root_directory)
|
||
|
self.load_vocab_files(join(root_directory, 'vocab', self.lang))
|
||
|
|
||
|
def load_vocab_files(self, vocab_dir):
|
||
|
load_vocabulary(vocab_dir, self.emitter)
|
||
|
|
||
|
def __handle_stop(self, event):
|
||
|
self.stop_time = time.time()
|
||
|
self.stop()
|
||
|
|
||
|
@abc.abstractmethod
|
||
|
def stop(self):
|
||
|
pass
|
||
|
|
||
|
def is_stop(self):
|
||
|
passed_time = time.time() - self.stop_time
|
||
|
return passed_time < self.stop_threshold
|