Source code for intents.model.parameter

"""
Intent Parameters are :class:`~intents.model.intent.Intent` class members. There
are two kinds of parameter in *Intents*:

* **NLU Parameters** are Entity types (or list of entities). They can be tagged
  in User utterance and extracted at prediction time. They are modelled by
  :class:`NluIntentParameter`.
* **SessionParameters** are values that are injected in the Conversation context
  via software, either when triggering an intent, or at
  :mod:`~intent.model.fulfillment` time. Session parameters can contain complex
  structured data, but cannot be tagged in user utterances; therefore an intent with
  Session parameters will never be predicted. Session Parameters are modelled in
  :class:`SessionIntentParameter`.
"""
import json
import logging
import dataclasses
from dataclasses import dataclass
from typing import Dict, Tuple, Any, Type, T, _GenericAlias

from dacite import from_dict

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

logger = logging.getLogger(__name__)

[docs]@dataclass class IntentParameter: """ Model metadata of a single Intent parameter. `NluIntentParameter` objects are built internally by :attr:`Intent.parameter_schema` based on the Intent dataclass fields. >>> OrderPizzaIntent.parameter_schema { 'pizza_type': NluIntentParameter(name='pizza_type', entity_cls=<...PizzaType'>, is_list=False, required=True, default=None), 'amount': NluIntentParameter(name='amount', entity_cls=<...Sys.Integer'>, is_list=False, required=False, default=1) } This is a **base class** for two different kinds of intent parameters: * :class:`NluParameter` - Those that are tagged in User utterances * :class:`SessionParameter` - Those that are injected in session by triggers and fulfillments Args: name: Parameter name entity_cls: Parameter type is_list: Parameter will match multiple values in the User utterance required: If True, user will be prompted for parameter value when that can't be tagged in his utterance default: If set, this value will be used when parameter value can't be tagged in the User's utterance """ name: str required: bool default: Any
[docs]@dataclass class NluIntentParameter(IntentParameter): """ These are :class:`IntentParameter`\ s that are tagged in User utterances (e.g. a `pizza_type` parameter can be tagged in the message of a user that is ordering pizza). Every member of an :class:`Intent` dataclass that is annotated with an Entity type will be recognized as a NLU Parameter. Args: name: Parameter name entity_cls: Parameter type is_list: Parameter will match multiple values in the User utterance required: If True, user will be prompted for parameter value when that can't be tagged in his utterance default: If set, this value will be used when parameter value can't be tagged in the User's utterance """ is_list: bool entity_cls: entity.EntityType @staticmethod def from_dataclass_field(param_field: dataclasses.Field) -> "NluIntentParameter": if isinstance(param_field.type, _GenericAlias): if param_field.type.__dict__.get('_name') != 'List': raise ValueError(f"Invalid typing '{param_field.type}' for NLU Parameter '{param_field.name}'. Only 'List' is supported.") if len(param_field.type.__dict__.get('__args__')) != 1: raise ValueError(f"Invalid List modifier '{param_field.type}' for NLU Parameter '{param_field.name}'. Must define exactly one inner type (e.g. 'List[Sys.Integer]')") # From here on, check the inner type (e.g. List[Sys.Integer] -> Sys.Integer) entity_cls = param_field.type.__dict__.get('__args__')[0] is_list = True else: entity_cls = param_field.type is_list = False if not issubclass(entity_cls, entity.EntityMixin): raise ValueError(f"NLU Parameter '{param_field.name}' is of type '{entity_cls}', which is not an Entity.") required, default = _is_required(param_field) if not required and is_list and default is not None and not isinstance(default, list): raise ValueError(f"List NLU Parameter {param_field.name} has non-list default value: {param_field}") return NluIntentParameter( name=param_field.name, entity_cls=entity_cls, is_list=is_list, required=required, default=default )
[docs]@dataclass class SessionIntentParameter(IntentParameter): """ These are :class:`IntentParameter`\ s that are injected in the conversation by triggers and fulfillment procedre (e.g. a `delivery_info` dict can be sent when triggering an intent for pushing an order delivery update to User). Every member of an :class:`Intent` class that is annotated with a primitive type, :class:`list`, :class:`dict` or a :class:`dataclass` type will be recognized as a Session Parameter. Args: name: Parameter name entity_cls: Parameter type is_list: Parameter will match multiple values in the User utterance required: If True, user will be prompted for parameter value when that can't be tagged in his utterance default: If set, this value will be used when parameter value can't be tagged in the User's utterance """ data_type: Type[T] def __post_init__(self): if not self.is_serializable(): raise ValueError(f"Parameter {self.name} with data type {self.data_type} " "is not serializable. Must be one of str, int, float, list, " "dict, or a dataclass.")
[docs] def is_serializable(self): """ Return True if the Parameter specification is serializable as a string by :meth:`serialize_value` Returns: True if parameter is serializable as a string, False otherwise """ if issubclass(self.data_type, (list, dict, int, float, str)): return True if is_dataclass_strict(self.data_type): return True return False
[docs] def serialize_value(self, value: Any) -> str: """ Serialize the given value based on `data_type`. These are the supported scenarios: * `data_type` is a JSON-serializable (:class:`list` or a :class:`dict`, :class:`str`, :class:`int`, :class:`float`) -> value is serialized as JSON * `data_type` is a `dataclass` -> value is converted to dict with custom :func:`~intents.helpers.data_classes.to_dict`. In addition to standard :func:`asdict` behavior, it will process Enums correctly. Args: value: The value to serialize based on the Parameter spec Raises: ValueError: If `data_type` is not serializable Returns: A string representation of `value` """ if issubclass(self.data_type, (int, float, str)): return str(value) if issubclass(self.data_type, (list, dict)): return json.dumps(value) if is_dataclass_strict(self.data_type): return json.dumps(to_dict(value)) raise ValueError(f"Data type {self.data_type} of field {self.name} is not serializable.")
def deserialize_value(self, data: str) -> Any: if issubclass(self.data_type, (int, float, str)): return self.data_type(data) if issubclass(self.data_type, (list, dict)): return json.loads(data) if is_dataclass_strict(self.data_type): data = json.loads(data) return from_dict(self.data_type, data) raise ValueError(f"Data type {self.data_type} of field {self.name} is not serializable.") @staticmethod def from_dataclass_field(param_field: dataclasses.Field) -> "SessionIntentParameter": if isinstance(param_field.type, _GenericAlias): if param_field.type.__dict__.get('_name') == 'List': data_type = list # TODO: list of dataclass! elif param_field.type.__dict__.get('_name') == 'Dict': data_type = dict else: raise ValueError(f"Invalid typing '{param_field.type}' for Session parameter '{param_field.name}'. Only 'List' and 'Dict' are supported.") else: data_type = param_field.type required, default = _is_required(param_field) if not required and issubclass(data_type, list) and default is not None and not isinstance(default, list): raise ValueError(f"List Session Parameter {param_field.name} has non-list default value: {param_field}") # Will raise ValueError if type is not compatible return SessionIntentParameter( name=param_field.name, data_type=data_type, required=required, default=default )
[docs]class ParameterSchema(dict): @property def nlu_parameters(self) -> Dict[str, NluIntentParameter]: """ Return the `ParameterSchema` subset that only contains NLU Parameters Returns: A map of NLU Parameters """ return {k: v for k, v in self.items() if isinstance(v, NluIntentParameter)} @property def session_parameters(self) -> Dict[str, SessionIntentParameter]: """ Return the `ParameterSchema` subset that only contains Session Parameters Returns: A map of Session Parameters """ return {k: v for k, v in self.items() if isinstance(v, SessionIntentParameter)}
def _is_required(dataclass_field: dataclasses.Field) -> Tuple[bool, Any]: """ Return `(True, None)` if field is required (i.e. it has no default value), `(False, <DEFAULT_VALUE>)` otherwise. Args: dataclass_field: A dataclass field, as it is returned by :func:`dataclasses.field` Returns: True/False if field is required or not, and its default value """ required = True default = None if not isinstance(dataclass_field.default, dataclasses._MISSING_TYPE): required = False default = dataclass_field.default if not isinstance(dataclass_field.default_factory, dataclasses._MISSING_TYPE): required = False default = dataclass_field.default_factory() return required, default