Source code for intents.model.agent

"""
The :class:`Agent` base class is the entry point of your project. You will
subclass it when defining your own Agent, and will later :meth:`Agent.register`
Intent classes and other resources into it.

Once the Agent is defined, you will connect it to a cloud service with a
:class:`Connector`, to make prediction and trigger requests.
"""

import re
import logging
import inspect
from types import ModuleType
from typing import List, Dict, Union
from dataclasses import dataclass

from intents.model.intent import Intent, _IntentMetaclass
from intents.model.entity import EntityMixin, SystemEntityMixin, _EntityMetaclass
from intents.model.context import Context, _ContextMetaclass
from intents.model.event import _EventMetaclass
from intents import language

logger = logging.getLogger(__name__)

class _AgentMetaclass(type):

    languages: List[language.LanguageCode] = None

    def __new__(cls, name, bases, dct):
        result_cls = super().__new__(cls, name, bases, dct)
        
        if name == 'Agent':
            assert not bases
            return result_cls

        if not result_cls.languages:
            result_cls.languages = language.agent_supported_languages(result_cls)

        languages = []
        for lan in result_cls.languages:
            if isinstance(lan, language.LanguageCode):
                languages.append(lan)
            elif isinstance(lan, str):
                languages.append(language.LanguageCode(lan))
            else:
                raise ValueError(f"Unsupported language '{lan}' for Agent '{result_cls}'. Must be a value of 'intents.LanguageCode'")
        result_cls.languages = languages

        return result_cls

[docs]class Agent(metaclass=_AgentMetaclass): """ As the name suggests, Agent is the base class that models an Agent definition within the *Intents* framework. Typically, you will define a single Agent class in your project, that could be as simple as this: .. code-block:: python from intents import Agent class MyAgent(Agent): \"\"\"A little docstring for your Agent\"\"\" You can optionally define the languages that you intend to support. *Intents* will look for language resources based on the `language` class variable: .. code-block:: python class MyAgent(Agent): \"\"\"A little docstring for your Agent\"\"\" languages = ["en", "it"] Languages are values from :class:`intents.language.LanguageCode`. If omitted, *Intents* will discover language resources by itself. You won't do much more with your *Agent* class, other than registering intents and resources with :meth:`Agent.register`, or passing it to a :class:`Connector` to make predictions. """ languages: List[language.LanguageCode] = None intents: List[Intent] = None _intents_by_name: Dict[str, Intent] = None _intents_by_event: Dict[str, Intent] = None _entities_by_name: Dict[str, _EntityMetaclass] = None _contexts_by_name: Dict[str, _ContextMetaclass] = None _events_by_name: Dict[str, _EventMetaclass] = None
[docs] @classmethod def register(cls, resource: Union[_IntentMetaclass, ModuleType]): """ Register the given resource in Agent. The resource could be: * An :class:`Intent` * A module. In this case, the module is scanned (non recursively) for Intents, and each Intent is added individually This is how you register a **single intent**: .. code-block:: python from intents import Agent, Intent class MyAgent(Agent): pass @dataclass class my_test_intent(Intent): \"\"\"A little docstring for my Intent...\"\"\" a_parameter: str another_parameter: str MyAgent.register(my_test_intent) Alternatively, you can register a **whole module** containing Intents. This is how you register all the intents that are defined in the `smalltalk` module of `example_agent`: .. code-block:: python from example_agent import smalltalk class MyAgent(Agent): pass MyAgent.register(smalltalk) Note that together with each Intent, the Agent will register all of its linked resources, such as Entities, Events and Contexts. :param resource: The resource to register (an Intent, or a module containing Intents) """ if isinstance(resource, _IntentMetaclass): cls._register_intent(resource) elif isinstance(resource, ModuleType): for member_name, member in inspect.getmembers(resource, inspect.isclass): if member.__module__ == resource.__name__ and issubclass(member, Intent): cls._register_intent(member)
@classmethod def _register_intent(cls, intent_cls: _IntentMetaclass): """ Register a single intent in the Agent class and check that language data is present for all supported languages (examples and responses). """ if not cls.intents or not cls._intents_by_name and not cls._intents_by_event: assert not cls.intents and not cls._intents_by_name and not cls._intents_by_event assert not cls._contexts_by_name and not cls._events_by_name cls.intents = [] cls._intents_by_name = {} cls._intents_by_event = {} cls._entities_by_name = {} cls._contexts_by_name = {} cls._events_by_name = {} if cls._intents_by_name.get(intent_cls.name): raise ValueError(f"Another intent exists with name {intent_cls.name}: {cls._intents_by_name[intent_cls.name]}") # TODO: check conflicting events # event_name = Agent._event_name(name) # if conflicting_intent := cls._intents_by_event.get(event_name): # raise ValueError(f"Intent name {name} is ambiguous and clashes with {conflicting_intent} ('{conflicting_intent.metadata.name}')") language.intent_language_data(cls, intent_cls) # TODO: Agent languages only for context_cls in intent_cls.input_contexts: cls._register_context(context_cls) for context in intent_cls.output_contexts: cls._register_context(context) for event_cls in intent_cls.events: cls._register_event(event_cls, intent_cls) for param_name, param_metadata in intent_cls.parameter_schema().items(): cls._register_entity(param_metadata.entity_cls, param_name, intent_cls.name) cls.intents.append(intent_cls) cls._intents_by_name[intent_cls.name] = intent_cls cls._intents_by_event[intent_cls.events[0].name] = intent_cls @classmethod def _register_entity(cls, entity_cls: _EntityMetaclass, parameter_name: str, intent_name: str): if not issubclass(entity_cls, EntityMixin): raise ValueError(f"Invalid type '{entity_cls}' for parameter '{parameter_name}' in Intent '{intent_name}': must be an Entity. Try system entities such as 'intents.Sys.Integer', or define your own custom entity.") if issubclass(entity_cls, SystemEntityMixin): return existing_cls = cls._entities_by_name.get(entity_cls.name) if not existing_cls: language.entity_language_data(cls, entity_cls) # Checks that language data is existing and consistent cls._entities_by_name[entity_cls.name] = entity_cls return if id(entity_cls) != id(existing_cls): existing_cls_path = f"{existing_cls.__module__}.{existing_cls.__qualname__}" entity_cls_path = f"{entity_cls.__module__}.{entity_cls.__qualname__}" raise ValueError(f"Two different Entity classes exist with the same name: '{existing_cls_path}' and '{entity_cls_path}'") @classmethod def _register_context(cls, context_obj_or_cls: Union[_ContextMetaclass, Context]): if isinstance(context_obj_or_cls, Context): context_cls = context_obj_or_cls.__class__ elif inspect.isclass(context_obj_or_cls) and issubclass(context_obj_or_cls, Context): context_cls = context_obj_or_cls else: raise ValueError(f"Context {context_obj_or_cls} is not a Context instance or subclass") existing_cls = cls._contexts_by_name.get(context_cls.name) if not existing_cls: cls._contexts_by_name[context_cls.name] = context_cls return if id(context_cls) != id(existing_cls): existing_cls_path = f"{existing_cls.__module__}.{existing_cls.__qualname__}" context_cls_path = f"{context_cls.__module__}.{context_cls.__qualname__}" raise ValueError(f"Two different Context classes exist with the same name: '{existing_cls_path}' and '{context_cls_path}'") @classmethod def _register_event(cls, event_cls: _EventMetaclass, intent_cls: _IntentMetaclass): existing_cls = cls._events_by_name.get(event_cls.name) if not existing_cls: cls._events_by_name[event_cls.name] = event_cls cls._intents_by_event[event_cls.name] = intent_cls return if id(existing_cls) != id(event_cls): existing_cls_path = f"{existing_cls.__module__}.{existing_cls.__qualname__}" event_cls_path = f"{event_cls.__module__}.{event_cls.__qualname__}" raise ValueError(f"Two different Event classes exist with the same name: '{existing_cls_path}' and '{event_cls_path}'") # TODO: model different intents with same event and different input # context (ok) vs. different intents with same event and same input # context (not ok). existing_intent = cls._intents_by_event[event_cls.name] raise ValueError(f"Event '{event_cls.name}' is alreadt associated to Intent '{existing_intent}'. An Event can only be associated with 1 intent. (differenciation by input contexts is not supported yet)") # def save_session(self): # """ # Store the current session (most importantly, the list of active # contexts) to a persisted storage. # """ # raise NotImplementedError("Context persistence is unsupported yet") # def load_session(self): # """ # Load session information (most importantly, the list of active contexts), # in a format that can be used by :meth:`Agent.predict` to restore the # state before prediction. # """ # raise NotImplementedError("Context persistence is unsupported yet") def _pylint_hack(self): raise NotImplementedError() @staticmethod def _event_name(intent_name: str) -> str: """ Generate the default event name that we associate with every intent. >>> _event_name('test.intent_name') 'E_TEST_INTENT_NAME' TODO: This is only used in Dialogflow -> Deprecate and move to DialogflowConnector """ return "E_" + intent_name.upper().replace('.', '_')