Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之FormAction

Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之FormAction
在这里插入图片描述

FormAction

实现和执行表单逻辑的操作Action


class FormAction(LoopAction):
    """Action which implements and executes the form logic."""

    def __init__(
        self, form_name: Text, action_endpoint: Optional[EndpointConfig]
    ) -> None:
        """Creates a `FormAction`.

        Args:
            form_name: Name of the form.
            action_endpoint: Endpoint to execute custom actions.
        """
        self._form_name = form_name
        self.action_endpoint = action_endpoint
        # creating it requires domain, which we don't have in init
        # we'll create it on the first call
        self._unique_entity_mappings: Set[Text] = set()
        self._have_unique_entity_mappings_been_initialized = False

    def name(self) -> Text:
        """Return the form name."""
        return self._form_name

activate

如果第一次调用表单,则激活表单。如果激活,请验证表单激活之前填充的所有所需词槽,并返回带有表单名称的’form’事件,以及预填充词槽验证中的任何’SlotSet’事件。

   async def activate(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Activate form if the form is called for the first time.

        If activating, validate any required slots that were filled before
        form activation and return `Form` event with the name of the form, as well
        as any `SlotSet` events from validation of pre-filled slots.

        Args:
            output_channel: The output channel which can be used to send messages
                to the user.
            nlg: `NaturalLanguageGenerator` to use for response generation.
            tracker: Current conversation tracker of the user.
            domain: Current model domain.

        Returns:
            Events from the activation.
        """
        logger.debug(f"Activated the form '{
      
      self.name()}'.")
        # collect values of required slots filled before activation
        prefilled_slots = {
    
    }

        for slot_name in self.required_slots(domain):
            if not self._should_request_slot(tracker, slot_name):
                prefilled_slots[slot_name] = tracker.get_slot(slot_name)

        if not prefilled_slots:
            logger.debug("No pre-filled required slots to validate.")
            return []

        logger.debug(f"Validating pre-filled required slots: {
      
      prefilled_slots}")
        return await self.validate_slots(
            prefilled_slots, tracker, domain, output_channel, nlg
        )

required_slots

表单必须填写的所需词槽的列表。


    def required_slots(self, domain: Domain) -> List[Text]:
        """A list of required slots that the form has to fill.

        Returns:
            A list of slot names.
        """
        return domain.required_slots_for_form(self.name())

_should_request_slot

检查表单动作是否应请求给定词槽

    @staticmethod
    def _should_request_slot(tracker: "DialogueStateTracker", slot_name: Text) -> bool:
        """Check whether form action should request given slot."""
        return tracker.get_slot(slot_name) is None

validate_slots

验证提取的词槽。如果自定义一个Action验证槽,我们调用自定义Action来验证它们,否则就没有验证


    async def validate_slots(
        self,
        slot_candidates: Dict[Text, Any],
        tracker: "DialogueStateTracker",
        domain: Domain,
        output_channel: OutputChannel,
        nlg: NaturalLanguageGenerator,
    ) -> List[Union[SlotSet, Event]]:
        """Validate the extracted slots.

        If a custom action is available for validating the slots, we call it to validate
        them. Otherwise there is no validation.

        Args:
            slot_candidates: Extracted slots which are candidates to fill the slots
                required by the form.
            tracker: The current conversation tracker.
            domain: The current model domain.
            output_channel: The output channel which can be used to send messages
                to the user.
            nlg:  `NaturalLanguageGenerator` to use for response generation.

        Returns:
            The validation events including potential bot messages and `SlotSet` events
            for the validated slots, if the custom form validation action is present in
            domain actions.
            Otherwise, returns empty list since the extracted slots already have
            corresponding `SlotSet` events in the tracker.
        """
        logger.debug(f"Validating extracted slots: {
      
      slot_candidates}")
        events: List[Union[SlotSet, Event]] = [
            SlotSet(slot_name, value) for slot_name, value in slot_candidates.items()
        ]

        validate_name = f"validate_{
      
      self.name()}"
        #如果domain文件无validate_的action方法,则不进行校验,返回空事件
        if validate_name not in domain.action_names_or_texts:
            return []
             
        # create temporary tracker with only the SlotSet events added
        # since last user utterance
        _tracker = self._temporary_tracker(tracker, events, domain)
       #调用微服务进行校验                           
        _action = RemoteAction(validate_name, self.action_endpoint)
        validate_events = await _action.run(output_channel, nlg, _tracker, domain)

        # Only return the validated SlotSet events by the custom form validation action
        # to avoid adding duplicate SlotSet events for slots that are already valid.
        return validate_events

_validate_if_required

从self.validate(…)返回事件列表

    def _validate_if_required(
        self,
        tracker: "DialogueStateTracker",
        domain: Domain,
        output_channel: OutputChannel,
        nlg: NaturalLanguageGenerator,
    ) -> List[Event]:
        """Return a list of events from `self.validate(...)`.

        Validation is required if:
           - the form is active
           - the form is called after `action_listen`
           - form validation was not cancelled
        """
        # No active_loop means there are no form filled slots to validate yet
        if not tracker.active_loop:
            return []

        needs_validation = (
            tracker.latest_action_name == ACTION_LISTEN_NAME
            and not tracker.active_loop.get(LOOP_INTERRUPTED, False)
        )

        if needs_validation:
            logger.debug(f"Validating user input '{
      
      tracker.latest_message}'.")
            return   self.validate(tracker, domain, output_channel, nlg)
        else:
            # Needed to determine which slots to request although there are no slots
            # to actually validate, which happens when coming back to the form after
            # an unhappy path
            return   self.validate_slots({
    
    }, tracker, domain, output_channel, nlg)

_user_rejected_manually

检查用户在slot_validation期间是否拒绝表单执行

    def _user_rejected_manually(self, validation_events: List[Event]) -> bool:
        """Checks if user rejected the form execution during a slot_validation.

        Args:
            validation_events: Events returned by the custom slot_validation action

        Returns:
            True if the validation_events include an ActionExecutionRejected event,
            else False.
        """
        return any(
            isinstance(event, ActionExecutionRejected) for event in validation_events
        )

_ask_for_slot

    def _ask_for_slot(
        self,
        domain: Domain,
        nlg: NaturalLanguageGenerator,
        output_channel: OutputChannel,
        slot_name: Text,
        tracker: DialogueStateTracker,
    ) -> List[Event]:
        logger.debug(f"Request next slot '{
      
      slot_name}'")

        action_name_to_ask_for_next_slot = self._name_of_utterance(domain, slot_name)
        if not action_name_to_ask_for_next_slot:
            # Use a debug log as the user might have asked as part of a custom action
            #如果 action_ask_表单名_词槽名、utter_ask_表单名_词槽名、 action_ask_词槽名、 utter_ask_词槽名方法在domain的action列表中都没有查询到,则没有发现请求填充词槽的Action,返回空事件,对于要填充的词槽不对用户进行提示说明。
            logger.debug(
                f"There was no action found to ask for slot '{
      
      slot_name}' "
                f"name to be filled."
            )
            return []

        action_to_ask_for_next_slot = action.action_for_name_or_text(
            action_name_to_ask_for_next_slot, domain, self.action_endpoint
        )
        #如果在domain中找到上述对应的方法,则按名称检索对应的ActionRetrieveResponse、ActionEndToEndResponse、FormAction、RemoteAction,调用其run方法执行,返回事件列表。
        return   action_to_ask_for_next_slot.run(
            output_channel, nlg, tracker, domain
        )

_name_of_utterance

构建search_path列表,构建found_actions Generator函数,next获取下一个值,4种查询的方式

  • action_ask_表单名_词槽名
  • utter_ask_表单名_词槽名
  • action_ask_词槽名
  • utter_ask_词槽名
  def _name_of_utterance(self, domain: Domain, slot_name: Text) -> Optional[Text]:
        search_path = [
            f"action_ask_{
      
      self._form_name}_{
      
      slot_name}",
            f"{
      
      UTTER_PREFIX}ask_{
      
      self._form_name}_{
      
      slot_name}",
            f"action_ask_{
      
      slot_name}",
            f"{
      
      UTTER_PREFIX}ask_{
      
      slot_name}",
        ]

        found_actions = (
            action_name
            for action_name in search_path
            if action_name in domain.action_names_or_texts
        )

        return next(found_actions, None)

_temporary_tracker

  • 插入SlotSet事件以确保请求的REQUESTED_SLOT属于活动表单
  • 插入ActionExecuted事件,以便清楚地区分哪些事件是新添加的。
    def _temporary_tracker(
        self,
        current_tracker: DialogueStateTracker,
        additional_events: List[Event],
        domain: Domain,
    ) -> DialogueStateTracker:
        return DialogueStateTracker.from_events(
            current_tracker.sender_id,
            current_tracker.events_after_latest_restart()
            # Insert SlotSet event to make sure REQUESTED_SLOT belongs to active form.
            + [SlotSet(REQUESTED_SLOT, self.get_slot_to_fill(current_tracker))]
            # Insert form execution event so that it's clearly distinguishable which
            # events were newly added.
            + [ActionExecuted(self.name())] + additional_events,
            slots=domain.slots,
        )

_find_next_slot_to_request

    def _find_next_slot_to_request(
        self, tracker: DialogueStateTracker, domain: Domain
    ) -> Optional[Text]:
        return next(
            (
                slot
                for slot in self.required_slots(domain)
                if self._should_request_slot(tracker, slot)
            ),
            None,
        )

request_next_slot

如果需要 ,请求下一个词槽和响应,否则返回“None”。

    def request_next_slot(
        self,
        tracker: "DialogueStateTracker",
        domain: Domain,
        output_channel: OutputChannel,
        nlg: NaturalLanguageGenerator,
        events_so_far: List[Event],
    ) -> List[Union[SlotSet, Event]]:
        """Request the next slot and response if needed, else return `None`."""
        request_slot_events: List[Event] = []

        if   self.is_done(output_channel, nlg, tracker, domain, events_so_far):
            # The custom action for slot validation decided to stop the form early
            return [SlotSet(REQUESTED_SLOT, None)]

        slot_to_request = next(
            (
                event.value
                for event in events_so_far
                if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT
            ),
            None,
        )

        temp_tracker = self._temporary_tracker(tracker, events_so_far, domain)

        if not slot_to_request:
            slot_to_request = self._find_next_slot_to_request(temp_tracker, domain)
            request_slot_events.append(SlotSet(REQUESTED_SLOT, slot_to_request))

        if slot_to_request:
            bot_message_events =   self._ask_for_slot(
                domain, nlg, output_channel, slot_to_request, temp_tracker
            )
            return request_slot_events + bot_message_events

        # no more required slots to fill
        return [SlotSet(REQUESTED_SLOT, None)]

do

 def do(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
        events_so_far: List[Event],
    ) -> List[Event]:
        events =   self._validate_if_required(tracker, domain, output_channel, nlg)

        if not self._user_rejected_manually(events):
            events +=   self.request_next_slot(
                tracker, domain, output_channel, nlg, events_so_far + events
            )

        return events

is_done

    def is_done(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
        events_so_far: List[Event],
    ) -> bool:
        """Checks if loop can be terminated."""
        if any(isinstance(event, ActionExecutionRejected) for event in events_so_far):
            return False

        # Custom validation actions can decide to terminate the loop early by
        # setting the requested slot to `None` or setting `ActiveLoop(None)`.
        # We explicitly check only the last occurrences for each possible termination
        # event instead of doing `return event in events_so_far` to make it possible
        # to override termination events which were returned earlier.
        return (
            next(
                (
                    event
                    for event in reversed(events_so_far)
                    if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT
                ),
                None,
            )
            == SlotSet(REQUESTED_SLOT, None)
            or next(
                (
                    event
                    for event in reversed(events_so_far)
                    if isinstance(event, ActiveLoop)
                ),
                None,
            )
            == ActiveLoop(None)
        )

ActiveLoop

如果给定’name’:用’name’激活循环,否则停用活动循环,ActiveLoop继承至Event,如果当前loop 被禁用,则名称为“None”。


class ActiveLoop(Event):
    """If `name` is given: activates a loop with `name` else deactivates active loop."""

    type_name = "active_loop"

    def __init__(
        self,
        name: Optional[Text],
        timestamp: Optional[float] = None,
        metadata: Optional[Dict[Text, Any]] = None,
    ) -> None:
        """Creates event for active loop.

        Args:
            name: Name of activated loop or `None` if current loop is deactivated.
            timestamp: When the event was created.
            metadata: Additional event metadata.
        """
        self.name = name
        super().__init__(timestamp, metadata)

    def __str__(self) -> Text:
        """Returns text representation of event."""
        return f"Loop({
      
      self.name})"

    def __hash__(self) -> int:
        """Returns unique hash for event."""
        return hash(self.name)

    def __eq__(self, other: Any) -> bool:
        """Compares object with other object."""
        if not isinstance(other, ActiveLoop):
            return NotImplemented

        return self.name == other.name

    def as_story_string(self) -> Text:
        """Returns text representation of event."""
        props = json.dumps({
    
    LOOP_NAME: self.name})
        return f"{
      
      ActiveLoop.type_name}{
      
      props}"

    @classmethod
    def _from_story_string(cls, parameters: Dict[Text, Any]) -> List["ActiveLoop"]:
        """Called to convert a parsed story line into an event."""
        return [
            ActiveLoop(
                parameters.get(LOOP_NAME),
                parameters.get("timestamp"),
                parameters.get("metadata"),
            )
        ]

    def as_dict(self) -> Dict[Text, Any]:
        """Returns serialized event."""
        d = super().as_dict()
        d.update({
    
    LOOP_NAME: self.name})
        return d

    def apply_to(self, tracker: "DialogueStateTracker") -> None:
        """Applies event to current conversation state."""
        tracker.change_loop_to(self.name)


Slot

Key-value 键-值存储,用于存储会话期间的信息

class Slot:
    """Key-value store for storing information during a conversation."""

    type_name = None

    def __init__(
        self,
        name: Text,
        mappings: List[Dict[Text, Any]],
        initial_value: Any = None,
        value_reset_delay: Optional[int] = None,
        influence_conversation: bool = True,
    ) -> None:
        """Create a Slot.

        Args:
            name: The name of the slot.
            initial_value: The initial value of the slot.
            mappings: List containing slot mappings.
            value_reset_delay: After how many turns the slot should be reset to the
                initial_value. This is behavior is currently not implemented.
            influence_conversation: If `True` the slot will be featurized and hence
                influence the predictions of the dialogue polices.
        """
        self.name = name
        self.mappings = mappings
        self._value = initial_value
        self.initial_value = initial_value
        self._value_reset_delay = value_reset_delay
        self.influence_conversation = influence_conversation
        self._has_been_set = False

    def feature_dimensionality(self) -> int:
        """How many features this single slot creates.

        Returns:
            The number of features. `0` if the slot is unfeaturized. The dimensionality
            of the array returned by `as_feature` needs to correspond to this value.
        """
        if not self.influence_conversation:
            return 0

        return self._feature_dimensionality()

    def _feature_dimensionality(self) -> int:
        """See the docstring for `feature_dimensionality`."""
        return 1

    def has_features(self) -> bool:
        """Indicate if the slot creates any features."""
        return self.feature_dimensionality() != 0

    def value_reset_delay(self) -> Optional[int]:
        """After how many turns the slot should be reset to the initial_value.

        If the delay is set to `None`, the slot will keep its value forever."""
        # TODO: FUTURE this needs to be implemented - slots are not reset yet
        return self._value_reset_delay

    def as_feature(self) -> List[float]:
        if not self.influence_conversation:
            return []

        return self._as_feature()

    def _as_feature(self) -> List[float]:
        raise NotImplementedError(
            "Each slot type needs to specify how its "
            "value can be converted to a feature. Slot "
            "'{}' is a generic slot that can not be used "
            "for predictions. Make sure you add this "
            "slot to your domain definition, specifying "
            "the type of the slot. If you implemented "
            "a custom slot type class, make sure to "
            "implement `.as_feature()`."
            "".format(self.name)
        )

    def reset(self) -> None:
        """Resets the slot's value to the initial value."""
        self.value = self.initial_value
        self._has_been_set = False

    @property
    def value(self) -> Any:
        """Gets the slot's value."""
        return self._value

    @value.setter
    def value(self, value: Any) -> None:
        """Sets the slot's value."""
        self._value = value
        self._has_been_set = True

    @property
    def has_been_set(self) -> bool:
        """Indicates if the slot's value has been set."""
        return self._has_been_set

    def __str__(self) -> Text:
        return f"{
      
      self.__class__.__name__}({
      
      self.name}: {
      
      self.value})"

    def __repr__(self) -> Text:
        return f"<{
      
      self.__class__.__name__}({
      
      self.name}: {
      
      self.value})>"

    @staticmethod
    def resolve_by_type(type_name: Text) -> Type["Slot"]:
        """Returns a slots class by its type name."""
        for cls in rasa.shared.utils.common.all_subclasses(Slot):
            if cls.type_name == type_name:
                return cls
        try:
            return rasa.shared.utils.common.class_from_module_path(type_name)
        except (ImportError, AttributeError):
            raise InvalidSlotTypeException(
                f"Failed to find slot type, '{
      
      type_name}' is neither a known type nor "
                f"user-defined. If you are creating your own slot type, make "
                f"sure its module path is correct. "
                f"You can find all build in types at {
      
      DOCS_URL_SLOTS}"
            )

    def persistence_info(self) -> Dict[str, Any]:
        """Returns relevant information to persist this slot."""
        return {
    
    
            "type": rasa.shared.utils.common.module_path_from_instance(self),
            "initial_value": self.initial_value,
            "influence_conversation": self.influence_conversation,
            "mappings": self.mappings,
        }

    def fingerprint(self) -> Text:
        """Returns a unique hash for the slot which is stable across python runs.

        Returns:
            fingerprint of the slot
        """
        data = {
    
    "slot_name": self.name, "slot_value": self.value}
        data.update(self.persistence_info())
        return rasa.shared.utils.io.get_dictionary_fingerprint(data)

AnySlotDict

通过按需创建词槽来假装每个词槽都存在的词槽字典。这只使用通用槽类型!这意味着某些功能将无法工作,例如特征化插槽

class AnySlotDict(dict):
    """A slot dictionary that pretends every slot exists, by creating slots on demand.

    This only uses the generic slot type! This means certain functionality wont work,
    e.g. properly featurizing the slot."""

    def __missing__(self, key: Text) -> Slot:
        value = self[key] = Slot(key, mappings=[])
        return value

    def __contains__(self, key: Any) -> bool:
        return True

SlotSet

用户指定词槽slot的值,每个词槽都有一个名称和一个值,SlotSet继承至Event类

class SlotSet(Event):
    """The user has specified their preference for the value of a `slot`.

    Every slot has a name and a value. This event can be used to set a
    value for a slot on a conversation.

    As a side effect the `Tracker`'s slots will be updated so
    that `tracker.slots[key]=value`.
    """

    type_name = "slot"

    def __init__(
        self,
        key: Text,
        value: Optional[Any] = None,
        timestamp: Optional[float] = None,
        metadata: Optional[Dict[Text, Any]] = None,
    ) -> None:
        """Creates event to set slot.

        Args:
            key: Name of the slot which is set.
            value: Value to which slot is set.
            timestamp: When the event was created.
            metadata: Additional event metadata.
        """
        self.key = key
        self.value = value
        super().__init__(timestamp, metadata)

    def __str__(self) -> Text:
        """Returns text representation of event."""
        return f"SlotSet(key: {
      
      self.key}, value: {
      
      self.value})"

    def __hash__(self) -> int:
        """Returns unique hash for event."""
        return hash((self.key, jsonpickle.encode(self.value)))

    def __eq__(self, other: Any) -> bool:
        """Compares object with other object."""
        if not isinstance(other, SlotSet):
            return NotImplemented

        return (self.key, self.value) == (other.key, other.value)

    def as_story_string(self) -> Text:
        """Returns text representation of event."""
        props = json.dumps({
    
    self.key: self.value}, ensure_ascii=False)
        return f"{
      
      self.type_name}{
      
      props}"

    @classmethod
    def _from_story_string(
        cls, parameters: Dict[Text, Any]
    ) -> Optional[List["SlotSet"]]:

        slots = []
        for slot_key, slot_val in parameters.items():
            slots.append(SlotSet(slot_key, slot_val))

        if slots:
            return slots
        else:
            return None

    def as_dict(self) -> Dict[Text, Any]:
        """Returns serialized event."""
        d = super().as_dict()
        d.update({
    
    "name": self.key, "value": self.value})
        return d

    @classmethod
    def _from_parameters(cls, parameters: Dict[Text, Any]) -> "SlotSet":
        try:
            return SlotSet(
                parameters.get("name"),
                parameters.get("value"),
                parameters.get("timestamp"),
                parameters.get("metadata"),
            )
        except KeyError as e:
            raise ValueError(f"Failed to parse set slot event. {
      
      e}")

    def apply_to(self, tracker: "DialogueStateTracker") -> None:
        """Applies event to current conversation state."""
        tracker._set_slot(self.key, self.value)

RemoteAction

class RemoteAction(Action):
    def __init__(self, name: Text, action_endpoint: Optional[EndpointConfig]) -> None:

        self._name = name
        self.action_endpoint = action_endpoint

    def _action_call_format(
        self, tracker: "DialogueStateTracker", domain: "Domain"
    ) -> Dict[Text, Any]:
        """Create the request json send to the action server."""
        from rasa.shared.core.trackers import EventVerbosity

        tracker_state = tracker.current_state(EventVerbosity.ALL)

        return {
    
    
            "next_action": self._name,
            "sender_id": tracker.sender_id,
            "tracker": tracker_state,
            "domain": domain.as_dict(),
            "version": rasa.__version__,
        }

    @staticmethod
    def action_response_format_spec() -> Dict[Text, Any]:
        """Expected response schema for an Action endpoint.

        Used for validation of the response returned from the
        Action endpoint."""
        schema = {
    
    
            "type": "object",
            "properties": {
    
    
                "events": EVENTS_SCHEMA,
                "responses": {
    
    "type": "array", "items": {
    
    "type": "object"}},
            },
        }
        return schema

    def _validate_action_result(self, result: Dict[Text, Any]) -> bool:
        from jsonschema import validate
        from jsonschema import ValidationError

        try:
            validate(result, self.action_response_format_spec())
            return True
        except ValidationError as e:
            e.message += (
                f". Failed to validate Action server response from API, "
                f"make sure your response from the Action endpoint is valid. "
                f"For more information about the format visit "
                f"{
      
      DOCS_BASE_URL}/custom-actions"
            )
            raise e

    @staticmethod
    async def _utter_responses(
        responses: List[Dict[Text, Any]],
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
    ) -> List[BotUttered]:
        """Use the responses generated by the action endpoint and utter them."""
        bot_messages = []
        for response in responses:
            generated_response = response.pop("response", None)
            if generated_response:
                draft = await nlg.generate(
                    generated_response, tracker, output_channel.name(), **response
                )
                if not draft:
                    continue
                draft["utter_action"] = generated_response
            else:
                draft = {
    
    }

            buttons = response.pop("buttons", []) or []
            if buttons:
                draft.setdefault("buttons", [])
                draft["buttons"].extend(buttons)

            # Avoid overwriting `draft` values with empty values
            response = {
    
    k: v for k, v in response.items() if v}
            draft.update(response)
            bot_messages.append(create_bot_utterance(draft))

        return bot_messages

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        json_body = self._action_call_format(tracker, domain)
        if not self.action_endpoint:
            raise RasaException(
                f"Failed to execute custom action '{
      
      self.name()}' "
                f"because no endpoint is configured to run this "
                f"custom action. Please take a look at "
                f"the docs and set an endpoint configuration via the "
                f"--endpoints flag. "
                f"{
      
      DOCS_BASE_URL}/custom-actions"
            )

        try:
            logger.debug(
                "Calling action endpoint to run action '{}'.".format(self.name())
            )
            response: Any = await self.action_endpoint.request(
                json=json_body, method="post", timeout=DEFAULT_REQUEST_TIMEOUT
            )

            self._validate_action_result(response)

            events_json = response.get("events", [])
            responses = response.get("responses", [])
            bot_messages: List[Event] = await self._utter_responses(
                responses, output_channel, nlg, tracker
            )

            evts = events.deserialise_events(events_json)
            return bot_messages + evts

        except ClientResponseError as e:
            if e.status == 400:
                response_data = json.loads(e.text)
                exception = ActionExecutionRejection(
                    response_data["action_name"], response_data.get("error")
                )
                logger.error(exception.message)
                raise exception
            else:
                raise RasaException("Failed to execute custom action.") from e

        except aiohttp.ClientConnectionError as e:
            logger.error(
                "Failed to run custom action '{}'. Couldn't connect "
                "to the server at '{}'. Is the server running? "
                "Error: {}".format(self.name(), self.action_endpoint.url, e)
            )
            raise RasaException("Failed to execute custom action.")

        except aiohttp.ClientError as e:
            # not all errors have a status attribute, but
            # helpful to log if they got it

            # noinspection PyUnresolvedReferences
            status = getattr(e, "status", None)
            raise RasaException(
                "Failed to run custom action '{}'. Action server "
                "responded with a non 200 status code of {}. "
                "Make sure your action server properly runs actions "
                "and returns a 200 once the action is executed. "
                "Error: {}".format(self.name(), status, e)
            )

    def name(self) -> Text:
        return self._name

Action

对对话状态的下一步回应行动

class Action:
    """Next action to be taken in response to a dialogue state."""

    def name(self) -> Text:
        """Unique identifier of this simple action."""

        raise NotImplementedError

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Execute the side effects of this action.

        Args:
            nlg: which ``nlg`` to use for response generation
            output_channel: ``output_channel`` to which to send the resulting message.
            tracker (DialogueStateTracker): the state tracker for the current
                user. You can access slot values using
                ``tracker.get_slot(slot_name)`` and the most recent user
                message is ``tracker.latest_message.text``.
            domain (Domain): the bot's domain

        Returns:
            A list of :class:`rasa.core.events.Event` instances
        """
        raise NotImplementedError

    def __str__(self) -> Text:
        """Returns text representation of form."""
        return f"{
      
      self.__class__.__name__}('{
      
      self.name()}')"

    def event_for_successful_execution(
        self, prediction: PolicyPrediction
    ) -> ActionExecuted:
        """Event which should be logged for the successful execution of this action.

        Args:
            prediction: Prediction which led to the execution of this event.

        Returns:
            Event which should be logged onto the tracker.
        """
        return ActionExecuted(
            self.name(),
            prediction.policy_name,
            prediction.max_confidence,
            hide_rule_turn=prediction.hide_rule_turn,
            metadata=prediction.action_metadata,
        )


action_for_name_or_text

根据Action动作的名称或它的文本(如果是端到端action)检索Action。

def action_for_name_or_text(
    action_name_or_text: Text, domain: Domain, action_endpoint: Optional[EndpointConfig]
) -> "Action":
    """Retrieves an action by its name or by its text in case it's an end-to-end action.

    Args:
        action_name_or_text: The name of the action.
        domain: The current model domain.
        action_endpoint: The endpoint to execute custom actions.

    Raises:
        ActionNotFoundException: If action not in current domain.

    Returns:
        The instantiated action.
    """
    if action_name_or_text not in domain.action_names_or_texts:
        domain.raise_action_not_found_exception(action_name_or_text)

    defaults = {
    
    a.name(): a for a in default_actions(action_endpoint)}

    if (
        action_name_or_text in defaults
        and action_name_or_text not in domain.user_actions_and_forms
    ):
        return defaults[action_name_or_text]

    if action_name_or_text.startswith(UTTER_PREFIX) and is_retrieval_action(
        action_name_or_text, domain.retrieval_intents
    ):
        return ActionRetrieveResponse(action_name_or_text)

    if action_name_or_text in domain.action_texts:
        return ActionEndToEndResponse(action_name_or_text)

    if action_name_or_text.startswith(UTTER_PREFIX):
        return ActionBotResponse(action_name_or_text)

    is_form = action_name_or_text in domain.form_names
    # Users can override the form by defining an action with the same name as the form
    user_overrode_form_action = is_form and action_name_or_text in domain.user_actions
    if is_form and not user_overrode_form_action:
        from rasa.core.actions.forms import FormAction

        return FormAction(action_name_or_text, action_endpoint)

    return RemoteAction(action_name_or_text, action_endpoint)

default_actions

默认Action的列表

def default_actions(action_endpoint: Optional[EndpointConfig] = None) -> List["Action"]:
    """List default actions."""
    from rasa.core.actions.two_stage_fallback import TwoStageFallbackAction

    return [
        ActionListen(),
        ActionRestart(),
        ActionSessionStart(),
        ActionDefaultFallback(),
        ActionDeactivateLoop(),
        ActionRevertFallbackEvents(),
        ActionDefaultAskAffirmation(),
        ActionDefaultAskRephrase(),
        TwoStageFallbackAction(action_endpoint),
        ActionUnlikelyIntent(),
        ActionBack(),
        ActionExtractSlots(action_endpoint),
    ]


ActionListen

在任何回合的第一个动作-机器人等待一个用户消息,机器人应该停止采取进一步的行动,等待用户说些什么。

class ActionListen(Action):
    """The first action in any turn - bot waits for a user message.

    The bot should stop taking further actions and wait for the user to say
    something."""

    def name(self) -> Text:
        return ACTION_LISTEN_NAME

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        return []

ActionRestart

将跟踪器重置为初始状态。如果可用,发出重启响应。

class ActionRestart(ActionBotResponse):
    """Resets the tracker to its initial state.

    Utters the restart response if available.
    """

    def name(self) -> Text:
        """Returns action restart name."""
        return ACTION_RESTART_NAME

    def __init__(self) -> None:
        """Initializes action restart."""
        super().__init__("utter_restart", silent_fail=True)

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        # only utter the response if it is available
        evts = await super().run(output_channel, nlg, tracker, domain)

        return evts + [Restarted()]

ActionSessionStart

应用会话开始。获取前一个会话中的所有“SlotSet”事件,并将它们应用到新会话中。run方法新加ActionExecuted(ACTION_LISTEN_NAME)事件


class ActionSessionStart(Action):
    """Applies a conversation session start.

    Takes all `SlotSet` events from the previous session and applies them to the new
    session.
    """

    def name(self) -> Text:
        """Returns action start name."""
        return ACTION_SESSION_START_NAME

    @staticmethod
    def _slot_set_events_from_tracker(
        tracker: "DialogueStateTracker",
    ) -> List["SlotSet"]:
        """Fetch SlotSet events from tracker and carry over key, value and metadata."""

        return [
            SlotSet(key=event.key, value=event.value, metadata=event.metadata)
            for event in tracker.applied_events()
            if isinstance(event, SlotSet)
        ]

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        _events: List[Event] = [SessionStarted()]

        if domain.session_config.carry_over_slots:
            _events.extend(self._slot_set_events_from_tracker(tracker))

        _events.append(ActionExecuted(ACTION_LISTEN_NAME))

        return _events

ActionDefaultFallback

执行fallback后退动作,run方法通过UserUtteranceReverted返回到对话的前一状态


class ActionDefaultFallback(ActionBotResponse):
    """Executes the fallback action and goes back to the prev state of the dialogue."""

    def name(self) -> Text:
        """Returns action default fallback name."""
        return ACTION_DEFAULT_FALLBACK_NAME

    def __init__(self) -> None:
        """Initializes action default fallback."""
        super().__init__("utter_default", silent_fail=True)

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        # only utter the response if it is available
        evts = await super().run(output_channel, nlg, tracker, domain)

        return evts + [UserUtteranceReverted()]

ActionDeactivateLoop

使活动循环失效,run方法中加入ActiveLoop(None), SlotSet(REQUESTED_SLOT, None)事件


class ActionDeactivateLoop(Action):
    """Deactivates an active loop."""

    def name(self) -> Text:
        return ACTION_DEACTIVATE_LOOP_NAME

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        return [ActiveLoop(None), SlotSet(REQUESTED_SLOT, None)]


ActionRevertFallbackEvents

恢复在’ TwoStageFallbackPolicy '期间完成的事件。这将恢复在TwoStageFallbackPolicy的回退过程中用户的消息和机器人的话语。为不同的路径编写定制的故事,这样做是没有必要的, 但只针对快乐的路径,已弃用,可在“TwoStageFallbackPolicy”删除。

class ActionRevertFallbackEvents(Action):
    """Reverts events which were done during the `TwoStageFallbackPolicy`.

    This reverts user messages and bot utterances done during a fallback
    of the `TwoStageFallbackPolicy`. By doing so it is not necessary to
    write custom stories for the different paths, but only of the happy
    path. This is deprecated and can be removed once the
    `TwoStageFallbackPolicy` is removed.
    """

    def name(self) -> Text:
        return ACTION_REVERT_FALLBACK_EVENTS_NAME

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        from rasa.core.policies.two_stage_fallback import has_user_rephrased

        # User rephrased
        if has_user_rephrased(tracker):
            return _revert_successful_rephrasing(tracker)
        # User affirmed
        elif has_user_affirmed(tracker):
            return _revert_affirmation_events(tracker)
        else:
            return []

ActionDefaultAskAffirmation

默认的实现,要求用户确认他的意图。建议使用自定义操作覆盖此默认操作,以便为确认提供更有意义的提示。例如,使用intent的描述而不是标识符名。

class ActionDefaultAskAffirmation(Action):
    """Default implementation which asks the user to affirm his intent.

    It is suggested to overwrite this default action with a custom action
    to have more meaningful prompts for the affirmations. E.g. have a
    description of the intent instead of its identifier name.
    """

    def name(self) -> Text:
        return ACTION_DEFAULT_ASK_AFFIRMATION_NAME

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        latest_message = tracker.latest_message
        if latest_message is None:
            raise TypeError(
                "Cannot find last user message for detecting fallback affirmation."
            )

        intent_to_affirm = latest_message.intent.get(INTENT_NAME_KEY)

        intent_ranking: List["IntentPrediction"] = (
            latest_message.parse_data.get(INTENT_RANKING_KEY) or []
        )
        if (
            intent_to_affirm == DEFAULT_NLU_FALLBACK_INTENT_NAME
            and len(intent_ranking) > 1
        ):
            intent_to_affirm = intent_ranking[1][INTENT_NAME_KEY]

        affirmation_message = f"Did you mean '{
      
      intent_to_affirm}'?"

        message = {
    
    
            "text": affirmation_message,
            "buttons": [
                {
    
    "title": "Yes", "payload": f"/{
      
      intent_to_affirm}"},
                {
    
    "title": "No", "payload": f"/{
      
      USER_INTENT_OUT_OF_SCOPE}"},
            ],
            "utter_action": self.name(),
        }

        return [create_bot_utterance(message)]

ActionDefaultAskRephrase

默认的实现,要求用户重新表达他的意图,继承至ActionBotResponse

class ActionDefaultAskRephrase(ActionBotResponse):
    """Default implementation which asks the user to rephrase his intent."""

    def name(self) -> Text:
        """Returns action default ask rephrase name."""
        return ACTION_DEFAULT_ASK_REPHRASE_NAME

    def __init__(self) -> None:
        """Initializes action default ask rephrase."""
        super().__init__("utter_ask_rephrase", silent_fail=True)

ActionBotResponse

在运行时发出响应 唯一有效的 Action

class ActionBotResponse(Action):
    """An action which only effect is to utter a response when it is run."""

    def __init__(self, name: Text, silent_fail: Optional[bool] = False) -> None:
        """Creates action.

        Args:
            name: Name of the action.
            silent_fail: `True` if the action should fail silently in case no response
                was defined for this action.
        """
        self.utter_action = name
        self.silent_fail = silent_fail

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Simple run implementation uttering a (hopefully defined) response."""
        message = await nlg.generate(self.utter_action, tracker, output_channel.name())
        if message is None:
            if not self.silent_fail:
                logger.error(
                    "Couldn't create message for response '{}'."
                    "".format(self.utter_action)
                )
            return []
        message["utter_action"] = self.utter_action

        return [create_bot_utterance(message)]

TwoStageFallbackAction

可以传入外部HTTP端点的配置的参数

class TwoStageFallbackAction(LoopAction):
    def __init__(self, action_endpoint: Optional[EndpointConfig] = None) -> None:
        self._action_endpoint = action_endpoint

    def name(self) -> Text:
        return ACTION_TWO_STAGE_FALLBACK_NAME

    async def do(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
        events_so_far: List[Event],
    ) -> List[Event]:
        if _user_should_affirm(tracker, events_so_far):
            return await self._ask_affirm(output_channel, nlg, tracker, domain)

        return await self._ask_rephrase(output_channel, nlg, tracker, domain)

ActionUnlikelyIntent

指示NLU所预测的意图是意外的动作。这个动作可以被’UnexpecTEDIntentPolicy '预测。

class ActionUnlikelyIntent(Action):
    """An action that indicates that the intent predicted by NLU is unexpected.

    This action can be predicted by `UnexpecTEDIntentPolicy`.
    """

    def name(self) -> Text:
        """Returns the name of the action."""
        return ACTION_UNLIKELY_INTENT_NAME

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        return []

ActionBack

通过两句用户话语恢复跟踪器状态,run方法中新加入2个UserUtteranceReverted事件


class ActionBack(ActionBotResponse):
    """Revert the tracker state by two user utterances."""

    def name(self) -> Text:
        """Returns action back name."""
        return ACTION_BACK_NAME

    def __init__(self) -> None:
        """Initializes action back."""
        super().__init__("utter_back", silent_fail=True)

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        # only utter the response if it is available
        evts = await super().run(output_channel, nlg, tracker, domain)

        return evts + [UserUtteranceReverted(), UserUtteranceReverted()]

ActionExtractSlots

在每个用户回合后运行的默认动作。在下一个预测操作运行之前,动作在MessageProcessor.handle_message(…)中自动执行。

根据指定的词槽映射, 设置词槽以从用户消息中提取值

class ActionExtractSlots(Action):
    """Default action that runs after each user turn.

    Action is executed automatically in MessageProcessor.handle_message(...)
    before the next predicted action is run.

    Sets slots to extracted values from user message
    according to assigned slot mappings.
    """

    def __init__(self, action_endpoint: Optional[EndpointConfig]) -> None:
        """Initializes default action extract slots."""
        self._action_endpoint = action_endpoint

    def name(self) -> Text:
        """Returns action_extract_slots name."""
        return ACTION_EXTRACT_SLOTS

    @staticmethod
    def _matches_mapping_conditions(
        mapping: Dict[Text, Any], tracker: "DialogueStateTracker", slot_name: Text
    ) -> bool:
        slot_mapping_conditions = mapping.get(MAPPING_CONDITIONS)

        if not slot_mapping_conditions:
            return True

        if (
            tracker.active_loop
            and tracker.active_loop.get(LOOP_REJECTED)
            and tracker.get_slot(REQUESTED_SLOT) == slot_name
        ):
            return False

        # check if found mapping conditions matches form
        for condition in slot_mapping_conditions:
            active_loop = condition.get(ACTIVE_LOOP)

            if active_loop and active_loop == tracker.active_loop_name:
                condition_requested_slot = condition.get(REQUESTED_SLOT)
                if not condition_requested_slot:
                    return True
                if condition_requested_slot == tracker.get_slot(REQUESTED_SLOT):
                    return True

        return False

    @staticmethod
    def _verify_mapping_conditions(
        mapping: Dict[Text, Any], tracker: "DialogueStateTracker", slot_name: Text
    ) -> bool:
        if mapping.get(MAPPING_CONDITIONS) and mapping[MAPPING_TYPE] != str(
            SlotMappingType.FROM_TRIGGER_INTENT
        ):
            if not ActionExtractSlots._matches_mapping_conditions(
                mapping, tracker, slot_name
            ):
                return False

        return True

    async def _run_custom_action(
        self,
        custom_action: Text,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        slot_events: List[Event] = []
        remote_action = RemoteAction(custom_action, self._action_endpoint)
        disallowed_types = set()

        try:
            custom_events = await remote_action.run(
                output_channel, nlg, tracker, domain
            )
            for event in custom_events:
                if isinstance(event, SlotSet):
                    if tracker.get_slot(event.key) != event.value:
                        slot_events.append(event)
                elif isinstance(event, BotUttered):
                    slot_events.append(event)
                else:
                    disallowed_types.add(event.type_name)
        except (RasaException, ClientResponseError) as e:
            logger.warning(
                f"Failed to execute custom action '{
      
      custom_action}' "
                f"as a result of error '{
      
      str(e)}'. The default action "
                f"'{
      
      self.name()}' failed to fill slots with custom "
                f"mappings."
            )

        for type_name in disallowed_types:
            logger.info(
                f"Running custom action '{
      
      custom_action}' has resulted "
                f"in an event of type '{
      
      type_name}'. This is "
                f"disallowed and the tracker will not be "
                f"updated with this event."
            )

        return slot_events

    async def _execute_custom_action(
        self,
        mapping: Dict[Text, Any],
        executed_custom_actions: Set[Text],
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> Tuple[List[Event], Set[Text]]:
        custom_action = mapping.get("action")

        if not custom_action or custom_action in executed_custom_actions:
            return [], executed_custom_actions

        slot_events = await self._run_custom_action(
            custom_action, output_channel, nlg, tracker, domain
        )

        executed_custom_actions.add(custom_action)

        return slot_events, executed_custom_actions

    async def _execute_validation_action(
        self,
        extraction_events: List[Event],
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        slot_events: List[SlotSet] = [
            event for event in extraction_events if isinstance(event, SlotSet)
        ]

        slot_candidates = "\n".join([e.key for e in slot_events])
        logger.debug(f"Validating extracted slots: {
      
      slot_candidates}")

        if ACTION_VALIDATE_SLOT_MAPPINGS not in domain.user_actions:
            return cast(List[Event], slot_events)

        _tracker = DialogueStateTracker.from_events(
            tracker.sender_id,
            tracker.events_after_latest_restart() + cast(List[Event], slot_events),
            slots=domain.slots,
        )
        validate_events = await self._run_custom_action(
            ACTION_VALIDATE_SLOT_MAPPINGS, output_channel, nlg, _tracker, domain
        )
        validated_slot_names = [
            event.key for event in validate_events if isinstance(event, SlotSet)
        ]

        # If the custom action doesn't return a SlotSet event for an extracted slot
        # candidate we assume that it was valid. The custom action has to return a
        # SlotSet(slot_name, None) event to mark a Slot as invalid.
        return validate_events + [
            event for event in slot_events if event.key not in validated_slot_names
        ]

    def _fails_unique_entity_mapping_check(
        self,
        slot_name: Text,
        mapping: Dict[Text, Any],
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> bool:
        from rasa.core.actions.forms import FormAction

        if mapping[MAPPING_TYPE] != str(SlotMappingType.FROM_ENTITY):
            return False

        form_name = tracker.active_loop_name

        if not form_name:
            return False

        if tracker.get_slot(REQUESTED_SLOT) == slot_name:
            return False

        form = FormAction(form_name, self._action_endpoint)

        if slot_name not in form.required_slots(domain):
            return False

        if form.entity_mapping_is_unique(mapping, domain):
            return False

        return True

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action. Please see parent class for the full docstring."""
        slot_events: List[Event] = []
        executed_custom_actions: Set[Text] = set()

        user_slots = [
            slot for slot in domain.slots if slot.name not in DEFAULT_SLOT_NAMES
        ]

        for slot in user_slots:
            for mapping in slot.mappings:
                mapping_type = SlotMappingType(mapping.get(MAPPING_TYPE))

                if not SlotMapping.check_mapping_validity(
                    slot_name=slot.name,
                    mapping_type=mapping_type,
                    mapping=mapping,
                    domain=domain,
                ):
                    continue

                intent_is_desired = SlotMapping.intent_is_desired(
                    mapping, tracker, domain
                )

                if not intent_is_desired:
                    continue

                if not ActionExtractSlots._verify_mapping_conditions(
                    mapping, tracker, slot.name
                ):
                    continue

                if self._fails_unique_entity_mapping_check(
                    slot.name, mapping, tracker, domain
                ):
                    continue

                if mapping_type.is_predefined_type():
                    value = extract_slot_value_from_predefined_mapping(
                        mapping_type, mapping, tracker
                    )
                else:
                    value = None

                if value:
                    if not isinstance(slot, ListSlot):
                        value = value[-1]

                    if tracker.get_slot(slot.name) != value:
                        slot_events.append(SlotSet(slot.name, value))

                should_fill_custom_slot = mapping_type == SlotMappingType.CUSTOM

                if should_fill_custom_slot:
                    (
                        custom_evts,
                        executed_custom_actions,
                    ) = await self._execute_custom_action(
                        mapping,
                        executed_custom_actions,
                        output_channel,
                        nlg,
                        tracker,
                        domain,
                    )
                    slot_events.extend(custom_evts)

        validated_events = await self._execute_validation_action(
            slot_events, output_channel, nlg, tracker, domain
        )

        return validated_events

ActionRetrieveResponse

查询响应选择器以获得适当响应的操作

class ActionRetrieveResponse(ActionBotResponse):
    """An action which queries the Response Selector for the appropriate response."""

    def __init__(self, name: Text, silent_fail: Optional[bool] = False) -> None:
        """Creates action. See docstring of parent class."""
        super().__init__(name, silent_fail)
        self.action_name = name
        self.silent_fail = silent_fail

    @staticmethod
    def intent_name_from_action(action_name: Text) -> Text:
        """Resolve the name of the intent from the action name."""
        return action_name.split(UTTER_PREFIX)[1]

    def get_full_retrieval_name(
        self, tracker: "DialogueStateTracker"
    ) -> Optional[Text]:
        """Returns full retrieval name for the action.

        Extracts retrieval intent from response selector and
        returns complete action utterance name.

        Args:
            tracker: Tracker containing past conversation events.

        Returns:
            Full retrieval name of the action if the last user utterance
            contains a response selector output, `None` otherwise.
        """
        latest_message = tracker.latest_message

        if latest_message is None:
            return None

        if RESPONSE_SELECTOR_PROPERTY_NAME not in latest_message.parse_data:
            return None

        response_selector_properties = latest_message.parse_data[
            RESPONSE_SELECTOR_PROPERTY_NAME
        ]

        if (
            self.intent_name_from_action(self.action_name)
            in response_selector_properties
        ):
            query_key = self.intent_name_from_action(self.action_name)
        elif RESPONSE_SELECTOR_DEFAULT_INTENT in response_selector_properties:
            query_key = RESPONSE_SELECTOR_DEFAULT_INTENT
        else:
            return None

        selected = response_selector_properties[query_key]
        full_retrieval_utter_action = selected[RESPONSE_SELECTOR_PREDICTION_KEY][
            RESPONSE_SELECTOR_UTTER_ACTION_KEY
        ]
        return full_retrieval_utter_action

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Query the appropriate response and create a bot utterance with that."""
        latest_message = tracker.latest_message

        if latest_message is None:
            return []

        response_selector_properties = latest_message.parse_data[
            RESPONSE_SELECTOR_PROPERTY_NAME
        ]

        if (
            self.intent_name_from_action(self.action_name)
            in response_selector_properties
        ):
            query_key = self.intent_name_from_action(self.action_name)
        elif RESPONSE_SELECTOR_DEFAULT_INTENT in response_selector_properties:
            query_key = RESPONSE_SELECTOR_DEFAULT_INTENT
        else:
            if not self.silent_fail:
                logger.error(
                    "Couldn't create message for response action '{}'."
                    "".format(self.action_name)
                )
            return []

        logger.debug(f"Picking response from selector of type {
      
      query_key}")
        selected = response_selector_properties[query_key]

        # Override utter action of ActionBotResponse
        # with the complete utter action retrieved from
        # the output of response selector.
        self.utter_action = selected[RESPONSE_SELECTOR_PREDICTION_KEY][
            RESPONSE_SELECTOR_UTTER_ACTION_KEY
        ]

        return await super().run(output_channel, nlg, tracker, domain)

    def name(self) -> Text:
        """Returns action name."""
        return self.action_name

ActionEndToEndResponse

向用户发出端到端响应的动作Action

class ActionEndToEndResponse(Action):
    """Action to utter end-to-end responses to the user."""

    def __init__(self, action_text: Text) -> None:
        """Creates action.

        Args:
            action_text: Text of end-to-end bot response.
        """
        self.action_text = action_text

    def name(self) -> Text:
        """Returns action name."""
        # In case of an end-to-end action there is no label (aka name) for the action.
        # We fake a name by returning the text which the bot sends back to the user.
        return self.action_text

    async def run(
        self,
        output_channel: "OutputChannel",
        nlg: "NaturalLanguageGenerator",
        tracker: "DialogueStateTracker",
        domain: "Domain",
    ) -> List[Event]:
        """Runs action (see parent class for full docstring)."""
        message = {
    
    "text": self.action_text}
        return [create_bot_utterance(message)]

    def event_for_successful_execution(
        self, prediction: PolicyPrediction
    ) -> ActionExecuted:
        """Event which should be logged for the successful execution of this action.

        Args:
            prediction: Prediction which led to the execution of this event.

        Returns:
            Event which should be logged onto the tracker.
        """
        return ActionExecuted(
            policy=prediction.policy_name,
            confidence=prediction.max_confidence,
            action_text=self.action_text,
            hide_rule_turn=prediction.hide_rule_turn,
            metadata=prediction.action_metadata,
        )

Event

描述对话中的事件以及事件如何影响对话状态, 在用户与助手对话期间发生的所有事情的不可变表示,告诉rasa.shared.core.trackers.DialogueStateTracker如何在事件发生时更新其状态。


class Event(ABC):
    """Describes events in conversation and how the affect the conversation state.

    Immutable representation of everything which happened during a conversation of the
    user with the assistant. Tells the `rasa.shared.core.trackers.DialogueStateTracker`
    how to update its state as the events occur.
    """

    type_name = "event"

    def __init__(
        self,
        timestamp: Optional[float] = None,
        metadata: Optional[Dict[Text, Any]] = None,
    ) -> None:
        self.timestamp = timestamp or time.time()
        self.metadata = metadata or {
    
    }

    def __ne__(self, other: Any) -> bool:
        # Not strictly necessary, but to avoid having both x==y and x!=y
        # True at the same time
        return not (self == other)

    @abc.abstractmethod
    def as_story_string(self) -> Optional[Text]:
        """Returns the event as story string.

        Returns:
            textual representation of the event or None.
        """
        # Every class should implement this
        raise NotImplementedError

    @staticmethod
    def from_story_string(
        event_name: Text,
        parameters: Dict[Text, Any],
        default: Optional[Type["Event"]] = None,
    ) -> Optional[List["Event"]]:
        event_class = Event.resolve_by_type(event_name, default)

        if not event_class:
            return None

        return event_class._from_story_string(parameters)

    @staticmethod
    def from_parameters(
        parameters: Dict[Text, Any], default: Optional[Type["Event"]] = None
    ) -> Optional["Event"]:

        event_name = parameters.get("event")
        if event_name is None:
            return None

        event_class: Optional[Type[Event]] = Event.resolve_by_type(event_name, default)
        if not event_class:
            return None

        return event_class._from_parameters(parameters)

    @classmethod
    def _from_story_string(
        cls: Type[E], parameters: Dict[Text, Any]
    ) -> Optional[List[E]]:
        """Called to convert a parsed story line into an event."""
        return [cls(parameters.get("timestamp"), parameters.get("metadata"))]

    def as_dict(self) -> Dict[Text, Any]:
        d = {
    
    "event": self.type_name, "timestamp": self.timestamp}

        if self.metadata:
            d["metadata"] = self.metadata

        return d

    def fingerprint(self) -> Text:
        """Returns a unique hash for the event which is stable across python runs.

        Returns:
            fingerprint of the event
        """
        data = self.as_dict()
        del data["timestamp"]
        return rasa.shared.utils.io.get_dictionary_fingerprint(data)

    @classmethod
    def _from_parameters(cls, parameters: Dict[Text, Any]) -> Optional["Event"]:
        """Called to convert a dictionary of parameters to a single event.

        By default uses the same implementation as the story line
        conversation ``_from_story_string``. But the subclass might
        decide to handle parameters differently if the parsed parameters
        don't origin from a story file.
        """
        result = cls._from_story_string(parameters)
        if len(result) > 1:
            logger.warning(
                f"Event from parameters called with parameters "
                f"for multiple events. This is not supported, "
                f"only the first event will be returned. "
                f"Parameters: {
      
      parameters}"
            )
        return result[0] if result else None

    @staticmethod
    def resolve_by_type(
        type_name: Text, default: Optional[Type["Event"]] = None
    ) -> Optional[Type["Event"]]:
        """Returns a slots class by its type name."""

        for cls in rasa.shared.utils.common.all_subclasses(Event):
            if cls.type_name == type_name:
                return cls
        if type_name == "topic":
            return None  # backwards compatibility to support old TopicSet evts
        elif default is not None:
            return default
        else:
            raise ValueError(f"Unknown event name '{
      
      type_name}'.")

    def apply_to(self, tracker: "DialogueStateTracker") -> None:
        """Applies event to current conversation state.

        Args:
            tracker: The current conversation state.
        """
        pass

    @abc.abstractmethod
    def __eq__(self, other: Any) -> bool:
        """Compares object with other object."""
        # Every class should implement this
        raise NotImplementedError()

    def __str__(self) -> Text:
        """Returns text representation of event."""
        return f"{
      
      self.__class__.__name__}()"

UserUtteranceReverted

Bot会还原最近的用户消息之前的所有内容。bot将在最近的“UserUttered”之后恢复所有事件,这也意味着跟踪器上的最后一个事件通常是“action_listen”,bot正在等待一个新的用户消息。

class UserUtteranceReverted(AlwaysEqualEventMixin):
    """Bot reverts everything until before the most recent user message.

    The bot will revert all events after the latest `UserUttered`, this
    also means that the last event on the tracker is usually `action_listen`
    and the bot is waiting for a new user message.
    """

    type_name = "rewind"

    def __hash__(self) -> int:
        """Returns unique hash for event."""
        return hash(32143124315)

    def as_story_string(self) -> Text:
        """Returns text representation of event."""
        return self.type_name

    def apply_to(self, tracker: "DialogueStateTracker") -> None:
        """Applies event to current conversation state."""
        tracker._reset()
        tracker.replay_events()

DefinePrevUserUtteredFeaturization

存储是否基于文本或意图预测操作的信息。

class DefinePrevUserUtteredFeaturization(SkipEventInMDStoryMixin):
    """Stores information whether action was predicted based on text or intent."""

    type_name = "user_featurization"

    def __init__(
        self,
        use_text_for_featurization: bool,
        timestamp: Optional[float] = None,
        metadata: Optional[Dict[Text, Any]] = None,
    ) -> None:
        """Creates event.

        Args:
            use_text_for_featurization: `True` if message text was used to predict
                action. `False` if intent was used.
            timestamp: When the event was created.
            metadata: Additional event metadata.
        """
        super().__init__(timestamp, metadata)
        self.use_text_for_featurization = use_text_for_featurization

    def __str__(self) -> Text:
        """Returns text representation of event."""
        return f"DefinePrevUserUtteredFeaturization({
      
      self.use_text_for_featurization})"

    def __hash__(self) -> int:
        """Returns unique hash for event."""
        return hash(self.use_text_for_featurization)

    @classmethod
    def _from_parameters(
        cls, parameters: Dict[Text, Any]
    ) -> "DefinePrevUserUtteredFeaturization":
        return DefinePrevUserUtteredFeaturization(
            parameters.get(USE_TEXT_FOR_FEATURIZATION),
            parameters.get("timestamp"),
            parameters.get("metadata"),
        )

    def as_dict(self) -> Dict[Text, Any]:
        """Returns serialized event."""
        d = super().as_dict()
        d.update({
    
    USE_TEXT_FOR_FEATURIZATION: self.use_text_for_featurization})
        return d

    def apply_to(self, tracker: "DialogueStateTracker") -> None:
        """Applies event to current conversation state.

        Args:
            tracker: The current conversation state.
        """
        if tracker.latest_action_name != ACTION_LISTEN_NAME:
            # featurization belong only to the last user message
            # a user message is always followed by action listen
            return

        if not tracker.latest_message:
            return

        # update previous user message's featurization based on this event
        tracker.latest_message.use_text_for_featurization = (
            self.use_text_for_featurization
        )

    def __eq__(self, other: Any) -> bool:
        """Compares object with other object."""
        if not isinstance(other, DefinePrevUserUtteredFeaturization):
            return NotImplemented

        return self.use_text_for_featurization == other.use_text_for_featurization

ActionExecuted

操作描述所采取的Action及其结果。

class ActionExecuted(Event):
    """An operation describes an action taken + its result.

    It comprises an action and a list of events. operations will be appended
    to the latest `Turn`` in `Tracker.turns`.
    """

    type_name = "action"

    def __init__(
        self,
        action_name: Optional[Text] = None,
        policy: Optional[Text] = None,
        confidence: Optional[float] = None,
        timestamp: Optional[float] = None,
        metadata: Optional[Dict] = None,
        action_text: Optional[Text] = None,
        hide_rule_turn: bool = False,
    ) -> None:
        """Creates event for a successful event execution.

        Args:
            action_name: Name of the action which was executed. `None` if it was an
                end-to-end prediction.
            policy: Policy which predicted action.
            confidence: Confidence with which policy predicted action.
            timestamp: When the event was created.
            metadata: Additional event metadata.
            action_text: In case it's an end-to-end action prediction, the text which
                was predicted.
            hide_rule_turn: If `True`, this action should be hidden in the dialogue
                history created for ML-based policies.
        """
        self.action_name = action_name
        self.policy = policy
        self.confidence = confidence
        self.unpredictable = False
        self.action_text = action_text
        self.hide_rule_turn = hide_rule_turn

        if self.action_name is None and self.action_text is None:
            raise ValueError(
                "Both the name of the action and the end-to-end "
                "predicted text are missing. "
                "The `ActionExecuted` event cannot be initialised."
            )

        super().__init__(timestamp, metadata)

    def __members__(self) -> Tuple[Optional[Text], Optional[Text], Text]:
        meta_no_nones = {
    
    k: v for k, v in self.metadata.items() if v is not None}
        return (self.action_name, self.action_text, jsonpickle.encode(meta_no_nones))

    def __repr__(self) -> Text:
        """Returns event as string for debugging."""
        return "ActionExecuted(action: {}, policy: {}, confidence: {})".format(
            self.action_name, self.policy, self.confidence
        )

    def __str__(self) -> Text:
        """Returns event as human readable string."""
        return str(self.action_name) or str(self.action_text)

    def __hash__(self) -> int:
        """Returns unique hash for event."""
        return hash(self.__members__())

    def __eq__(self, other: Any) -> bool:
        """Compares object with other object."""
        if not isinstance(other, ActionExecuted):
            return NotImplemented

        return self.__members__() == other.__members__()

    def as_story_string(self) -> Optional[Text]:
        """Returns event in Markdown format."""
        if self.action_text:
            raise UnsupportedFeatureException(
                f"Printing end-to-end bot utterances is not supported in the "
                f"Markdown training format. Please use the YAML training data format "
                f"instead. Please see {
      
      DOCS_URL_TRAINING_DATA} for more information."
            )

        return self.action_name

    @classmethod
    def _from_story_string(
        cls, parameters: Dict[Text, Any]
    ) -> Optional[List["ActionExecuted"]]:
        return [
            ActionExecuted(
                parameters.get("name"),
                parameters.get("policy"),
                parameters.get("confidence"),
                parameters.get("timestamp"),
                parameters.get("metadata"),
                parameters.get("action_text"),
                parameters.get("hide_rule_turn", False),
            )
        ]

    def as_dict(self) -> Dict[Text, Any]:
        """Returns serialized event."""
        d = super().as_dict()
        d.update(
            {
    
    
                "name": self.action_name,
                "policy": self.policy,
                "confidence": self.confidence,
                "action_text": self.action_text,
                "hide_rule_turn": self.hide_rule_turn,
            }
        )
        return d

    def as_sub_state(self) -> Dict[Text, Text]:
        """Turns ActionExecuted into a dictionary containing action name or action text.

        One action cannot have both set at the same time

        Returns:
            a dictionary containing action name or action text with the corresponding
            key.
        """
        if self.action_name:
            return {
    
    ACTION_NAME: self.action_name}
        else:
            # FIXME: we should define the type better here, and require either
            #        `action_name` or `action_text`
            return {
    
    ACTION_TEXT: cast(Text, self.action_text)}

    def apply_to(self, tracker: "DialogueStateTracker") -> None:
        """Applies event to current conversation state."""
        tracker.set_latest_action(self.as_sub_state())
        tracker.clear_followup_action()

ActionExecutionRejected

通知核心 执行操作已被拒绝。

class ActionExecutionRejected(SkipEventInMDStoryMixin):
    """Notify Core that the execution of the action has been rejected."""

    type_name = "action_execution_rejected"

    def __init__(
        self,
        action_name: Text,
        policy: Optional[Text] = None,
        confidence: Optional[float] = None,
        timestamp: Optional[float] = None,
        metadata: Optional[Dict[Text, Any]] = None,
    ) -> None:
        """Creates event.

        Args:
            action_name: Action which was rejected.
            policy: Policy which predicted the rejected action.
            confidence: Confidence with which the reject action was predicted.
            timestamp: When the event was created.
            metadata: Additional event metadata.
        """
        self.action_name = action_name
        self.policy = policy
        self.confidence = confidence
        super().__init__(timestamp, metadata)

    def __str__(self) -> Text:
        """Returns text representation of event."""
        return (
            "ActionExecutionRejected("
            "action: {}, policy: {}, confidence: {})"
            "".format(self.action_name, self.policy, self.confidence)
        )

    def __hash__(self) -> int:
        """Returns unique hash for event."""
        return hash(self.action_name)

    def __eq__(self, other: Any) -> bool:
        """Compares object with other object."""
        if not isinstance(other, ActionExecutionRejected):
            return NotImplemented

        return self.action_name == other.action_name

    @classmethod
    def _from_parameters(cls, parameters: Dict[Text, Any]) -> "ActionExecutionRejected":
        return ActionExecutionRejected(
            parameters.get("name"),
            parameters.get("policy"),
            parameters.get("confidence"),
            parameters.get("timestamp"),
            parameters.get("metadata"),
        )

    def as_dict(self) -> Dict[Text, Any]:
        """Returns serialized event."""
        d = super().as_dict()
        d.update(
            {
    
    
                "name": self.action_name,
                "policy": self.policy,
                "confidence": self.confidence,
            }
        )
        return d

    def apply_to(self, tracker: "DialogueStateTracker") -> None:
        """Applies event to current conversation state."""
        tracker.reject_action(self.action_name)

Agent

Agent类为最重要的Rasa功能提供了一个接口。这包括训练、处理消息、加载对话模型、获取下一个action操作和处理通道。


class Agent:
    """The Agent class provides an interface for the most important Rasa functionality.

    This includes training, handling messages, loading a dialogue model,
    getting the next action, and handling a channel.
    """

    def __init__(
        self,
        domain: Optional[Domain] = None,
        generator: Union[EndpointConfig, NaturalLanguageGenerator, None] = None,
        tracker_store: Optional[TrackerStore] = None,
        lock_store: Optional[LockStore] = None,
        action_endpoint: Optional[EndpointConfig] = None,
        fingerprint: Optional[Text] = None,
        model_server: Optional[EndpointConfig] = None,
        remote_storage: Optional[Text] = None,
        http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
    ):
        """Initializes an `Agent`."""
        self.domain = domain
        self.processor: Optional[MessageProcessor] = None

        self.nlg = NaturalLanguageGenerator.create(generator, self.domain)
        self.tracker_store = self._create_tracker_store(tracker_store, self.domain)
        self.lock_store = self._create_lock_store(lock_store)
        self.action_endpoint = action_endpoint
        self.http_interpreter = http_interpreter

        self._set_fingerprint(fingerprint)
        self.model_server = model_server
        self.remote_storage = remote_storage

    @classmethod
    def load(
        cls,
        model_path: Union[Text, Path],
        domain: Optional[Domain] = None,
        generator: Union[EndpointConfig, NaturalLanguageGenerator, None] = None,
        tracker_store: Optional[TrackerStore] = None,
        lock_store: Optional[LockStore] = None,
        action_endpoint: Optional[EndpointConfig] = None,
        fingerprint: Optional[Text] = None,
        model_server: Optional[EndpointConfig] = None,
        remote_storage: Optional[Text] = None,
        http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
    ) -> Agent:
        """Constructs a new agent and loads the processer and model."""
        agent = Agent(
            domain=domain,
            generator=generator,
            tracker_store=tracker_store,
            lock_store=lock_store,
            action_endpoint=action_endpoint,
            fingerprint=fingerprint,
            model_server=model_server,
            remote_storage=remote_storage,
            http_interpreter=http_interpreter,
        )
        agent.load_model(model_path=model_path, fingerprint=fingerprint)
        return agent

EndpointConfig

外部HTTP端点的配置


class EndpointConfig:
    """Configuration for an external HTTP endpoint."""

    def __init__(
        self,
        url: Optional[Text] = None,
        params: Optional[Dict[Text, Any]] = None,
        headers: Optional[Dict[Text, Any]] = None,
        basic_auth: Optional[Dict[Text, Text]] = None,
        token: Optional[Text] = None,
        token_name: Text = "token",
        cafile: Optional[Text] = None,
        **kwargs: Any,
    ) -> None:
        """Creates an `EndpointConfig` instance."""
        self.url = url
        self.params = params or {
    
    }
        self.headers = headers or {
    
    }
        self.basic_auth = basic_auth or {
    
    }
        self.token = token
        self.token_name = token_name
        self.type = kwargs.pop("store_type", kwargs.pop("type", None))
        self.cafile = cafile
        self.kwargs = kwargs

    def session(self) -> aiohttp.ClientSession:
        """Creates and returns a configured aiohttp client session."""
        # create authentication parameters
        if self.basic_auth:
            auth = aiohttp.BasicAuth(
                self.basic_auth["username"], self.basic_auth["password"]
            )
        else:
            auth = None

        return aiohttp.ClientSession(
            headers=self.headers,
            auth=auth,
            timeout=aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT),
        )

Domain

域指定bot的策略在其中运行的范围。域子类提供了机器人可以采取的行动、可以识别的意图和实体。


class Domain:
    """The domain specifies the universe in which the bot's policy acts.

    A Domain subclass provides the actions the bot can take, the intents
    and entities it can recognise.
    """

    @classmethod
    def empty(cls) -> "Domain":
        """Returns empty Domain."""
        return Domain.from_dict({
    
    })

    @classmethod
    def load(cls, paths: Union[List[Union[Path, Text]], Text, Path]) -> "Domain":
        """Returns loaded Domain after merging all domain files."""
        if not paths:
            raise InvalidDomain(
                "No domain file was specified. Please specify a path "
                "to a valid domain file."
            )
        elif not isinstance(paths, list) and not isinstance(paths, set):
            paths = [paths]

        domain = Domain.empty()
        for path in paths:
            other = cls.from_path(path)
            domain = domain.merge(other)

        return domain

    @classmethod
    def from_path(cls, path: Union[Text, Path]) -> "Domain":
        """Loads the `Domain` from a path."""
        path = os.path.abspath(path)

        if os.path.isfile(path):
            domain = cls.from_file(path)
        elif os.path.isdir(path):
            domain = cls.from_directory(path)
        else:
            raise InvalidDomain(
                "Failed to load domain specification from '{}'. "
                "File not found!".format(os.path.abspath(path))
            )

        return domain

from_yaml

验证后从YAML文本加载“域”

   @classmethod
    def from_yaml(cls, yaml: Text, original_filename: Text = "") -> "Domain":
        """Loads the `Domain` from YAML text after validating it."""
        try:
            rasa.shared.utils.validation.validate_yaml_schema(yaml, DOMAIN_SCHEMA_FILE)

            data = rasa.shared.utils.io.read_yaml(yaml)
            if not rasa.shared.utils.validation.validate_training_data_format_version(
                data, original_filename
            ):
                return Domain.empty()
            return cls.from_dict(data)
        except YamlException as e:
            e.filename = original_filename
            raise e

required_slots_for_form

检索域中定义的表单所需词槽的列表。

    def required_slots_for_form(self, form_name: Text) -> List[Text]:
        """Retrieve the list of required slot names for a form defined in the domain.

        Args:
            form_name: The name of the form.

        Returns:
            The list of slot names or an empty list if no form was found.
        """
        form = self.forms.get(form_name)
        if form:
            return form[REQUIRED_SLOTS_KEY]

        return []

CollectingOutputChannel

在列表中收集发送消息的输出通道,(不发送到任何地方,只收集)


class CollectingOutputChannel(OutputChannel):
    """Output channel that collects send messages in a list

    (doesn't send them anywhere, just collects them)."""

    def __init__(self) -> None:
        """Initialise list to collect messages."""
        self.messages: List[Dict[Text, Any]] = []

    @classmethod
    def name(cls) -> Text:
        """Name of the channel."""
        return "collector"

    @staticmethod
    def _message(
        recipient_id: Text,
        text: Text = None,
        image: Text = None,
        buttons: List[Dict[Text, Any]] = None,
        attachment: Text = None,
        custom: Dict[Text, Any] = None,
    ) -> Dict:
        """Create a message object that will be stored."""

        obj = {
    
    
            "recipient_id": recipient_id,
            "text": text,
            "image": image,
            "buttons": buttons,
            "attachment": attachment,
            "custom": custom,
        }

        # filter out any values that are `None`
        return {
    
    k: v for k, v in obj.items() if v is not None}

    def latest_output(self) -> Optional[Dict[Text, Any]]:
        if self.messages:
            return self.messages[-1]
        else:
            return None

    async def _persist_message(self, message: Dict[Text, Any]) -> None:
        self.messages.append(message)

    async def send_text_message(
        self, recipient_id: Text, text: Text, **kwargs: Any
    ) -> None:
        for message_part in text.strip().split("\n\n"):
            await self._persist_message(self._message(recipient_id, text=message_part))

    async def send_image_url(
        self, recipient_id: Text, image: Text, **kwargs: Any
    ) -> None:
        """Sends an image. Default will just post the url as a string."""

        await self._persist_message(self._message(recipient_id, image=image))

    async def send_attachment(
        self, recipient_id: Text, attachment: Text, **kwargs: Any
    ) -> None:
        """Sends an attachment. Default will just post as a string."""

        await self._persist_message(self._message(recipient_id, attachment=attachment))

    async def send_text_with_buttons(
        self,
        recipient_id: Text,
        text: Text,
        buttons: List[Dict[Text, Any]],
        **kwargs: Any,
    ) -> None:
        await self._persist_message(
            self._message(recipient_id, text=text, buttons=buttons)
        )

    async def send_custom_json(
        self, recipient_id: Text, json_message: Dict[Text, Any], **kwargs: Any
    ) -> None:
        await self._persist_message(self._message(recipient_id, custom=json_message))

TemplatedNaturalLanguageGenerator

基于响应生成消息的自然语言生成器,回答可以根据对话的状态使用变量来定制话语

class TemplatedNaturalLanguageGenerator(NaturalLanguageGenerator):
    """Natural language generator that generates messages based on responses.

    The responses can use variables to customize the utterances based on the
    state of the dialogue.
    """

    def __init__(self, responses: Dict[Text, List[Dict[Text, Any]]]) -> None:
        """Creates a Template Natural Language Generator.

        Args:
            responses: responses that will be used to generate messages.
        """
        self.responses = responses

    def _matches_filled_slots(
        self, filled_slots: Dict[Text, Any], response: Dict[Text, Any]
    ) -> bool:
        """Checks if the conditional response variation matches the filled slots."""
        constraints = response.get(RESPONSE_CONDITION, [])
        for constraint in constraints:
            name = constraint["name"]
            value = constraint["value"]
            if filled_slots.get(name) != value:
                return False

        return True

    def _responses_for_utter_action(
        self, utter_action: Text, output_channel: Text, filled_slots: Dict[Text, Any]
    ) -> List[Dict[Text, Any]]:
        """Returns array of responses that fit the channel, action and condition."""
        default_responses = list(
            filter(
                lambda x: (x.get(RESPONSE_CONDITION) is None),
                self.responses[utter_action],
            )
        )
        conditional_responses = list(
            filter(
                lambda x: (
                    x.get(RESPONSE_CONDITION)
                    and self._matches_filled_slots(
                        filled_slots=filled_slots, response=x
                    )
                ),
                self.responses[utter_action],
            )
        )

        conditional_channel = list(
            filter(lambda x: (x.get(CHANNEL) == output_channel), conditional_responses)
        )
        conditional_no_channel = list(
            filter(lambda x: (x.get(CHANNEL) is None), conditional_responses)
        )
        default_channel = list(
            filter(lambda x: (x.get(CHANNEL) == output_channel), default_responses)
        )
        default_no_channel = list(
            filter(lambda x: (x.get(CHANNEL) is None), default_responses)
        )

        if conditional_channel:
            return conditional_channel

        if default_channel:
            return default_channel

        if conditional_no_channel:
            return conditional_no_channel

        return default_no_channel

    # noinspection PyUnusedLocal
    def _random_response_for(
        self, utter_action: Text, output_channel: Text, filled_slots: Dict[Text, Any]
    ) -> Optional[Dict[Text, Any]]:
        """Select random response for the utter action from available ones.

        If channel-specific responses for the current output channel are given,
        only choose from channel-specific ones.
        """
        import numpy as np

        if utter_action in self.responses:
            suitable_responses = self._responses_for_utter_action(
                utter_action, output_channel, filled_slots
            )

            if suitable_responses:
                selected_response = np.random.choice(suitable_responses)
                condition = selected_response.get(RESPONSE_CONDITION)
                if condition:
                    formatted_response_conditions = self._format_response_conditions(
                        condition
                    )
                    logger.debug(
                        "Selecting response variation with conditions:"
                        f"{
      
      formatted_response_conditions}"
                    )
                return selected_response
            else:
                return None
        else:
            return None

    async def generate(
        self,
        utter_action: Text,
        tracker: DialogueStateTracker,
        output_channel: Text,
        **kwargs: Any,
    ) -> Optional[Dict[Text, Any]]:
        """Generate a response for the requested utter action."""
        filled_slots = tracker.current_slot_values()
        return self.generate_from_slots(
            utter_action, filled_slots, output_channel, **kwargs
        )

    def generate_from_slots(
        self,
        utter_action: Text,
        filled_slots: Dict[Text, Any],
        output_channel: Text,
        **kwargs: Any,
    ) -> Optional[Dict[Text, Any]]:
        """Generate a response for the requested utter action."""
        # Fetching a random response for the passed utter action
        r = copy.deepcopy(
            self._random_response_for(utter_action, output_channel, filled_slots)
        )
        # Filling the slots in the response with placeholders and returning the response
        if r is not None:
            return self._fill_response(r, filled_slots, **kwargs)
        else:
            return None

    def _fill_response(
        self,
        response: Dict[Text, Any],
        filled_slots: Optional[Dict[Text, Any]] = None,
        **kwargs: Any,
    ) -> Dict[Text, Any]:
        """Combine slot values and key word arguments to fill responses."""
        # Getting the slot values in the response variables
        response_vars = self._response_variables(filled_slots, kwargs)

        keys_to_interpolate = [
            "text",
            "image",
            "custom",
            "buttons",
            "attachment",
            "quick_replies",
        ]
        if response_vars:
            for key in keys_to_interpolate:
                if key in response:
                    response[key] = interpolator.interpolate(
                        response[key], response_vars
                    )
        return response

    @staticmethod
    def _response_variables(
        filled_slots: Dict[Text, Any], kwargs: Dict[Text, Any]
    ) -> Dict[Text, Any]:
        """Combine slot values and key word arguments to fill responses."""
        if filled_slots is None:
            filled_slots = {
    
    }

        # Copying the filled slots in the response variables.
        response_vars = filled_slots.copy()
        response_vars.update(kwargs)
        return response_vars

    @staticmethod
    def _format_response_conditions(response_conditions: List[Dict[Text, Any]]) -> Text:
        formatted_response_conditions = [""]
        for index, condition in enumerate(response_conditions):
            constraints = []
            constraints.append(f"type: {
      
      str(condition['type'])}")
            constraints.append(f"name: {
      
      str(condition['name'])}")
            constraints.append(f"value: {
      
      str(condition['value'])}")

            condition_message = " | ".join(constraints)
            formatted_condition = f"[condition {
      
      str(index + 1)}] {
      
      condition_message}"
            formatted_response_conditions.append(formatted_condition)

        return "\n".join(formatted_response_conditions)

generate

为所请求的utter action.生成响应。

    def generate(
        self,
        utter_action: Text,
        tracker: DialogueStateTracker,
        output_channel: Text,
        **kwargs: Any,
    ) -> Optional[Dict[Text, Any]]:
        """Generate a response for the requested utter action."""
        filled_slots = tracker.current_slot_values()
        return self.generate_from_slots(
            utter_action, filled_slots, output_channel, **kwargs
        )

generate_from_slots

为所请求的utter action 生成响应response 。

    def generate_from_slots(
        self,
        utter_action: Text,
        filled_slots: Dict[Text, Any],
        output_channel: Text,
        **kwargs: Any,
    ) -> Optional[Dict[Text, Any]]:
        """Generate a response for the requested utter action."""
        # Fetching a random response for the passed utter action
        r = copy.deepcopy(
            self._random_response_for(utter_action, output_channel, filled_slots)
        )
        # Filling the slots in the response with placeholders and returning the response
        if r is not None:
            return self._fill_response(r, filled_slots, **kwargs)
        else:
            return None

_fill_response

结合词槽和关键字参数来填充响应

  def _fill_response(
        self,
        response: Dict[Text, Any],
        filled_slots: Optional[Dict[Text, Any]] = None,
        **kwargs: Any,
    ) -> Dict[Text, Any]:
        """Combine slot values and key word arguments to fill responses."""
        # Getting the slot values in the response variables
        response_vars = self._response_variables(filled_slots, kwargs)

        keys_to_interpolate = [
            "text",
            "image",
            "custom",
            "buttons",
            "attachment",
            "quick_replies",
        ]
        if response_vars:
            for key in keys_to_interpolate:
                if key in response:
                    response[key] = interpolator.interpolate(
                        response[key], response_vars
                    )
        return response

DialogueStateTracker

保持对话的状态。


class DialogueStateTracker:
    """Maintains the state of a conversation.

    The field max_event_history will only give you these last events,
    it can be set in the tracker_store"""

    @classmethod
    def from_dict(
        cls,
        sender_id: Text,
        events_as_dict: List[Dict[Text, Any]],
        slots: Optional[Iterable[Slot]] = None,
        max_event_history: Optional[int] = None,
    ) -> "DialogueStateTracker":
        """Create a tracker from dump.

        The dump should be an array of dumped events. When restoring
        the tracker, these events will be replayed to recreate the state.
        """
        evts = events.deserialise_events(events_as_dict)

        return cls.from_events(sender_id, evts, slots, max_event_history)

    @classmethod
    def from_events(
        cls,
        sender_id: Text,
        evts: List[Event],
        slots: Optional[Iterable[Slot]] = None,
        max_event_history: Optional[int] = None,
        sender_source: Optional[Text] = None,
        domain: Optional[Domain] = None,
    ) -> "DialogueStateTracker":
        """Creates tracker from existing events.

        Args:
            sender_id: The ID of the conversation.
            evts: Existing events which should be applied to the new tracker.
            slots: Slots which can be set.
            max_event_history: Maximum number of events which should be stored.
            sender_source: File source of the messages.
            domain: The current model domain.

        Returns:
            Instantiated tracker with its state updated according to the given
            events.
        """
        tracker = cls(sender_id, slots, max_event_history, sender_source)

        for e in evts:
            tracker.update(e, domain)

        return tracker

    def __init__(
        self,
        sender_id: Text,
        slots: Optional[Iterable[Slot]],
        max_event_history: Optional[int] = None,
        sender_source: Optional[Text] = None,
        is_rule_tracker: bool = False,
    ) -> None:
        """Initialize the tracker.

        A set of events can be stored externally, and we will run through all
        of them to get the current state. The tracker will represent all the
        information we captured while processing messages of the dialogue."""

        # maximum number of events to store
        self._max_event_history = max_event_history
        # list of previously seen events
        self.events = self._create_events([])
        # id of the source of the messages
        self.sender_id = sender_id
        # slots that can be filled in this domain
        if slots is not None:
            self.slots = {
    
    slot.name: copy.copy(slot) for slot in slots}
        else:
            self.slots = AnySlotDict()
        # file source of the messages
        self.sender_source = sender_source
        # whether the tracker belongs to a rule-based data
        self.is_rule_tracker = is_rule_tracker

        ###
        # current state of the tracker - MUST be re-creatable by processing
        # all the events. This only defines the attributes, values are set in
        # `reset()`
        ###
        # if tracker is paused, no actions should be taken
        self._paused = False
        # A deterministically scheduled action to be executed next
        self.followup_action = ACTION_LISTEN_NAME
        self.latest_action = None
        # Stores the most recent message sent by the user
        self.latest_message: Optional[UserUttered] = None
        self.latest_bot_utterance = None
        self._reset()
        self.active_loop: "TrackerActiveLoop" = {
    
    }

        # Optional model_id to add to all events.
        self.model_id: Optional[Text] = None

E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\constants.py

# Keys related to Forms (in the Domain)
REQUIRED_SLOTS_KEY = "required_slots"
IGNORED_INTENTS = "ignored_intents"

get_slot

获取词槽的值

    def get_slot(self, key: Text) -> Optional[Any]:
        """Retrieves the value of a slot."""

        if key in self.slots:
            return self.slots[key].value
        else:
            logger.info(f"Tried to access non existent slot '{
      
      key}'")
            return None

from_events

从现有事件创建跟踪程序

    @classmethod
    def from_events(
        cls,
        sender_id: Text,
        evts: List[Event],
        slots: Optional[Iterable[Slot]] = None,
        max_event_history: Optional[int] = None,
        sender_source: Optional[Text] = None,
        domain: Optional[Domain] = None,
    ) -> "DialogueStateTracker":
        """Creates tracker from existing events.

        Args:
            sender_id: The ID of the conversation.
            evts: Existing events which should be applied to the new tracker.
            slots: Slots which can be set.
            max_event_history: Maximum number of events which should be stored.
            sender_source: File source of the messages.
            domain: The current model domain.

        Returns:
            Instantiated tracker with its state updated according to the given
            events.
        """
        tracker = cls(sender_id, slots, max_event_history, sender_source)

        for e in evts:
            tracker.update(e, domain)

        return tracker

MessageProcessor

消息处理器是与机器人模型通信的接口


class MessageProcessor:
    """The message processor is interface for communicating with a bot model."""

    def __init__(
        self,
        model_path: Union[Text, Path],
        tracker_store: rasa.core.tracker_store.TrackerStore,
        lock_store: LockStore,
        generator: NaturalLanguageGenerator,
        action_endpoint: Optional[EndpointConfig] = None,
        max_number_of_predictions: int = MAX_NUMBER_OF_PREDICTIONS,
        on_circuit_break: Optional[LambdaType] = None,
        http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
    ) -> None:
        """Initializes a `MessageProcessor`."""
        self.nlg = generator
        self.tracker_store = tracker_store
        self.lock_store = lock_store
        self.max_number_of_predictions = max_number_of_predictions
        self.on_circuit_break = on_circuit_break
        self.action_endpoint = action_endpoint
        self.model_filename, self.model_metadata, self.graph_runner = self._load_model(
            model_path
        )
        self.model_path = Path(model_path)
        self.domain = self.model_metadata.domain
        self.http_interpreter = http_interpreter

    @staticmethod
    def _load_model(
        model_path: Union[Text, Path]
    ) -> Tuple[Text, ModelMetadata, GraphRunner]:
        """Unpacks a model from a given path using the graph model loader."""
        try:
            if os.path.isfile(model_path):
                model_tar = model_path
            else:
                model_tar = get_latest_model(model_path)
                if not model_tar:
                    raise ModelNotFound(f"No model found at path '{
      
      model_path}'.")
        except TypeError:
            raise ModelNotFound(f"Model {
      
      model_path} can not be loaded.")

        logger.info(f"Loading model {
      
      model_tar}...")
        with tempfile.TemporaryDirectory() as temporary_directory:
            try:
                metadata, runner = loader.load_predict_graph_runner(
                    Path(temporary_directory),
                    Path(model_tar),
                    LocalModelStorage,
                    DaskGraphRunner,
                )
                return os.path.basename(model_tar), metadata, runner
            except tarfile.ReadError:
                raise ModelNotFound(f"Model {
      
      model_path} can not be loaded.")

constants 常量

E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\core\constants.py


DEFAULT_CATEGORICAL_SLOT_VALUE = "__other__"

USER_INTENT_RESTART = "restart"
USER_INTENT_BACK = "back"
USER_INTENT_OUT_OF_SCOPE = "out_of_scope"
USER_INTENT_SESSION_START = "session_start"
SESSION_START_METADATA_SLOT = "session_started_metadata"

DEFAULT_INTENTS = [
    USER_INTENT_RESTART,
    USER_INTENT_BACK,
    USER_INTENT_OUT_OF_SCOPE,
    USER_INTENT_SESSION_START,
    constants.DEFAULT_NLU_FALLBACK_INTENT_NAME,
]

LOOP_NAME = "name"

ACTION_LISTEN_NAME = "action_listen"
ACTION_RESTART_NAME = "action_restart"
ACTION_SESSION_START_NAME = "action_session_start"
ACTION_DEFAULT_FALLBACK_NAME = "action_default_fallback"
ACTION_DEACTIVATE_LOOP_NAME = "action_deactivate_loop"
ACTION_REVERT_FALLBACK_EVENTS_NAME = "action_revert_fallback_events"
ACTION_DEFAULT_ASK_AFFIRMATION_NAME = "action_default_ask_affirmation"
ACTION_DEFAULT_ASK_REPHRASE_NAME = "action_default_ask_rephrase"
ACTION_BACK_NAME = "action_back"
ACTION_TWO_STAGE_FALLBACK_NAME = "action_two_stage_fallback"
ACTION_UNLIKELY_INTENT_NAME = "action_unlikely_intent"
RULE_SNIPPET_ACTION_NAME = "..."
ACTION_EXTRACT_SLOTS = "action_extract_slots"
ACTION_VALIDATE_SLOT_MAPPINGS = "action_validate_slot_mappings"

DEFAULT_ACTION_NAMES = [
    ACTION_LISTEN_NAME,
    ACTION_RESTART_NAME,
    ACTION_SESSION_START_NAME,
    ACTION_DEFAULT_FALLBACK_NAME,
    ACTION_DEACTIVATE_LOOP_NAME,
    ACTION_REVERT_FALLBACK_EVENTS_NAME,
    ACTION_DEFAULT_ASK_AFFIRMATION_NAME,
    ACTION_DEFAULT_ASK_REPHRASE_NAME,
    ACTION_TWO_STAGE_FALLBACK_NAME,
    ACTION_UNLIKELY_INTENT_NAME,
    ACTION_BACK_NAME,
    RULE_SNIPPET_ACTION_NAME,
    ACTION_EXTRACT_SLOTS,
]

E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\constants.py

LATEST_TRAINING_DATA_FORMAT_VERSION = "3.1"
DEFAULT_NLU_FALLBACK_INTENT_NAME = "nlu_fallback"

E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\core\constants.py

LOOP_NAME = "name"
REQUESTED_SLOT = "requested_slot"

# slots for knowledge base
SLOT_LISTED_ITEMS = "knowledge_base_listed_objects"
SLOT_LAST_OBJECT = "knowledge_base_last_object"
SLOT_LAST_OBJECT_TYPE = "knowledge_base_last_object_type"
DEFAULT_KNOWLEDGE_BASE_ACTION = "action_query_knowledge_base"

DEFAULT_SLOT_NAMES = {
    
    
    REQUESTED_SLOT,
    SESSION_START_METADATA_SLOT,
    SLOT_LISTED_ITEMS,
    SLOT_LAST_OBJECT,
    SLOT_LAST_OBJECT_TYPE,
}


SLOT_MAPPINGS = "mappings"
MAPPING_CONDITIONS = "conditions"
MAPPING_TYPE = "type"

E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\nlu\constants.py

TEXT = "text"
TEXT_TOKENS = "text_tokens"
INTENT = "intent"
NOT_INTENT = "not_intent"
RESPONSE = "response"
RESPONSE_SELECTOR = "response_selector"
INTENT_RESPONSE_KEY = "intent_response_key"
ACTION_TEXT = "action_text"
ACTION_NAME = "action_name"
INTENT_NAME_KEY = "name"
FULL_RETRIEVAL_INTENT_NAME_KEY = "full_retrieval_intent_name"
METADATA = "metadata"
METADATA_INTENT = "intent"
METADATA_EXAMPLE = "example"
METADATA_MODEL_ID = "model_id"
INTENT_RANKING_KEY = "intent_ranking"
PREDICTED_CONFIDENCE_KEY = "confidence"

E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\constants.py

DEFAULT_SENDER_ID = "default"
UTTER_PREFIX = "utter_"

train

训练 Rasa模型 (Core and NLU).

def train(
    domain: Text,
    config: Text,
    training_files: Optional[Union[Text, List[Text]]],
    output: Text = rasa.shared.constants.DEFAULT_MODELS_PATH,
    dry_run: bool = False,
    force_training: bool = False,
    fixed_model_name: Optional[Text] = None,
    persist_nlu_training_data: bool = False,
    core_additional_arguments: Optional[Dict] = None,
    nlu_additional_arguments: Optional[Dict] = None,
    model_to_finetune: Optional[Text] = None,
    finetuning_epoch_fraction: float = 1.0,
) -> TrainingResult:
    """Trains a Rasa model (Core and NLU).

    Args:
        domain: Path to the domain file.
        config: Path to the config file.
        training_files: List of paths to training data files.
        output: Output directory for the trained model.
        dry_run: If `True` then no training will be done, and the information about
            whether the training needs to be done will be printed.
        force_training: If `True` retrain model even if data has not changed.
        fixed_model_name: Name of model to be stored.
        persist_nlu_training_data: `True` if the NLU training data should be persisted
            with the model.
        core_additional_arguments: Additional training parameters for core training.
        nlu_additional_arguments: Additional training parameters forwarded to training
            method of each NLU component.
        model_to_finetune: Optional path to a model which should be finetuned or
            a directory in case the latest trained model should be used.
        finetuning_epoch_fraction: The fraction currently specified training epochs
            in the model configuration which should be used for finetuning.

    Returns:
        An instance of `TrainingResult`.
    """
    file_importer = TrainingDataImporter.load_from_config(
        config, domain, training_files
    )

    stories = file_importer.get_stories()
    nlu_data = file_importer.get_nlu_data()

    training_type = TrainingType.BOTH

    if nlu_data.has_e2e_examples():
        rasa.shared.utils.common.mark_as_experimental_feature("end-to-end training")
        training_type = TrainingType.END_TO_END

    if stories.is_empty() and nlu_data.contains_no_pure_nlu_data():
        rasa.shared.utils.cli.print_error(
            "No training data given. Please provide stories and NLU data in "
            "order to train a Rasa model using the '--data' argument."
        )
        return TrainingResult(code=1)

    domain_object = file_importer.get_domain()
    if domain_object.is_empty():
        rasa.shared.utils.cli.print_warning(
            "Core training was skipped because no valid domain file was found. "
            "Only an NLU-model was created. Please specify a valid domain using "
            "the '--domain' argument or check if the provided domain file exists."
        )
        training_type = TrainingType.NLU

    elif stories.is_empty():
        rasa.shared.utils.cli.print_warning(
            "No stories present. Just a Rasa NLU model will be trained."
        )
        training_type = TrainingType.NLU

    # We will train nlu if there are any nlu example, including from e2e stories.
    elif nlu_data.contains_no_pure_nlu_data() and not nlu_data.has_e2e_examples():
        rasa.shared.utils.cli.print_warning(
            "No NLU data present. Just a Rasa Core model will be trained."
        )
        training_type = TrainingType.CORE

    with telemetry.track_model_training(file_importer, model_type="rasa"):
        return _train_graph(
            file_importer,
            training_type=training_type,
            output_path=output,
            fixed_model_name=fixed_model_name,
            model_to_finetune=model_to_finetune,
            force_full_training=force_training,
            persist_nlu_training_data=persist_nlu_training_data,
            finetuning_epoch_fraction=finetuning_epoch_fraction,
            dry_run=dry_run,
            **(core_additional_arguments or {
    
    }),
            **(nlu_additional_arguments or {
    
    }),
        )

TrainingResult

保存关于训练结果的信息

class TrainingResult(NamedTuple):
    """Holds information about the results of training."""

    model: Optional[Text] = None
    code: int = 0
    dry_run_results: Optional[Dict[Text, Union[FingerprintStatus, Any]]] = None

TrainingCache

将训练结果存储在持久缓存中。当数据/配置在两次训练之间没有变化时,用于最小化再训练

class TrainingCache(abc.ABC):
    """Stores training results in a persistent cache.

    Used to minimize re-retraining when the data / config didn't change in between
    training runs.
    """

    @abc.abstractmethod
    def cache_output(
        self,
        fingerprint_key: Text,
        output: Any,
        output_fingerprint: Text,
        model_storage: ModelStorage,
    ) -> None:
        """Adds the output to the cache.

        If the output is of type `Cacheable` the output is persisted to disk in addition
        to its fingerprint.

        Args:
            fingerprint_key: The fingerprint key serves as key for the cache. Graph
                components can use their fingerprint key to lookup fingerprints of
                previous training runs.
            output: The output. The output is only cached to disk if it's of type
                `Cacheable`.
            output_fingerprint: The fingerprint of their output. This can be used
                to lookup potentially persisted outputs on disk.
            model_storage: Required for caching `Resource` instances. E.g. `Resource`s
                use that to copy data from the model storage to the cache.
        """

    ...

    @abc.abstractmethod
    def get_cached_output_fingerprint(self, fingerprint_key: Text) -> Optional[Text]:
        """Retrieves fingerprint of output based on fingerprint key.

        Args:
            fingerprint_key: The fingerprint serves as key for the lookup of output
                fingerprints.

        Returns:
            The fingerprint of a matching output or `None` in case no cache entry was
            found for the given fingerprint key.
        """
        ...

    @abc.abstractmethod
    def get_cached_result(
        self, output_fingerprint_key: Text, node_name: Text, model_storage: ModelStorage
    ) -> Optional[Cacheable]:
        """Returns a potentially cached output result.

        Args:
            output_fingerprint_key: The fingerprint key of the output serves as lookup
                key for a potentially cached version of this output.
            node_name: The name of the graph node which wants to use this cached result.
            model_storage: The current model storage (e.g. used when restoring
                `Resource` objects so that they can fill the model storage with data).

        Returns:
            `None` if no matching result was found or restored `Cacheable`.
        """
        ...

LocalTrainingCache

将训练结果缓存到本地磁盘上


class LocalTrainingCache(TrainingCache):
    """Caches training results on local disk (see parent class for full docstring)."""

    Base: DeclarativeMeta = declarative_base()

    class CacheEntry(Base):
        """Stores metadata about a single cache entry."""

        __tablename__ = "cache_entry"

        fingerprint_key = sa.Column(sa.String(), primary_key=True)
        output_fingerprint_key = sa.Column(sa.String(), nullable=False, index=True)
        last_used = sa.Column(sa.DateTime(timezone=True), nullable=False)
        rasa_version = sa.Column(sa.String(255), nullable=False)
        result_location = sa.Column(sa.String())
        result_type = sa.Column(sa.String())

    def __init__(self) -> None:
        """Creates cache.

        The `Cache` setting can be configured via environment variables.
        """
        self._cache_location = LocalTrainingCache._get_cache_location()

        self._max_cache_size = float(
            os.environ.get(CACHE_SIZE_ENV, DEFAULT_CACHE_SIZE_MB)
        )

        self._cache_database_name = os.environ.get(
            CACHE_DB_NAME_ENV, DEFAULT_CACHE_NAME
        )

        if not self._cache_location.exists() and not self._is_disabled():
            logger.debug(
                f"Creating caching directory '{
      
      self._cache_location}' because "
                f"it doesn't exist yet."
            )
            self._cache_location.mkdir(parents=True)

        self._sessionmaker = self._create_database()

        self._drop_cache_entries_from_incompatible_versions()

LockStore

class LockStore:
    @staticmethod
    def create(obj: Union["LockStore", EndpointConfig, None]) -> "LockStore":
        """Factory to create a lock store."""

        if isinstance(obj, LockStore):
            return obj

        try:
            return _create_from_endpoint_config(obj)
        except ConnectionError as error:
            raise ConnectionException("Cannot connect to lock store.") from error

    @staticmethod
    def create_lock(conversation_id: Text) -> TicketLock:
        """Create a new `TicketLock` for `conversation_id`."""

        return TicketLock(conversation_id)

    def get_lock(self, conversation_id: Text) -> Optional[TicketLock]:
        """Fetch lock for `conversation_id` from storage."""

        raise NotImplementedError

    def delete_lock(self, conversation_id: Text) -> None:
        """Delete lock for `conversation_id` from storage."""

        raise NotImplementedError

    def save_lock(self, lock: TicketLock) -> None:
        """Commit `lock` to storage."""

        raise NotImplementedError

    def issue_ticket(
        self, conversation_id: Text, lock_lifetime: float = LOCK_LIFETIME
    ) -> int:
        """Issue new ticket with `lock_lifetime` for lock associated with
        `conversation_id`.

        Creates a new lock if none is found.
        """
        logger.debug(f"Issuing ticket for conversation '{
      
      conversation_id}'.")
        try:
            lock = self.get_or_create_lock(conversation_id)
            ticket = lock.issue_ticket(lock_lifetime)
            self.save_lock(lock)

            return ticket
        except Exception as e:
            raise LockError(f"Error while acquiring lock. Error:\n{
      
      e}")

    @asynccontextmanager
    async def lock(
        self,
        conversation_id: Text,
        lock_lifetime: float = LOCK_LIFETIME,
        wait_time_in_seconds: float = 1,
    ) -> AsyncGenerator[TicketLock, None]:
        """Acquire lock with lifetime `lock_lifetime`for `conversation_id`.

        Try acquiring lock with a wait time of `wait_time_in_seconds` seconds
        between attempts. Raise a `LockError` if lock has expired.
        """
        ticket = self.issue_ticket(conversation_id, lock_lifetime)
        try:

            yield await self._acquire_lock(
                conversation_id, ticket, wait_time_in_seconds
            )
        finally:
            self.cleanup(conversation_id, ticket)

    async def _acquire_lock(
        self, conversation_id: Text, ticket: int, wait_time_in_seconds: float
    ) -> TicketLock:
        logger.debug(f"Acquiring lock for conversation '{
      
      conversation_id}'.")
        while True:
            # fetch lock in every iteration because lock might no longer exist
            lock = self.get_lock(conversation_id)

            # exit loop if lock does not exist anymore (expired)
            if not lock:
                break

            # acquire lock if it isn't locked
            if not lock.is_locked(ticket):
                logger.debug(f"Acquired lock for conversation '{
      
      conversation_id}'.")
                return lock

            items_before_this = ticket - (lock.now_serving or 0)

            logger.debug(
                f"Failed to acquire lock for conversation ID '{
      
      conversation_id}' "
                f"because {
      
      items_before_this} other item(s) for this "
                f"conversation ID have to be finished processing first. "
                f"Retrying in {
      
      wait_time_in_seconds} seconds ..."
            )

            # sleep and update lock
            await asyncio.sleep(wait_time_in_seconds)
            self.update_lock(conversation_id)

        raise LockError(
            f"Could not acquire lock for conversation_id '{
      
      conversation_id}'."
        )

    def update_lock(self, conversation_id: Text) -> None:
        """Fetch lock for `conversation_id`, remove expired tickets and save lock."""

        lock = self.get_lock(conversation_id)
        if lock:
            lock.remove_expired_tickets()
            self.save_lock(lock)

    def get_or_create_lock(self, conversation_id: Text) -> TicketLock:
        """Fetch existing lock for `conversation_id` or create a new one if
        it doesn't exist."""

        existing_lock = self.get_lock(conversation_id)

        if existing_lock:
            return existing_lock

        return self.create_lock(conversation_id)

    def is_someone_waiting(self, conversation_id: Text) -> bool:
        """Return whether someone is waiting for lock associated with
        `conversation_id`."""

        lock = self.get_lock(conversation_id)
        if lock:
            return lock.is_someone_waiting()

        return False

    def finish_serving(self, conversation_id: Text, ticket_number: int) -> None:
        """Finish serving ticket with `ticket_number` for `conversation_id`.

        Removes ticket from lock and saves lock.
        """

        lock = self.get_lock(conversation_id)
        if lock:
            lock.remove_ticket_for(ticket_number)
            self.save_lock(lock)

    def cleanup(self, conversation_id: Text, ticket_number: int) -> None:
        """Remove lock for `conversation_id` if no one is waiting."""

        self.finish_serving(conversation_id, ticket_number)
        if not self.is_someone_waiting(conversation_id):
            self.delete_lock(conversation_id)

    @staticmethod
    def _log_deletion(conversation_id: Text, deletion_successful: bool) -> None:
        if deletion_successful:
            logger.debug(f"Deleted lock for conversation '{
      
      conversation_id}'.")
        else:
            logger.debug(f"Could not delete lock for conversation '{
      
      conversation_id}'.")


TrackerStore


class TrackerStore:
    """Represents common behavior and interface for all `TrackerStore`s."""

    def __init__(
        self,
        domain: Optional[Domain],
        event_broker: Optional[EventBroker] = None,
        **kwargs: Dict[Text, Any],
    ) -> None:
        """Create a TrackerStore.

        Args:
            domain: The `Domain` to initialize the `DialogueStateTracker`.
            event_broker: An event broker to publish any new events to another
                destination.
            kwargs: Additional kwargs.
        """
        self._domain = domain or Domain.empty()
        self.event_broker = event_broker
        self.max_event_history = None

    @staticmethod
    def create(
        obj: Union["TrackerStore", EndpointConfig, None],
        domain: Optional[Domain] = None,
        event_broker: Optional[EventBroker] = None,
    ) -> "TrackerStore":
        """Factory to create a tracker store."""
        if isinstance(obj, TrackerStore):
            return obj

        from botocore.exceptions import BotoCoreError
        import pymongo.errors
        import sqlalchemy.exc

        try:
            return _create_from_endpoint_config(obj, domain, event_broker)
        except (
            BotoCoreError,
            pymongo.errors.ConnectionFailure,
            sqlalchemy.exc.OperationalError,
            ConnectionError,
            pymongo.errors.OperationFailure,
        ) as error:
            raise ConnectionException(
                "Cannot connect to tracker store." + str(error)
            ) from error

    def get_or_create_tracker(
        self,
        sender_id: Text,
        max_event_history: Optional[int] = None,
        append_action_listen: bool = True,
    ) -> "DialogueStateTracker":
        """Returns tracker or creates one if the retrieval returns None.

        Args:
            sender_id: Conversation ID associated with the requested tracker.
            max_event_history: Value to update the tracker store's max event history to.
            append_action_listen: Whether or not to append an initial `action_listen`.
        """
        self.max_event_history = max_event_history

        tracker = self.retrieve(sender_id)

        if tracker is None:
            tracker = self.create_tracker(
                sender_id, append_action_listen=append_action_listen
            )

        return tracker

    def init_tracker(self, sender_id: Text) -> "DialogueStateTracker":
        """Returns a Dialogue State Tracker."""
        return DialogueStateTracker(
            sender_id,
            self.domain.slots,
            max_event_history=self.max_event_history,
        )

Gavin大咖语录

Nlp或者说对话机器人确实是一个引人入胜的一个领域,就是你越研究越感觉有意思,而且越研究你也有可能感觉越复杂,有很多问题,如果你不深入下去或者你没有经验的话,你不会意识到他是问题,但是一旦这个项目很复杂的时候,它就会暴露出很多技术的问题,或者说你必须考虑的点。

我们整个 NLP高手之路课程,我们有 kernel版本,kernel版本就是101课,我们这个也有PRO版本的,PRO版本是137课,我相信你听着听到这里面你肯定是已经参加了我们pro课程的内容,当然我们也有168节课的 master版本的内容,我们的课程随着课程推进,只能说项目越来越复杂,同时谈的架构或者是背后的思想越来越具有统治的力量,它不仅仅是信息或者知识,他是给你带来能够再生其他的信息或者支持,或者能够让你一听这个东西会对以前的很多信息或者知识突然贯通,或者说一些以前困惑你的问题,你感觉突然不困惑,突然恍然大悟的一些感觉,这就是技术具有生产力的一些东西或者统治力的东西。

我们随着课程的进行,会跟大家谈更多这方面的,尤其是基于Gavin大咖过去这么多年做星空对话机器人的一些经验,会结合Rasa的代码或者项目跟大家都会有更多的分享,也包括我们的整个Bayesian deep learning的部分,Bayesian这个是大家做deep learning或者做神经网络,是相当于一个皇冠上的技能上的巅峰性的东西,我们会至少有20~30节的课的时间跟大家讲解。好,大家如果对这个内容都很感兴趣的话,可以随时联系Gavin大咖,也可以联系我们的工作人员。今天的内容我们就到这里。

Gavin大咖简介

星空智能对话机器人创始人、AI通用双线思考法作者,现工作于硅谷顶级的AI实验室。专精于Conversational AI. 在美国曾先后工作于硅谷最顶级的机器学习和人工智能实验室

Gavin大咖微信:NLP_Matrix_Space

联系电话:+1 650-603-1290

联系邮箱:[email protected]

助教老师微信:Spark_AI_NLP
在这里插入图片描述

在这里插入图片描述

Rasa 3.x系列博客分享

猜你喜欢

转载自blog.csdn.net/duan_zhihua/article/details/124125665