diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a1b6e5e9b5d6..0148853a6068 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,16 +14,18 @@ Added - `FallbackPolicy` can now be configured to trigger when the difference between confidences of two predicted intents is too narrow - throw error during training when triggers are defined in the domain without ``MappingPolicy`` being present in the policy ensemble -- The tracker is now avaialble within the interpreter's ``parse`` method, giving the ability to create interpreter classes that use the tracker state (eg. slot values) during the parsing of the message. More details on motivation of this change see issues/3015 +- experimental training data importer which supports training with data of multiple + sub bots. Please see the + `docs `_ for more + information. +- The tracker is now available within the interpreter's ``parse`` method, giving the ability to create interpreter classes that + use the tracker state (eg. slot values) during the parsing of the message. More details on motivation of this change see issues/3015 Changed ------- - added character-level ``CountVectorsFeaturizer`` with empirically found parameters into the ``supervised_embeddings`` NLU pipeline template - bot messages contain the `timestamp` of the `BotUttered` event, which can be used in channels - -Changed -------- - NLU evaluations now also stores its output in the output directory like the core evaluation Removed diff --git a/data/test_multi_domain/config.yml b/data/test_multi_domain/config.yml index 799e431c5cd2..e7c4d7616564 100644 --- a/data/test_multi_domain/config.yml +++ b/data/test_multi_domain/config.yml @@ -7,7 +7,7 @@ policies: - name: KerasPolicy importers: - - name: SkillSelector + - name: MultiProjectImporter imports: - data/MoodBot diff --git a/docs/api/training-data-importers.rst b/docs/api/training-data-importers.rst index f3e32a1c77b3..51b230691bf2 100644 --- a/docs/api/training-data-importers.rst +++ b/docs/api/training-data-importers.rst @@ -8,6 +8,9 @@ Training Data Importers .. edit-link:: +.. contents:: + :local: + By default, you can use command line arguments to specify where Rasa should look for training data on your disk. Rasa then loads any potential training files and uses them to train your assistant. @@ -51,6 +54,89 @@ configuration file: importers: - name: "RasaFileImporter" +MultiProjectImporter (experimental) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + + This feature is currently experimental and might change or be removed in the future. + Please share your feedback on it in the `forum `_ to help + us making this feature ready for production. + +With this importer you can build a contextual AI assistant by combining multiple +reusable Rasa projects. +You might, for example, handle chitchat with one project and greet your users with +another. These projects can be developed in isolation, and then combined at train time +to create your assistant. + +An example directory structure could look like this: + +.. code-block:: bash + + . + ├── config.yml + └── projects + ├── GreetBot + │   ├── data + │   │   ├── nlu.md + │   │   └── stories.md + │   └── domain.yml + └── ChitchatBot + ├── config.yml + ├── data + │   ├── nlu.md + │   └── stories.md + └── domain.yml + +In this example the contextual AI assistant imports the ``ChitchatBot`` project which in turn +imports the ``GreetBot`` project. Project imports are defined in the configuration files of +each project. +To instruct Rasa to use the ``MultiProjectImporter`` module, put this section in the config +file of your root project: + +.. code-block:: yaml + + importers: + - name: MultiProjectImporter + + +Then specify which projects you want to import. +In our example, the ``config.yml`` in the root project would look like this: + +.. code-block:: yaml + + imports: + - projects/ChitchatBot + +The configuration file of the ``ChitchatBot`` in turn references the ``GreetBot``: + +.. code-block:: yaml + + imports: + - ../GreetBot + +The ``GreetBot`` project does not specify further projects so the ``config.yml`` can be +omitted. + +Rasa uses relative paths from the referencing configuration file to import projects. +These can be anywhere on your file system as long as the file access is permitted. + +During the training process Rasa will import all required training files, combine +them, and train a unified AI assistant. The merging of the training data happens during +runtime, so no additional files with training data are created or visible. + +.. note:: + + Rasa will use the policy and NLU pipeline configuration of the root project + directory during training. **Policy or NLU configurations of imported projects + will be ignored.** + +.. note:: + + Equal intents, entities, slots, templates, actions and forms will be merged, + e.g. if two projects have training data for an intent ``greet``, + their training data will be combined. + Writing a Custom Importer ~~~~~~~~~~~~~~~~~~~~~~~~~ If you are writing a custom importer, this importer has to implement the interface of diff --git a/docs/conf.py b/docs/conf.py index 111c157160f7..e285610f8b9d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -87,7 +87,6 @@ "Thumbs.db", ".DS_Store", # ignore doc pages that we don't show to appease keep_warnings - "multi-skill-assistants.rst", "core/old-core-change-log.rst", "core/old-core-migration-guide.rst", "nlu/old-nlu-change-log.rst", diff --git a/docs/multi-skill-assistants.rst b/docs/multi-skill-assistants.rst deleted file mode 100644 index 965e062348cc..000000000000 --- a/docs/multi-skill-assistants.rst +++ /dev/null @@ -1,81 +0,0 @@ -:desc: Iterate quickly by developing reusable building blocks of AI assistant skills - and combining them at training time. - -.. _multi-skill-assistants: - -Multi-skill Assistants -====================== - -.. edit-link:: - -You can build a contextual AI assistant by combining reusable "building blocks" -called skills. -You might, for example, handle chitchat with one skill and greet your users with -another. These skills can be developed in isolation, and then combined at train time -to create your assistant. - -An example directory structure could look like this: - -.. code-block:: bash - - . - ├── config.yml - └── skills - ├── GreetBot - │   ├── data - │   │   ├── nlu.md - │   │   └── stories.md - │   └── domain.yml - └── ChitchatBot - ├── config.yml - ├── data - │   ├── nlu.md - │   └── stories.md - └── domain.yml - -In this example the contextual AI assistant imports the ``ChitchatBot`` skill which in turn -imports the ``GreetBot`` skill. Skill imports are defined in the configuration files of -each project. -To instruct Rasa to use the the module to import skills, put this section in the -config file of your root project: - -.. code-block:: yaml - - importers: - - name: SkillSelector - - -Then specify which skills you want to import. -In our example, the ``config.yml`` in the root project would look like this: - -.. code-block:: yaml - - imports: - - skills/ChitchatBot - -The configuration file of the ``ChitchatBot`` in turn references the ``GreetBot``: - -.. code-block:: yaml - - imports: - - ../GreetBot - -The ``GreetBot`` skill does not specify further skills so the ``config.yml`` can be -omitted. - -Rasa uses relative paths from the referencing configuration file to import skills. -These can be anywhere on your file system as long as the file access is permitted. - -During the training process Rasa will import all required training files, combine -them, and train a unified AI assistant. - -.. note:: - - Rasa will use the policy and NLU pipeline configuration of the root project - directory during the training. Policy or NLU configurations of imported skills will - be ignored. - -.. note:: - - Equal identifiers will be merged, e.g. if two skills have training data - for an intent ``greet``, their training data will be combined. diff --git a/rasa/data.py b/rasa/data.py index 4efcda624972..a080a9da7162 100644 --- a/rasa/data.py +++ b/rasa/data.py @@ -64,8 +64,6 @@ def get_core_nlu_files( Args: paths: List of paths to training files or folders containing them. - skill_imports: `SkillSelector` instance which determines which files - should be loaded. Returns: Tuple of paths to story and NLU files. diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index 83dd14a2899b..6663ff848ee4 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -148,14 +148,14 @@ def _importer_from_dict( domain_path: Optional[Text] = None, training_data_paths: Optional[List[Text]] = None, ) -> Optional["TrainingDataImporter"]: - from rasa.importers.skill import SkillSelector + from rasa.importers.multi_project import MultiProjectImporter from rasa.importers.rasa import RasaFileImporter module_path = importer_config.pop("name", None) if module_path == RasaFileImporter.__name__: importer_class = RasaFileImporter - elif module_path == SkillSelector.__name__: - importer_class = SkillSelector + elif module_path == MultiProjectImporter.__name__: + importer_class = MultiProjectImporter else: try: importer_class = common_utils.class_from_module_path(module_path) diff --git a/rasa/importers/skill.py b/rasa/importers/multi_project.py similarity index 96% rename from rasa/importers/skill.py rename to rasa/importers/multi_project.py index e0236a34d597..e18bd7922c77 100644 --- a/rasa/importers/skill.py +++ b/rasa/importers/multi_project.py @@ -12,11 +12,12 @@ from rasa.importers import utils from rasa.nlu.training_data import TrainingData from rasa.core.training.structures import StoryGraph +import rasa.utils.common logger = logging.getLogger(__name__) -class SkillSelector(TrainingDataImporter): +class MultiProjectImporter(TrainingDataImporter): def __init__( self, config_file: Text, @@ -44,11 +45,15 @@ def __init__( self._nlu_paths += list(extra_nlu_files) logger.debug( - "Selected skills: {}".format( + "Selected projects: {}".format( "".join(["\n-{}".format(i) for i in self._imports]) ) ) + rasa.utils.common.mark_as_experimental_feature( + feature_name="MultiProjectImporter" + ) + def _init_from_path(self, path: Text) -> None: if os.path.isfile(path): self._init_from_file(path) diff --git a/rasa/utils/common.py b/rasa/utils/common.py index 96e4786314ab..fa90eedcfa2e 100644 --- a/rasa/utils/common.py +++ b/rasa/utils/common.py @@ -247,3 +247,14 @@ def not_found(): return c[name] else: return not_found() + + +def mark_as_experimental_feature(feature_name: Text) -> None: + """Warns users that they are using an experimental feature.""" + + logger.warning( + "The {} is currently experimental and might change or be " + "removed in the future 🔬 Please share your feedback on it in the " + "forum (https://forum.rasa.com) to help us make this feature " + "ready for production.".format(feature_name) + ) diff --git a/tests/importers/__init__.py b/tests/importers/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/test_importer.py b/tests/importers/test_importer.py similarity index 70% rename from tests/test_importer.py rename to tests/importers/test_importer.py index 79b37c7c7ad9..8342ed9b08cc 100644 --- a/tests/test_importer.py +++ b/tests/importers/test_importer.py @@ -4,7 +4,6 @@ import pytest from rasa.constants import DEFAULT_CONFIG_PATH, DEFAULT_DOMAIN_PATH, DEFAULT_DATA_PATH -from rasa.core.domain import Domain from rasa.importers.importer import ( CombinedDataImporter, TrainingDataImporter, @@ -14,7 +13,7 @@ from rasa.importers.rasa import RasaFileImporter # noinspection PyUnresolvedReferences -from rasa.importers.skill import SkillSelector +from rasa.importers.multi_project import MultiProjectImporter # noinspection PyUnresolvedReferences from tests.core.conftest import project @@ -34,44 +33,6 @@ async def test_use_of_interface(): await f() -async def test_rasa_file_importer(project: Text): - config_path = os.path.join(project, DEFAULT_CONFIG_PATH) - domain_path = os.path.join(project, DEFAULT_DOMAIN_PATH) - default_data_path = os.path.join(project, DEFAULT_DATA_PATH) - - importer = RasaFileImporter(config_path, domain_path, [default_data_path]) - - domain = await importer.get_domain() - assert len(domain.intents) == 6 - assert domain.slots == [] - assert domain.entities == [] - assert len(domain.action_names) == 13 - assert len(domain.templates) == 5 - - stories = await importer.get_stories() - assert len(stories.story_steps) == 4 - - nlu_data = await importer.get_nlu_data("en") - assert len(nlu_data.intents) == 6 - assert len(nlu_data.intent_examples) == 39 - - -async def test_rasa_file_importer_with_invalid_config(): - importer = RasaFileImporter(config_file="invalid path") - actual = await importer.get_config() - - assert actual == {} - - -async def test_rasa_file_importer_with_invalid_domain(tmp_path: Path): - config_file = tmp_path / "config.yml" - config_file.write_text("") - importer = TrainingDataImporter.load_from_dict({}, str(config_file), None, []) - - actual = await importer.get_domain() - assert actual.as_dict() == Domain.empty().as_dict() - - async def test_combined_file_importer_with_single_importer(project: Text): config_path = os.path.join(project, DEFAULT_CONFIG_PATH) domain_path = os.path.join(project, DEFAULT_DOMAIN_PATH) @@ -103,13 +64,22 @@ async def test_combined_file_importer_with_single_importer(project: Text): ({"importers": [{"name": "RasaFileImporter"}]}, [RasaFileImporter]), ({"importers": [{"name": "NotExistingModule"}]}, [RasaFileImporter]), ( - {"importers": [{"name": "rasa.importers.skill.SkillSelector"}]}, - [SkillSelector], + { + "importers": [ + {"name": "rasa.importers.multi_project.MultiProjectImporter"} + ] + }, + [MultiProjectImporter], ), - ({"importers": [{"name": "SkillSelector"}]}, [SkillSelector]), + ({"importers": [{"name": "MultiProjectImporter"}]}, [MultiProjectImporter]), ( - {"importers": [{"name": "RasaFileImporter"}, {"name": "SkillSelector"}]}, - [RasaFileImporter, SkillSelector], + { + "importers": [ + {"name": "RasaFileImporter"}, + {"name": "MultiProjectImporter"}, + ] + }, + [RasaFileImporter, MultiProjectImporter], ), ], ) @@ -134,11 +104,13 @@ def test_load_from_config(tmpdir: Path): config_path = str(tmpdir / "config.yml") - io_utils.write_yaml_file({"importers": [{"name": "SkillSelector"}]}, config_path) + io_utils.write_yaml_file( + {"importers": [{"name": "MultiProjectImporter"}]}, config_path + ) importer = TrainingDataImporter.load_from_config(config_path) assert isinstance(importer, CombinedDataImporter) - assert isinstance(importer._importers[0], SkillSelector) + assert isinstance(importer._importers[0], MultiProjectImporter) async def test_nlu_only(project: Text): diff --git a/tests/test_skill.py b/tests/importers/test_multi_project.py similarity index 58% rename from tests/test_skill.py rename to tests/importers/test_multi_project.py index 04d052b45cef..fbcca57d9c56 100644 --- a/tests/test_skill.py +++ b/tests/importers/test_multi_project.py @@ -8,43 +8,47 @@ from rasa import model from rasa.core import utils from rasa.core.domain import Domain -from rasa.importers.skill import SkillSelector +from rasa.importers.multi_project import MultiProjectImporter from rasa.train import train_async def test_load_imports_from_directory_tree(tmpdir_factory: TempdirFactory): root = tmpdir_factory.mktemp("Parent Bot") - root_imports = {"imports": ["Skill A"]} + root_imports = {"imports": ["Project A"]} utils.dump_obj_as_yaml_to_file(root / "config.yml", root_imports) - skill_a_directory = root / "Skill A" - skill_a_directory.mkdir() - skill_a_imports = {"imports": ["../Skill B"]} - utils.dump_obj_as_yaml_to_file(skill_a_directory / "config.yml", skill_a_imports) + project_a_directory = root / "Project A" + project_a_directory.mkdir() + project_a_imports = {"imports": ["../Project B"]} + utils.dump_obj_as_yaml_to_file( + project_a_directory / "config.yml", project_a_imports + ) - skill_b_directory = root / "Skill B" - skill_b_directory.mkdir() - skill_b_imports = {"some other": ["../Skill C"]} - utils.dump_obj_as_yaml_to_file(skill_b_directory / "config.yml", skill_b_imports) + project_b_directory = root / "Project B" + project_b_directory.mkdir() + project_b_imports = {"some other": ["../Project C"]} + utils.dump_obj_as_yaml_to_file( + project_b_directory / "config.yml", project_b_imports + ) - skill_b_subskill_directory = skill_b_directory / "Skill B-1" - skill_b_subskill_directory.mkdir() - skill_b_1_imports = {"imports": ["../../Skill A"]} + project_b_subproject_directory = project_b_directory / "Project B-1" + project_b_subproject_directory.mkdir() + project_b_1_imports = {"imports": ["../../Project A"]} # Check if loading from `.yaml` also works utils.dump_obj_as_yaml_to_file( - skill_b_subskill_directory / "config.yaml", skill_b_1_imports + project_b_subproject_directory / "config.yaml", project_b_1_imports ) # should not be imported - subdirectory_3 = root / "Skill C" + subdirectory_3 = root / "Project C" subdirectory_3.mkdir() expected = { - os.path.join(str(skill_a_directory)), - os.path.join(str(skill_b_directory)), + os.path.join(str(project_a_directory)), + os.path.join(str(project_b_directory)), } - actual = SkillSelector(str(root / "config.yml")) + actual = MultiProjectImporter(str(root / "config.yml")) assert actual._imports == expected @@ -54,17 +58,17 @@ def test_load_imports_without_imports(tmpdir_factory: TempdirFactory): root = tmpdir_factory.mktemp("Parent Bot") utils.dump_obj_as_yaml_to_file(root / "config.yml", empty_config) - skill_a_directory = root / "Skill A" - skill_a_directory.mkdir() - utils.dump_obj_as_yaml_to_file(skill_a_directory / "config.yml", empty_config) + project_a_directory = root / "Project A" + project_a_directory.mkdir() + utils.dump_obj_as_yaml_to_file(project_a_directory / "config.yml", empty_config) - skill_b_directory = root / "Skill B" - skill_b_directory.mkdir() - utils.dump_obj_as_yaml_to_file(skill_b_directory / "config.yml", empty_config) + project_b_directory = root / "Project B" + project_b_directory.mkdir() + utils.dump_obj_as_yaml_to_file(project_b_directory / "config.yml", empty_config) - actual = SkillSelector(str(root / "config.yml")) + actual = MultiProjectImporter(str(root / "config.yml")) - assert actual.is_imported(str(root / "Skill C")) + assert actual.is_imported(str(root / "Project C")) @pytest.mark.parametrize("input_dict", [{}, {"imports": None}]) @@ -73,24 +77,28 @@ def test_load_from_none(input_dict: Dict, tmpdir_factory: TempdirFactory): config_path = root / "config.yml" utils.dump_obj_as_yaml_to_file(root / "config.yml", input_dict) - actual = SkillSelector(str(config_path)) + actual = MultiProjectImporter(str(config_path)) assert actual._imports == set() -def test_load_if_subskill_is_more_specific_than_parent(tmpdir_factory: TempdirFactory): +def test_load_if_subproject_is_more_specific_than_parent( + tmpdir_factory: TempdirFactory +): root = tmpdir_factory.mktemp("Parent Bot") config_path = str(root / "config.yml") utils.dump_obj_as_yaml_to_file(root / "config.yml", {}) - skill_a_directory = root / "Skill A" - skill_a_directory.mkdir() - skill_a_imports = {"imports": ["Skill B"]} - utils.dump_obj_as_yaml_to_file(skill_a_directory / "config.yml", skill_a_imports) + project_a_directory = root / "Project A" + project_a_directory.mkdir() + project_a_imports = {"imports": ["Project B"]} + utils.dump_obj_as_yaml_to_file( + project_a_directory / "config.yml", project_a_imports + ) - actual = SkillSelector(config_path) + actual = MultiProjectImporter(config_path) - assert actual.is_imported(str(skill_a_directory)) + assert actual.is_imported(str(project_a_directory)) @pytest.mark.parametrize( @@ -101,7 +109,7 @@ def test_in_imports(input_path: Text, tmpdir_factory: TempdirFactory): config_path = str(root / "config.yml") utils.dump_obj_as_yaml_to_file(root / "config.yml", {"imports": ["A/A/A", "A/B/A"]}) - importer = SkillSelector(config_path, project_directory=os.getcwd()) + importer = MultiProjectImporter(config_path, project_directory=os.getcwd()) assert importer.is_imported(input_path) @@ -111,49 +119,57 @@ def test_not_in_imports(input_path: Text, tmpdir_factory: TempdirFactory): root = tmpdir_factory.mktemp("Parent Bot") config_path = str(root / "config.yml") utils.dump_obj_as_yaml_to_file(root / "config.yml", {"imports": ["A/A/A", "A/B/A"]}) - importer = SkillSelector(config_path, project_directory=os.getcwd()) + importer = MultiProjectImporter(config_path, project_directory=os.getcwd()) assert not importer.is_imported(input_path) def test_cyclic_imports(tmpdir_factory): root = tmpdir_factory.mktemp("Parent Bot") - skill_imports = {"imports": ["Skill A"]} - utils.dump_obj_as_yaml_to_file(root / "config.yml", skill_imports) + project_imports = {"imports": ["Project A"]} + utils.dump_obj_as_yaml_to_file(root / "config.yml", project_imports) - skill_a_directory = root / "Skill A" - skill_a_directory.mkdir() - skill_a_imports = {"imports": ["../Skill B"]} - utils.dump_obj_as_yaml_to_file(skill_a_directory / "config.yml", skill_a_imports) + project_a_directory = root / "Project A" + project_a_directory.mkdir() + project_a_imports = {"imports": ["../Project B"]} + utils.dump_obj_as_yaml_to_file( + project_a_directory / "config.yml", project_a_imports + ) - skill_b_directory = root / "Skill B" - skill_b_directory.mkdir() - skill_b_imports = {"imports": ["../Skill A"]} - utils.dump_obj_as_yaml_to_file(skill_b_directory / "config.yml", skill_b_imports) + project_b_directory = root / "Project B" + project_b_directory.mkdir() + project_b_imports = {"imports": ["../Project A"]} + utils.dump_obj_as_yaml_to_file( + project_b_directory / "config.yml", project_b_imports + ) - actual = SkillSelector(str(root / "config.yml")) + actual = MultiProjectImporter(str(root / "config.yml")) - assert actual._imports == {str(skill_a_directory), str(skill_b_directory)} + assert actual._imports == {str(project_a_directory), str(project_b_directory)} def test_import_outside_project_directory(tmpdir_factory): root = tmpdir_factory.mktemp("Parent Bot") - skill_imports = {"imports": ["Skill A"]} - utils.dump_obj_as_yaml_to_file(root / "config.yml", skill_imports) + project_imports = {"imports": ["Project A"]} + utils.dump_obj_as_yaml_to_file(root / "config.yml", project_imports) - skill_a_directory = root / "Skill A" - skill_a_directory.mkdir() - skill_a_imports = {"imports": ["../Skill B"]} - utils.dump_obj_as_yaml_to_file(skill_a_directory / "config.yml", skill_a_imports) + project_a_directory = root / "Project A" + project_a_directory.mkdir() + project_a_imports = {"imports": ["../Project B"]} + utils.dump_obj_as_yaml_to_file( + project_a_directory / "config.yml", project_a_imports + ) - skill_b_directory = root / "Skill B" - skill_b_directory.mkdir() - skill_b_imports = {"imports": ["../Skill C"]} - utils.dump_obj_as_yaml_to_file(skill_b_directory / "config.yml", skill_b_imports) + project_b_directory = root / "Project B" + project_b_directory.mkdir() + project_b_imports = {"imports": ["../Project C"]} + utils.dump_obj_as_yaml_to_file( + project_b_directory / "config.yml", project_b_imports + ) - actual = SkillSelector(str(skill_a_directory / "config.yml")) + actual = MultiProjectImporter(str(project_a_directory / "config.yml")) - assert actual._imports == {str(skill_b_directory), str(root / "Skill C")} + assert actual._imports == {str(project_b_directory), str(root / "Project C")} def test_importing_additional_files(tmpdir_factory): @@ -166,7 +182,7 @@ def test_importing_additional_files(tmpdir_factory): # create intermediate directories and fake files additional_file.write("""## story""", ensure=True) - selector = SkillSelector( + selector = MultiProjectImporter( config_path, training_data_paths=[str(root / "directory"), str(additional_file)] ) @@ -181,7 +197,7 @@ def test_not_importing_not_relevant_additional_files(tmpdir_factory): utils.dump_obj_as_yaml_to_file(config_path, config) additional_file = root / "directory" / "file.yml" - selector = SkillSelector( + selector = MultiProjectImporter( config_path, training_data_paths=[str(root / "data"), str(additional_file)] ) @@ -203,12 +219,14 @@ def test_single_additional_file(tmpdir_factory): additional_file = root / "directory" / "file.yml" additional_file.write({}, ensure=True) - selector = SkillSelector(config_path, training_data_paths=str(additional_file)) + selector = MultiProjectImporter( + config_path, training_data_paths=str(additional_file) + ) assert selector.is_imported(str(additional_file)) -async def test_multi_skill_training(): +async def test_multi_project_training(): example_directory = "data/test_multi_domain" config_file = os.path.join(example_directory, "config.yml") domain_file = os.path.join(example_directory, "domain.yml") diff --git a/tests/importers/test_rasa.py b/tests/importers/test_rasa.py new file mode 100644 index 000000000000..8a261f7673b8 --- /dev/null +++ b/tests/importers/test_rasa.py @@ -0,0 +1,49 @@ +from pathlib import Path +from typing import Text +import os + +from rasa.constants import DEFAULT_CONFIG_PATH, DEFAULT_DOMAIN_PATH, DEFAULT_DATA_PATH +from rasa.core.domain import Domain +from rasa.importers.importer import TrainingDataImporter +from rasa.importers.rasa import RasaFileImporter + +# noinspection PyUnresolvedReferences +from tests.core.conftest import project + + +async def test_rasa_file_importer(project: Text): + config_path = os.path.join(project, DEFAULT_CONFIG_PATH) + domain_path = os.path.join(project, DEFAULT_DOMAIN_PATH) + default_data_path = os.path.join(project, DEFAULT_DATA_PATH) + + importer = RasaFileImporter(config_path, domain_path, [default_data_path]) + + domain = await importer.get_domain() + assert len(domain.intents) == 6 + assert domain.slots == [] + assert domain.entities == [] + assert len(domain.action_names) == 13 + assert len(domain.templates) == 5 + + stories = await importer.get_stories() + assert len(stories.story_steps) == 4 + + nlu_data = await importer.get_nlu_data("en") + assert len(nlu_data.intents) == 6 + assert len(nlu_data.intent_examples) == 39 + + +async def test_rasa_file_importer_with_invalid_config(): + importer = RasaFileImporter(config_file="invalid path") + actual = await importer.get_config() + + assert actual == {} + + +async def test_rasa_file_importer_with_invalid_domain(tmp_path: Path): + config_file = tmp_path / "config.yml" + config_file.write_text("") + importer = TrainingDataImporter.load_from_dict({}, str(config_file), None, []) + + actual = await importer.get_domain() + assert actual.as_dict() == Domain.empty().as_dict()