Repository: RasaHQ/rasa

Issue:
rasa train command error message
**Rasa version**: 1.1.13

**Issue**:

If you run the `rasa train` command and don't provide a `--domain` option, a python stack trace results instead of an error message message that the domain file was a required option.

**Error (including full traceback)**:
```
docker run -v $(pwd)/data:/app/data rasa/rasa:latest-full train core --config /app/data/config.yml --stories /app/data/stories --out /app/data/models --augmentation 0 --quiet
2019-06-15 00:52:47 ERROR    rasa.core.training.dsl  - Error in line 2: 'NoneType' object has no attribute 'intents'
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 301, in process_lines
    await self.add_user_messages(user_messages, line_num)
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 383, in add_user_messages
    *[self._parse_message(m, line_num) for m in messages]
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 367, in _parse_message
    if intent_name not in self.domain.intents:
AttributeError: 'NoneType' object has no attribute 'intents'
2019-06-15 00:52:47 ERROR    rasa.core.training.dsl  - Invalid story file format. Failed to parse '/tmp/tmpz84lkggv/c24e99a4b7ff480f80047ec55ce5b484_claimzen.md'
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 301, in process_lines
    await self.add_user_messages(user_messages, line_num)
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 383, in add_user_messages
    *[self._parse_message(m, line_num) for m in messages]
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 367, in _parse_message
    if intent_name not in self.domain.intents:
AttributeError: 'NoneType' object has no attribute 'intents'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 214, in read_from_file
    return await reader.process_lines(lines)
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 313, in process_lines
    raise ValueError(msg)
ValueError: Error in line 2: 'NoneType' object has no attribute 'intents'
Training Core model...
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 301, in process_lines
    await self.add_user_messages(user_messages, line_num)
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 383, in add_user_messages
    *[self._parse_message(m, line_num) for m in messages]
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 367, in _parse_message
    if intent_name not in self.domain.intents:
AttributeError: 'NoneType' object has no attribute 'intents'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/bin/rasa", line 10, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.6/site-packages/rasa/__main__.py", line 70, in main
    cmdline_arguments.func(cmdline_arguments)
  File "/usr/local/lib/python3.6/site-packages/rasa/cli/train.py", line 121, in train_core
    kwargs=extract_additional_arguments(args),
  File "/usr/local/lib/python3.6/site-packages/rasa/train.py", line 215, in train_core
    kwargs=kwargs,
  File "uvloop/loop.pyx", line 1451, in uvloop.loop.Loop.run_until_complete
  File "/usr/local/lib/python3.6/site-packages/rasa/train.py", line 277, in train_core_async
    kwargs=kwargs,
  File "/usr/local/lib/python3.6/site-packages/rasa/train.py", line 303, in _train_core_with_validated_data
    kwargs=kwargs,
  File "/usr/local/lib/python3.6/site-packages/rasa/core/train.py", line 57, in train
    stories_file, exclusion_percentage=exclusion_percentage, **data_load_args
  File "/usr/local/lib/python3.6/site-packages/rasa/core/agent.py", line 616, in load_data
    exclusion_percentage=exclusion_percentage,
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/__init__.py", line 50, in load_data
    resource_name, domain, exclusion_percentage=exclusion_percentage
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/__init__.py", line 29, in extract_story_graph
    exclusion_percentage=exclusion_percentage,
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 186, in read_from_folder
    f, domain, interpreter, template_variables, use_e2e
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 214, in read_from_file
    return await reader.process_lines(lines)
  File "/usr/local/lib/python3.6/site-packages/rasa/core/training/dsl.py", line 313, in process_lines
    raise ValueError(msg)
```

**Command or request that led to error**:
```
docker run -v $(pwd)/data:/app/data rasa/rasa:latest-full train core --config /app/data/config.yml --stories /app/data/stories --out /app/data/models --augmentation 0 --quiet
```


Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 790785a1f48..488f760280c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -36,6 +36,7 @@ Fixed
 - ``x in AnySlotDict`` is now ``True`` for any ``x``, which fixes empty slot warnings in
   interactive learning
 - ``rasa train`` now also includes NLU files in other formats than the Rasa format
+- ``rasa train core`` no longer crashes without a ``--domain`` arg
 
 
 [1.1.4] - 2019-06-18
@@ -57,7 +58,6 @@ Fixed
 - take FB quick reply payload rather than text as input
 - fixed bug where `training_data` path in `metadata.json` was an absolute path
 
-
 [1.1.3] - 2019-06-14
 ^^^^^^^^^^^^^^^^^^^^
 
diff --git a/rasa/train.py b/rasa/train.py
index 018694e8975..0802536fba4 100644
--- a/rasa/train.py
+++ b/rasa/train.py
@@ -71,12 +71,8 @@ async def train_async(
     try:
         domain = Domain.load(domain, skill_imports)
         domain.check_missing_templates()
-    except InvalidDomain as e:
-        print_error(
-            "Could not load domain due to error: {} \nTo specify a valid domain "
-            "path, use the '--domain' argument.".format(e)
-        )
-        return None
+    except InvalidDomain:
+        domain = None
 
     story_directory, nlu_data_directory = data.get_core_nlu_directories(
         training_files, skill_imports
@@ -87,6 +83,11 @@ async def train_async(
         nlu_data = stack.enter_context(TempDirectoryPath(nlu_data_directory))
         story = stack.enter_context(TempDirectoryPath(story_directory))
 
+        if domain is None:
+            return handle_domain_if_not_exists(
+                config, nlu_data_directory, output_path, fixed_model_name
+            )
+
         return await _train_async_internal(
             domain,
             config,
@@ -99,6 +100,27 @@ async def train_async(
             kwargs,
         )
 
+    if domain is None:
+        return handle_domain_if_not_exists(
+            config, nlu_data_directory, output_path, fixed_model_name
+        )
+
+
+def handle_domain_if_not_exists(
+    config, nlu_data_directory, output_path, fixed_model_name
+):
+    nlu_model_only = _train_nlu_with_validated_data(
+        config=config,
+        nlu_data_directory=nlu_data_directory,
+        output=output_path,
+        fixed_model_name=fixed_model_name,
+    )
+    print_warning(
+        "Core training was skipped because no valid domain file was found. Only an nlu-model was created."
+        "Please specify a valid domain using '--domain' argument or check if the provided domain file exists."
+    )
+    return nlu_model_only
+
 
 async def _train_async_internal(
     domain: Union[Domain, Text],
@@ -293,16 +315,15 @@ async def train_core_async(
 
     skill_imports = SkillSelector.load(config, stories)
 
-    if isinstance(domain, str):
-        try:
-            domain = Domain.load(domain, skill_imports)
-            domain.check_missing_templates()
-        except InvalidDomain as e:
-            print_error(
-                "Could not load domain due to: '{}'. To specify a valid domain path "
-                "use the '--domain' argument.".format(e)
-            )
-            return None
+    try:
+        domain = Domain.load(domain, skill_imports)
+        domain.check_missing_templates()
+    except InvalidDomain:
+        print_error(
+            "Core training was skipped because no valid domain file was found. "
+            "Please specify a valid domain using '--domain' argument or check if the provided domain file exists."
+        )
+        return None
 
     train_context = TempDirectoryPath(data.get_core_directory(stories, skill_imports))
 


Test patch:
diff --git a/tests/cli/test_rasa_train.py b/tests/cli/test_rasa_train.py
index 9062567a4f0..7d3c879030e 100644
--- a/tests/cli/test_rasa_train.py
+++ b/tests/cli/test_rasa_train.py
@@ -4,6 +4,8 @@ import tempfile
 
 import pytest
 
+from rasa import model
+
 from rasa.cli.train import _get_valid_config
 from rasa.constants import (
     CONFIG_MANDATORY_KEYS_CORE,
@@ -37,6 +39,32 @@ def test_train(run_in_default_project):
     assert os.path.basename(files[0]) == "test-model.tar.gz"
 
 
+def test_train_no_domain_exists(run_in_default_project):
+
+    os.remove("domain.yml")
+    run_in_default_project(
+        "train",
+        "-c",
+        "config.yml",
+        "--data",
+        "data",
+        "--out",
+        "train_models_no_domain",
+        "--fixed-model-name",
+        "nlu-model-only",
+    )
+
+    assert os.path.exists("train_models_no_domain")
+    files = list_files("train_models_no_domain")
+    assert len(files) == 1
+
+    trained_model_path = "train_models_no_domain/nlu-model-only.tar.gz"
+    unpacked = model.unpack_model(trained_model_path)
+
+    metadata_path = os.path.join(unpacked, "nlu", "metadata.json")
+    assert os.path.exists(metadata_path)
+
+
 def test_train_skip_on_model_not_changed(run_in_default_project):
     temp_dir = os.getcwd()
 
@@ -118,6 +146,28 @@ def test_train_core(run_in_default_project):
     assert os.path.isfile("train_rasa_models/rasa-model.tar.gz")
 
 
+def test_train_core_no_domain_exists(run_in_default_project):
+
+    os.remove("domain.yml")
+    run_in_default_project(
+        "train",
+        "core",
+        "--config",
+        "config.yml",
+        "--domain",
+        "domain1.yml",
+        "--stories",
+        "data",
+        "--out",
+        "train_rasa_models_no_domain",
+        "--fixed-model-name",
+        "rasa-model",
+    )
+
+    assert not os.path.exists("train_rasa_models_no_domain/rasa-model.tar.gz")
+    assert not os.path.isfile("train_rasa_models_no_domain/rasa-model.tar.gz")
+
+
 def count_rasa_temp_files():
     count = 0
     for entry in os.scandir(tempfile.gettempdir()):