diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 6a88d8e62927..232b977d2db8 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -37,6 +37,7 @@ ) from rasa.shared.core import events from rasa.shared.core.constants import ( + DIALOGUE_STACK_SLOT, USER_INTENT_OUT_OF_SCOPE, ACTION_LISTEN_NAME, ACTION_RESTART_NAME, @@ -829,6 +830,13 @@ async def run( ) evts = events.deserialise_events(events_json) + # filter out `SlotSet` events for internal `dialogue_stack` slot + evts = [ + event + for event in evts + if not (isinstance(event, SlotSet) and event.key == DIALOGUE_STACK_SLOT) + ] + return cast(List[Event], bot_messages) + evts except ClientResponseError as e: diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py index 62132cd2d5bf..73531d08271d 100644 --- a/rasa/shared/core/domain.py +++ b/rasa/shared/core/domain.py @@ -993,7 +993,12 @@ def _add_flow_slots(self) -> None: for flow_slot in FLOW_SLOT_NAMES: if flow_slot not in slot_names: self.slots.append( - AnySlot(flow_slot, mappings=[], influence_conversation=False) + AnySlot( + flow_slot, + mappings=[], + influence_conversation=False, + is_builtin=True, + ) ) else: # TODO: figure out what to do here. @@ -1016,6 +1021,7 @@ def _add_requested_slot(self) -> None: rasa.shared.core.constants.REQUESTED_SLOT, mappings=[], influence_conversation=False, + is_builtin=True, ) ) @@ -1041,12 +1047,21 @@ def _add_knowledge_base_slots(self) -> None: for slot in KNOWLEDGE_BASE_SLOT_NAMES: if slot not in slot_names: self.slots.append( - TextSlot(slot, mappings=[], influence_conversation=False) + TextSlot( + slot, + mappings=[], + influence_conversation=False, + is_builtin=True, + ) ) def _add_session_metadata_slot(self) -> None: self.slots.append( - AnySlot(rasa.shared.core.constants.SESSION_START_METADATA_SLOT, mappings=[]) + AnySlot( + rasa.shared.core.constants.SESSION_START_METADATA_SLOT, + mappings=[], + is_builtin=True, + ) ) def index_for_action(self, action_name: Text) -> int: diff --git a/rasa/shared/core/slots.py b/rasa/shared/core/slots.py index b6f6eb44415b..9630d00d7f76 100644 --- a/rasa/shared/core/slots.py +++ b/rasa/shared/core/slots.py @@ -35,6 +35,7 @@ def __init__( initial_value: Any = None, value_reset_delay: Optional[int] = None, influence_conversation: bool = True, + is_builtin: bool = False, ) -> None: """Create a Slot. @@ -46,6 +47,9 @@ def __init__( 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. + is_builtin: `True` if the slot is a built-in slot, `False` otherwise. + Built-in slots also encompass user writable slots (via custom actions), + such as `return_value`. """ self.name = name self.mappings = mappings @@ -54,6 +58,7 @@ def __init__( self._value_reset_delay = value_reset_delay self.influence_conversation = influence_conversation self._has_been_set = False + self.is_builtin = is_builtin def feature_dimensionality(self) -> int: """How many features this single slot creates. @@ -180,6 +185,7 @@ def __init__( max_value: float = 1.0, min_value: float = 0.0, influence_conversation: bool = True, + is_builtin: bool = False, ) -> None: """Creates a FloatSlot. @@ -188,7 +194,12 @@ def __init__( UserWarning, if initial_value is outside the min-max range. """ super().__init__( - name, mappings, initial_value, value_reset_delay, influence_conversation + name, + mappings, + initial_value, + value_reset_delay, + influence_conversation, + is_builtin, ) self.max_value = max_value self.min_value = min_value @@ -318,10 +329,16 @@ def __init__( initial_value: Any = None, value_reset_delay: Optional[int] = None, influence_conversation: bool = True, + is_builtin: bool = False, ) -> None: """Creates a `Categorical Slot` (see parent class for detailed docstring).""" super().__init__( - name, mappings, initial_value, value_reset_delay, influence_conversation + name, + mappings, + initial_value, + value_reset_delay, + influence_conversation, + is_builtin, ) if values and None in values: rasa.shared.utils.io.raise_warning( @@ -417,6 +434,7 @@ def __init__( initial_value: Any = None, value_reset_delay: Optional[int] = None, influence_conversation: bool = False, + is_builtin: bool = False, ) -> None: """Creates an `Any Slot` (see parent class for detailed docstring). @@ -433,7 +451,12 @@ def __init__( ) super().__init__( - name, mappings, initial_value, value_reset_delay, influence_conversation + name, + mappings, + initial_value, + value_reset_delay, + influence_conversation, + is_builtin, ) def __eq__(self, other: Any) -> bool: diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py index 2b80edef8876..61a3638dab62 100644 --- a/tests/core/test_actions.py +++ b/tests/core/test_actions.py @@ -3039,3 +3039,25 @@ def test_default_actions_and_names_consistency(): RULE_SNIPPET_ACTION_NAME } assert names_of_default_actions == names_of_executable_actions_in_constants + + +async def test_filter_out_dialogue_stack_slot_set_in_a_custom_action( + default_channel: OutputChannel, + default_nlg: NaturalLanguageGenerator, + default_tracker: DialogueStateTracker, + domain: Domain, +) -> None: + endpoint = EndpointConfig("https://example.com/webhooks/actions") + remote_action = action.RemoteAction("my_action", endpoint) + events = [SlotSet(DIALOGUE_STACK_SLOT, {}), SlotSet("some_slot", "some_value")] + events_as_dict = [event.as_dict() for event in events] + response = {"events": events_as_dict, "responses": []} + with aioresponses() as mocked: + mocked.post("https://example.com/webhooks/actions", payload=response) + + events = await remote_action.run( + default_channel, default_nlg, default_tracker, domain + ) + + assert len(events) == 1 + assert events[0] == SlotSet("some_slot", "some_value") diff --git a/tests/shared/core/test_domain.py b/tests/shared/core/test_domain.py index 08e8164ef81f..e6c313f07f42 100644 --- a/tests/shared/core/test_domain.py +++ b/tests/shared/core/test_domain.py @@ -23,6 +23,7 @@ from rasa.shared.core.slots import InvalidSlotTypeException, TextSlot from rasa.shared.core.constants import ( DEFAULT_INTENTS, + KNOWLEDGE_BASE_SLOT_NAMES, SLOT_LISTED_ITEMS, SLOT_LAST_OBJECT, SLOT_LAST_OBJECT_TYPE, @@ -2371,3 +2372,12 @@ def test_domain_with_slots_without_mappings(caplog: LogCaptureFixture) -> None: "Slot 'slot_without_mappings' has no mappings defined. " "We will continue with an empty list of mappings." ) in caplog.text + + +def test_domain_default_slots_are_marked_as_builtin(domain: Domain) -> None: + all_default_slot_names = DEFAULT_SLOT_NAMES.union(KNOWLEDGE_BASE_SLOT_NAMES) + domain_default_slots = [ + slot for slot in domain.slots if slot.name in all_default_slot_names + ] + + assert all(slot.is_builtin for slot in domain_default_slots) diff --git a/tests/shared/core/test_slots.py b/tests/shared/core/test_slots.py index 25744b2cc878..efd686e48b17 100644 --- a/tests/shared/core/test_slots.py +++ b/tests/shared/core/test_slots.py @@ -156,6 +156,10 @@ def test_slot_fingerprint_uniqueness( f2 = slot.fingerprint() assert f1 != f2 + def test_slot_is_not_builtin_by_default(self, mappings: List[Dict[Text, Any]]): + slot = self.create_slot(mappings, influence_conversation=False) + assert not slot.is_builtin + class TestTextSlot(SlotTestCollection): def create_slot(