Repository: rasahq/rasa
Issue: Merging domains is broken if out_of_scope intent is defined by user / make default actions configurable

**Rasa version**: master

**Issue**: The `out_of_scope` intent is added to the [list of default intents](https://github.com/RasaHQ/rasa/blame/master/rasa/core/constants.py#L46) and hence its omitted when the domain is fetched as a dict because of this [line](https://github.com/RasaHQ/rasa/blob/f62839e5cb0e8ecb8cb7d85ac0f8fb078035c8c8/rasa/core/domain.py#L857)
While merging multiple domains, the domains are [fetched as dicts](https://github.com/RasaHQ/rasa/blob/master/rasa/core/domain.py#L237) which would not contain `out_of_scope` intent even if the user has defined it in their domain.

@wochinge mentioned that `out_of_scope` intent was added to default intents because we need it for the `TwoStageFallbackAction` in order to know whether the user denied the suggestion. Currently there is no way to configure default actions and so the only way is to make certain things default. We could make the deny intent configurable, something like - 
```
forms:
- two_stage_fallback:
    deny_intent: "out_of_scope"
```

👉 [Slack thread](https://rasa-hq.slack.com/archives/C36SS4N8M/p1599581976115300)


Changed files:
rasa/shared/core/domain.py, tests/shared/core/test_domain.py

diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py
index 9ca99c3e274..22965b3b9d7 100644
--- a/rasa/shared/core/domain.py
+++ b/rasa/shared/core/domain.py
@@ -2,9 +2,20 @@ import copy
 import collections
 import json
 import logging
-import typing
 import os
-from typing import Any, Dict, List, NamedTuple, Optional, Set, Text, Tuple, Union
+from typing import (
+    Any,
+    Dict,
+    List,
+    NamedTuple,
+    Optional,
+    Set,
+    Text,
+    Tuple,
+    Union,
+    NoReturn,
+    TYPE_CHECKING,
+)
 from pathlib import Path
 
 from ruamel.yaml import YAMLError
@@ -18,7 +29,7 @@ import rasa.shared.utils.common
 from rasa.shared.core.events import SlotSet, UserUttered
 from rasa.shared.core.slots import Slot, UnfeaturizedSlot, CategoricalSlot
 
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
     from rasa.shared.core.trackers import DialogueStateTracker
 
 CARRY_OVER_SLOTS_KEY = "carry_over_slots_to_new_session"
@@ -410,7 +421,7 @@ class Domain:
 
     def __init__(
         self,
-        intents: Union[Set[Text], List[Union[Text, Dict[Text, Any]]]],
+        intents: Union[Set[Text], List[Text], List[Dict[Text, Any]]],
         entities: List[Text],
         slots: List[Slot],
         templates: Dict[Text, List[Dict[Text, Any]]],
@@ -422,6 +433,9 @@ class Domain:
     ) -> None:
 
         self.intent_properties = self.collect_intent_properties(intents, entities)
+        self.overriden_default_intents = self._collect_overridden_default_intents(
+            intents
+        )
         self.entities = entities
 
         self.forms: Dict[Text, Any] = {}
@@ -448,6 +462,24 @@ class Domain:
         self.store_entities_as_slots = store_entities_as_slots
         self._check_domain_sanity()
 
+    @staticmethod
+    def _collect_overridden_default_intents(
+        intents: Union[Set[Text], List[Text], List[Dict[Text, Any]]]
+    ) -> List[Text]:
+        """Collects the default intents overridden by the user.
+
+        Args:
+            intents: User-provided intents.
+
+        Returns:
+            User-defined intents that are default intents.
+        """
+        intent_names: Set[Text] = {
+            list(intent.keys())[0] if isinstance(intent, dict) else intent
+            for intent in intents
+        }
+        return sorted(intent_names & set(rasa.shared.core.constants.DEFAULT_INTENTS))
+
     def _initialize_forms(self, forms: Union[Dict[Text, Any], List[Text]]) -> None:
         """Initialize the domain's `self.form` and `self.form_names` attributes.
 
@@ -565,7 +597,7 @@ class Domain:
         except ValueError:
             self.raise_action_not_found_exception(action_name)
 
-    def raise_action_not_found_exception(self, action_name) -> typing.NoReturn:
+    def raise_action_not_found_exception(self, action_name) -> NoReturn:
         action_names = "\\n".join([f"\\t - {a}" for a in self.action_names])
         raise NameError(
             f"Cannot access action '{action_name}', "
@@ -836,7 +868,10 @@ class Domain:
         intents_for_file = []
 
         for intent_name, intent_props in intent_properties.items():
-            if intent_name in rasa.shared.core.constants.DEFAULT_INTENTS:
+            if (
+                intent_name in rasa.shared.core.constants.DEFAULT_INTENTS
+                and intent_name not in self.overriden_default_intents
+            ):
                 # Default intents should be not dumped with the domain
                 continue
             use_entities = set(intent_props[USED_ENTITIES_KEY])
diff --git a/tests/shared/core/test_domain.py b/tests/shared/core/test_domain.py
index ab0ff81b0bc..34fb4868599 100644
--- a/tests/shared/core/test_domain.py
+++ b/tests/shared/core/test_domain.py
@@ -368,6 +368,27 @@ responses:
     assert domain.session_config == SessionConfig(20, True)
 
 
+@pytest.mark.parametrize("default_intent", DEFAULT_INTENTS)
+def test_merge_yaml_domains_with_default_intents(default_intent: Text):
+    test_yaml_1 = """intents: []"""
+
+    # this domain contains an overridden default intent
+    test_yaml_2 = f"""intents:
+- greet
+- {default_intent}"""
+
+    domain_1 = Domain.from_yaml(test_yaml_1)
+    domain_2 = Domain.from_yaml(test_yaml_2)
+    domain = domain_1.merge(domain_2)
+
+    # check that the default intents were merged correctly
+    assert default_intent in domain.intents
+    assert domain.intents == sorted(["greet", *DEFAULT_INTENTS])
+
+    # ensure that the default intent is contain the domain's dictionary dump
+    assert list(domain.as_dict()["intents"][1].keys())[0] == default_intent
+
+
 def test_merge_session_config_if_first_is_not_default():
     yaml1 = """
 session_config: