Beispiel #1
0
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
Beispiel #2
0
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))
Beispiel #3
0
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