Source code for intents.model.intent

"""
An **intent** is a categorical representation of the User intention in a single
conversation turn. For instance, utterances like "I want a pizza", "I'd like to
order a pizza" and such, could be mapped to a single `OrderPizza` intent.

Your agent will typically define a number of *intents*, representing all the
types of messages the Agent can understand and answer to. This is done by
defining :class:`Intent` sub-classes and their language resources (see
:mod:`intents.language`), and registering them to an :class:`intents.Agent`
class with :meth:`intents.Agent.register`.
"""
from __future__ import annotations

import inspect
import logging
import dataclasses
from dataclasses import dataclass, replace, fields
from typing import List, Dict, Union, Any, Type, T, _GenericAlias

# pylint: disable=unused-import # needed for docs
import intents
from intents import LanguageCode
from intents.model import entity, names
from intents.model.parameter import IntentParameter, SessionIntentParameter, NluIntentParameter, ParameterSchema
from intents.helpers.data_classes import is_dataclass_strict, to_dict

logger = logging.getLogger(__name__)

#
# Intent Fulfillment Structures
#

@dataclass
class FulfillmentSession:
    """
    This models information about the current session that is returned in
    :class:`FulfillmentContext` objects.
    """
    id: str

[docs]@dataclass class FulfillmentContext: """ `FulfillmentContext` objects are produced by Connectors and are input arguments to :meth:`Intent.fulfill`. """ session: FulfillmentSession confidence: float fulfillment_text: str fulfillment_messages: intents.language.intent_language.IntentResponseDict language: LanguageCode
[docs]@dataclass class FulfillmentResult: """ `FulfillmentResult` are produced by `Intent.fulfill`, and then converted by Connectors into Service-actionable responses. .. note:: At the moment this class is only used internally. It is safe to return simple :class:`Intent` instances in :meth:`Intent.fulfill` """ trigger: Intent = None fulfillment_text: List[str] = None fulfillment_messages: List["intents.language.intent_language.IntentResponse"] = None
[docs] @staticmethod def ensure(fulfill_return_value: Union[FulfillmentResult, Intent]) -> FulfillmentResult: """ Convert the given object to a :class:`FulfillmentResult` instance, if it is not already one. Args: fulfill_return_value: An object, as it is returned by :meth:`Intent.fulfill` Raises: ValueError: If input object is not a valid return value for :meth:`Intent.fulfill` Returns: FulfillmentResult: A `FulfillmentResult` object representing input """ if fulfill_return_value is None: return if isinstance(fulfill_return_value, FulfillmentResult): return fulfill_return_value if isinstance(fulfill_return_value, Intent): return FulfillmentResult(trigger=fulfill_return_value) raise ValueError(f"Unsupported fulfillment return value: {fulfill_return_value}")
# # Intent # class IntentType(type): name: str = None def __new__(cls, name, bases, dct): result_cls = super().__new__(cls, name, bases, dct) # Do not process Intent base class if name == 'Intent': assert not bases return result_cls for base_cls in bases: if base_cls is not Intent: if issubclass(base_cls, Intent) and not is_dataclass_strict(base_cls): logger.warning("Intent '%s' is inheriting from non-dataclass '%s'. Did you forget the decorator?", result_cls, base_cls) if "name" not in result_cls.__dict__: result_cls.name = _intent_name_from_class(result_cls) names.check_name(result_cls.name) # TODO: check that custom parameters don't overlap Intent fields return result_cls @property def parameter_schema(cls) -> ParameterSchema: if cls is Intent: return {} if not is_dataclass_strict(cls): logger.warning("%s is not a dataclass. This may cause unexpected behavior: consider " "adding a @dataclass decorator to your Intent class.", cls) cls = dataclass(cls) result = {} for param_field in fields(cls): if inspect.isclass(param_field.type) and issubclass(param_field.type, Intent): continue exc_nlu = None exc_session = None try: result[param_field.name] = NluIntentParameter.from_dataclass_field(param_field) continue except ValueError as exc: exc_nlu = exc try: result[param_field.name] = SessionIntentParameter.from_dataclass_field(param_field) continue except ValueError as exc: exc_session = exc if exc_nlu and exc_session: raise ValueError(f"Parameter {param_field.name} of intent {cls} cannot be " f"processed neither as a NLU Parameter ({exc_nlu}) or as a " f"Session Parameter ({exc_session})") return ParameterSchema(result)
[docs]class Intent(metaclass=IntentType): """ This is a base class for user defined intents, which are the fundamental building blocks of your Agent. In its simplest form, an Intent can be defined as follows: .. code-block:: python from intents import Intent class UserSaysHello(Intent): \"\"\"A little docstring for my Intent\"\"\" *Intents* will then look for language resources in the folder where your Agent class is defined, and specifically in `language/<LANGUAGE-CODE>/smalltalk.UserSaysHello.yaml`. Note that the name of the module where the Intent is defined (`smalltalk.py`) is used as a prefix. More details in :mod:`intents.language`. Intents can be more complex than this, for instance: .. code-block:: python from dataclasses import dataclass from intents import Intent, Sys @dataclass class UserSaysHello(Intent): \"\"\"A little docstring for my Intent\"\"\" user_name: Sys.Person name = "hello_custom_name" lifespan = 10 This Intent has a **custom name**, so it will appear as "hello_custom_name" when exported to prediction services such as Dialogflow, and its language file will just be `hello_custom_name.yaml`, without module prefix. See :func:`~intents.model.names.check_name` for more details on the rules that intent names must follow. This intent has a **lifespan** member. This is used in :mod:`intents.model.relations` to determine how long should the Intent stay in the conversation context after it is predicted. Most importantly, this intent has a `user_name` **parameter** of type :class:`Sys.Person` (check out :class:`intents.model.entity.Sys` for available system entities). With adequate examples in its language file, it will be able to match utterances like "Hello, my name is John", tagging "John" as an Entity. When a connector is instantiated, predictions will look like this: >>> predicted = connector.predict("My name is John") >>> predicted.intent UserSaysHello(user_name="John") >>> predicted.intent.user_name "John" >>> predicted.fulfillment_text "Hi John, I'm Agent" .. warning:: Parameter names are meant to be **unique** within an Agent. That is, the same parameter must always be defined with the same type. This is because 1) Some services (e.g. `Alexa <https://developer.amazon.com/en-US/docs/alexa/custom-skills/create-the-interaction-model-for-your-skill.html#intent-slot-names>`_) require so. 2) Predictions overwrite parameters in context based on their name: better make sure they are compatible. Last, we notice the **@dataclass** decorator. We require it to be added explicitly, this way IDEs will recognize the Intent class as a dataclass and provide correct hints for names and types. """ # TODO: check parameter names: no underscore, no reserved names, max length name: str = None lifespan: int = 5 @property def parameter_schema(self) -> ParameterSchema: """ Return a :class:`ParameterSchema` object that represens the Intent parameter definition. TODO: this doesn't show up in docs """ return self.__class__.parameter_schema
[docs] def parameter_dict(self) -> Dict[str, IntentParameter]: """ Return the Intent parameters and their values as a dict. Returns: A dict of parameters """ result = {} for parameter_name in self.parameter_schema.keys(): result[parameter_name] = getattr(self, parameter_name) return result
[docs] @classmethod def parent_intents(cls) -> List[Type[Intent]]: """ Return a list of the Intent parent classes. This wraps `cls.mro()` filtering out non-Intent parents (e.g. :class:`object`), :class:`Intent` and `cls` itself. >>> AskEspresso.parent_intents() [AskCoffee, AskDrink] Returns: A list of parent Intent classes """ result = [] for c in cls.mro(): if issubclass(c, Intent) and c is not Intent and c is not cls: result.append(c) return result
[docs] def replace(self, *, lifespan: int=None, **kwargs) -> T: """ Return a copy of the given intent object replacing the given field values >>> OrderPizza(pizza_type="margherita").replace(lifespan=3, pizza_type="marinara") OrderPizza(pizza_type="marinara") This method is meant for **internal use**. Changing lifespans and parameters will most likely have an impact on session and contexts that have to be dealt with separately. Args: lifespan (int, optional): Intent lifespan. Defaults to None. Return: An Intent of the same type, with updated fields """ if not is_dataclass_strict(self): logger.warning("%s is not a dataclass instance. This may cause unexpected behavior: consider " "adding a @dataclass decorator to your Intent class.", self) self = dataclass(self) result = replace(self, **kwargs) result.lifespan = lifespan if lifespan else self.lifespan return result
[docs] def fulfill(self, context: FulfillmentContext, **kwargs ) -> Intent: """ Override this method to define a custom procedure that will be invoked when the Intent is predicted, possibly triggering another intent as a result. For instance: .. code-block:: python @dataclass class UserConfirmsPayment(Intent): parent_order: UserOrdersSomething = follow() def fulfill(self, context): if shop_api.is_item_available(parent_order.item_id): shop_api.make_order(parent_order.item_id) return OrderSuccessResponse() else: return OrderFailedResponse() In the example, when `UserConfirmsPayment` is predicted, it will run some business logic, and choose a different response depending on the result. Note that response objects are full-fledged Intents, and even though they probably won't include example utterances, they can define parameters, relations, and their own :meth:`fulfillment` implementation, that will be executed recursively. More details about fulfillments can be found in the :mod:`~intents.fulfillment` module documentation. Args: context: Context information about the fulfillment request (confidence, language, ...) Returns: A `FulfillmentResult` object, that can contain a followup intent to trigger, or just some responses to post back to user. """ return None
def _intent_name_from_class(intent_cls: Type[Intent]) -> str: full_name = f"{intent_cls.__module__}.{intent_cls.__name__}" if intent_cls.__module__.startswith("_"): full_name = intent_cls.__name__ # full_name = re.sub(r"_+", "_", full_name) return ".".join(full_name.split(".")[-2:])