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 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 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