Example #1
0
def test_intent_relations__no_relations():
    @dataclass
    class NoRelationsIntent(Intent):
        pass

    result = intent_relations(NoRelationsIntent)
    assert isinstance(result, IntentRelationMap)
    assert result.follow == []
Example #2
0
def _build_need_context_set(agent_cls: type(Agent)) -> Set[Intent]:
    """
    Return a list of intents that need to spawn a context, based on their
    relations:

    * If intent *B* follows intent *A*, then intent *A* needs to spawn a context
    """
    result = set()
    for intent in agent_cls.intents:
        related = intent_relations(intent)
        for rel in related.follow:
            result.add(rel.target_cls)
    return result
Example #3
0
def test_intent_relations__follow():
    @dataclass
    class FollowedIntent(Intent):
        pass

    @dataclass
    class FollowingIntent(Intent):
        parent_followed_intent: FollowedIntent = follow()

    result = intent_relations(FollowingIntent)
    expected = [
        FollowIntentRelation(
            relation_parameters=FollowRelationParameters(new_lifespan=None),
            field_name='parent_followed_intent',
            source_cls=FollowingIntent,
            target_cls=FollowedIntent)
    ]
    assert result.follow == expected
    assert result.follow[0].relation_type == RelationType.FOLLOW
Example #4
0
    def _df_body_to_intent(
            self,
            df_body: PredictionBody,
            build_related_cls: Type[Intent] = None,
            visited_intents: Set[Type[Intent]] = None) -> Intent:
        """
        Convert a Dialogflow prediction response into an instance of
        :class:`Intent`.
        
        This method is recursive on intent relations. When an intent has a
        :meth:`~intents.model.relations.follow` field, that field must be filled
        with an instance of the followed intent; in this case
        :meth:`_df_body_to_intent` will call itself passing the parent intent
        class as `build_related_cls`, to force building that intent from the
        same `df_body`; contexts and parameters will be checked for consistency.

        Args:
            df_body: A Dialogflow Response
            build_related_cls: Force to build the related intent instead of
                the predicted one
            visited_intents: This is used internally to prevent recursion loops
        """
        if not visited_intents:
            visited_intents = set()

        contexts, context_parameters = df_body.contexts()

        # Slot filling in progress
        # TODO: also check queryResult.cancelsSlotFilling
        # if "__system_counters__" in contexts:
        if not df_body.queryResult.allRequiredParamsPresent:
            logger.warning(
                "Prediction doesn't have values for all required parameters. "
                "Slot filling may be in progress, but this is not modeled yet: "
                "Intent object will be None")
            return None

        if build_related_cls:
            # TODO: adjust lifespan
            intent_cls = build_related_cls
            df_parameters = {
                p_name: p.value
                for p_name, p in context_parameters.items()
                if p_name in intent_cls.parameter_schema
            }
        else:
            intent_name = df_body.intent_name
            intent_cls: Intent = self.agent_cls._intents_by_name.get(
                intent_name)
            if not intent_cls:
                raise ValueError(
                    f"Prediction returned intent '{intent_name}', " +
                    "but this was not found in Agent definition. Make sure to restore a latest "
                    +
                    "Agent export from `services.dialogflow_es.export.export()`. If the problem "
                    + "persists, please file a bug on the Intents repository.")
            df_parameters = df_body.intent_parameters

        visited_intents.add(intent_cls)
        parameter_dict = deserialize_intent_parameters(df_parameters,
                                                       intent_cls,
                                                       self.entity_mappings)
        related_intents_dict = {}
        for rel in intent_relations(intent_cls).follow:
            if rel.target_cls in visited_intents:
                raise ValueError(
                    f"Loop detected: {rel.target_cls} was already visited. Make sure "
                    "your Agent has no circular dependencies")
            related_intent = self._df_body_to_intent(df_body, rel.target_cls,
                                                     visited_intents)
            related_intent.lifespan = df_body.context_lifespans.get(
                df_names.context_name(rel.target_cls), 0)

            related_intents_dict[rel.field_name] = related_intent

        result = intent_cls(**parameter_dict, **related_intents_dict)
        result.lifespan = df_body.context_lifespans.get(
            df_names.context_name(intent_cls), 0)
        return result