Repository: RasaHQ/rasa
Pull request: make sure new intents, actions, and utterances are non-empty
**Proposed changes**:
- a new questionary validator to check whether the input has something other than whitespace
- use this validator for new intent and action names, and utterance texts
- closes #2821 (again)

**Status (please check what you already did)**:
- [x] made PR ready for code review
- [x] added some tests for the functionality
- [ ] updated the documentation
- [x] updated the changelog
- [x] reformat files using `black` (please check [Readme](https://github.com/RasaHQ/rasa_nlu#code-style) for instructions)

P.S. I didn't run into any issues with https://github.com/RasaHQ/rasa/pull/3896 while working on this PR.


Problem statement:
Exporting interactive training session failes if intent 'None' appears during training
**Rasa Core version**:
0.12.x
**Python version**: 
3.6
**Operating system** (windows, osx, ...):
ubuntu
**Issue**:
Exporting fails if e.g. during a FormAction you only provide an answer for a slot, without assigning the answer to an intent. In this case the story has an element like this:

> * shopping.add_items_to_wishlist
>     - form_shopping.search_items
>     - form{"name": "form_shopping.search_items"}
>     - slot{"requested_slot": "shopping.item"}
> * form: None

In that scenario, once you try to quit & export the execution crashes.

I think it should be either OK to not declare an intent or the user should be forced to do it. Otherwise it might crash after a long training session and you need to start all over.

Hints from issue discussion:
Thanks for raising this issue, @Ghostvv  will get back to you about it soon.
Hi, could you please elaborate on: `during a FormAction you only provide an answer for a slot, without assigning the answer to an intent.`?
@Ghostvv : After you provide an answer to the form it asks you to confirms e.g. like this, right?

`Is the NLU classification for '3' with intent 'inform' correct?  Yes `

But sometimes it doesn't recognize an intent and then asks:

`Is the NLU classification for '3' with intent None correct?  Yes `

In the second case, where the user confirms the `None` intent, the export failes with `KeyError: 'intent'`. I assume it's because the message has no intent and, thus, the key doesn't exist. Here's the full stacktrace:

```
2018-12-13 10:44:39 ERROR    rasa_core.training.interactive  - An exception occurred while recording messages.
Traceback (most recent call last):
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 1283, in record_messages
    _enter_user_message(sender_id, endpoint)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 1131, in _enter_user_message
    lambda a: not a["message"])
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 309, in _ask_questions
    should_retry = _ask_if_quit(sender_id, endpoint)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 615, in _ask_if_quit
    _write_nlu_to_file(nlu_path, evts)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 793, in _write_nlu_to_file
    f.write(nlu_data.as_markdown())
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/training_data.py", line 144, in as_markdown
    return MarkdownWriter().dumps(self)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/formats/markdown.py", line 170, in dumps
    md += self._generate_training_examples_md(training_data)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/formats/markdown.py", line 180, in _generate_training_examples_md
    key=lambda k: k['intent'])
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/formats/markdown.py", line 180, in <lambda>
    key=lambda k: k['intent'])
KeyError: 'intent'
Exception in thread Thread-1:
Traceback (most recent call last):
  File "/opt/conda/envs/env/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/opt/conda/envs/env/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 1283, in record_messages
    _enter_user_message(sender_id, endpoint)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 1131, in _enter_user_message
    lambda a: not a["message"])
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 309, in _ask_questions
    should_retry = _ask_if_quit(sender_id, endpoint)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 615, in _ask_if_quit
    _write_nlu_to_file(nlu_path, evts)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_core/training/interactive.py", line 793, in _write_nlu_to_file
    f.write(nlu_data.as_markdown())
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/training_data.py", line 144, in as_markdown
    return MarkdownWriter().dumps(self)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/formats/markdown.py", line 170, in dumps
    md += self._generate_training_examples_md(training_data)
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/formats/markdown.py", line 180, in _generate_training_examples_md
    key=lambda k: k['intent'])
  File "/opt/conda/envs/env/lib/python3.6/site-packages/rasa_nlu/training_data/formats/markdown.py", line 180, in <lambda>
    key=lambda k: k['intent'])
KeyError: 'intent'
```

and it's not related to the `FromAction` right?
Yeah, you're probably right. I just realized it during the FormAction because I didn't check whether the intent was right for the form, since the intent shouldn't matter while the Form is active, right?
it depends on how you implement validation logic in the forms, but intent `None` is not a good idea
I agree, but if you have the slot extracted from the text, you don't really care about the intent, and if you miss out on correcting the intent, your training session just abruptly crashes and you lose all your progress. Maybe it's better to have a warning there and force the user to correct the intent then?
yes, I agree if intent is classified as `None` during interactive learning, a user should be forced to set a proper intent
Just ran into this issue, still relevant -- looks like we handled this by asking the user for the intent, but it still allows you not to input anything, which leads to the fail.
```
? Your input -> asdlfkj
The NLU classification for 'asdlfkj' returned 'None'
? What intent is it?  <create_new_intent>
? Please type the intent name:
? Please mark the entities using [value](type) notation asdlfkjasf
```
leads to same error traceback above upon attempting to export.

@wochinge can we use the new validation method you introduced to make sure the user enters *anything* for the intent name?

Commit message: Merge branch 'master' into empty-intent

Patch to review:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index fa1fcfbb941f..1362685c63b8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -41,6 +41,7 @@ Fixed
 - ``rasa train`` now also includes NLU files in other formats than the Rasa format
 - ``rasa train core`` no longer crashes without a ``--domain`` arg
 - ``rasa interactive`` now looks for endpoints in ``endpoints.yml`` if no ``--endpoints`` arg is passed
+- actions, intents, and utterances created in ``rasa interactive`` can no longer be empty
 
 
 [1.1.4] - 2019-06-18
diff --git a/rasa/core/training/interactive.py b/rasa/core/training/interactive.py
index 96da8a1644df..f51673641ada 100644
--- a/rasa/core/training/interactive.py
+++ b/rasa/core/training/interactive.py
@@ -334,12 +334,22 @@ def _selection_choices_from_intent_prediction(
 
 
 async def _request_free_text_intent(sender_id: Text, endpoint: EndpointConfig) -> Text:
-    question = questionary.text("Please type the intent name:")
+    question = questionary.text(
+        message="Please type the intent name:",
+        validate=io_utils.questionary_not_empty_text_validator(
+            "Please enter an intent name"
+        ),
+    )
     return await _ask_questions(question, sender_id, endpoint)
 
 
 async def _request_free_text_action(sender_id: Text, endpoint: EndpointConfig) -> Text:
-    question = questionary.text("Please type the action name:")
+    question = questionary.text(
+        message="Please type the action name:",
+        validate=io_utils.questionary_not_empty_text_validator(
+            "Please enter an action name"
+        ),
+    )
     return await _ask_questions(question, sender_id, endpoint)
 
 
@@ -347,7 +357,13 @@ async def _request_free_text_utterance(
     sender_id: Text, endpoint: EndpointConfig, action: Text
 ) -> Text:
     question = questionary.text(
-        "Please type the message for your new utter_template '{}':".format(action)
+        message=(
+            "Please type the message for your new utterance "
+            "template '{}':".format(action)
+        ),
+        validate=io_utils.questionary_not_empty_text_validator(
+            "Please enter a template message"
+        ),
     )
     return await _ask_questions(question, sender_id, endpoint)
 
diff --git a/rasa/utils/io.py b/rasa/utils/io.py
index b504b7d71ed0..45ba7a7554d5 100644
--- a/rasa/utils/io.py
+++ b/rasa/utils/io.py
@@ -277,3 +277,21 @@ def validate(self, document: Document) -> None:
                 raise ValidationError(message=error_message)
 
     return ExportPathValidator
+
+
+def questionary_not_empty_text_validator(error_message: Text) -> Type["Validator"]:
+    """Creates a `Validator` class which can be used with `questionary` to validate
+    that the user entered something other than whitespace.
+    """
+
+    from prompt_toolkit.validation import Validator, ValidationError
+    from prompt_toolkit.document import Document
+
+    class NotEmptyTextValidator(Validator):
+        def validate(self, document: Document) -> None:
+            input = document.text
+            is_valid = input is not None and input.strip() != ""
+            if not is_valid:
+                raise ValidationError(message=error_message)
+
+    return NotEmptyTextValidator
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ebeadc064877..07a7e990a876 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -154,3 +154,30 @@ def test_file_path_validator_with_valid_paths(actual_path):
     document = Document(actual_path)
     # If the path is valid there shouldn't be an exception
     assert validator.validate(document) is None
+
+
+@pytest.mark.parametrize("input", ["", "   "])
+def test_non_empty_text_validator_with_empty_input(input):
+    from prompt_toolkit.validation import ValidationError
+    from prompt_toolkit.document import Document
+
+    test_error_message = "enter something"
+
+    validator = rasa.utils.io.questionary_not_empty_text_validator(test_error_message)()
+
+    document = Document(input)
+    with pytest.raises(ValidationError) as e:
+        validator.validate(document)
+
+    assert e.value.message == test_error_message
+
+
+@pytest.mark.parametrize("input", ["utter_greet", "greet", "Hi there!"])
+def test_non_empty_text_validator_with_valid_input(input):
+    from prompt_toolkit.document import Document
+
+    validator = rasa.utils.io.questionary_not_empty_text_validator("error message")()
+
+    document = Document(input)
+    # If there is input there shouldn't be an exception
+    assert validator.validate(document) is None


Code review comments:
diff_hunk: @@ -277,3 +277,21 @@ def validate(self, document: Document) -> None:
                 raise ValidationError(message=error_message)
 
     return ExportPathValidator
+
+
+def questionary_not_empty_text_validator(error_message: Text) -> Type["Validator"]:
path: rasa/utils/io.py
text: @user1:
can we refactor the two validators to use some common behavior?

I thought of sth like:

```python
def path_validator(some_params, error_message) -> Validator:
  def is_valid(text: Text) -> bool:
     return text.endswith("yml")
  
  return create_validator(is_valid, error_message)

def blank_validator(some_params, error_message) -> Validator:
  def is_valid(text: Text) -> bool:
     return text != ""
  
  return create_validator(is_valid, error_message)

def create_validator(f, message) -> Type[Validator]:
  # Create class and use given function `f` to validate things


@author:
haha, i think this is basically rewriting the from_callable isn't it?

@user1:
probably :-D we can remove the shizzle as soon as tom merges my pr :-D

@author:
hm maybe this doesn't need to be done now then?

@user1:
mhm, but who knows whether we remember to this once the pr is merged and then it's stuck in the code forever.

@author:
hahaha ok true