Files
wehub-resource-sync dc6079821b
Docs Tests / Check for file changes (push) Has been cancelled
Docs Tests / Test Documentation (push) Has been cancelled
Docs Tests / Documentation Linting Checks (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Has been cancelled
Continuous Integration / Check for file changes (push) Has been cancelled
Continuous Integration / Wait for docs tests (push) Has been cancelled
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Check for changelog (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Upload coverage reports to codeclimate (push) Has been cancelled
Continuous Integration / Run Non-Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Run Broker Integration Tests (push) Has been cancelled
Continuous Integration / Run Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Build Docker base images and setup environment (push) Has been cancelled
Continuous Integration / Build Docker (default) (push) Has been cancelled
Continuous Integration / Build Docker (full) (push) Has been cancelled
Continuous Integration / Build Docker (mitie-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-de) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-it) (push) Has been cancelled
Continuous Integration / Deploy to PyPI (push) Has been cancelled
Continuous Integration / Notify Slack & Publish Release Notes (push) Has been cancelled
Publish Documentation / Evaluate release tag (push) Has been cancelled
Publish Documentation / Prebuild Docs (push) Has been cancelled
Publish Documentation / Preview Docs (push) Has been cancelled
Publish Documentation / Check for file changes (push) Has been cancelled
Publish Documentation / Publish Docs (push) Has been cancelled
Automatic PR Merger / mergepal (push) Has been cancelled
CI Github Actions / Run Tests (push) Has been cancelled
Semgrep / Semgrep Workflow Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:47 +08:00

738 lines
26 KiB
Python

import copy
from typing import Text, List, Optional, Union, Any, Dict, Set
import itertools
import logging
import structlog
import json
from rasa.core.actions import action
from rasa.core.actions.loops import LoopAction
from rasa.core.channels import OutputChannel
from rasa.shared.core.domain import Domain, KEY_SLOTS
from rasa.shared.core.constants import SlotMappingType, SLOT_MAPPINGS, MAPPING_TYPE
from rasa.core.actions.action import ActionExecutionRejection, RemoteAction
from rasa.shared.core.constants import (
ACTION_EXTRACT_SLOTS,
ACTION_LISTEN_NAME,
REQUESTED_SLOT,
)
from rasa.shared.constants import UTTER_PREFIX
from rasa.shared.core.events import (
Event,
SlotSet,
ActionExecuted,
ActiveLoop,
ActionExecutionRejected,
Restarted,
)
from rasa.core.nlg import NaturalLanguageGenerator
from rasa.shared.core.slot_mappings import SlotMapping
from rasa.shared.core.slots import ListSlot
from rasa.shared.core.trackers import DialogueStateTracker
from rasa.utils.endpoints import EndpointConfig
logger = logging.getLogger(__name__)
structlogger = structlog.get_logger()
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
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())
def from_entity(
self,
entity: Text,
intent: Optional[Union[Text, List[Text]]] = None,
not_intent: Optional[Union[Text, List[Text]]] = None,
role: Optional[Text] = None,
group: Optional[Text] = None,
) -> Dict[Text, Any]:
"""A dictionary for slot mapping to extract slot value.
From:
- an extracted entity
- conditioned on
- intent if it is not None
- not_intent if it is not None,
meaning user intent should not be this intent
- role if it is not None
- group if it is not None
"""
intent, not_intent = (
SlotMapping.to_list(intent),
SlotMapping.to_list(not_intent),
)
return {
"type": str(SlotMappingType.FROM_ENTITY),
"entity": entity,
"intent": intent,
"not_intent": not_intent,
"role": role,
"group": group,
}
def get_mappings_for_slot(
self, slot_to_fill: Text, domain: Domain
) -> List[Dict[Text, Any]]:
"""Get mappings for requested slot.
If None, map requested slot to an entity with the same name
"""
domain_slots = domain.as_dict().get(KEY_SLOTS, {})
requested_slot_mappings = domain_slots.get(slot_to_fill, {}).get("mappings", [])
# check provided slot mappings
for requested_slot_mapping in requested_slot_mappings:
if (
not isinstance(requested_slot_mapping, dict)
or requested_slot_mapping.get("type") is None
):
raise TypeError("Provided incompatible slot mapping")
return requested_slot_mappings
def _create_unique_entity_mappings(self, domain: Domain) -> Set[Text]:
"""Finds mappings of type `from_entity` that uniquely set a slot.
For example in the following form:
some_form:
departure_city:
- type: from_entity
entity: city
role: from
- type: from_entity
entity: city
arrival_city:
- type: from_entity
entity: city
role: to
- type: from_entity
entity: city
An entity `city` with a role `from` uniquely sets the slot `departure_city`
and an entity `city` with a role `to` uniquely sets the slot `arrival_city`,
so corresponding mappings are unique.
But an entity `city` without a role can fill both `departure_city`
and `arrival_city`, so corresponding mapping is not unique.
Args:
domain: The domain.
Returns:
A set of json dumps of unique mappings of type `from_entity`.
"""
unique_entity_slot_mappings: Set[Text] = set()
duplicate_entity_slot_mappings: Set[Text] = set()
domain_slots = domain.as_dict().get(KEY_SLOTS, {})
for slot in domain.required_slots_for_form(self.name()):
for slot_mapping in domain_slots.get(slot, {}).get(SLOT_MAPPINGS, []):
if slot_mapping.get(MAPPING_TYPE) == str(SlotMappingType.FROM_ENTITY):
mapping_as_string = json.dumps(slot_mapping, sort_keys=True)
if mapping_as_string in unique_entity_slot_mappings:
unique_entity_slot_mappings.remove(mapping_as_string)
duplicate_entity_slot_mappings.add(mapping_as_string)
elif mapping_as_string not in duplicate_entity_slot_mappings:
unique_entity_slot_mappings.add(mapping_as_string)
return unique_entity_slot_mappings
def entity_mapping_is_unique(
self, slot_mapping: Dict[Text, Any], domain: Domain
) -> bool:
"""Verifies if the from_entity mapping is unique."""
if not self._have_unique_entity_mappings_been_initialized:
# create unique entity mappings on the first call
self._unique_entity_mappings = self._create_unique_entity_mappings(domain)
self._have_unique_entity_mappings_been_initialized = True
mapping_as_string = json.dumps(slot_mapping, sort_keys=True)
return mapping_as_string in self._unique_entity_mappings
@staticmethod
def get_entity_value_for_slot(
name: Text,
tracker: "DialogueStateTracker",
slot_to_be_filled: Text,
role: Optional[Text] = None,
group: Optional[Text] = None,
) -> Any:
"""Extract entities for given name and optional role and group.
Args:
name: entity type (name) of interest
tracker: the tracker
slot_to_be_filled: Slot which is supposed to be filled by this entity.
role: optional entity role of interest
group: optional entity group of interest
Returns:
Value of entity.
"""
# list is used to cover the case of list slot type
value = list(
tracker.get_latest_entity_values(name, entity_group=group, entity_role=role)
)
if isinstance(tracker.slots.get(slot_to_be_filled), ListSlot):
return value
if len(value) == 0:
return None
if len(value) == 1:
return value[0]
return value
def get_slot_to_fill(self, tracker: "DialogueStateTracker") -> Optional[str]:
"""Gets the name of the slot which should be filled next.
When switching to another form, the requested slot setting is still from the
previous form and must be ignored.
Returns:
The slot name or `None`
"""
return (
tracker.get_slot(REQUESTED_SLOT)
if tracker.active_loop_name == self.name()
else None
)
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.
"""
structlogger.debug(
"forms.slots.validate", slot_candidates=copy.deepcopy(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()}"
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
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,
)
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
)
@staticmethod
def _get_events_since_last_user_uttered(
tracker: "DialogueStateTracker",
) -> List[SlotSet]:
# TODO: Better way to get this latest_message index is through an instance
# variable, eg. tracker.latest_message_index
index_from_end = next(
(
i
for i, event in enumerate(reversed(tracker.events))
if event == Restarted() or event == tracker.latest_message
),
len(tracker.events) - 1,
)
index = len(tracker.events) - index_from_end - 1
events_since_last_user_uttered = [
event
for event in itertools.islice(tracker.events, index, None)
if isinstance(event, SlotSet)
]
return events_since_last_user_uttered
def _update_slot_values(
self,
event: SlotSet,
tracker: "DialogueStateTracker",
domain: Domain,
slot_values: Dict[Text, Any],
) -> Dict[Text, Any]:
slot_values[event.key] = event.value
return slot_values
def _add_dynamic_slots_requested_by_dynamic_forms(
self, tracker: "DialogueStateTracker", domain: Domain
) -> Set[Text]:
required_slots = set(self.required_slots(domain))
requested_slot = self.get_slot_to_fill(tracker)
if requested_slot:
required_slots.add(requested_slot)
return required_slots
def _get_slot_extractions(
self, tracker: "DialogueStateTracker", domain: Domain
) -> Dict[Text, Any]:
events_since_last_user_uttered = FormAction._get_events_since_last_user_uttered(
tracker
)
slot_values: Dict[Text, Any] = {}
required_slots = self._add_dynamic_slots_requested_by_dynamic_forms(
tracker, domain
)
for event in events_since_last_user_uttered:
if event.key not in required_slots:
continue
slot_values = self._update_slot_values(event, tracker, domain, slot_values)
return slot_values
async def validate(
self,
tracker: "DialogueStateTracker",
domain: Domain,
output_channel: OutputChannel,
nlg: NaturalLanguageGenerator,
) -> List[Union[SlotSet, Event]]:
"""Extract and validate value of requested slot and other slots.
Returns:
The new validation events created by the custom form validation action
Raises:
ActionExecutionRejection exception to reject execution of form action
if nothing was extracted.
Subclass this method to add custom validation and rejection logic.
"""
extracted_slot_values = self._get_slot_extractions(tracker, domain)
validation_events = await self.validate_slots(
extracted_slot_values, tracker, domain, output_channel, nlg
)
some_slots_were_validated = any(
isinstance(event, SlotSet) and not event.key == REQUESTED_SLOT
for event in validation_events
# Ignore `SlotSet`s for `REQUESTED_SLOT` as that's not a slot which needs
# to be filled by the user.
)
# extract requested slot
slot_to_fill = self.get_slot_to_fill(tracker)
if (
slot_to_fill
and not extracted_slot_values
and not some_slots_were_validated
and not self._user_rejected_manually(validation_events)
):
# reject to execute the form action
# if some slot was requested but nothing was extracted
# it will allow other policies to predict another action
#
# don't raise it here if the user rejected manually, to allow slots other
# than the requested slot to be filled.
#
raise ActionExecutionRejection(
self.name(),
f"Failed to extract slot {slot_to_fill} with action {self.name()}",
)
return validation_events
async 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 await 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 = await 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)]
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,
)
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)
async 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
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
)
return await action_to_ask_for_next_slot.run(
output_channel, nlg, tracker, domain
)
async 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 that it is called during activation
needs_validation = not tracker.active_loop or (
tracker.latest_action_name == ACTION_LISTEN_NAME
and not tracker.is_active_loop_interrupted
)
if needs_validation:
structlogger.debug(
"forms.validation.required",
tracker_latest_message=copy.deepcopy(tracker.latest_message),
)
return await 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 await self.validate_slots({}, tracker, domain, output_channel, nlg)
@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
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, run action_extract_slots to fill slots with
mapping conditions from trigger intents.
Validate any required slots that can be filled, and return any `SlotSet`
events from the extraction and validation of these 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 = {}
action_extract_slots = action.action_for_name_or_text(
ACTION_EXTRACT_SLOTS, domain, self.action_endpoint
)
logger.debug(
f"Executing default action '{ACTION_EXTRACT_SLOTS}' at form activation."
)
extraction_events = await action_extract_slots.run(
output_channel, nlg, tracker, domain
)
events_as_str = "\n".join(str(e) for e in extraction_events)
logger.debug(
f"The execution of '{ACTION_EXTRACT_SLOTS}' resulted in "
f"these events: {events_as_str}."
)
tracker.update_with_events(extraction_events, domain)
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.")
else:
structlogger.debug(
"forms.validate.prefilled_slots",
prefilled_slots=copy.deepcopy(prefilled_slots),
)
validate_name = f"validate_{self.name()}"
if validate_name not in domain.action_names_or_texts:
logger.debug(
f"There is no validation action '{validate_name}' "
f"to execute at form activation."
)
return [event for event in extraction_events if isinstance(event, SlotSet)]
logger.debug(
f"Executing validation action '{validate_name}' at form activation."
)
validated_events = await self.validate_slots(
prefilled_slots, tracker, domain, output_channel, nlg
)
validated_slot_names = [
event.key for event in validated_events if isinstance(event, SlotSet)
]
return validated_events + [
event
for event in extraction_events
if isinstance(event, SlotSet) and event.key not in validated_slot_names
]
async def do(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
events_so_far: List[Event],
) -> List[Event]:
"""Executes form loop after activation."""
events: List[Event] = []
"""
Call to validation is not required when the slots are already validated
at the time of form activation.
events_so_far:
- empty when slots have not been validated.
- has SlotSet objects when already validated.
- ActiveLoop object when events have not been validated.
Hence the events are filtered to remove ActiveLoop object that was added
at the time of form activation.
"""
filtered_events = [
event for event in events_so_far if not isinstance(event, ActiveLoop)
]
if not filtered_events:
events = await self._validate_if_required(
tracker, domain, output_channel, nlg
)
if not self._user_rejected_manually(events):
events += await self.request_next_slot(
tracker, domain, output_channel, nlg, events_so_far + events
)
return events
async 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
)
async def deactivate(self, *args: Any, **kwargs: Any) -> List[Event]:
"""Deactivates form."""
logger.debug(f"Deactivating the form '{self.name()}'")
return []
async def _activate_loop(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
events = self._default_activation_events()
temp_tracker = tracker.copy()
temp_tracker.update_with_events(events, domain)
events += await self.activate(output_channel, nlg, temp_tracker, domain)
return events