def register_alarm_intent(engine: IntentDeterminationEngine): alarm_keywords = [ "alarm" ] for ak in alarm_keywords: engine.register_entity(ak, "AlarmKeyword") engine.register_regex_entity("(for|at) (?P<Time>.*)") weekdays = [ "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday" ] for w in weekdays: engine.register_entity(w, "Weekday") # structure intent alarm_intent = IntentBuilder("AlarmIntent")\ .require("AlarmKeyword")\ .require("Time")\ .optionally("Weekday")\ .build() engine.register_intent_parser(alarm_intent)
class MusicPlayerIntentParser(): def __init__(self): self.engine = IntentDeterminationEngine() # define music vocabulary music_verbs = ["listen", "hear", "play", "stop"] for mv in music_verbs: self.engine.register_entity(mv, "MusicVerb") music_keywords = ["songs", "music"] for mk in music_keywords: self.engine.register_entity(mk, "MusicKeyword") self.engine.register_regex_entity( "(play|hear|listen|listen to)\s*(the)?\s*(song|album)?\s*(?P<Media>.*)$" # NoQA ) music_intent = IntentBuilder("MusicIntent")\ .require("MusicVerb")\ .optionally("MusicKeyword")\ .optionally("Media")\ .build() self.engine.register_intent_parser(music_intent) def parse(self, sentence): for intent in self.engine.determine_intent(sentence): if intent.get('confidence') > 0: return intent
class IntentEngineTests(unittest.TestCase): def setUp(self): self.engine = IntentDeterminationEngine() def testRegisterIntentParser(self): assert len(self.engine.intent_parsers) == 0 try: self.engine.register_intent_parser("NOTAPARSER") assert "Did not fail to register invalid intent parser" and False except ValueError as e: pass parser = IntentBuilder("Intent").build() self.engine.register_intent_parser(parser) assert len(self.engine.intent_parsers) == 1 def testRegisterRegexEntity(self): assert len(self.engine._regex_strings) == 0 assert len(self.engine.regular_expressions_entities) == 0 self.engine.register_regex_entity(".*") assert len(self.engine._regex_strings) == 1 assert len(self.engine.regular_expressions_entities) == 1 def testSelectBestIntent(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_entity("tree", "Entity1") self.engine.register_intent_parser(parser1) utterance = "go to the tree house" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' parser2 = IntentBuilder("Parser2").require("Entity1").require( "Entity2").build() self.engine.register_entity("house", "Entity2") self.engine.register_intent_parser(parser2) intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser2' def testIntentMissingEntity(self): utterance1 = "give me One home" utterance2 = "give me One or Two" parser3 = IntentBuilder("Parser3").require("One").require( "Two").build() self.engine.register_entity("One", "One") self.engine.register_entity("Two", "Two") self.engine.register_intent_parser(parser3) intent2 = self.engine.determine_intent(utterance2) try: intent2 = next(intent2) except BaseException: pass intent1 = self.engine.determine_intent(utterance1) try: intent1 = next(intent1) except BaseException: pass
def register_timer_intent(engine: IntentDeterminationEngine): timer_keywords = ["timer"] for tk in timer_keywords: engine.register_entity(tk, "TimerKeyword") engine.register_regex_entity("for (?P<Time>.*)") # structure intent timer_intent = IntentBuilder("TimerIntent")\ .require("TimerKeyword")\ .optionally("Time")\ .build() engine.register_intent_parser(timer_intent)
def register_todo_intent(engine: IntentDeterminationEngine): commands = ["add", "remove", "get", "tell me", "clear"] for c in commands: engine.register_entity(c, "TodoCommand") engine.register_regex_entity("(add|remove) (?P<Item>.*) (to|from) my") engine.register_regex_entity("my (?P<ListType>.*) list") todo_intent = IntentBuilder("TodoIntent")\ .require("TodoCommand")\ .optionally("Item")\ .require("ListType")\ .build() engine.register_intent_parser(todo_intent)
def register_reminder_intent(engine: IntentDeterminationEngine): reminder_keywords = ["remind"] for rk in reminder_keywords: engine.register_entity(rk, "ReminderKeyword") engine.register_regex_entity("to (?P<Action>.*) in") engine.register_regex_entity("in (?P<Time>.*)") # structure intent reminder_intent = IntentBuilder("ReminderIntent")\ .require("ReminderKeyword")\ .require("Action") \ .require("Time") \ .build() engine.register_intent_parser(reminder_intent)
def register_weather_intent(engine: IntentDeterminationEngine): # create and register weather vocabulary weather_keyword = ["weather"] for wk in weather_keyword: engine.register_entity(wk, "WeatherKeyword") # create regex to parse out locations engine.register_regex_entity("in (?P<Location>.*)") # structure intent weather_intent = IntentBuilder("WeatherIntent")\ .require("WeatherKeyword")\ .optionally("Location")\ .build() engine.register_intent_parser(weather_intent)
def register_what_is_intent(engine: IntentDeterminationEngine): # create and register what is vocabulary what_is_keywords = ["what is", "tell me"] for wk in what_is_keywords: engine.register_entity(wk, "WhatIsKeyword") # create regex to parse out subjects engine.register_regex_entity("is (?P<Subject>.*)") engine.register_regex_entity("about (?P<Subject>.*)") # structure intent what_is_intent = IntentBuilder("WhatIsIntent")\ .require("WhatIsKeyword")\ .require("Subject")\ .build() engine.register_intent_parser(what_is_intent)
def register_news_intent(engine: IntentDeterminationEngine): # create and register news vocabulary news_keyword = ["news"] for nk in news_keyword: engine.register_entity(nk, "NewsKeyword") # create regex to parse out topics engine.register_regex_entity("about (?P<Topic>.*)") engine.register_regex_entity("for (?P<Topic>.*)") # structure intent news_intent = IntentBuilder("NewsIntent")\ .require("NewsKeyword")\ .optionally("Topic")\ .build() engine.register_intent_parser(news_intent)
def put_in_engin(text): engine = IntentDeterminationEngine() # Register entities on engine for entity, keywords in entities.items(): for keyword in keywords: engine.register_entity(keyword, entity) for entity in multi_regex_entities: for regex in entity: engine.register_regex_entity(regex) # Register intents on engine for intent in intents: engine.register_intent_parser(intent) for intent in engine.determine_intent(text): # return dict[intenct] # will return function print(intent) print('finished')
class IntentEngineTests(unittest.TestCase): def setUp(self): self.engine = IntentDeterminationEngine() def testRegisterIntentParser(self): assert len(self.engine.intent_parsers) == 0 try: self.engine.register_intent_parser("NOTAPARSER") assert "Did not fail to register invalid intent parser" and False except ValueError as e: pass parser = IntentBuilder("Intent").build() self.engine.register_intent_parser(parser) assert len(self.engine.intent_parsers) == 1 def testRegisterRegexEntity(self): assert len(self.engine._regex_strings) == 0 assert len(self.engine.regular_expressions_entities) == 0 self.engine.register_regex_entity(".*") assert len(self.engine._regex_strings) == 1 assert len(self.engine.regular_expressions_entities) == 1 def testSelectBestIntent(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_intent_parser(parser1) self.engine.register_entity("tree", "Entity1") utterance = "go to the tree house" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' parser2 = IntentBuilder("Parser2").require("Entity1").require("Entity2").build() self.engine.register_intent_parser(parser2) self.engine.register_entity("house", "Entity2") intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser2'
class WeatherIntentParser(): def __init__(self): self.engine = IntentDeterminationEngine() weather_verbs = ["weather", "temperature", "forecast"] for mv in weather_verbs: self.engine.register_entity(mv, "WeatherVerb") self.engine.register_regex_entity( "in\s*(?P<Location>[A-Z][^\s]*\s*?)+.*$" # NoQA ) weather_intent = IntentBuilder("WeatherIntent")\ .require("WeatherVerb")\ .require("Location")\ .build() self.engine.register_intent_parser(weather_intent) def parse(self, sentence): for intent in self.engine.determine_intent(sentence): if intent.get('confidence') > 0: return intent
class IntentService(object): def __init__(self, emitter): self.config = Configuration.get().get('context', {}) self.engine = IntentDeterminationEngine() self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.emitter = emitter self.emitter.on('register_vocab', self.handle_register_vocab) self.emitter.on('register_intent', self.handle_register_intent) self.emitter.on('recognizer_loop:utterance', self.handle_utterance) self.emitter.on('detach_intent', self.handle_detach_intent) self.emitter.on('detach_skill', self.handle_detach_skill) # Context related handlers self.emitter.on('add_context', self.handle_add_context) self.emitter.on('remove_context', self.handle_remove_context) self.emitter.on('clear_context', self.handle_clear_context) # Converse method self.emitter.on('skill.converse.response', self.handle_converse_response) self.emitter.on('mycroft.speech.recognition.unknown', self.reset_converse) def add_active_skill_handler(message): self.add_active_skill(message.data['skill_id']) self.emitter.on('active_skill_request', add_active_skill_handler) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills def reset_converse(self, message): """Let skills know there was a problem with speech recognition""" lang = message.data.get('lang', "en-us") for skill in self.active_skills: self.do_converse(None, skill[0], lang) def do_converse(self, utterances, skill_id, lang): self.emitter.emit(Message("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang})) self.waiting = True self.result = False start_time = time.time() t = 0 while self.waiting and t < 5: t = time.time() - start_time time.sleep(0.1) self.waiting = False return self.result def handle_converse_response(self, message): # id = message.data["skill_id"] # no need to crosscheck id because waiting before new request is made # no other skill will make this request is safe assumption result = message.data["result"] self.result = result self.waiting = False def remove_active_skill(self, skill_id): for skill in self.active_skills: if skill[0] == skill_id: self.active_skills.remove(skill) def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference self.remove_active_skill(skill_id) # add skill with timestamp to start of skill_list self.active_skills.insert(0, [skill_id, time.time()]) def update_context(self, intent): """ updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def handle_utterance(self, message): # Get language of the utterance lang = message.data.get('lang', "en-us") utterances = message.data.get('utterances', '') # check for conversation time-out self.active_skills = [skill for skill in self.active_skills if time.time() - skill[ 1] <= self.converse_timeout * 60] # check if any skill wants to handle utterance for skill in self.active_skills: if self.do_converse(utterances, skill[0], lang): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not self.add_active_skill(skill[0]) return # no skill wants to handle utterance best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. best_intent = next(self.engine.determine_intent( normalize(utterance, lang), 100, include_tags=True, context_manager=self.context_manager)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration: # don't show error in log continue except e: LOG.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: self.update_context(best_intent) reply = message.reply( best_intent.get('intent_type'), best_intent) self.emitter.emit(reply) # update active skills skill_id = int(best_intent['intent_type'].split(":")[0]) self.add_active_skill(skill_id) else: self.emitter.emit(Message("intent_failure", { "utterance": utterances[0], "lang": lang })) def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity( start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): print "Registering: " + str(message.data) intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_id = message.data.get('skill_id') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id)] self.engine.intent_parsers = new_parsers def handle_add_context(self, message): """ Handles adding context from the message bus. The data field must contain a context keyword and may contain a word if a specific word should be injected as a match for the provided context keyword. """ entity = {'confidence': 1.0} context = message.data.get('context') word = message.data.get('word') or '' # if not a string type try creating a string from it if not isinstance(word, basestring): word = str(word) entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word self.context_manager.inject_context(entity) def handle_remove_context(self, message): """ Handles removing context from the message bus. The data field must contain the 'context' to remove. """ context = message.data.get('context') if context: self.context_manager.remove_context(context) def handle_clear_context(self, message): """ Clears all keywords from context. """ self.context_manager.clear_context()
class AdaptService: """Intent service wrapping the Apdapt intent Parser.""" def __init__(self, config): self.config = config self.engine = IntentDeterminationEngine() # Context related intializations self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.lock = Lock() def update_context(self, intent): """Updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def match_intent(self, utterances, _=None, __=None): """Run the Adapt engine to search for an matching intent. Args: utterances (iterable): utterances for consideration in intent matching. As a practical matter, a single utterance will be passed in most cases. But there are instances, such as streaming STT that could pass multiple. Each utterance is represented as a tuple containing the raw, normalized, and possibly other variations of the utterance. Returns: Intent structure, or None if no match was found. """ best_intent = {} def take_best(intent, utt): nonlocal best_intent best = best_intent.get('confidence', 0.0) if best_intent else 0.0 conf = intent.get('confidence', 0.0) if conf > best: best_intent = intent # TODO - Shouldn't Adapt do this? best_intent['utterance'] = utt for utt_tup in utterances: for utt in utt_tup: try: intents = [ i for i in self.engine.determine_intent( utt, 100, include_tags=True, context_manager=self.context_manager) ] if intents: utt_best = max(intents, key=lambda x: x.get('confidence', 0.0)) take_best(utt_best, utt_tup[0]) except Exception as err: LOG.exception(err) if best_intent: self.update_context(best_intent) skill_id = best_intent['intent_type'].split(":")[0] ret = IntentMatch('Adapt', best_intent['intent_type'], best_intent, skill_id) else: ret = None return ret # TODO 22.02: Remove this deprecated method def register_vocab(self, start_concept, end_concept, alias_of, regex_str): """Register Vocabulary. DEPRECATED This method should not be used, it has been replaced by register_vocabulary(). """ self.register_vocabulary(start_concept, end_concept, alias_of, regex_str) def register_vocabulary(self, entity_value, entity_type, alias_of, regex_str): """Register skill vocabulary as adapt entity. This will handle both regex registration and registration of normal keywords. if the "regex_str" argument is set all other arguments will be ignored. Argument: entity_value: the natural langauge word entity_type: the type/tag of an entity instance alias_of: entity this is an alternative for """ with self.lock: if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity(entity_value, entity_type, alias_of=alias_of) def register_intent(self, intent): """Register new intent with adapt engine. Args: intent (IntentParser): IntentParser to register """ with self.lock: self.engine.register_intent_parser(intent) def detach_skill(self, skill_id): """Remove all intents for skill. Args: skill_id (str): skill to process """ with self.lock: skill_parsers = [ p.name for p in self.engine.intent_parsers if p.name.startswith(skill_id) ] self.engine.drop_intent_parser(skill_parsers) self._detach_skill_keywords(skill_id) self._detach_skill_regexes(skill_id) def _detach_skill_keywords(self, skill_id): """Detach all keywords registered with a particular skill. Arguments: skill_id (str): skill identifier """ skill_id = _entity_skill_id(skill_id) def match_skill_entities(data): return data and data[1].startswith(skill_id) self.engine.drop_entity(match_func=match_skill_entities) def _detach_skill_regexes(self, skill_id): """Detach all regexes registered with a particular skill. Arguments: skill_id (str): skill identifier """ skill_id = _entity_skill_id(skill_id) def match_skill_regexes(regexp): return any( [r.startswith(skill_id) for r in regexp.groupindex.keys()]) self.engine.drop_regex_entity(match_func=match_skill_regexes) def detach_intent(self, intent_name): """Detatch a single intent Args: intent_name (str): Identifier for intent to remove. """ new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name ] self.engine.intent_parsers = new_parsers
class IntentService(object): def __init__(self, emitter): self.engine = IntentDeterminationEngine() self.emitter = emitter self.emitter.on('register_vocab', self.handle_register_vocab) self.emitter.on('register_intent', self.handle_register_intent) self.emitter.on('recognizer_loop:utterance', self.handle_utterance) self.emitter.on('detach_intent', self.handle_detach_intent) self.emitter.on('detach_skill', self.handle_detach_skill) def handle_utterance(self, message): # Get language of the utterance lang = message.data.get('lang', None) if not lang: lang = "en-us" utterances = message.data.get('utterances', '') best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. best_intent = next(self.engine.determine_intent( normalize(utterance, lang), 100)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration as e: logger.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: reply = message.reply( best_intent.get('intent_type'), best_intent) self.emitter.emit(reply) elif len(utterances) == 1: self.emitter.emit(Message("intent_failure", { "utterance": utterances[0], "lang": lang })) else: self.emitter.emit(Message("multi_utterance_intent_failure", { "utterances": utterances, "lang": lang })) def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity( start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_name = message.data.get('skill_name') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_name)] self.engine.intent_parsers = new_parsers
class AdaptExtractor(IntentExtractor): keyword_based = True regex_entity_support = True def __init__(self, normalize=False, *args, **kwargs): super().__init__(*args, **kwargs) self.normalize = normalize self.engine = IntentDeterminationEngine() def register_entity(self, entity_name, samples=None): samples = samples or [entity_name] for kw in samples: self.engine.register_entity(kw, entity_name) super().register_entity(entity_name, samples) def register_regex_entity(self, entity_name, samples): if isinstance(samples, str): self.engine.register_regex_entity(samples) if isinstance(samples, list): for s in samples: self.engine.register_regex_entity(s) def register_regex_intent(self, intent_name, samples): self.register_regex_entity(intent_name + "_adapt_rx", samples) self.register_intent(intent_name, [intent_name + "_adapt_rx"]) def register_intent(self, intent_name, samples=None, optional_samples=None, rx_samples=None): """ :param intent_name: intent_name :param samples: list of required registered entities (names) :param optional_samples: list of optional registered samples (names) :return: """ super().register_entity(intent_name, samples) if not samples: samples = [intent_name] self.register_entity(intent_name, samples) optional_samples = optional_samples or [] # structure intent intent = IntentBuilder(intent_name) for kw in samples: intent.require(kw) for kw in optional_samples: intent.optionally(kw) self.engine.register_intent_parser(intent.build()) return intent def calc_intent(self, utterance): utterance = utterance.strip() if self.normalize: utterance = normalize(utterance, self.lang, True) for intent in self.engine.determine_intent(utterance, 100, include_tags=True, context_manager=self.context_manager): if intent and intent.get('confidence') > 0: intent.pop("target") matches = {k: v for k, v in intent.items() if k not in ["intent_type", "confidence", "__tags__"]} intent["entities"] = {} for k in matches: intent["entities"][k] = intent.pop(k) intent["conf"] = intent.pop("confidence") intent["utterance"] = utterance intent["intent_engine"] = "adapt" remainder = get_utterance_remainder( utterance, samples=[v for v in matches.values()]) intent["utterance_remainder"] = remainder return intent return {"conf": 0, "intent_type": "unknown", "entities": {}, "utterance_remainder": utterance, "utterance": utterance, "intent_engine": "adapt"} def calc_intents(self, utterance, min_conf=0.5): bucket = {} for ut in self.segmenter.segment(utterance): intent = self.calc_intent(ut) if intent["conf"] < min_conf: bucket[ut] = None else: bucket[ut] = intent return bucket def calc_intents_list(self, utterance, min_conf=0.5): utterance = utterance.strip() # spaces should not mess with exact matches bucket = {} for ut in self.segmenter.segment(utterance): if self.normalize: ut = normalize(ut, self.lang, True) bucket[ut] = [] for intent in self.engine.determine_intent(ut, 100, include_tags=True, context_manager=self.context_manager): if intent: intent.pop("target") matches = {k: v for k, v in intent.items() if k not in ["intent_type", "confidence", "__tags__"]} intent["entities"] = {} for k in matches: intent["entities"][k] = intent.pop(k) intent["conf"] = intent.pop("confidence") intent["utterance"] = ut intent["intent_engine"] = "adapt" remainder = get_utterance_remainder( utterance, samples=[v for v in matches.values()]) intent["utterance_remainder"] = remainder if intent["conf"] >= min_conf: bucket[ut] += [intent] return bucket def intent_scores(self, utterance): utterance = utterance.strip() # spaces should not mess with exact matches bucket = [] for intent in self.engine.determine_intent(utterance, 100, include_tags=True, context_manager=self.context_manager): if intent: intent.pop("target") matches = {k: v for k, v in intent.items() if k not in ["intent_type", "confidence", "__tags__"]} intent["entities"] = {} for k in matches: intent["entities"][k] = intent.pop(k) intent["conf"] = intent.pop("confidence") intent["intent_engine"] = "adapt" intent["utterance"] = utterance remainder = get_utterance_remainder( utterance, samples=[v for v in matches.values()]) intent["utterance_remainder"] = remainder bucket += [intent] return bucket def intent_remainder(self, utterance, _prev=""): utterance = utterance.strip() # spaces should not mess with exact matches if self.normalize: utterance = normalize(utterance, self.lang, True) return IntentExtractor.intent_remainder(self, utterance) def intents_remainder(self, utterance, min_conf=0.5): """ segment utterance and for each chunk recursively check for intents in utterance remainer :param utterance: :param min_conf: :return: """ utterance = utterance.strip() # spaces should not mess with exact matches bucket = {} for utterance in self.segmenter.segment(utterance): if self.normalize: utterance = normalize(utterance, self.lang, True) bucket[utterance] = self.intent_remainder(utterance) return bucket def segment(self, text): if self.normalize: text = normalize(text, self.lang, True) return self.segment(text) def detach_intent(self, intent_name): LOG.debug("detaching adapt intent: " + intent_name) new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def detach_skill(self, skill_id): LOG.debug("detaching adapt skill: " + skill_id) new_parsers = [ p.name for p in self.engine.intent_parsers if p.name.startswith(skill_id)] for intent_name in new_parsers: self.detach_intent(intent_name) def manifest(self): # TODO vocab, skill ids, intent_data return { "intent_names": [p.name for p in self.engine.intent_parsers] }
engine = IntentDeterminationEngine() # create and register weather vocabulary weather_keyword = ["weather"] for wk in weather_keyword: engine.register_entity(wk, "WeatherKeyword") weather_types = ["snow", "rain", "wind", "sleet", "sun"] for wt in weather_types: engine.register_entity(wt, "WeatherType") # create regex to parse out locations engine.register_regex_entity("in (?P<Location>.*)") # structure intent weather_intent = IntentBuilder("WeatherIntent")\ .require("WeatherKeyword")\ .optionally("WeatherType")\ .require("Location")\ .build() engine.register_intent_parser(weather_intent) @app.route('/', methods=['GET']) def parseString(): string = request.args.get('string') for intent in engine.determine_intent(string):
class AdaptService: """Intent service wrapping the Apdapt intent Parser.""" def __init__(self, config): self.config = config self.engine = IntentDeterminationEngine() # Context related intializations self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) def update_context(self, intent): """Updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def match_intent(self, utterances, _=None, __=None): """Run the Adapt engine to search for an matching intent. Arguments: utterances (iterable): iterable of utterances, expected order [raw, normalized, other] Returns: Intent structure, or None if no match was found. """ best_intent = {} def take_best(intent, utt): nonlocal best_intent best = best_intent.get('confidence', 0.0) if best_intent else 0.0 conf = intent.get('confidence', 0.0) if conf > best: best_intent = intent # TODO - Shouldn't Adapt do this? best_intent['utterance'] = utt for utt_tup in utterances: for utt in utt_tup: try: intents = [ i for i in self.engine.determine_intent( utt, 100, include_tags=True, context_manager=self.context_manager) ] if intents: take_best(intents[0], utt_tup[0]) except Exception as err: LOG.exception(err) if best_intent: self.update_context(best_intent) skill_id = best_intent['intent_type'].split(":")[0] ret = IntentMatch('Adapt', best_intent['intent_type'], best_intent, skill_id) else: ret = None return ret def register_vocab(self, start_concept, end_concept, alias_of, regex_str): """Register vocabulary.""" if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity(start_concept, end_concept, alias_of=alias_of) def register_intent(self, intent): """Register new intent with adapt engine. Arguments: intent (IntentParser): IntentParser to register """ self.engine.register_intent_parser(intent) def detach_skill(self, skill_id): """Remove all intents for skill. Arguments: skill_id (str): skill to process """ new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id) ] self.engine.intent_parsers = new_parsers def detach_intent(self, intent_name): """Detatch a single intent Arguments: intent_name (str): Identifier for intent to remove. """ new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name ] self.engine.intent_parsers = new_parsers
class IntentService(object): def __init__(self, bus): self.config = Configuration.get().get('context', {}) self.engine = IntentDeterminationEngine() # Dictionary for translating a skill id to a name self.skill_names = {} # Context related intializations self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.bus = bus self.bus.on('register_vocab', self.handle_register_vocab) self.bus.on('register_intent', self.handle_register_intent) self.bus.on('recognizer_loop:utterance', self.handle_utterance) self.bus.on('detach_intent', self.handle_detach_intent) self.bus.on('detach_skill', self.handle_detach_skill) # Context related handlers self.bus.on('add_context', self.handle_add_context) self.bus.on('remove_context', self.handle_remove_context) self.bus.on('clear_context', self.handle_clear_context) # Converse method self.bus.on('skill.converse.response', self.handle_converse_response) self.bus.on('skill.converse.error', self.handle_converse_error) self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse) self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict) def add_active_skill_handler(message): self.add_active_skill(message.data['skill_id']) self.bus.on('active_skill_request', add_active_skill_handler) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills self.waiting_for_converse = False self.converse_result = False self.converse_skill_id = "" def update_skill_name_dict(self, message): """ Messagebus handler, updates dictionary of if to skill name conversions. """ self.skill_names[message.data['id']] = message.data['name'] def get_skill_name(self, skill_id): """ Get skill name from skill ID. Args: skill_id: a skill id as encoded in Intent handlers. Returns: (str) Skill name or the skill id if the skill wasn't found """ return self.skill_names.get(skill_id, skill_id) def reset_converse(self, message): """Let skills know there was a problem with speech recognition""" lang = message.data.get('lang', "en-us") for skill in self.active_skills: self.do_converse(None, skill[0], lang) def do_converse(self, utterances, skill_id, lang): self.waiting_for_converse = True self.converse_result = False self.converse_skill_id = skill_id self.bus.emit(Message("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang})) start_time = time.time() t = 0 while self.waiting_for_converse and t < 5: t = time.time() - start_time time.sleep(0.1) self.waiting_for_converse = False self.converse_skill_id = "" return self.converse_result def handle_converse_error(self, message): skill_id = message.data["skill_id"] if message.data["error"] == "skill id does not exist": self.remove_active_skill(skill_id) if skill_id == self.converse_skill_id: self.converse_result = False self.waiting_for_converse = False def handle_converse_response(self, message): skill_id = message.data["skill_id"] if skill_id == self.converse_skill_id: self.converse_result = message.data.get("result", False) self.waiting_for_converse = False def remove_active_skill(self, skill_id): for skill in self.active_skills: if skill[0] == skill_id: self.active_skills.remove(skill) def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference self.remove_active_skill(skill_id) # add skill with timestamp to start of skill_list self.active_skills.insert(0, [skill_id, time.time()]) def update_context(self, intent): """ Updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def send_metrics(self, intent, context, stopwatch): """ Send timing metrics to the backend. NOTE: This only applies to those with Opt In. """ ident = context['ident'] if context else None if intent: # Recreate skill name from skill id parts = intent.get('intent_type', '').split(':') intent_type = self.get_skill_name(parts[0]) if len(parts) > 1: intent_type = ':'.join([intent_type] + parts[1:]) report_timing(ident, 'intent_service', stopwatch, {'intent_type': intent_type}) else: report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'intent_failure'}) def handle_utterance(self, message): """ Main entrypoint for handling user utterances with Mycroft skills Monitor the messagebus for 'recognizer_loop:utterance', typically generated by a spoken interaction but potentially also from a CLI or other method of injecting a 'user utterance' into the system. Utterances then work through this sequence to be handled: 1) Active skills attempt to handle using converse() 2) Adapt intent handlers 3) Padatious intent handlers 4) Other fallbacks Args: message (Message): The messagebus data """ try: # Get language of the utterance lang = message.data.get('lang', "en-us") utterances = message.data.get('utterances', '') stopwatch = Stopwatch() with stopwatch: # Give active skills an opportunity to handle the utterance converse = self._converse(utterances, lang) if not converse: # No conversation, use intent system to handle utterance intent = self._adapt_intent_match(utterances, lang) padatious_intent = PadatiousService.instance.calc_intent( utterances[0]) if converse: # Report that converse handled the intent and return ident = message.context['ident'] if message.context else None report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'converse'}) return elif intent and not (padatious_intent and padatious_intent.conf >= 0.95): # Send the message to the Adapt intent's handler unless # Padatious is REALLY sure it was directed at it instead. reply = message.reply(intent.get('intent_type'), intent) else: # Allow fallback system to handle utterance # NOTE: Padatious intents are handled this way, too reply = message.reply('intent_failure', {'utterance': utterances[0], 'lang': lang}) self.bus.emit(reply) self.send_metrics(intent, message.context, stopwatch) except Exception as e: LOG.exception(e) def _converse(self, utterances, lang): """ Give active skills a chance at the utterance Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: bool: True if converse handled it, False if no skill processes it """ # check for conversation time-out self.active_skills = [skill for skill in self.active_skills if time.time() - skill[ 1] <= self.converse_timeout * 60] # check if any skill wants to handle utterance for skill in self.active_skills: if self.do_converse(utterances, skill[0], lang): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not self.add_active_skill(skill[0]) return True return False def _adapt_intent_match(self, utterances, lang): """ Run the Adapt engine to search for an matching intent Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: Intent structure, or None if no match was found. """ best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. best_intent = next(self.engine.determine_intent( normalize(utterance, lang), 100, include_tags=True, context_manager=self.context_manager)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration: # don't show error in log continue except Exception as e: LOG.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: self.update_context(best_intent) # update active skills skill_id = best_intent['intent_type'].split(":")[0] self.add_active_skill(skill_id) # adapt doesn't handle context injection for one_of keywords # correctly. Workaround this issue if possible. try: best_intent = workaround_one_of_context(best_intent) except LookupError: LOG.error('Error during workaround_one_of_context') return best_intent def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity( start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_id = message.data.get('skill_id') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id)] self.engine.intent_parsers = new_parsers def handle_add_context(self, message): """ Add context Args: message: data contains the 'context' item to add optionally can include 'word' to be injected as an alias for the context item. """ entity = {'confidence': 1.0} context = message.data.get('context') word = message.data.get('word') or '' origin = message.data.get('origin') or '' # if not a string type try creating a string from it if not isinstance(word, str): word = str(word) entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word entity['origin'] = origin self.context_manager.inject_context(entity) def handle_remove_context(self, message): """ Remove specific context Args: message: data contains the 'context' item to remove """ context = message.data.get('context') if context: self.context_manager.remove_context(context) def handle_clear_context(self, message): """ Clears all keywords from context """ self.context_manager.clear_context()
for wk in weather_keyword: engine.register_entity(wk, "WeatherKeyword") weather_types = [ "snow", "rain", "wind", "sleet", "sun" ] for wt in weather_types: engine.register_entity(wt, "WeatherType") # create regex to parse out locations engine.register_regex_entity("in (?P<Location>.*)") # structure intent weather_intent = IntentBuilder("WeatherIntent")\ .require("WeatherKeyword")\ .optionally("WeatherType")\ .require("Location")\ .build() engine.register_intent_parser(weather_intent) if __name__ == "__main__": for intent in engine.determine_intent(" ".join(sys.argv[1:])): if intent.get('confidence') > 0: print(json.dumps(intent, indent=4))
weather_types = ["snow", "rain", "wind", "sleet", "sun"] for wt in weather_types: engine.register_entity(wt, "WeatherType") locations = ["Seattle", "San Francisco", "Tokyo", "Delhi"] for l in locations: engine.register_entity(l, "Location") # create regex to parse out locations #engine.register_regex_entity("in (?P<Location>.*)") #create regex to parse date engine.register_regex_entity("in (?P<WeatherDay>^[0-9]+$)") #engine.register_regex_entity("in (?P<Month>.*)") # structure intent #QuestionLike: what is the weather in tokyo weather_intent = IntentBuilder("WeatherIntent")\ .require("WeatherKeyword")\ .optionally("WeatherType")\ .require("Location")\ .optionally("WeatherDay")\ .build() #QuestionLike: is the weather in tokyo #weather_yesno_intent = IntentBuilder("Weather_YesNo_Intent")\ # .optionally("WeatherType")\ # .require("Location")\ # .build()
class IntentService(object): def __init__(self, emitter): self.config = Configuration.get().get('context', {}) self.engine = IntentDeterminationEngine() # Dictionary for translating a skill id to a name self.skill_names = {} # Context related intializations self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.emitter = emitter self.emitter.on('register_vocab', self.handle_register_vocab) self.emitter.on('register_intent', self.handle_register_intent) self.emitter.on('recognizer_loop:utterance', self.handle_utterance) self.emitter.on('detach_intent', self.handle_detach_intent) self.emitter.on('detach_skill', self.handle_detach_skill) # Context related handlers self.emitter.on('add_context', self.handle_add_context) self.emitter.on('remove_context', self.handle_remove_context) self.emitter.on('clear_context', self.handle_clear_context) # Converse method self.emitter.on('skill.converse.response', self.handle_converse_response) self.emitter.on('mycroft.speech.recognition.unknown', self.reset_converse) self.emitter.on('mycroft.skills.loaded', self.update_skill_name_dict) def add_active_skill_handler(message): self.add_active_skill(message.data['skill_id']) self.emitter.on('active_skill_request', add_active_skill_handler) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills def update_skill_name_dict(self, message): """ Messagebus handler, updates dictionary of if to skill name conversions. """ self.skill_names[message.data['id']] = message.data['name'] def get_skill_name(self, skill_id): """ Get skill name from skill ID. Args: skill_id: a skill id as encoded in Intent handlers. Returns: (str) Skill name or the skill id if the skill wasn't found """ return self.skill_names.get(skill_id, skill_id) def reset_converse(self, message): """Let skills know there was a problem with speech recognition""" lang = message.data.get('lang', "en-us") for skill in self.active_skills: self.do_converse(None, skill[0], lang) def do_converse(self, utterances, skill_id, lang): self.emitter.emit(Message("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang})) self.waiting = True self.result = False start_time = time.time() t = 0 while self.waiting and t < 5: t = time.time() - start_time time.sleep(0.1) self.waiting = False return self.result def handle_converse_response(self, message): # id = message.data["skill_id"] # no need to crosscheck id because waiting before new request is made # no other skill will make this request is safe assumption result = message.data["result"] self.result = result self.waiting = False def remove_active_skill(self, skill_id): for skill in self.active_skills: if skill[0] == skill_id: self.active_skills.remove(skill) def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference self.remove_active_skill(skill_id) # add skill with timestamp to start of skill_list self.active_skills.insert(0, [skill_id, time.time()]) def update_context(self, intent): """ Updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def send_metrics(self, intent, context, stopwatch): """ Send timing metrics to the backend. NOTE: This only applies to those with Opt In. """ LOG.debug('Sending metric if opt_in is enabled') ident = context['ident'] if context else None if intent: # Recreate skill name from skill id parts = intent.get('intent_type', '').split(':') intent_type = self.get_skill_name(parts[0]) if len(parts) > 1: intent_type = ':'.join([intent_type] + parts[1:]) report_timing(ident, 'intent_service', stopwatch, {'intent_type': intent_type}) else: report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'intent_failure'}) def handle_utterance(self, message): """ Main entrypoint for handling user utterances with Mycroft skills Monitor the messagebus for 'recognizer_loop:utterance', typically generated by a spoken interaction but potentially also from a CLI or other method of injecting a 'user utterance' into the system. Utterances then work through this sequence to be handled: 1) Active skills attempt to handle using converse() 2) Adapt intent handlers 3) Padatious intent handlers 4) Other fallbacks Args: message (Message): The messagebus data """ try: # Get language of the utterance lang = message.data.get('lang', "en-us") utterances = message.data.get('utterances', '') stopwatch = Stopwatch() with stopwatch: # Give active skills an opportunity to handle the utterance converse = self._converse(utterances, lang) if not converse: # No conversation, use intent system to handle utterance intent = self._adapt_intent_match(utterances, lang) if converse: # Report that converse handled the intent and return ident = message.context['ident'] if message.context else None report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'converse'}) return elif intent: # Send the message to the intent handler reply = message.reply(intent.get('intent_type'), intent) else: # Allow fallback system to handle utterance # NOTE: Padatious intents are handled this way, too reply = message.reply('intent_failure', {'utterance': utterances[0], 'lang': lang}) self.emitter.emit(reply) self.send_metrics(intent, message.context, stopwatch) except Exception as e: LOG.exception(e) def _converse(self, utterances, lang): """ Give active skills a chance at the utterance Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: bool: True if converse handled it, False if no skill processes it """ # check for conversation time-out self.active_skills = [skill for skill in self.active_skills if time.time() - skill[ 1] <= self.converse_timeout * 60] # check if any skill wants to handle utterance for skill in self.active_skills: if self.do_converse(utterances, skill[0], lang): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not self.add_active_skill(skill[0]) return True return False def _adapt_intent_match(self, utterances, lang): """ Run the Adapt engine to search for an matching intent Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: Intent structure, or None if no match was found. """ best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. best_intent = next(self.engine.determine_intent( normalize(utterance, lang), 100, include_tags=True, context_manager=self.context_manager)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration: # don't show error in log continue except Exception as e: LOG.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: self.update_context(best_intent) # update active skills skill_id = best_intent['intent_type'].split(":")[0] self.add_active_skill(skill_id) return best_intent def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity( start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_id = message.data.get('skill_id') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id)] self.engine.intent_parsers = new_parsers def handle_add_context(self, message): """ Add context Args: message: data contains the 'context' item to add optionally can include 'word' to be injected as an alias for the context item. """ entity = {'confidence': 1.0} context = message.data.get('context') word = message.data.get('word') or '' # if not a string type try creating a string from it if not isinstance(word, str): word = str(word) entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word self.context_manager.inject_context(entity) def handle_remove_context(self, message): """ Remove specific context Args: message: data contains the 'context' item to remove """ context = message.data.get('context') if context: self.context_manager.remove_context(context) def handle_clear_context(self, message): """ Clears all keywords from context """ self.context_manager.clear_context()
class IntentEngineTests(unittest.TestCase): def setUp(self): self.engine = IntentDeterminationEngine() def testRegisterIntentParser(self): assert len(self.engine.intent_parsers) == 0 try: self.engine.register_intent_parser("NOTAPARSER") assert "Did not fail to register invalid intent parser" and False except ValueError as e: pass parser = IntentBuilder("Intent").build() self.engine.register_intent_parser(parser) assert len(self.engine.intent_parsers) == 1 def testRegisterRegexEntity(self): assert len(self.engine._regex_strings) == 0 assert len(self.engine.regular_expressions_entities) == 0 self.engine.register_regex_entity(".*") assert len(self.engine._regex_strings) == 1 assert len(self.engine.regular_expressions_entities) == 1 def testSelectBestIntent(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_intent_parser(parser1) self.engine.register_entity("tree", "Entity1") utterance = "go to the tree house" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' parser2 = IntentBuilder("Parser2").require("Entity1").require( "Entity2").build() self.engine.register_intent_parser(parser2) self.engine.register_entity("house", "Entity2") intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser2' def testDropIntent(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_intent_parser(parser1) self.engine.register_entity("tree", "Entity1") parser2 = (IntentBuilder("Parser2").require("Entity1").require( "Entity2").build()) self.engine.register_intent_parser(parser2) self.engine.register_entity("house", "Entity2") utterance = "go to the tree house" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser2' assert self.engine.drop_intent_parser('Parser2') is True intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' def testDropEntity(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_intent_parser(parser1) self.engine.register_entity("laboratory", "Entity1") self.engine.register_entity("lab", "Entity1") utterance = "get out of my lab" utterance2 = "get out of my laboratory" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' intent = next(self.engine.determine_intent(utterance2)) assert intent assert intent['intent_type'] == 'Parser1' # Remove Entity and re-register laboratory and make sure only that # matches. self.engine.drop_entity(entity_type='Entity1') self.engine.register_entity("laboratory", "Entity1") # Sentence containing lab should not produce any results with self.assertRaises(StopIteration): intent = next(self.engine.determine_intent(utterance)) # But sentence with laboratory should intent = next(self.engine.determine_intent(utterance2)) assert intent assert intent['intent_type'] == 'Parser1' def testCustomDropEntity(self): parser1 = (IntentBuilder("Parser1").one_of("Entity1", "Entity2").build()) self.engine.register_intent_parser(parser1) self.engine.register_entity("laboratory", "Entity1") self.engine.register_entity("lab", "Entity2") utterance = "get out of my lab" utterance2 = "get out of my laboratory" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' intent = next(self.engine.determine_intent(utterance2)) assert intent assert intent['intent_type'] == 'Parser1' def matcher(data): return data[1].startswith('Entity') self.engine.drop_entity(match_func=matcher) self.engine.register_entity("laboratory", "Entity1") # Sentence containing lab should not produce any results with self.assertRaises(StopIteration): intent = next(self.engine.determine_intent(utterance)) # But sentence with laboratory should intent = next(self.engine.determine_intent(utterance2)) assert intent def testDropRegexEntity(self): self.engine.register_regex_entity(r"the dog (?P<Dog>.*)") self.engine.register_regex_entity(r"the cat (?P<Cat>.*)") assert len(self.engine._regex_strings) == 2 assert len(self.engine.regular_expressions_entities) == 2 self.engine.drop_regex_entity(entity_type='Cat') assert len(self.engine._regex_strings) == 1 assert len(self.engine.regular_expressions_entities) == 1 def testCustomDropRegexEntity(self): self.engine.register_regex_entity(r"the dog (?P<SkillADog>.*)") self.engine.register_regex_entity(r"the cat (?P<SkillACat>.*)") self.engine.register_regex_entity(r"the mangy dog (?P<SkillBDog>.*)") assert len(self.engine._regex_strings) == 3 assert len(self.engine.regular_expressions_entities) == 3 def matcher(regexp): """Matcher for all match groups defined for SkillB""" match_groups = regexp.groupindex.keys() return any([k.startswith('SkillB') for k in match_groups]) self.engine.drop_regex_entity(match_func=matcher) assert len(self.engine._regex_strings) == 2 assert len(self.engine.regular_expressions_entities) == 2 def testAddingOfRemovedRegexp(self): self.engine.register_regex_entity(r"the cool (?P<thing>.*)") def matcher(regexp): """Matcher for all match groups defined for SkillB""" match_groups = regexp.groupindex.keys() return any([k.startswith('thing') for k in match_groups]) self.engine.drop_regex_entity(match_func=matcher) assert len(self.engine.regular_expressions_entities) == 0 self.engine.register_regex_entity(r"the cool (?P<thing>.*)") assert len(self.engine.regular_expressions_entities) == 1 def testUsingOfRemovedRegexp(self): self.engine.register_regex_entity(r"the cool (?P<thing>.*)") parser = IntentBuilder("Intent").require("thing").build() self.engine.register_intent_parser(parser) def matcher(regexp): """Matcher for all match groups defined for SkillB""" match_groups = regexp.groupindex.keys() return any([k.startswith('thing') for k in match_groups]) self.engine.drop_regex_entity(match_func=matcher) assert len(self.engine.regular_expressions_entities) == 0 utterance = "the cool cat" intents = [match for match in self.engine.determine_intent(utterance)] assert len(intents) == 0 def testEmptyTags(self): # Validates https://github.com/MycroftAI/adapt/issues/114 engine = IntentDeterminationEngine() engine.register_entity("Kevin", "who") # same problem if several entities builder = IntentBuilder("Buddies") builder.optionally("who") # same problem if several entity types engine.register_intent_parser(builder.build()) intents = [i for i in engine.determine_intent("Julien is a friend")] assert len(intents) == 0 def testResultsAreSortedByConfidence(self): self.engine.register_entity('what is', 'Query', None) self.engine.register_entity('weather', 'Weather', None) self.engine.register_regex_entity('(at|in) (?P<Location>.+)') self.engine.register_regex_entity('(?P<Entity>.*)') i = IntentBuilder("CurrentWeatherIntent").require( "Weather").optionally("Location").build() self.engine.register_intent_parser(i) utterance = "what is the weather like in stockholm" intents = [ i for i in self.engine.determine_intent(utterance, num_results=100) ] confidences = [intent.get('confidence', 0.0) for intent in intents] assert len(confidences) > 1 assert all(confidences[i] >= confidences[i + 1] for i in range(len(confidences) - 1))
class IntentService(object): def __init__(self, emitter): self.config = Configuration.get().get('context', {}) self.engine = IntentDeterminationEngine() # Dictionary for translating a skill id to a name self.skill_names = {} # Context related intializations self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.emitter = emitter self.emitter.on('register_vocab', self.handle_register_vocab) self.emitter.on('register_intent', self.handle_register_intent) self.emitter.on('recognizer_loop:utterance', self.handle_utterance) self.emitter.on('detach_intent', self.handle_detach_intent) self.emitter.on('detach_skill', self.handle_detach_skill) # Context related handlers self.emitter.on('add_context', self.handle_add_context) self.emitter.on('remove_context', self.handle_remove_context) self.emitter.on('clear_context', self.handle_clear_context) # Converse method self.emitter.on('skill.converse.response', self.handle_converse_response) self.emitter.on('mycroft.speech.recognition.unknown', self.reset_converse) self.emitter.on('mycroft.skills.loaded', self.update_skill_name_dict) def add_active_skill_handler(message): self.add_active_skill(message.data['skill_id']) self.emitter.on('active_skill_request', add_active_skill_handler) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills def update_skill_name_dict(self, message): """ Messagebus handler, updates dictionary of if to skill name conversions. """ self.skill_names[message.data['id']] = message.data['name'] def get_skill_name(self, skill_id): """ Get skill name from skill ID. Args skill_id: a skill id as encoded in Intent handlers. Returns: (str) Skill name or the skill id if the skill wasn't found in the dict. """ return self.skill_names.get(int(skill_id), skill_id) def reset_converse(self, message): """Let skills know there was a problem with speech recognition""" lang = message.data.get('lang', "en-us") for skill in self.active_skills: self.do_converse(None, skill[0], lang) def do_converse(self, utterances, skill_id, lang): self.emitter.emit(Message("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang})) self.waiting = True self.result = False start_time = time.time() t = 0 while self.waiting and t < 5: t = time.time() - start_time time.sleep(0.1) self.waiting = False return self.result def handle_converse_response(self, message): # id = message.data["skill_id"] # no need to crosscheck id because waiting before new request is made # no other skill will make this request is safe assumption result = message.data["result"] self.result = result self.waiting = False def remove_active_skill(self, skill_id): for skill in self.active_skills: if skill[0] == skill_id: self.active_skills.remove(skill) def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference self.remove_active_skill(skill_id) # add skill with timestamp to start of skill_list self.active_skills.insert(0, [skill_id, time.time()]) def update_context(self, intent): """ updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def send_metrics(self, intent, context, stopwatch): """ Send timing metrics to the backend. """ LOG.debug('Sending metric') ident = context['ident'] if context else None if intent: # Recreate skill name from skill id parts = intent.get('intent_type', '').split(':') intent_type = self.get_skill_name(parts[0]) if len(parts) > 1: intent_type = ':'.join([intent_type] + parts[1:]) report_timing(ident, 'intent_service', stopwatch, {'intent_type': intent_type}) else: report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'intent_failure'}) def handle_utterance(self, message): """ Messagebus handler for the recognizer_loop:utterance message """ try: # Get language of the utterance lang = message.data.get('lang', "en-us") utterances = message.data.get('utterances', '') stopwatch = Stopwatch() with stopwatch: # Parse the sentence converse = self.parse_converse(utterances, lang) if not converse: # no skill wants to handle utterance intent = self.parse_utterances(utterances, lang) if converse: # Report that converse handled the intent and return ident = message.context['ident'] if message.context else None report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'converse'}) return elif intent: # Send the message on to the intent handler reply = message.reply(intent.get('intent_type'), intent) else: # or if no match send sentence to fallback system reply = message.reply('intent_failure', {'utterance': utterances[0], 'lang': lang}) self.emitter.emit(reply) self.send_metrics(intent, message.context, stopwatch) except Exception as e: LOG.exception(e) def parse_converse(self, utterances, lang): """ Converse, check if a recently invoked skill wants to handle the utterance and override normal adapt handling. Returns: True if converse handled the utterance, else False. """ # check for conversation time-out self.active_skills = [skill for skill in self.active_skills if time.time() - skill[ 1] <= self.converse_timeout * 60] # check if any skill wants to handle utterance for skill in self.active_skills: if self.do_converse(utterances, skill[0], lang): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not self.add_active_skill(skill[0]) return True return False def parse_utterances(self, utterances, lang): """ Parse the utteracne using adapt to find a matching intent. Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: Intent structure, or None if no match was found. """ best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. best_intent = next(self.engine.determine_intent( normalize(utterance, lang), 100, include_tags=True, context_manager=self.context_manager)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration: # don't show error in log continue except Exception as e: LOG.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: self.update_context(best_intent) # update active skills skill_id = int(best_intent['intent_type'].split(":")[0]) self.add_active_skill(skill_id) return best_intent def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity( start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_id = message.data.get('skill_id') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id)] self.engine.intent_parsers = new_parsers def handle_add_context(self, message): """ Handles adding context from the message bus. The data field must contain a context keyword and may contain a word if a specific word should be injected as a match for the provided context keyword. """ entity = {'confidence': 1.0} context = message.data.get('context') word = message.data.get('word') or '' # if not a string type try creating a string from it if not isinstance(word, basestring): word = str(word) entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word self.context_manager.inject_context(entity) def handle_remove_context(self, message): """ Handles removing context from the message bus. The data field must contain the 'context' to remove. """ context = message.data.get('context') if context: self.context_manager.remove_context(context) def handle_clear_context(self, message): """ Clears all keywords from context. """ self.context_manager.clear_context()
from adapt.engine import IntentDeterminationEngine from DB_Skill.task_manager.IntentsGrouper import all_entities_dic as entities, all_intents as intents, all_MRA as multi_regex_entities from DB_Skill.task_manager import Handler engine = IntentDeterminationEngine() # Register entities in engine for entity, keywords in entities.items(): for keyword in keywords: engine.register_entity(keyword, entity) for entity in multi_regex_entities: for regex in entity: engine.register_regex_entity(regex) # Register intents on engine for intent in intents: engine.register_intent_parser(intent) text1 = 'what is the number of active jobs today?' text2 = 'create assignment buy mic for john' for intent in engine.determine_intent(text1): print(intent) for intent in engine.determine_intent(text2): print(intent) def f1(): print(' i am connecting todo')
class IntentService(object): def __init__(self, emitter): self.engine = IntentDeterminationEngine() self.emitter = emitter self.emitter.on('register_vocab', self.handle_register_vocab) self.emitter.on('register_intent', self.handle_register_intent) self.emitter.on('onyx_recognizer:utterance', self.handle_utterance) self.emitter.on('detach_intent', self.handle_detach_intent) self.emitter.on('detach_skill', self.handle_detach_skill) def handle_utterance(self, message): lang = message.data.get('lang', None) if not lang: lang = "en-US" user = message.data.get('user', None) url = message.data.get('url', None) utterances = message.data.get('utterances', '') best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy" best_intent = next( self.engine.determine_intent(normalize(utterance, lang), 100)) best_intent['utterance'] = utterance except StopIteration as e: logger.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: best_intent['lang'] = lang best_intent['user'] = user reply = message.reply(best_intent.get('intent_type'), best_intent) self.emitter.emit(reply) elif len(utterances) == 1: self.emitter.emit( Message("intent_failure", { "utterance": utterances[0], "lang": lang })) else: self.emitter.emit( Message("multi_utterance_intent_failure", { "utterances": utterances, "lang": lang })) def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity(start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name ] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_name = message.data.get('skill_name') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_name) ] self.engine.intent_parsers = new_parsers
from adapt.engine import IntentDeterminationEngine engine = IntentDeterminationEngine() DOMTree = parse('config.xml') for intent in DOMTree.getElementsByTagName("intent"): builder = IntentBuilder(intent.attributes["name"].value) for node in intent.getElementsByTagName("keyword"): keyword = node.attributes["name"].value required = node.attributes["required"].value ktype = node.attributes["type"].value if ktype == 'normal': for child in node.getElementsByTagName("item"): engine.register_entity(child.childNodes[0].nodeValue, keyword) else: engine.register_regex_entity(node.childNodes[0].nodeValue) if required == 'true': builder.require(keyword) else: builder.optionally(keyword) engine.register_intent_parser(builder.build()) # create and register weather vocabulary '''weather_keyword = [ "weather" ] for wk in weather_keyword: engine.register_entity(wk, "WeatherKeyword") weather_types = [
class IntentSkill(BoomerSkill): def __init__(self): BoomerSkill.__init__(self, name="IntentSkill") self.engine = IntentDeterminationEngine() def initialize(self): self.emitter.on('register_vocab', self.handle_register_vocab) self.emitter.on('register_intent', self.handle_register_intent) self.emitter.on('recognizer_loop:utterance', self.handle_utterance) self.emitter.on('detach_intent', self.handle_detach_intent) def handle_utterance(self, message): timer = Stopwatch() timer.start() metrics = MetricsAggregator() utterances = message.data.get('utterances', '') best_intent = None for utterance in utterances: metrics.increment("utterances.count") for intent in self.engine.determine_intent( utterance, num_results=100): metrics.increment("intents.count") intent['utterance'] = utterance best_confidence = best_intent.get('confidence') \ if best_intent else 0.0 cur_confidence = intent.get('confidence', 0.0) if best_confidence < cur_confidence: best_intent = intent if best_intent and best_intent.get('confidence', 0.0) > 0.0: reply = message.reply( best_intent.get('intent_type'), data=best_intent) self.emitter.emit(reply) elif len(utterances) == 1: self.emitter.emit( Message("intent_failure", data={"utterance": utterances[0]})) else: self.emitter.emit( Message("multi_utterance_intent_failure", data={"utterances": utterances})) metrics.timer("parse.time", timer.stop()) metrics.flush() def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity( start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def stop(self): pass
class IntentEngineTests(unittest.TestCase): def setUp(self): self.engine = IntentDeterminationEngine() def testRegisterIntentParser(self): assert len(self.engine.intent_parsers) == 0 try: self.engine.register_intent_parser("NOTAPARSER") assert "Did not fail to register invalid intent parser" and False except ValueError as e: pass parser = IntentBuilder("Intent").build() self.engine.register_intent_parser(parser) assert len(self.engine.intent_parsers) == 1 def testRegisterRegexEntity(self): assert len(self.engine._regex_strings) == 0 assert len(self.engine.regular_expressions_entities) == 0 self.engine.register_regex_entity(".*") assert len(self.engine._regex_strings) == 1 assert len(self.engine.regular_expressions_entities) == 1 def testSelectBestIntent(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_intent_parser(parser1) self.engine.register_entity("tree", "Entity1") utterance = "go to the tree house" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' parser2 = IntentBuilder("Parser2").require("Entity1").require( "Entity2").build() self.engine.register_intent_parser(parser2) self.engine.register_entity("house", "Entity2") intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser2' def testDropIntent(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_intent_parser(parser1) self.engine.register_entity("tree", "Entity1") parser2 = (IntentBuilder("Parser2").require("Entity1").require( "Entity2").build()) self.engine.register_intent_parser(parser2) self.engine.register_entity("house", "Entity2") utterance = "go to the tree house" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser2' assert self.engine.drop_intent_parser('Parser2') is True intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' def testDropEntity(self): parser1 = IntentBuilder("Parser1").require("Entity1").build() self.engine.register_intent_parser(parser1) self.engine.register_entity("laboratory", "Entity1") self.engine.register_entity("lab", "Entity1") utterance = "get out of my lab" utterance2 = "get out of my laboratory" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' intent = next(self.engine.determine_intent(utterance2)) assert intent assert intent['intent_type'] == 'Parser1' # Remove Entity and re-register laboratory and make sure only that # matches. self.engine.drop_entity(entity_type='Entity1') self.engine.register_entity("laboratory", "Entity1") # Sentence containing lab should not produce any results with self.assertRaises(StopIteration): intent = next(self.engine.determine_intent(utterance)) # But sentence with laboratory should intent = next(self.engine.determine_intent(utterance2)) assert intent assert intent['intent_type'] == 'Parser1' def testCustomDropEntity(self): parser1 = (IntentBuilder("Parser1").one_of("Entity1", "Entity2").build()) self.engine.register_intent_parser(parser1) self.engine.register_entity("laboratory", "Entity1") self.engine.register_entity("lab", "Entity2") utterance = "get out of my lab" utterance2 = "get out of my laboratory" intent = next(self.engine.determine_intent(utterance)) assert intent assert intent['intent_type'] == 'Parser1' intent = next(self.engine.determine_intent(utterance2)) assert intent assert intent['intent_type'] == 'Parser1' def matcher(data): return data[1].startswith('Entity') self.engine.drop_entity(match_func=matcher) self.engine.register_entity("laboratory", "Entity1") # Sentence containing lab should not produce any results with self.assertRaises(StopIteration): intent = next(self.engine.determine_intent(utterance)) # But sentence with laboratory should intent = next(self.engine.determine_intent(utterance2)) assert intent def testDropRegexEntity(self): self.engine.register_regex_entity(r"the dog (?P<Dog>.*)") self.engine.register_regex_entity(r"the cat (?P<Cat>.*)") assert len(self.engine._regex_strings) == 2 assert len(self.engine.regular_expressions_entities) == 2 self.engine.drop_regex_entity(entity_type='Cat') assert len(self.engine._regex_strings) == 1 assert len(self.engine.regular_expressions_entities) == 1 def testCustomDropRegexEntity(self): self.engine.register_regex_entity(r"the dog (?P<SkillADog>.*)") self.engine.register_regex_entity(r"the cat (?P<SkillACat>.*)") self.engine.register_regex_entity(r"the mangy dog (?P<SkillBDog>.*)") assert len(self.engine._regex_strings) == 3 assert len(self.engine.regular_expressions_entities) == 3 def matcher(regexp): """Matcher for all match groups defined for SkillB""" match_groups = regexp.groupindex.keys() return any([k.startswith('SkillB') for k in match_groups]) self.engine.drop_regex_entity(match_func=matcher) assert len(self.engine._regex_strings) == 2 assert len(self.engine.regular_expressions_entities) == 2
class IntentService(object): def __init__(self, emitter): self.config = Configuration.get().get('context', {}) self.engine = IntentDeterminationEngine() self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.emitter = emitter self.emitter.on('register_vocab', self.handle_register_vocab) self.emitter.on('register_intent', self.handle_register_intent) self.emitter.on('recognizer_loop:utterance', self.handle_utterance) self.emitter.on('detach_intent', self.handle_detach_intent) self.emitter.on('detach_skill', self.handle_detach_skill) # Context related handlers self.emitter.on('add_context', self.handle_add_context) self.emitter.on('remove_context', self.handle_remove_context) self.emitter.on('clear_context', self.handle_clear_context) # Converse method self.emitter.on('skill.converse.response', self.handle_converse_response) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills def do_converse(self, utterances, skill_id, lang): self.emitter.emit(Message("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang})) self.waiting = True self.result = False start_time = time.time() t = 0 while self.waiting and t < 5: t = time.time() - start_time time.sleep(0.1) self.waiting = False return self.result def handle_converse_response(self, message): # id = message.data["skill_id"] # no need to crosscheck id because waiting before new request is made # no other skill will make this request is safe assumption result = message.data["result"] self.result = result self.waiting = False def remove_active_skill(self, skill_id): for skill in self.active_skills: if skill[0] == skill_id: self.active_skills.remove(skill) def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference self.remove_active_skill(skill_id) # add skill with timestamp to start of skill_list self.active_skills.insert(0, [skill_id, time.time()]) def update_context(self, intent): """ updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def handle_utterance(self, message): # Get language of the utterance lang = message.data.get('lang', None) if not lang: lang = "en-us" utterances = message.data.get('utterances', '') # check for conversation time-out self.active_skills = [skill for skill in self.active_skills if time.time() - skill[ 1] <= self.converse_timeout * 60] # check if any skill wants to handle utterance for skill in self.active_skills: if self.do_converse(utterances, skill[0], lang): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not self.add_active_skill(skill[0]) return # no skill wants to handle utterance best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. best_intent = next(self.engine.determine_intent( normalize(utterance, lang), 100, include_tags=True, context_manager=self.context_manager)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration: # don't show error in log continue except e: LOG.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: self.update_context(best_intent) reply = message.reply( best_intent.get('intent_type'), best_intent) self.emitter.emit(reply) # update active skills skill_id = int(best_intent['intent_type'].split(":")[0]) self.add_active_skill(skill_id) else: self.emitter.emit(Message("intent_failure", { "utterance": utterances[0], "lang": lang })) def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity( start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): print "Registering: " + str(message.data) intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_id = message.data.get('skill_id') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id)] self.engine.intent_parsers = new_parsers def handle_add_context(self, message): """ Handles adding context from the message bus. The data field must contain a context keyword and may contain a word if a specific word should be injected as a match for the provided context keyword. """ entity = {'confidence': 1.0} context = message.data.get('context') word = message.data.get('word') or '' # if not a string type try creating a string from it if not isinstance(word, basestring): word = str(word) entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word self.context_manager.inject_context(entity) def handle_remove_context(self, message): """ Handles removing context from the message bus. The data field must contain the 'context' to remove. """ context = message.data.get('context') if context: self.context_manager.remove_context(context) def handle_clear_context(self, message): """ Clears all keywords from context. """ self.context_manager.clear_context()
class IntentService: def __init__(self, bus): self.config = Configuration.get().get('context', {}) self.engine = IntentDeterminationEngine() # Dictionary for translating a skill id to a name self.skill_names = {} # Context related intializations self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.bus = bus self.bus.on('register_vocab', self.handle_register_vocab) self.bus.on('register_intent', self.handle_register_intent) self.bus.on('recognizer_loop:utterance', self.handle_utterance) self.bus.on('detach_intent', self.handle_detach_intent) self.bus.on('detach_skill', self.handle_detach_skill) # Context related handlers self.bus.on('add_context', self.handle_add_context) self.bus.on('remove_context', self.handle_remove_context) self.bus.on('clear_context', self.handle_clear_context) # Converse method self.bus.on('skill.converse.response', self.handle_converse_response) self.bus.on('skill.converse.error', self.handle_converse_error) self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse) self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict) def add_active_skill_handler(message): self.add_active_skill(message.data['skill_id']) self.bus.on('active_skill_request', add_active_skill_handler) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills self.waiting_for_converse = False self.converse_result = False self.converse_skill_id = "" # Intents API self.registered_intents = [] self.registered_vocab = [] self.bus.on('intent.service.adapt.get', self.handle_get_adapt) self.bus.on('intent.service.intent.get', self.handle_get_intent) self.bus.on('intent.service.skills.get', self.handle_get_skills) self.bus.on('intent.service.active_skills.get', self.handle_get_active_skills) self.bus.on('intent.service.adapt.manifest.get', self.handle_manifest) self.bus.on('intent.service.adapt.vocab.manifest.get', self.handle_vocab_manifest) def update_skill_name_dict(self, message): """ Messagebus handler, updates dictionary of if to skill name conversions. """ self.skill_names[message.data['id']] = message.data['name'] def get_skill_name(self, skill_id): """ Get skill name from skill ID. Args: skill_id: a skill id as encoded in Intent handlers. Returns: (str) Skill name or the skill id if the skill wasn't found """ return self.skill_names.get(skill_id, skill_id) def reset_converse(self, message): """Let skills know there was a problem with speech recognition""" lang = message.data.get('lang', "en-us") set_active_lang(lang) for skill in self.active_skills: self.do_converse(None, skill[0], lang) def do_converse(self, utterances, skill_id, lang, message): self.waiting_for_converse = True self.converse_result = False self.converse_skill_id = skill_id self.bus.emit( message.reply("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang })) start_time = time.time() t = 0 while self.waiting_for_converse and t < 5: t = time.time() - start_time time.sleep(0.1) self.waiting_for_converse = False self.converse_skill_id = "" return self.converse_result def handle_converse_error(self, message): skill_id = message.data["skill_id"] if message.data["error"] == "skill id does not exist": self.remove_active_skill(skill_id) if skill_id == self.converse_skill_id: self.converse_result = False self.waiting_for_converse = False def handle_converse_response(self, message): skill_id = message.data["skill_id"] if skill_id == self.converse_skill_id: self.converse_result = message.data.get("result", False) self.waiting_for_converse = False def remove_active_skill(self, skill_id): for skill in self.active_skills: if skill[0] == skill_id: self.active_skills.remove(skill) def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference self.remove_active_skill(skill_id) # add skill with timestamp to start of skill_list self.active_skills.insert(0, [skill_id, time.time()]) def update_context(self, intent): """ Updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def send_metrics(self, intent, context, stopwatch): """ Send timing metrics to the backend. NOTE: This only applies to those with Opt In. """ ident = context['ident'] if 'ident' in context else None if intent: # Recreate skill name from skill id parts = intent.get('intent_type', '').split(':') intent_type = self.get_skill_name(parts[0]) if len(parts) > 1: intent_type = ':'.join([intent_type] + parts[1:]) report_timing(ident, 'intent_service', stopwatch, {'intent_type': intent_type}) else: report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'intent_failure'}) def handle_utterance(self, message): """ Main entrypoint for handling user utterances with Mycroft skills Monitor the messagebus for 'recognizer_loop:utterance', typically generated by a spoken interaction but potentially also from a CLI or other method of injecting a 'user utterance' into the system. Utterances then work through this sequence to be handled: 1) Active skills attempt to handle using converse() 2) Padatious high match intents (conf > 0.95) 3) Adapt intent handlers 5) Fallbacks: - Padatious near match intents (conf > 0.8) - General fallbacks - Padatious loose match intents (conf > 0.5) - Unknown intent handler Args: message (Message): The messagebus data """ try: # Get language of the utterance lang = message.data.get('lang', "en-us") set_active_lang(lang) utterances = message.data.get('utterances', []) # normalize() changes "it's a boy" to "it is a boy", etc. norm_utterances = [ normalize(u.lower(), remove_articles=False) for u in utterances ] # Build list with raw utterance(s) first, then optionally a # normalized version following. combined = utterances + list( set(norm_utterances) - set(utterances)) LOG.debug("Utterances: {}".format(combined)) stopwatch = Stopwatch() intent = None padatious_intent = None with stopwatch: # Give active skills an opportunity to handle the utterance converse = self._converse(combined, lang, message) if not converse: # No conversation, use intent system to handle utterance intent = self._adapt_intent_match(utterances, norm_utterances, lang) for utt in combined: _intent = PadatiousService.instance.calc_intent(utt) if _intent: best = padatious_intent.conf if padatious_intent \ else 0.0 if best < _intent.conf: padatious_intent = _intent LOG.debug("Padatious intent: {}".format(padatious_intent)) LOG.debug(" Adapt intent: {}".format(intent)) if converse: # Report that converse handled the intent and return LOG.debug("Handled in converse()") ident = None if message.context and 'ident' in message.context: ident = message.context['ident'] report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'converse'}) return elif (intent and intent.get('confidence', 0.0) > 0.0 and not (padatious_intent and padatious_intent.conf >= 0.95)): # Send the message to the Adapt intent's handler unless # Padatious is REALLY sure it was directed at it instead. self.update_context(intent) # update active skills skill_id = intent['intent_type'].split(":")[0] self.add_active_skill(skill_id) # Adapt doesn't handle context injection for one_of keywords # correctly. Workaround this issue if possible. try: intent = workaround_one_of_context(intent) except LookupError: LOG.error('Error during workaround_one_of_context') reply = message.reply(intent.get('intent_type'), intent) else: # Allow fallback system to handle utterance # NOTE: A matched padatious_intent is handled this way, too # TODO: Need to redefine intent_failure when STT can return # multiple hypothesis -- i.e. len(utterances) > 1 reply = message.reply( 'intent_failure', { 'utterance': utterances[0], 'norm_utt': norm_utterances[0], 'lang': lang }) self.bus.emit(reply) self.send_metrics(intent, message.context, stopwatch) except Exception as e: LOG.exception(e) def _converse(self, utterances, lang, message): """ Give active skills a chance at the utterance Args: utterances (list): list of utterances lang (string): 4 letter ISO language code message (Message): message to use to generate reply Returns: bool: True if converse handled it, False if no skill processes it """ # check for conversation time-out self.active_skills = [ skill for skill in self.active_skills if time.time() - skill[1] <= self.converse_timeout * 60 ] # check if any skill wants to handle utterance for skill in self.active_skills: if self.do_converse(utterances, skill[0], lang, message): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not self.add_active_skill(skill[0]) return True return False def _adapt_intent_match(self, raw_utt, norm_utt, lang): """ Run the Adapt engine to search for an matching intent Args: raw_utt (list): list of utterances norm_utt (list): same list of utterances, normalized lang (string): language code, e.g "en-us" Returns: Intent structure, or None if no match was found. """ best_intent = None def take_best(intent, utt): nonlocal best_intent best = best_intent.get('confidence', 0.0) if best_intent else 0.0 conf = intent.get('confidence', 0.0) if conf > best: best_intent = intent # TODO - Shouldn't Adapt do this? best_intent['utterance'] = utt for idx, utt in enumerate(raw_utt): try: intents = [ i for i in self.engine.determine_intent( utt, 100, include_tags=True, context_manager=self.context_manager) ] if intents: take_best(intents[0], utt) # Also test the normalized version, but set the utterance to # the raw version so skill has access to original STT norm_intents = [ i for i in self.engine.determine_intent( norm_utt[idx], 100, include_tags=True, context_manager=self.context_manager) ] if norm_intents: take_best(norm_intents[0], utt) except Exception as e: LOG.exception(e) return best_intent def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity(start_concept, end_concept, alias_of=alias_of) self.registered_vocab.append(message.data) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name ] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_id = message.data.get('skill_id') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id) ] self.engine.intent_parsers = new_parsers def handle_add_context(self, message): """ Add context Args: message: data contains the 'context' item to add optionally can include 'word' to be injected as an alias for the context item. """ entity = {'confidence': 1.0} context = message.data.get('context') word = message.data.get('word') or '' origin = message.data.get('origin') or '' # if not a string type try creating a string from it if not isinstance(word, str): word = str(word) entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word entity['origin'] = origin self.context_manager.inject_context(entity) def handle_remove_context(self, message): """ Remove specific context Args: message: data contains the 'context' item to remove """ context = message.data.get('context') if context: self.context_manager.remove_context(context) def handle_clear_context(self, message): """ Clears all keywords from context """ self.context_manager.clear_context() def handle_get_adapt(self, message): utterance = message.data["utterance"] lang = message.data.get("lang", "en-us") norm = normalize(utterance, lang, remove_articles=False) intent = self._adapt_intent_match([utterance], [norm], lang) self.bus.emit( message.reply("intent.service.adapt.reply", {"intent": intent})) def handle_get_intent(self, message): utterance = message.data["utterance"] lang = message.data.get("lang", "en-us") norm = normalize(utterance, lang, remove_articles=False) intent = self._adapt_intent_match([utterance], [norm], lang) # Adapt intent's handler is used unless # Padatious is REALLY sure it was directed at it instead. padatious_intent = PadatiousService.instance.calc_intent(utterance) if not padatious_intent and norm != utterance: padatious_intent = PadatiousService.instance.calc_intent(norm) if intent is None or (padatious_intent and padatious_intent.conf >= 0.95): intent = padatious_intent.__dict__ self.bus.emit( message.reply("intent.service.intent.reply", {"intent": intent})) def handle_get_skills(self, message): self.bus.emit( message.reply("intent.service.skills.reply", {"skills": self.skill_names})) def handle_get_active_skills(self, message): self.bus.emit( message.reply("intent.service.active_skills.reply", {"skills": [s[0] for s in self.active_skills]})) def handle_manifest(self, message): self.bus.emit( message.reply("intent.service.adapt.manifest", {"intents": self.registered_intents})) def handle_vocab_manifest(self, message): self.bus.emit( message.reply("intent.service.adapt.vocab.manifest", {"vocab": self.registered_vocab}))
def skyAdapt(): engine = IntentDeterminationEngine() #dota vocabulary dota_keywords = [ 'dota', 'dotes', 'dote'] for dk in dota_keywords: engine.register_entity(dk, "DotaKeyword") happening_keywords = [ 'happening', 'anyone up for', 'when is', 'what time', 'tonight', 'this evening?', 'anyone about for', 'around', 'want to', 'fancy some', 'playing some', 'anyone playing' ] for hk in happening_keywords: engine.register_entity(hk, "HappeningKeyword") dota_query_intent = IntentBuilder("DotaIntent")\ .require("DotaKeyword")\ .require("HappeningKeyword")\ .build() stack_intent_words = [ 'stack', 'stacked' ] for sik in stack_intent_words: engine.register_entity(hk, "StackKeyword") stack_optionals = [ 'are we', 'do we have a', 'how many', 'who\'s playing' ] for osk in stack_optionals: engine.register_entity(hk, "StackOptionalKeyword") stack_intent = IntentBuilder("StackIntent")\ .require("StackKeyword")\ .optionally("StackOptionalKeyword")\ .build() engine.register_regex_entity("at (?P<Time>.*)") new_dota_intent = IntentBuilder("NewDotaIntent")\ .require("DotaKeyword")\ .require("Time")\ .build() engine.register_intent_parser(dota_query_intent) engine.register_intent_parser(stack_intent) engine.register_intent_parser(new_dota_intent) return engine
def register_add_event_intent(engine: IntentDeterminationEngine): # <event_name> - done # <event_location> - done # <start_date> - done # <start_time> - done # <end_date> - done # <end_time> - done # called <event_name> from the <start_date> to the <end_date> # schedule an event called OOP exam from the 21st of May to the 22nd of May # called <event_name> at <event_location> from the <start_date> to the<end_date> # schedule an event called OOP exam at FMI 325 from the 21st of May to the 22nd of May # called <event_name> on the <start_date> at <start_time> until the <end_date> at <end_time> # schedule an event called OOP exam on the 21st of May at 12:30 p.m. until the 22nd of May at 13:30 p.m. # called <event_name> at <event_location> on the <start_date> at <start_time> until the <end_date> at <end_time> # create an event called OOP exam at FMI 325 on the 21st of June at 12:30 p.m. until the 21nd of June at 13:30 p.m. # other key words: # from, to, at, on, until # (from|on)?.+?(?<!until).+?(the (?P<start_date>\d+?)(?=st|nd|td|th)) event_keyword = ["event", "schedule"] for ek in event_keyword: engine.register_entity(ek, "AddEventKeyword") # event name engine.register_regex_entity( 'called (?P<event_name>.+?)(?=from|to|at|on|until)') # matches location engine.register_regex_entity( "at (?P<location>[a-zA-Z0-9 ]+?)(?=from|to|at|on|until)") # start_date, when only date is present engine.register_regex_entity( '(from the)(?<!to the) ((?P<start_date>\d+?)(?=st|nd|rd|th))') # start_date, when time is present engine.register_regex_entity( '(on the)(?<!until the) ((?P<start_date>\d+?)(?=st|nd|rd|th))') # start_time engine.register_regex_entity( 'at (?P<start_time>([0-9]{1,4}:?[0-9]{0,2} ?(p\.m\.|a\.m\.)?)).*') # end_date, when only date is present engine.register_regex_entity( '(to the) ((?P<end_date>\d+?)(?=st|nd|rd|th))') # end_date, when time is present engine.register_regex_entity( '(until the) ((?P<end_date>\d+?)(?=st|nd|rd|th))') # end_time engine.register_regex_entity( '(?<=until the \d{2}(st|nd|rd|th) of) \w*? at (?P<end_time>([0-9]{1,4}:?[0-9]{0,2} ?(p\.m\.|a\.m\.)?))' ) # structure intent add_event_intent = IntentBuilder("AddEventIntent") \ .require("AddEventKeyword")\ .optionally("event_name")\ .optionally("location")\ .optionally("start_date")\ .optionally("start_time")\ .optionally("end_date")\ .optionally("end_time")\ .build() engine.register_intent_parser(add_event_intent)
if len(sys.argv) > 2: sys.path.insert(0, sys.argv[2]) from adapt.intent import IntentBuilder from adapt.engine import IntentDeterminationEngine engine = IntentDeterminationEngine() schema = json.loads(sys.argv[1]) for entity in schema["entities"]: if entity["type"] == "string": for value in entity["values"]: engine.register_entity(value, entity["name"]) elif entity["type"] == "regex": engine.register_regex_entity(entity["pattern"]) for intent in schema["intents"]: ib = IntentBuilder(intent["name"].encode("utf-8")) for requirement in intent["requirements"]: ib.require(requirement["entity"], requirement["attribute"]) for optional in intent["optionals"]: ib.optionally(optional["entity"], optional["attribute"]) engine.register_intent_parser(ib.build()) if __name__ == "__main__": while True: line = sys.stdin.readline() query = json.loads(line) intents = list(engine.determine_intent(query["input"])) response = {"intents": intents}
class IntentService(object): def __init__(self, bus): self.config = Configuration.get().get('context', {}) self.engine = IntentDeterminationEngine() # Dictionary for translating a skill id to a name self.skill_names = {} # Context related intializations self.context_keywords = self.config.get('keywords', []) self.context_max_frames = self.config.get('max_frames', 3) self.context_timeout = self.config.get('timeout', 2) self.context_greedy = self.config.get('greedy', False) self.context_manager = ContextManager(self.context_timeout) self.bus = bus self.bus.on('register_vocab', self.handle_register_vocab) self.bus.on('register_intent', self.handle_register_intent) self.bus.on('recognizer_loop:utterance', self.handle_utterance) self.bus.on('detach_intent', self.handle_detach_intent) self.bus.on('detach_skill', self.handle_detach_skill) # Context related handlers self.bus.on('add_context', self.handle_add_context) self.bus.on('remove_context', self.handle_remove_context) self.bus.on('clear_context', self.handle_clear_context) # Converse method self.bus.on('skill.converse.response', self.handle_converse_response) self.bus.on('skill.converse.error', self.handle_converse_error) self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse) self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict) def add_active_skill_handler(message): self.add_active_skill(message.data['skill_id']) self.bus.on('active_skill_request', add_active_skill_handler) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills self.waiting_for_converse = False self.converse_result = False self.converse_skill_id = "" def update_skill_name_dict(self, message): """ Messagebus handler, updates dictionary of if to skill name conversions. """ self.skill_names[message.data['id']] = message.data['name'] def get_skill_name(self, skill_id): """ Get skill name from skill ID. Args: skill_id: a skill id as encoded in Intent handlers. Returns: (str) Skill name or the skill id if the skill wasn't found """ return self.skill_names.get(skill_id, skill_id) def reset_converse(self, message): """Let skills know there was a problem with speech recognition""" lang = message.data.get('lang', "en-us") for skill in self.active_skills: self.do_converse(None, skill[0], lang) def do_converse(self, utterances, skill_id, lang): self.waiting_for_converse = True self.converse_result = False self.converse_skill_id = skill_id self.bus.emit( Message("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang })) start_time = time.time() t = 0 while self.waiting_for_converse and t < 5: t = time.time() - start_time time.sleep(0.1) self.waiting_for_converse = False self.converse_skill_id = "" return self.converse_result def handle_converse_error(self, message): skill_id = message.data["skill_id"] if message.data["error"] == "skill id does not exist": self.remove_active_skill(skill_id) if skill_id == self.converse_skill_id: self.converse_result = False self.waiting_for_converse = False def handle_converse_response(self, message): skill_id = message.data["skill_id"] if skill_id == self.converse_skill_id: self.converse_result = message.data.get("result", False) self.waiting_for_converse = False def remove_active_skill(self, skill_id): for skill in self.active_skills: if skill[0] == skill_id: self.active_skills.remove(skill) def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference self.remove_active_skill(skill_id) # add skill with timestamp to start of skill_list self.active_skills.insert(0, [skill_id, time.time()]) def update_context(self, intent): """ Updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent keywords. This is under investigation in adapt, PR pending. Args: intent: Intent to scan for keywords """ for tag in intent['__tags__']: if 'entities' not in tag: continue context_entity = tag['entities'][0] if self.context_greedy: self.context_manager.inject_context(context_entity) elif context_entity['data'][0][1] in self.context_keywords: self.context_manager.inject_context(context_entity) def send_metrics(self, intent, context, stopwatch): """ Send timing metrics to the backend. NOTE: This only applies to those with Opt In. """ ident = context['ident'] if context else None if intent: # Recreate skill name from skill id parts = intent.get('intent_type', '').split(':') intent_type = self.get_skill_name(parts[0]) if len(parts) > 1: intent_type = ':'.join([intent_type] + parts[1:]) report_timing(ident, 'intent_service', stopwatch, {'intent_type': intent_type}) else: report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'intent_failure'}) def handle_utterance(self, message): """ Main entrypoint for handling user utterances with Mycroft skills Monitor the messagebus for 'recognizer_loop:utterance', typically generated by a spoken interaction but potentially also from a CLI or other method of injecting a 'user utterance' into the system. Utterances then work through this sequence to be handled: 1) Active skills attempt to handle using converse() 2) Adapt intent handlers 3) Padatious intent handlers 4) Other fallbacks Args: message (Message): The messagebus data """ # JN: Code borrowed from get_scheduled_event_status() in core.py completed_callback = False completed_status = 'failed' # assume fail def completion_handler(message): #JN nonlocal completed_callback nonlocal completed_status LOG.debug("Calback called: " + message.serialize()) LOG.debug(' type ' + str(type(message))) if message.data is not None: completed_status = message.data['status'] LOG.debug('Completed status is ' + completed_status) completed_callback = True def wait_for_reply(): #JN nonlocal completed_callback num_tries = 0 # wait upto 30 secs. weather takes e.g. 8 seconds LOG.debug('Waiting for reply, completed callback is ' + str(completed_callback)) while completed_callback is False and num_tries < 300: #LOG.info('Sleepiong') time.sleep(0.1) num_tries += 1 LOG.debug('Waited for reply, num_tries is ' + str(num_tries)) LOG.debug(' completed callback is ' + str(completed_callback)) completed_callback = False # for next time try: # Get language of the utterance lang = message.data.get('lang', "en-us") utterances = message.data.get('utterances', '') self.bus.on('skill.handler.complete', completion_handler) #JN: stopwatch doesn't seem to be used, so removed the with stopwatch... stopwatch = Stopwatch() #JN: Give active skills an opportunity to handle the utterance converse = self._converse(utterances, lang) #JN: code moved to here, finishes the converse stuff if converse: # Report that converse handled the intent and return LOG.debug('Converse handling intent') ident = message.context['ident'] if message.context else None report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'converse'}) return # if not converse: - redundant # No conversation, use intent system to handle utterance for intent in self._adapt_intent_match(utterances, lang): # JN uses generator padatious_intent = PadatiousService.instance.calc_intent( utterances[0]) if intent and not (padatious_intent and padatious_intent.conf >= 0.95): # Send the message to the Adapt intent's handler unless # Padatious is REALLY sure it was directed at it instead. reply = message.reply(intent.get('intent_type'), intent) else: # Allow fallback system to handle utterance # NOTE: Padatious intents are handled this way, too LOG.info('Pedatious handing or failure?') reply = message.reply('intent_failure', { 'utterance': utterances[0], 'lang': lang }) LOG.debug('Intent bus call msg ' + reply.serialize()) self.bus.emit(reply) self.send_metrics(intent, message.context, stopwatch) wait_for_reply() if completed_status == 'succeeded': # we are finished now with this utterance LOG.debug('intent succeeded, utterance handled by ' + str(intent)) self.bus.remove('skill.handler.complete', completion_handler) return else: LOG.debug('intent failed, trying next one ' + str(intent)) LOG.info('Intent loop finished') # we couldn't find a successful handler # TODO: a handler that says why the semantics of every intent failed # rather than generic messages reply = message.reply('intent_failure', { 'utterance': utterances[0], 'lang': lang }) self.bus.emit(reply) self.bus.remove('skill.handler.complete', completion_handler) except Exception as e: LOG.exception(e) def _converse(self, utterances, lang): """ Give active skills a chance at the utterance Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: bool: True if converse handled it, False if no skill processes it """ # check for conversation time-out self.active_skills = [ skill for skill in self.active_skills if time.time() - skill[1] <= self.converse_timeout * 60 ] # check if any skill wants to handle utterance for skill in self.active_skills: if self.do_converse(utterances, skill[0], lang): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not self.add_active_skill(skill[0]) return True return False def _adapt_intent_match(self, utterances, lang): """ Run the Adapt engine to search for an matching intent Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: Intent structure, or None if no match was found. """ best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. normal_utterance = normalize(utterance, lang) #best_intent = next(self.engine.determine_intent( # normal_utterance, 100, # include_tags=True, # context_manager=self.context_manager)) # TODO - Should Adapt handle this? # JN changed from next(determine_intent), single value only for best_intent in self.engine.determine_good_intents( normal_utterance, 100, include_tags=True, context_manager=self.context_manager): best_intent['utterance'] = utterance best_intent['retry_on_fail'] = True #JN if best_intent and best_intent.get('confidence', 0.0) > 0.0: best_intent['utterance'] = utterance self.update_context(best_intent) # update active skills skill_id = best_intent['intent_type'].split(":")[0] self.add_active_skill(skill_id) # adapt doesn't handle context injection for one_of keywords # correctly. Workaround this issue if possible. try: best_intent = workaround_one_of_context( best_intent) except LookupError: LOG.error('Error during workaround_one_of_context') yield best_intent # JN except StopIteration: # don't show error in log continue except Exception as e: LOG.exception(e) continue def handle_register_vocab(self, message): start_concept = message.data.get('start') end_concept = message.data.get('end') regex_str = message.data.get('regex') alias_of = message.data.get('alias_of') if regex_str: self.engine.register_regex_entity(regex_str) else: self.engine.register_entity(start_concept, end_concept, alias_of=alias_of) def handle_register_intent(self, message): intent = open_intent_envelope(message) self.engine.register_intent_parser(intent) def handle_detach_intent(self, message): intent_name = message.data.get('intent_name') new_parsers = [ p for p in self.engine.intent_parsers if p.name != intent_name ] self.engine.intent_parsers = new_parsers def handle_detach_skill(self, message): skill_id = message.data.get('skill_id') new_parsers = [ p for p in self.engine.intent_parsers if not p.name.startswith(skill_id) ] self.engine.intent_parsers = new_parsers def handle_add_context(self, message): """ Add context Args: message: data contains the 'context' item to add optionally can include 'word' to be injected as an alias for the context item. """ entity = {'confidence': 1.0} context = message.data.get('context') word = message.data.get('word') or '' origin = message.data.get('origin') or '' # if not a string type try creating a string from it if not isinstance(word, str): word = str(word) entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word entity['origin'] = origin self.context_manager.inject_context(entity) def handle_remove_context(self, message): """ Remove specific context Args: message: data contains the 'context' item to remove """ context = message.data.get('context') if context: self.context_manager.remove_context(context) def handle_clear_context(self, message): """ Clears all keywords from context """ self.context_manager.clear_context()
class EvaIntent(object): """ Wrapper around the adapt intent engine. """ def __init__(self): self.engine = IntentDeterminationEngine() self.results = [] self.entities = {} def add_option(self, intent_name, entity_name, value=None, required=True): """ Method used to add parser values to the intent engine. """ if intent_name not in self.entities: # New intent_name, a new plugin is adding some intents. self.entities[intent_name] = {} if entity_name not in self.entities[intent_name]: if value is None: # Assume regex is not value is provided. self.entities[intent_name][entity_name] = { 'required': required } else: # First time adding value to this entity_name. self.entities[intent_name][entity_name] = { 'values': [value], 'required': required } else: # Already have values on this entity_name, add this one as well. self.entities[intent_name][entity_name]['values'].append(value) # Updated the required value in case it's being overriden. self.entities[intent_name][entity_name]['required'] = required def build(self): """ Builds out all the options specified and compiles them into the intent engine. """ for intent_name, entities in self.entities.items(): new_intent = IntentBuilder(intent_name) for entity_name, data in entities.items(): # If regex entity. if 'values' not in data: self.engine.register_regex_entity(entity_name) # Parse out name in regex (assume between < and >). keyword = entity_name.split('<')[1].split('>')[0] if data['required']: new_intent = new_intent.require(keyword) else: new_intent = new_intent.optionally(keyword) else: for value in data['values']: self.engine.register_entity(value, entity_name) if data['required']: new_intent = new_intent.require(entity_name) else: new_intent = new_intent.optionally(entity_name) new_intent = new_intent.build() self.engine.register_intent_parser(new_intent) def compile_results(self, text): """ Feed the input_text to the intent engine and order the results by confidence. """ results = [] for intent in self.engine.determine_intent(text): results.append(intent) # Sort by confidence, descending. self.results = sorted(results, key=itemgetter('confidence'), reverse=True) def best_intent(self): """ Returns the intent with the highest confidence. """ if len(self.results) > 0: return self.results[0] return None def all_intents(self): """ Returns all intents found by the intent engine. """ return self.results