Repository: RasaHQ/rasa
Pull request: slots extracted `from_intent` or `from_entity` for an un-specified intent/entity raises no error/warning
**Proposed changes**:
- fix #10122 

**Status (please check what you already did)**:
- [x] added some tests for the functionality
- [ ] updated the documentation
- [ ] updated the changelog (please check [changelog](https://github.com/RasaHQ/rasa/tree/main/changelog) for instructions)
- [x] reformat files using `black` (please check [Readme](https://github.com/RasaHQ/rasa#code-style) for instructions)


Problem statement:
slots extracted `from_intent` for an un-specified intent raises no error/warning
### Rasa Open Source version

3.0.0rc1

### Rasa SDK version

_No response_

### Rasa X version

_No response_

### Python version

3.8

### What operating system are you using?

OSX

### What happened?

Start with a `rasa init` project and specify a slot that should be extracted from an intent that does not exist (i.e. not specified in domain / data). Expected some error message but the slot was just not extracted. (Accidentally encountered that when using "agree" instead of "affirm" as intent).

### Command / Request

_No response_

### Relevant log output

_No response_

Hints from issue discussion:
same happens with entities: a mapping from_entity for a non-existent entity does not lead to any warning/error as far as I see

Commit message: Merge branch 'main' into bug/10122

Patch to review:
diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py
index dcc2923089be..3ca28e7c0cd1 100644
--- a/rasa/core/actions/action.py
+++ b/rasa/core/actions/action.py
@@ -1154,6 +1154,8 @@ async def run(
 
         for slot in user_slots:
             for mapping in slot.mappings:
+                _check_mapping_validity(mapping, domain)
+
                 intent_is_desired = SlotMapping.intent_is_desired(
                     mapping, tracker, domain
                 )
@@ -1206,6 +1208,29 @@ async def run(
         return validated_events
 
 
+def _check_mapping_validity(mapping: Dict[Text, Any], domain: "Domain") -> bool:
+    """Checks if intent and entity specified in a mapping exist in domain."""
+    if (
+        mapping.get(MAPPING_TYPE) == str(SlotMapping.FROM_ENTITY)
+        and mapping.get(ENTITY_ATTRIBUTE_TYPE) not in domain.entities
+    ):
+        rasa.shared.utils.io.raise_warning(
+            f"Slot uses a 'from_entity' mapping for a non-existent entity '{mapping.get(ENTITY_ATTRIBUTE_TYPE)}'"
+        )
+        return False
+
+    if (
+        mapping.get(MAPPING_TYPE) == str(SlotMapping.FROM_INTENT)
+        and mapping.get("intent") not in domain.intents
+    ):
+        rasa.shared.utils.io.raise_warning(
+            f"Slot uses a 'from_intent' mapping for a non-existent intent '{mapping.get('intent')}'"
+        )
+        return False
+
+    return True
+
+
 def extract_slot_value_from_predefined_mapping(
     mapping: Dict[Text, Any], tracker: "DialogueStateTracker",
 ) -> List[Any]:
diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py
index 21e531cd2f41..82ac62dd97da 100644
--- a/tests/core/test_actions.py
+++ b/tests/core/test_actions.py
@@ -1194,12 +1194,14 @@ async def test_action_extract_slots_predefined_mappings(
 
     action_extract_slots = ActionExtractSlots(action_endpoint=None)
     tracker = DialogueStateTracker.from_events("sender", evts=[user])
-    events = await action_extract_slots.run(
-        CollectingOutputChannel(),
-        TemplatedNaturalLanguageGenerator(domain.responses),
-        tracker,
-        domain,
-    )
+
+    with pytest.warns(None):
+        events = await action_extract_slots.run(
+            CollectingOutputChannel(),
+            TemplatedNaturalLanguageGenerator(domain.responses),
+            tracker,
+            domain,
+        )
 
     assert events == [SlotSet(slot_name, slot_value)]
 
@@ -2289,15 +2291,90 @@ async def test_action_extract_slots_with_empty_conditions():
 
     action_extract_slots = ActionExtractSlots(None)
 
-    events = await action_extract_slots.run(
-        CollectingOutputChannel(),
-        TemplatedNaturalLanguageGenerator(domain.responses),
-        tracker,
-        domain,
-    )
+    with pytest.warns(None):
+        events = await action_extract_slots.run(
+            CollectingOutputChannel(),
+            TemplatedNaturalLanguageGenerator(domain.responses),
+            tracker,
+            domain,
+        )
     assert events == [SlotSet("location", "Berlin")]
 
 
+async def test_action_extract_slots_with_not_existing_entity():
+    domain_yaml = textwrap.dedent(
+        """
+        version: "3.0"
+
+        entities:
+        - city
+
+        slots:
+          location:
+            type: float
+            influence_conversation: false
+            mappings:
+            - type: from_entity
+              entity: city2
+              conditions: []
+        """
+    )
+    domain = Domain.from_yaml(domain_yaml)
+    event = UserUttered("Hi", entities=[{"entity": "city", "value": "Berlin"}])
+    tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
+
+    action_extract_slots = ActionExtractSlots(None)
+
+    with pytest.warns(
+        None,
+        match="Slot uses a `from_entity` mapping for a non-existent entity 'city2'",
+    ):
+        events = await action_extract_slots.run(
+            CollectingOutputChannel(),
+            TemplatedNaturalLanguageGenerator(domain.responses),
+            tracker,
+            domain,
+        )
+    assert events == []
+
+
+async def test_action_extract_slots_with_not_existing_intent():
+    domain_yaml = textwrap.dedent(
+        """
+        version: "3.0"
+
+        intents:
+        - greet
+
+        slots:
+          location:
+            type: text
+            influence_conversation: false
+            mappings:
+            - type: from_intent
+              intent: affirm
+              value: some_value
+        """
+    )
+    domain = Domain.from_yaml(domain_yaml)
+    event = UserUttered("Hi", entities=[{"entity": "city", "value": "Berlin"}])
+    tracker = DialogueStateTracker.from_events(sender_id="test_id", evts=[event])
+
+    action_extract_slots = ActionExtractSlots(None)
+
+    with pytest.warns(
+        UserWarning,
+        match=r"Slot uses a 'from_intent' mapping for a non-existent intent 'affirm'",
+    ):
+        events = await action_extract_slots.run(
+            CollectingOutputChannel(),
+            TemplatedNaturalLanguageGenerator(domain.responses),
+            tracker,
+            domain,
+        )
+    assert events == []
+
+
 async def test_action_extract_slots_with_none_value_predefined_mapping():
     domain_yaml = textwrap.dedent(
         """


Code review comments:
diff_hunk: @@ -1154,6 +1154,8 @@ async def run(
 
         for slot in user_slots:
             for mapping in slot.mappings:
+                _check_mapping_validity(mapping, domain)
path: rasa/core/actions/action.py
text: @user1:
Should we not skip the mapping if is invalid?
```suggestion
                if not _check_mapping_validity(mapping, domain):
                       continue
```

diff_hunk: @@ -1206,6 +1208,29 @@ async def run(
         return validated_events
 
 
+def _check_mapping_validity(mapping: Dict[Text, Any], domain: "Domain") -> bool:
path: rasa/core/actions/action.py
text: @user1:
I think this code should rather sit in `SlotMapping` class, either as a separate public method, or private method used by the `validate` method in case we even want to raise `InvalidDomain` for this type of issue?

diff_hunk: @@ -2289,15 +2291,90 @@ async def test_action_extract_slots_with_empty_conditions():
 
     action_extract_slots = ActionExtractSlots(None)
 
-    events = await action_extract_slots.run(
-        CollectingOutputChannel(),
-        TemplatedNaturalLanguageGenerator(domain.responses),
-        tracker,
-        domain,
-    )
+    with pytest.warns(None):
+        events = await action_extract_slots.run(
+            CollectingOutputChannel(),
+            TemplatedNaturalLanguageGenerator(domain.responses),
+            tracker,
+            domain,
+        )
     assert events == [SlotSet("location", "Berlin")]
 
 
+async def test_action_extract_slots_with_not_existing_entity():
+    domain_yaml = textwrap.dedent(
+        """
+        version: "3.0"
+
+        entities:
+        - city
+
+        slots:
+          location:
+            type: float
+            influence_conversation: false
+            mappings:
+            - type: from_entity
+              entity: city2
+              conditions: []
path: tests/core/test_actions.py
text: @user1:
The conditions can be removed here since I don't think they add any value/ are part of the test?