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
class ContextManagerIntegrationTest(unittest.TestCase): def setUp(self): self.context_manager = ContextManager() self.engine = IntentDeterminationEngine() def testBasicContextualFollowup(self): intent1 = IntentBuilder("TimeQueryIntent")\ .require("TimeQuery")\ .require("Location")\ .build() intent2 = IntentBuilder("WeatherQueryIntent")\ .require("WeatherKeyword")\ .require("Location")\ .build() self.engine.register_intent_parser(intent1) self.engine.register_intent_parser(intent2) self.engine.register_entity("what time is it", "TimeQuery") self.engine.register_entity("seattle", "Location") self.engine.register_entity("miami", "Location") self.engine.register_entity("weather", "WeatherKeyword") utterance1 = "what time is it in seattle" intent = next(self.engine.determine_intent(utterance1, include_tags=True, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == 'TimeQueryIntent' assert '__tags__' in intent for tag in intent['__tags__']: context_entity = tag.get('entities')[0] self.context_manager.inject_context(context_entity) utterance2 = "what's the weather like?" intent = next(self.engine.determine_intent(utterance2, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == 'WeatherQueryIntent' def testContextOnlyUsedOnce(self): intent_parser = IntentBuilder("DummyIntent")\ .require("Foo")\ .optionally("Foo", "Foo2")\ .build() context_entity = {'confidence': 1.0, 'data': [('foo', 'Foo')], 'match': 'foo', 'key': 'foo'} self.context_manager.inject_context(context_entity) self.engine.register_intent_parser(intent_parser) self.engine.register_entity("foo", "Foo") self.engine.register_entity("fop", "Foo") intent = next(self.engine.determine_intent("foo", include_tags=True, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == "DummyIntent" assert not (intent.get("Foo") and intent.get("Foo2"))
class ContextManagerIntegrationTest(unittest.TestCase): def setUp(self): self.context_manager = ContextManager() self.engine = IntentDeterminationEngine() def testBasicContextualFollowup(self): intent1 = IntentBuilder("TimeQueryIntent")\ .require("TimeQuery")\ .require("Location")\ .build() intent2 = IntentBuilder("WeatherQueryIntent")\ .require("WeatherKeyword")\ .require("Location")\ .build() self.engine.register_intent_parser(intent1) self.engine.register_intent_parser(intent2) self.engine.register_entity("what time is it", "TimeQuery") self.engine.register_entity("seattle", "Location") self.engine.register_entity("miami", "Location") self.engine.register_entity("weather", "WeatherKeyword") utterance1 = "what time is it in seattle" intent = next(self.engine.determine_intent(utterance1, include_tags=True, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == 'TimeQueryIntent' assert '__tags__' in intent for tag in intent['__tags__']: context_entity = tag.get('entities')[0] self.context_manager.inject_context(context_entity) utterance2 = "what's the weather like?" intent = next(self.engine.determine_intent(utterance2, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == 'WeatherQueryIntent' def testContextOnlyUsedOnce(self): intent_parser = IntentBuilder("DummyIntent")\ .require("Foo")\ .optionally("Foo", "Foo2")\ .build() context_entity = {'confidence': 1.0, 'data': [('foo', 'Foo')], 'match': 'foo', 'key': 'foo'} self.context_manager.inject_context(context_entity) self.engine.register_intent_parser(intent_parser) self.engine.register_entity("foo", "Foo") self.engine.register_entity("fop", "Foo") intent = next(self.engine.determine_intent("foo", include_tags=True, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == "DummyIntent" assert not (intent.get("Foo") and intent.get("Foo2"))
class SmartHomeIntentParser(): def __init__(self): self.engine = IntentDeterminationEngine() # smart home intent vocabalory switch_tasks_keyword = ["switch", "turn"] for stk in switch_tasks_keyword: self.engine.register_entity(stk, "SwitchTasksKeyword") on_off_keyword = ["on", "off"] for stk in on_off_keyword: self.engine.register_entity(stk, "OnOffKeyword") equipment_keyword = ["lights", "light", "fan"] for stk in equipment_keyword: self.engine.register_entity(stk, "EquipmentKeyword") smart_home_intent = IntentBuilder("SmartHomeIntent")\ .require("SwitchTasksKeyword")\ .require("OnOffKeyword")\ .require("EquipmentKeyword")\ .build() self.engine.register_intent_parser(smart_home_intent) def parse(self, sentence): for intent in self.engine.determine_intent(sentence): if intent.get('confidence') > 0: return 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 IntentSkill(MycroftSkill): def __init__(self): MycroftSkill.__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): utterances = message.data.get('utterances', '') best_intent = None for utterance in utterances: try: best_intent = next(self.engine.determine_intent( utterance, 100)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration, 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]})) else: self.emitter.emit( Message("multi_utterance_intent_failure", {"utterances": utterances}))
class JokeIntentParser(): def __init__(self): self.engine = IntentDeterminationEngine() joke_verbs = [ "tell", "crack" ] for mv in joke_verbs: self.engine.register_entity(mv, "JokeVerb") joke_keywords = [ "joke" ] for mk in joke_keywords: self.engine.register_entity(mk, "JokeKeyword") joke_intent = IntentBuilder("JokeIntent")\ .require("JokeVerb")\ .optionally("JokeKeyword")\ .build() self.engine.register_intent_parser(joke_intent) def parse(self, sentence): for intent in self.engine.determine_intent(sentence): if intent.get('confidence') > 0: return intent
def testContextAndOneOf(self): # test to cover https://github.com/MycroftAI/adapt/issues/86 engine = IntentDeterminationEngine() context_manager = ContextManager() # define vocabulary weather_keyword = ["weather"] for wk in weather_keyword: engine.register_entity(wk, "WeatherKeyword") # structure intent weather_intent = IntentBuilder("WeatherIntent") \ .require("WeatherKeyword") \ .one_of("Location", "LocationContext").build() engine.register_intent_parser(weather_intent) word = 'lizard' context = 'LocationContext' entity = {} entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word context_manager.inject_context(entity) intents = list( engine.determine_intent('weather', context_manager=context_manager)) self.assertEqual(1, len(intents), "Incorrect number of intents") result = intents[0] self.assertEqual("lizard", result.get("LocationContext"), "Context not matched") self.assertEqual(0.75, result.get('confidence'), "Context confidence not properly applied.")
class IntentSkill(MycroftSkill): def __init__(self): MycroftSkill.__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): utterances = message.metadata.get('utterances', '') best_intent = None for utterance in utterances: try: best_intent = next(self.engine.determine_intent(utterance, num_results=100)) best_intent['utterance'] = utterance # TODO - Should Adapt handle this? except StopIteration, e: continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: reply = message.reply(best_intent.get('intent_type'), metadata=best_intent) self.emitter.emit(reply) elif len(utterances) == 1: self.emitter.emit(Message("intent_failure", metadata={"utterance": utterances[0]})) else: self.emitter.emit(Message("multi_utterance_intent_failure", metadata={"utterances": utterances}))
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
class input_engine: """Manages the intent engine and natural language input parser""" def __init__(self): self.engine = IntentDeterminationEngine() def register_entity(self, keywords, name): """Registers an intenty to be found in an input""" for k in keywords: self.engine.register_entity(k, name) def register_intent(self, intent): """Registers an intent that can be found in an input""" self.engine.register_intent_parser(intent) def get_intent(self, input_string): """Returns an intent from an input string if one is found""" intent = self.engine.determine_intent(input_string) for intent in self.engine.determine_intent(input_string): if intent.get("confidence") > 0: return intent return None
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 IntentSkill(MycroftSkill): def __init__(self): MycroftSkill.__init__(self, name="IntentSkill") self.engine = IntentDeterminationEngine() self.reload_skill = False 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): # 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, 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 }))
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, 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 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 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
def get_intent(message): engine = IntentDeterminationEngine() keywords = [ 'service', 'med', 'clinic', 'walk in', ] for key in keywords: engine.register_entity(key, "KeyWords") print(os.getcwd()) with open(os.getcwd() + '/home/addresses.csv', 'rb') as csvfile: records = csv.reader(csvfile, delimiter=',') street_number = [] street_name = [] for row in records: street_number.append(row[8]) street_name.append(row[11]) for key in street_number: engine.register_entity(key, "StreetNumber") for key in street_name: engine.register_entity(key, "StreetName") address_intent = IntentBuilder("AddressIntent")\ .require("KeyWords")\ .optionally("StreetNumber")\ .optionally("StreetName")\ .build() engine.register_intent_parser(address_intent) for intent in engine.determine_intent(''.join(message)): return intent
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()
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 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) 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 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] }
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}))
class ContextManagerIntegrationTest(unittest.TestCase): def setUp(self): self.context_manager = ContextManager() self.engine = IntentDeterminationEngine() def testBasicContextualFollowup(self): intent1 = IntentBuilder("TimeQueryIntent")\ .require("TimeQuery")\ .require("Location")\ .build() intent2 = IntentBuilder("WeatherQueryIntent")\ .require("WeatherKeyword")\ .require("Location")\ .build() self.engine.register_intent_parser(intent1) self.engine.register_intent_parser(intent2) self.engine.register_entity("what time is it", "TimeQuery") self.engine.register_entity("seattle", "Location") self.engine.register_entity("miami", "Location") self.engine.register_entity("weather", "WeatherKeyword") utterance1 = "what time is it in seattle" intent = next( self.engine.determine_intent(utterance1, include_tags=True, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == 'TimeQueryIntent' assert '__tags__' in intent for tag in intent['__tags__']: context_entity = tag.get('entities')[0] self.context_manager.inject_context(context_entity) utterance2 = "what's the weather like?" intent = next( self.engine.determine_intent(utterance2, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == 'WeatherQueryIntent' def testContextOnlyUsedOnce(self): intent_parser = IntentBuilder("DummyIntent")\ .require("Foo")\ .optionally("Foo", "Foo2")\ .build() context_entity = { 'confidence': 1.0, 'data': [('foo', 'Foo')], 'match': 'foo', 'key': 'foo' } self.context_manager.inject_context(context_entity) self.engine.register_intent_parser(intent_parser) self.engine.register_entity("foo", "Foo") self.engine.register_entity("fop", "Foo") intent = next( self.engine.determine_intent("foo", include_tags=True, context_manager=self.context_manager)) assert intent assert intent['intent_type'] == "DummyIntent" assert not (intent.get("Foo") and intent.get("Foo2")) def testContextAndOneOf(self): # test to cover https://github.com/MycroftAI/adapt/issues/86 engine = IntentDeterminationEngine() context_manager = ContextManager() # define vocabulary weather_keyword = ["weather"] for wk in weather_keyword: engine.register_entity(wk, "WeatherKeyword") # structure intent weather_intent = IntentBuilder("WeatherIntent") \ .require("WeatherKeyword") \ .one_of("Location", "LocationContext").build() engine.register_intent_parser(weather_intent) word = 'lizard' context = 'LocationContext' entity = {} entity['data'] = [(word, context)] entity['match'] = word entity['key'] = word context_manager.inject_context(entity) intents = list( engine.determine_intent('weather', context_manager=context_manager)) self.assertEqual(1, len(intents), "Incorrect number of intents") result = intents[0] self.assertEqual("lizard", result.get("LocationContext"), "Context not matched") self.assertEqual(0.75, result.get('confidence'), "Context confidence not properly applied.")
if text.startswith("b'update"): 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()) else: for intent in engine.determine_intent(" " + text): if intent.get('confidence') > 0: client.send( str(json.dumps(intent, indent=4)).encode(encoding='utf_8', errors='strict')) client.close() server.close()
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
# 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') h1 = Handler.Handler(intents[0], f1) h1.compute() #
self.intent = 'courtesyIntent' # define vocabulary self.courtesy_keyword = [ "bonjour" ] # structure intent self.courtesy_intent = IntentBuilder(self.intent)\ .require("CourtesyKeyword")\ .build() def register(self, engine): for wk in self.courtesy_keyword: engine.register_entity(wk, "courtesyKeyword") engine.register_intent_parser(self.courtesy_intent) def process(self, json): result = sympy.sympify(json.get('CourtesyKeyword')) if json.get('CourtesyKeyword') == "bonjour": return "Bonjour monsieur" if __name__ == "__main__": engine = IntentDeterminationEngine() skill = courtesySkill() skill.register(engine) for intent in engine.determine_intent(' '.join(sys.argv[1:])): if intent and intent.get('confidence') > 0: print(json.dumps(intent, indent=4))
class CrmnextChatBot: def __init__(self): """ Init for logger Intent engine Corpus """ self.engine = IntentDeterminationEngine() self.bot = BotLogger() self.cr = BotNextCorpus() self.first_time = True self.intent = [] self.train_data = {'intent': "", 'user_text': "", 'bot_response': ""} self.train_intent() self.senti = SentimentIntensityAnalyzer() def train_intent(self): """ Training intent engine to classify text intent :return: """ for k in self.cr.train_intent_list.keys(): for w in self.cr.train_intent_list[k]: self.engine.register_entity(w, k) for w in self.cr.train_intent.keys(): self.intent.append(IntentBuilder(w) \ .require(self.cr.train_intent[w]) \ .build()) for obj in self.intent: self.engine.register_intent_parser(obj) def run_bot(self, conv): """ Thread starts from here. :return: """ self.bot.log_debug("Received Json in run_bot: {}".format(conv)) msg = conv["user_text"] isFirst = True if conv["intent_type"] == "" else False if self.sent(msg): if isFirst: try: intent = json.loads(self.intent_parser(msg)) except Exception: intent = { "intent_type": "", "Card": "", "CardLost": "", "target": "", "confidence": 0.0 } conv["intent_type"] = intent["intent_type"] return self.p_flow(self.cr.chat_data, conv) else: return self.p_flow(self.cr.chat_data, conv) else: return self.neg_res(self.cr.chat_data, conv) def intent_parser(self, conv): """ Method to get intent of user text. :param conv: user data :return: """ for intent in self.engine.determine_intent(conv): if intent.get('confidence') > 0: return json.dumps(intent, indent=4) else: pass def p_flow(self, corpus, ud): """ Method to implement process for user. :param corpus: It provides data for chatbot :param ud: User related data. :return: response """ response = dict(userId="123", user_intent="", response_text="", card_type="", recommendation=[], entities=[], user_stage=0) self.bot.log_debug("User Data: {}".format(ud)) p_data = corpus[ud['intent_type']][ud['user_stage']] user_txt = self.clean_text(ud['user_text']) if user_txt in p_data.keys(): response['response_text'] = random.choice(p_data[user_txt]) response['card_type'] = "" elif user_txt in p_data['user_text']: print("p_data: >>" + str(p_data)) response = self.update_res(response, p_data) elif user_txt == 'quit': q_data = corpus['EXIT'][0] response['user_intent'] = q_data['intent_type'] response['user_stage'] = q_data['user_stage'] response['response_text'] = random.choice(q_data['response']) response['card_type'] = q_data['card_type'] else: self.build_train_cr(ud) if 'default_speech' in p_data.keys( ) and p_data['default_speech'] is not '': response['response_text'] = random.choice( p_data['default_speech']) else: response[ 'response_text'] = "Please select below option to move forward" response['card_type'] = "#IntentOptions" response['user_stage'] = ud['user_stage'] response['user_intent'] = ud['intent_type'] if len(p_data['recommendation']) == 0: response['recommendation'] = self.cr.all_skills else: response['recommendation'] = p_data['recommendation'] print("response: " + str(response)) return response def update_res(self, response, p_data): """ :param response: :param p_data: :return: """ response['response_text'] = random.choice(p_data['response']) response['user_intent'] = p_data['intent_type'] response['card_type'] = p_data['card_type'] response['user_stage'] = p_data['user_stage'] response['entities'] = p_data['entities'] return response def neg_res(self, cr_data, conv): """ Generate response if sentiment is -ve :param cr_data: :param conv: :return: """ neg_data = cr_data["neg_sent"][0] return dict(user_intent="", response_text=random.choice(neg_data["response"]), user_stage=0, card_type='', recommendation=[]) def sent(self, conv): """ Polarity is considered pos if > -0.2 (for tuning our scenario) :param conv: :return: """ t = TextBlob(conv) vs = self.senti.polarity_scores(conv) return False if t.polarity < -0.3 else True def clean_text(self, statement): """ Remove any consecutive whitespace characters from the statement text. """ return re.sub(r'[^a-zA-Z0-9 ]', r'', statement).rstrip().lower() def build_train_cr(self, ud): intent = ud['intent_type'] if intent is not '': self.train_data['intent'] = intent self.train_data['user_text'] = ud['user_text'] else: self.train_data['intent'] = "" self.train_data['user_text'] = ud['user_text'] with open('train_data.json', 'a') as f: json.dump(self.train_data, f, indent=True) # bot = CrmnextChatBot() # user_stage = 0 # intent = '' # print(bot.clean_text("Hi I am #avinash ? ")) # while True: # # text = input("Enter text: ") # reconnect = input('re-connect: ') # print(bool(reconnect)) # data = {'userId': '123', "intent_type":intent, "user_text": text, 'user_name': 'Avinash Gaur', # 'contactNumber': '89892398128', 'cardCount': 2, 'user_stage': user_stage, 're_connect': bool(reconnect)} # print(data) # print(user_stage) # print(intent) # d = bot.run_bot(data) # intent = d['user_intent'] # user_stage = d['user_stage'] # # # print(d)
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 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 = ConfigurationManager.get().get('context', {}) self.engine = IntentDeterminationEngine() self.context_keywords = self.config.get('keywords', ['Location']) 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, 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 }))
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} print(json.dumps(response)) sys.stdout.flush()
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()
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
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 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 IntentService(object): def __init__(self, emitter): self.config = ConfigurationManager.get().get('context', {}) self.engine = IntentDeterminationEngine() self.context_keywords = self.config.get('keywords', ['Location']) 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, 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 }))
engine.register_regex_entity(regex) for regex in location_regex_keywords: engine.register_regex_entity(regex) for regex in with_regex_keywords: engine.register_regex_entity(regex) # construt an intent parser add_event_intent = IntentBuilder('EventIntent') \ .require('AddKeyword') \ .require('EventKeyword') \ .require('Personkeyword') \ .optionally('LocationKeyword') \ .optionally('Date') \ .build() engine.register_intent_parser(add_event_intent) x='Add event with Hana Bouzid in Midoune Room starts 10 am' for intent in engine.determine_intent(x): clist=[] alist = x.split("with ") print(alist) blist=alist[1].split(" in") lliste=blist[1].split("starts") print(blist) print(lliste) if("and")in blist[0]: clist = blist[0].split("and") print(clist) # liste des attendees cités dans x print(type(clist)) else : clist.append(blist[0]) print(clist) #liste des attendees cités dans x print(type(clist))
class VoiceCommands: ACTIONS = 'Action' NAME = 'Name' ACTUATOR_TYPE = 'ActuatorType' @typechecked() def __init__(self, job_controll: AsyncActuatorCommands, logging: RootLogger): self.__job_controll = job_controll self.__logging = logging def configure(self): self.__engine = IntentDeterminationEngine() actions = ['on', 'off'] self.__register_entity(actions, self.ACTIONS) locations = ['living', 'kitchen', 'hollway', 'wemo'] self.__register_entity(locations, self.NAME) actuator_types = ['light', 'switch', 'courtains', 'door'] self.__register_entity(actuator_types, self.ACTUATOR_TYPE) actuator_intent = IntentBuilder("ActuatorIntent") \ .require(self.ACTIONS) \ .require(self.ACTUATOR_TYPE) \ .require(self.NAME) \ .build() self.__engine.register_intent_parser(actuator_intent) self.__commands_map = [ { 'entities': { 'name': 'living', 'type': 'light' }, 'actuator': 'livingLight' }, { 'entities': { 'name': 'living', 'type': 'courtains' }, 'actuator': 'livingCourtains' }, { 'entities': { 'name': 'hollway', 'type': 'light' }, 'actuator': 'holwayLight' }, { 'entities': { 'name': 'wemo', 'type': 'switch' }, 'actuator': 'wemoSwitch1' }, ] return self @typechecked() def execute(self, command: str) -> None: command = self.__normalize_command(command) for intent in self.__engine.determine_intent(command): if intent and intent.get('confidence') > 0: self.__logging.info(intent) command = self.__get_matching_command(intent) self.__run_command(command, intent) def __register_entity(self, wordlist, name): for action in wordlist: self.__engine.register_entity(action, name) def __get_matching_command(self, intent): for command in self.__commands_map: if command['entities']['name'] == intent[self.NAME] and \ command['entities']['type'] == intent[self.ACTUATOR_TYPE]: return command def __run_command(self, command, intent): if None == command: return actuator_state = (False, True)[intent[self.ACTIONS] == 'on'] self.__logging.info('Changin actuator {0} value: {1}'.format( command['actuator'], actuator_state)) self.__job_controll.change_actuator(command['actuator'], actuator_state) def __normalize_command(self, command): replaces = [('life', 'light'), ('leaving', 'living'), ('hallway', 'hollway'), ('quarters', 'courtains')] for replace in replaces: command = re.sub(replace[0], replace[1], command) return command
class Brain(AppDaemon): def initialize(self): self.morph = pymorphy2.MorphAnalyzer() self.engine = IntentDeterminationEngine() self.context_managers = {} self.last_request = {} def query(self, query, connector): nquery = self.normalize(query) if not self.context_managers.get(connector): self.context_managers[connector] = ContextManager() if not self.last_request.get(connector): self.last_request[connector] = datetime.datetime.now() last_rq_delta = datetime.datetime.now() - self.last_request[connector] if last_rq_delta.seconds > 300: # resetting context, if last conversation was more than 15 minutes # ago self.context_managers[connector].frame_stack = [] message = "Прости, что то пошло не так" for intent in self.engine.determine_intent( nquery, include_tags=True, context_manager=self.context_managers[connector]): # after enabling context management, it can become more important if intent and intent.get('confidence') > 0: if self.get_app(intent["intent_type"]).context_sensitive: for tag in intent["__tags__"]: context_entity = tag.get('entities')[0] if self.get_app(intent["intent_type"]).context_blacklist\ and context_entity['data'][0][1]\ in self.get_app(intent["intent_type"]).context_blacklist: continue self.context_managers[connector].inject_context( context_entity) message = self.get_app(intent["intent_type"]).handle(intent) self.last_request[connector] = datetime.datetime.now() # Some code for self-analyze self.get_app("analysis").add_entry( connector, intent["intent_type"], query, len(self.context_managers[connector].frame_stack)) self.get_app(connector).answer(message) def normalize(self, text): """Remove punctuation, set all words to normal form""" nwords = [] text = re.sub('\?|\!|\.|\,', '', text).lower() for word in text.split(' '): nword = self.morph.parse(word)[0].normal_form nwords.append(nword) nwords = " ".join(nwords) nwords = text_to_number(nwords) return nwords def read_dialog_files(self, dialog_file): """ Reads file with phrases and returns list of them """ dialog = [] with open(dialog_file, 'r') as d: for line in d: dialog.append(line[:-1]) return dialog def my_name(self): names = ["Jumper"] return random.choice(names) def greeting(self): phrases = [ "Голосовой интерфейс активирован", "%s, я тут!", "Привет, %s!" ] phrase = random.choice(phrases) try: return phrase % self.my_name() except TypeError: return phrase def dont_understand(self): phrases = [ "Прости, %s, я не поняла", "Повтори пожалуйста", "Прости, что?" ] phrase = random.choice(phrases) try: return phrase % self.my_name() except TypeError: return phrase
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()
"third eye blind", "the who", "the clash", "john mayer", "kings of leon", "adelle" ] for a in artists: engine.register_entity(a, "Artist") music_verbs = ["listen", "hear", "play"] for mv in music_verbs: engine.register_entity(mv, "MusicVerb") music_keywords = ["songs", "music"] for mk in music_keywords: engine.register_entity(mk, "MusicKeyword") music_intent = IntentBuilder("MusicIntent")\ .require("MusicVerb")\ .optionally("MusicKeyword")\ .optionally("Artist")\ .build() engine.register_intent_parser(weather_intent) engine.register_intent_parser(music_intent) if __name__ == "__main__": for intent in engine.determine_intent(' '.join(sys.argv[1:])): if intent and intent.get('confidence') > 0: print(json.dumps(intent, indent=4))