Repository: RasaHQ/rasa
Pull request: Keyword classifier
This reopens PR [1741](https://github.com/RasaHQ/rasa_nlu/pull/1741) which I accidentally closed and adds the modifications that were still required.

@FabianReich Sorry for moving this to another PR, I messed up while trying to add to your original PR.


**Proposed changes**:
- Adding a custom keyword intent classifier
- fixes #1740

**Status (please check what you already did)**:
- [x] made PR ready for code review
- [x] updated the changelog
- [x] Added tests
- [x] Updated the documentation

Problem statement:
App Simple Keyword intent classifier
**Feature**:
I want to have a simple keyword matching model. 

E.g.
Building a model that recognizes if the sentence is about a female or a male person.

```
Sentence: "I got inspired by my mother."
Intent: "Female"
```
Then I want to provide a list of female people (mother, girl, women, etc.) and a list of male people (man, father, dad, etc.). The model should just look if one of the keywords is contained in the sentence and return the fitting intent.

Hints from issue discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Commit message: intent_keyword_map is now instance attr, `load` method fixed

Patch to review:
diff --git a/.travis.yml b/.travis.yml
index 648b65f143d1..eece45adaf23 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -37,6 +37,8 @@ jobs:
     python: 3.6
     script:
     - make types
+    - pip install pytype==2019.5.31
+    - pytype --keep-going rasa
   - &run-tests
     stage: test
     name: "Test 3.5"
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 41f631c6c690..a5c39a0be76d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -13,6 +13,7 @@ Added
 -----
 - added optional pymongo dependencies ``[tls, srv]`` to ``requirements.txt`` for better mongodb support
 - ``case_sensitive`` option added to ``WhiteSpaceTokenizer`` with ``true`` as default.
+- a new ``keyword_intent_classifier``, can be used to classify messages based on keywords
 
 Changed
 -------
diff --git a/docs/nlu/components.rst b/docs/nlu/components.rst
index d73a681891bf..e7856a609cd1 100644
--- a/docs/nlu/components.rst
+++ b/docs/nlu/components.rst
@@ -256,23 +256,8 @@ CountVectorsFeaturizer
 Intent Classifiers
 ------------------
 
-KeywordIntentClassifier
-~~~~~~~~~~~~~~~~~~~~~~~
 
-:Short: Simple keyword matching intent classifier. Not intended to be used.
-:Outputs: ``intent``
-:Requires: nothing
-:Output-Example:
 
-    .. code-block:: json
-
-        {
-            "intent": {"name": "greet", "confidence": 0.98343}
-        }
-
-:Description:
-    This classifier is mostly used as a placeholder. It is able to recognize `hello` and
-    `goodbye` intents by searching for these keywords in the passed messages.
 
 MitieIntentClassifier
 ~~~~~~~~~~~~~~~~~~~~~
@@ -454,6 +439,36 @@ EmbeddingIntentClassifier
               starspace algorithm in the case ``mu_neg = mu_pos`` and ``use_max_sim_neg = False``.
               See `starspace paper <https://arxiv.org/abs/1709.03856>`_ for details.
 
+
+.. _keyword_intent_classifier:
+
+KeywordIntentClassifier
+~~~~~~~~~~~~~~~~~~~~~~~
+
+:Short: Simple keyword matching intent classifier, intended for small, short-term projects.
+:Outputs: ``intent``
+:Requires: nothing
+:Output-Example:
+
+    .. code-block:: json
+
+        {
+            "intent": {"name": "greet", "confidence": 1.0}
+        }
+
+:Description:
+    This classifier works by searching a message for keywords.
+    The matching is case sensitive and search only for exact matches of the keyword-string.
+    The keywords for an intent are the examples of that intent in the NLU training data.
+    .. note:: This means the entire example is the keyword, not the individual words in the example.
+
+:Configuration:
+
+    .. code-block:: yaml
+
+        pipeline:
+        - name: "KeywordIntentClassifier"
+
 Tokenizers
 ----------
 
diff --git a/docs/nlu/training-data-format.rst b/docs/nlu/training-data-format.rst
index b25d8660897c..c269b1e3793c 100644
--- a/docs/nlu/training-data-format.rst
+++ b/docs/nlu/training-data-format.rst
@@ -74,7 +74,7 @@ The most important one is ``common_examples``.
             "common_examples": [],
             "regex_features" : [],
             "lookup_tables"  : [],
-            "entity_synonyms": []
+            "entity_synonyms": [],
         }
     }
 
@@ -283,3 +283,5 @@ directly specified in the training data.
     For example if there are false positives or other noise in the table,
     this can hurt performance. So make sure your lookup tables contain
     clean data.
+
+
diff --git a/rasa/core/training/dsl.py b/rasa/core/training/dsl.py
index 69fd57d0f49b..bbfe6ecbe0dc 100644
--- a/rasa/core/training/dsl.py
+++ b/rasa/core/training/dsl.py
@@ -359,7 +359,7 @@ async def _parse_message(self, message, line_num):
         if message.startswith(INTENT_MESSAGE_PREFIX):
             parse_data = await RegexInterpreter().parse(message)
         else:
-            parse_data = await self.interpreter.parse(message)
+            parse_data = await self.interpreter.map_keyword_to_intent(message)
         utterance = UserUttered(
             message, parse_data.get("intent"), parse_data.get("entities"), parse_data
         )
diff --git a/rasa/core/training/visualization.py b/rasa/core/training/visualization.py
index 764469ac752a..f796aa145bf3 100644
--- a/rasa/core/training/visualization.py
+++ b/rasa/core/training/visualization.py
@@ -261,7 +261,7 @@ async def _replace_edge_labels_with_nodes(
     for s, e, k, d in edges:
         if k != EDGE_NONE_LABEL:
             if message_generator and d.get("label", k) is not None:
-                parsed_info = await interpreter.parse(d.get("label", k))
+                parsed_info = await interpreter.map_keyword_to_intent(d.get("label", k))
                 label = message_generator.message_for_data(parsed_info)
             else:
                 label = d.get("label", k)
diff --git a/rasa/nlu/classifiers/keyword_intent_classifier.py b/rasa/nlu/classifiers/keyword_intent_classifier.py
index 092465a36f15..1104f86badfa 100644
--- a/rasa/nlu/classifiers/keyword_intent_classifier.py
+++ b/rasa/nlu/classifiers/keyword_intent_classifier.py
@@ -1,32 +1,99 @@
-from typing import Any, Optional, Text
+import os
+import logging
+import re
+import typing
+from typing import Any, Dict, Optional, Text
 
+from rasa.nlu import utils
 from rasa.nlu.components import Component
 from rasa.nlu.training_data import Message
 
+logger = logging.getLogger(__name__)
+
+if typing.TYPE_CHECKING:
+    from rasa.nlu.config import RasaNLUModelConfig
+    from rasa.nlu.training_data import TrainingData
+    from rasa.nlu.model import Metadata
+    from rasa.nlu.training_data import Message
+
 
 class KeywordIntentClassifier(Component):
+    """Intent classifier using simple keyword matching.
+
+
+    The classifier takes a list of keywords and associated intents as an input.
+    A input sentence is checked for the keywords and the intent is returned.
+
+    """
 
     provides = ["intent"]
 
-    his = ["hello", "hi", "hey"]
+    def __init__(self, intent_keyword_map: Optional[Dict] = None):
+
+        if intent_keyword_map is None:
+            self.intent_keyword_map = {}
+        else:
+            self.intent_keyword_map = intent_keyword_map
 
-    byes = ["bye", "goodbye"]
+    def train(
+        self,
+        training_data: "TrainingData",
+        cfg: Optional["RasaNLUModelConfig"] = None,
+        **kwargs: Any
+    ) -> None:
+        self.intent_keyword_map = {}
+        for intent in training_data.intents:
+            self.intent_keyword_map[intent] = [
+                ex.text
+                for ex in training_data.training_examples
+                if ex.get("intent") == intent
+            ]
 
     def process(self, message: Message, **kwargs: Any) -> None:
+        intent_name = self.map_keyword_to_intent(message.text)
+        if intent_name is not None:
+            intent = {"name": intent_name, "confidence": 1.0}
+            message.set("intent", intent, add_to_output=True)
 
-        intent = {"name": self.parse(message.text), "confidence": 1.0}
-        message.set("intent", intent, add_to_output=True)
+    def map_keyword_to_intent(self, text: Text) -> Optional[Text]:
 
-    def parse(self, text: Text) -> Optional[Text]:
+        for intent, keywords in self.intent_keyword_map.items():
+            for string in keywords:
+                if re.search(r"\b" + string + r"\b", text):
+                    return intent
 
-        _text = text.lower()
+        return None
 
-        def is_present(x):
-            return x in _text
+    def persist(self, file_name: Text, model_dir: Text) -> Dict[Text, Any]:
 
-        if any(map(is_present, self.his)):
-            return "greet"
-        elif any(map(is_present, self.byes)):
-            return "goodbye"
-        else:
-            return None
+        """Persist this model into the passed directory.
+
+        Return the metadata necessary to load the model again.
+        """
+        file_name = file_name + ".json"
+        keyword_file = os.path.join(model_dir, file_name)
+        utils.write_json_to_file(keyword_file, self.intent_keyword_map)
+
+        return {"file": file_name}
+
+    @classmethod
+    def load(
+        cls,
+        meta: Dict[Text, Any],
+        model_dir: Optional[Text] = None,
+        model_metadata: "Metadata" = None,
+        cached_component: Optional["KeywordIntentClassifier"] = None,
+        **kwargs: Any
+    ) -> "KeywordIntentClassifier":
+
+        if model_dir and meta.get("file"):
+            file_name = meta.get("file")
+            keyword_file = os.path.join(model_dir, file_name)
+            if os.path.exists(keyword_file):
+                intent_keyword_map = utils.read_json_file(keyword_file)
+            else:
+                logger.warning(
+                    "Failed to load IntentKeywordClassifier, maybe "
+                    "{} does not exist.".format(keyword_file)
+                )
+        return cls(intent_keyword_map)
diff --git a/tests/core/test_interpreter.py b/tests/core/test_interpreter.py
index f86994be54c8..fae06ac115fa 100644
--- a/tests/core/test_interpreter.py
+++ b/tests/core/test_interpreter.py
@@ -17,7 +17,7 @@ def regex_interpreter():
 
 async def test_regex_interpreter_intent(regex_interpreter):
     text = INTENT_MESSAGE_PREFIX + "my_intent"
-    result = await regex_interpreter.parse(text)
+    result = await regex_interpreter.map_keyword_to_intent(text)
     assert result["text"] == text
     assert len(result["intent_ranking"]) == 1
     assert (
@@ -33,7 +33,7 @@ async def test_regex_interpreter_intent(regex_interpreter):
 
 async def test_regex_interpreter_entities(regex_interpreter):
     text = INTENT_MESSAGE_PREFIX + 'my_intent{"foo":"bar"}'
-    result = await regex_interpreter.parse(text)
+    result = await regex_interpreter.map_keyword_to_intent(text)
     assert result["text"] == text
     assert len(result["intent_ranking"]) == 1
     assert (
@@ -51,7 +51,7 @@ async def test_regex_interpreter_entities(regex_interpreter):
 
 async def test_regex_interpreter_confidence(regex_interpreter):
     text = INTENT_MESSAGE_PREFIX + "my_intent@0.5"
-    result = await regex_interpreter.parse(text)
+    result = await regex_interpreter.map_keyword_to_intent(text)
     assert result["text"] == text
     assert len(result["intent_ranking"]) == 1
     assert (
@@ -67,7 +67,7 @@ async def test_regex_interpreter_confidence(regex_interpreter):
 
 async def test_regex_interpreter_confidence_and_entities(regex_interpreter):
     text = INTENT_MESSAGE_PREFIX + 'my_intent@0.5{"foo":"bar"}'
-    result = await regex_interpreter.parse(text)
+    result = await regex_interpreter.map_keyword_to_intent(text)
     assert result["text"] == text
     assert len(result["intent_ranking"]) == 1
     assert (
@@ -84,7 +84,7 @@ async def test_regex_interpreter_confidence_and_entities(regex_interpreter):
 
 
 async def test_regex_interpreter_adds_intent_prefix(regex_interpreter):
-    r = await regex_interpreter.parse('mood_greet{"name": "rasa"}')
+    r = await regex_interpreter.map_keyword_to_intent('mood_greet{"name": "rasa"}')
 
     assert r.get("text") == '/mood_greet{"name": "rasa"}'
 
diff --git a/tests/nlu/base/test_components.py b/tests/nlu/base/test_components.py
index 1a611fd84273..90b6fa4ebfd1 100644
--- a/tests/nlu/base/test_components.py
+++ b/tests/nlu/base/test_components.py
@@ -89,5 +89,5 @@ def test_example_component(component_builder, tmpdir_factory):
         config=conf,
     )
 
-    r = interpreter.parse("test")
+    r = interpreter.map_keyword_to_intent("test")
     assert r is not None
diff --git a/tests/nlu/base/test_interpreter.py b/tests/nlu/base/test_interpreter.py
index 320b9d9a4a6a..05a892fbbeaf 100644
--- a/tests/nlu/base/test_interpreter.py
+++ b/tests/nlu/base/test_interpreter.py
@@ -31,7 +31,7 @@ def test_interpreter(pipeline_template, component_builder, tmpdir):
     texts = ["good bye", "i am looking for an indian spot"]
 
     for text in texts:
-        result = interpreter.parse(text, time=None)
+        result = interpreter.map_keyword_to_intent(text, time=None)
         assert result["text"] == text
         assert not result["intent"]["name"] or result["intent"]["name"] in td.intents
         assert result["intent"]["confidence"] >= 0
diff --git a/tests/nlu/training/test_train.py b/tests/nlu/training/test_train.py
index 09f9421f38ba..5126e24e6691 100644
--- a/tests/nlu/training/test_train.py
+++ b/tests/nlu/training/test_train.py
@@ -42,10 +42,10 @@ def pipelines_for_tests():
                 "SpacyEntityExtractor",
                 "DucklingHTTPExtractor",
                 "EntitySynonymMapper",
-                "KeywordIntentClassifier",
                 "SklearnIntentClassifier",
                 "MitieIntentClassifier",
                 "EmbeddingIntentClassifier",
+                "KeywordIntentClassifier",
             ),
         ),
         (
@@ -56,6 +56,7 @@ def pipelines_for_tests():
                 "MitieFeaturizer",
                 "MitieEntityExtractor",
                 "SklearnIntentClassifier",
+                "KeywordIntentClassifier",
             ),
         ),
     ]


Code review comments:
diff_hunk: @@ -1,32 +1,99 @@
-from typing import Any, Optional, Text
+import os
+import logging
+import re
+import typing
+from typing import Any, Dict, Optional, Text
 
+from rasa.nlu import utils
 from rasa.nlu.components import Component
 from rasa.nlu.training_data import Message
 
+logger = logging.getLogger(__name__)
+
+if typing.TYPE_CHECKING:
+    from rasa.nlu.config import RasaNLUModelConfig
+    from rasa.nlu.training_data import TrainingData
+    from rasa.nlu.model import Metadata
+    from rasa.nlu.training_data import Message
+
 
 class KeywordIntentClassifier(Component):
+    """Intent classifier using simple keyword matching.
+
+
+    The classifier takes a list of keywords and associated intents as an input.
+    A input sentence is checked for the keywords and the intent is returned.
+
+    """
 
     provides = ["intent"]
 
-    his = ["hello", "hi", "hey"]
+    def __init__(self, intent_keyword_map: Optional[Dict] = None):
+
+        if intent_keyword_map is None:
+            self.intent_keyword_map = {}
+        else:
+            self.intent_keyword_map = intent_keyword_map
 
-    byes = ["bye", "goodbye"]
+    def train(
+        self,
+        training_data: "TrainingData",
+        cfg: Optional["RasaNLUModelConfig"] = None,
+        **kwargs: Any
+    ) -> None:
+        self.intent_keyword_map = {}
+        for intent in training_data.intents:
+            self.intent_keyword_map[intent] = [
+                ex.text
+                for ex in training_data.training_examples
+                if ex.get("intent") == intent
+            ]
 
     def process(self, message: Message, **kwargs: Any) -> None:
+        intent_name = self.map_keyword_to_intent(message.text)
+        if intent_name is not None:
+            intent = {"name": intent_name, "confidence": 1.0}
+            message.set("intent", intent, add_to_output=True)
 
-        intent = {"name": self.parse(message.text), "confidence": 1.0}
-        message.set("intent", intent, add_to_output=True)
+    def map_keyword_to_intent(self, text: Text) -> Optional[Text]:
 
-    def parse(self, text: Text) -> Optional[Text]:
+        for intent, keywords in self.intent_keyword_map.items():
+            for string in keywords:
+                if re.search(r"\b" + string + r"\b", text):
path: rasa/nlu/classifiers/keyword_intent_classifier.py
text: @user1:
does it works if string contains spaces?

@author:
Yes, it returns the intent iff the entered text is the same as one of the training examples. 
Actually the regex was needed when we were still searching for keywords, now that it's just string matching i think `==` will suffice

@user1:
hm, but then `yes, blabla` will not be classified as yes, is it intended?

@author:
probably better to just use `in`

diff_hunk: @@ -1,32 +1,99 @@
-from typing import Any, Optional, Text
+import os
+import logging
+import re
+import typing
+from typing import Any, Dict, Optional, Text
 
+from rasa.nlu import utils
 from rasa.nlu.components import Component
 from rasa.nlu.training_data import Message
 
+logger = logging.getLogger(__name__)
+
+if typing.TYPE_CHECKING:
+    from rasa.nlu.config import RasaNLUModelConfig
+    from rasa.nlu.training_data import TrainingData
+    from rasa.nlu.model import Metadata
+    from rasa.nlu.training_data import Message
+
 
 class KeywordIntentClassifier(Component):
+    """Intent classifier using simple keyword matching.
+
+
+    The classifier takes a list of keywords and associated intents as an input.
+    A input sentence is checked for the keywords and the intent is returned.
+
+    """
 
     provides = ["intent"]
 
-    his = ["hello", "hi", "hey"]
+    def __init__(self, intent_keyword_map: Optional[Dict] = None):
+
+        if intent_keyword_map is None:
+            self.intent_keyword_map = {}
+        else:
+            self.intent_keyword_map = intent_keyword_map
 
-    byes = ["bye", "goodbye"]
+    def train(
+        self,
+        training_data: "TrainingData",
+        cfg: Optional["RasaNLUModelConfig"] = None,
+        **kwargs: Any
+    ) -> None:
+        self.intent_keyword_map = {}
+        for intent in training_data.intents:
+            self.intent_keyword_map[intent] = [
+                ex.text
+                for ex in training_data.training_examples
+                if ex.get("intent") == intent
+            ]
 
     def process(self, message: Message, **kwargs: Any) -> None:
+        intent_name = self.map_keyword_to_intent(message.text)
path: rasa/nlu/classifiers/keyword_intent_classifier.py
text: @user1:
```suggestion
        intent_name = self._map_keyword_to_intent(message.text)
```

diff_hunk: @@ -1,32 +1,99 @@
-from typing import Any, Optional, Text
+import os
+import logging
+import re
+import typing
+from typing import Any, Dict, Optional, Text
 
+from rasa.nlu import utils
 from rasa.nlu.components import Component
 from rasa.nlu.training_data import Message
 
+logger = logging.getLogger(__name__)
+
+if typing.TYPE_CHECKING:
+    from rasa.nlu.config import RasaNLUModelConfig
+    from rasa.nlu.training_data import TrainingData
+    from rasa.nlu.model import Metadata
+    from rasa.nlu.training_data import Message
+
 
 class KeywordIntentClassifier(Component):
+    """Intent classifier using simple keyword matching.
+
+
+    The classifier takes a list of keywords and associated intents as an input.
+    A input sentence is checked for the keywords and the intent is returned.
+
+    """
 
     provides = ["intent"]
 
-    his = ["hello", "hi", "hey"]
+    def __init__(self, intent_keyword_map: Optional[Dict] = None):
path: rasa/nlu/classifiers/keyword_intent_classifier.py
text: @user1:
let's make it standard with `component_config` and calling `super.init` etc

diff_hunk: @@ -1,32 +1,99 @@
-from typing import Any, Optional, Text
+import os
+import logging
+import re
+import typing
+from typing import Any, Dict, Optional, Text
 
+from rasa.nlu import utils
 from rasa.nlu.components import Component
 from rasa.nlu.training_data import Message
 
+logger = logging.getLogger(__name__)
+
+if typing.TYPE_CHECKING:
+    from rasa.nlu.config import RasaNLUModelConfig
+    from rasa.nlu.training_data import TrainingData
+    from rasa.nlu.model import Metadata
+    from rasa.nlu.training_data import Message
+
 
 class KeywordIntentClassifier(Component):
+    """Intent classifier using simple keyword matching.
+
+
+    The classifier takes a list of keywords and associated intents as an input.
+    A input sentence is checked for the keywords and the intent is returned.
+
+    """
 
     provides = ["intent"]
 
-    his = ["hello", "hi", "hey"]
+    def __init__(self, intent_keyword_map: Optional[Dict] = None):
+
+        if intent_keyword_map is None:
+            self.intent_keyword_map = {}
+        else:
+            self.intent_keyword_map = intent_keyword_map
 
-    byes = ["bye", "goodbye"]
+    def train(
+        self,
+        training_data: "TrainingData",
+        cfg: Optional["RasaNLUModelConfig"] = None,
+        **kwargs: Any
+    ) -> None:
+        self.intent_keyword_map = {}
+        for intent in training_data.intents:
+            self.intent_keyword_map[intent] = [
+                ex.text
+                for ex in training_data.training_examples
+                if ex.get("intent") == intent
+            ]
 
     def process(self, message: Message, **kwargs: Any) -> None:
+        intent_name = self.map_keyword_to_intent(message.text)
+        if intent_name is not None:
+            intent = {"name": intent_name, "confidence": 1.0}
+            message.set("intent", intent, add_to_output=True)
 
-        intent = {"name": self.parse(message.text), "confidence": 1.0}
-        message.set("intent", intent, add_to_output=True)
+    def map_keyword_to_intent(self, text: Text) -> Optional[Text]:
 
-    def parse(self, text: Text) -> Optional[Text]:
+        for intent, keywords in self.intent_keyword_map.items():
+            for string in keywords:
+                if re.search(r"\b" + string + r"\b", text):
+                    return intent
 
-        _text = text.lower()
+        return None
 
-        def is_present(x):
-            return x in _text
+    def persist(self, file_name: Text, model_dir: Text) -> Dict[Text, Any]:
 
-        if any(map(is_present, self.his)):
-            return "greet"
-        elif any(map(is_present, self.byes)):
-            return "goodbye"
-        else:
-            return None
+        """Persist this model into the passed directory.
+
+        Return the metadata necessary to load the model again.
+        """
+        file_name = file_name + ".json"
path: rasa/nlu/classifiers/keyword_intent_classifier.py
text: @user1:
```suggestion

        file_name = file_name + ".json"
```

diff_hunk: @@ -1,32 +1,99 @@
-from typing import Any, Optional, Text
+import os
+import logging
+import re
+import typing
+from typing import Any, Dict, Optional, Text
 
+from rasa.nlu import utils
 from rasa.nlu.components import Component
 from rasa.nlu.training_data import Message
 
+logger = logging.getLogger(__name__)
+
+if typing.TYPE_CHECKING:
+    from rasa.nlu.config import RasaNLUModelConfig
+    from rasa.nlu.training_data import TrainingData
+    from rasa.nlu.model import Metadata
+    from rasa.nlu.training_data import Message
+
 
 class KeywordIntentClassifier(Component):
+    """Intent classifier using simple keyword matching.
+
+
+    The classifier takes a list of keywords and associated intents as an input.
+    A input sentence is checked for the keywords and the intent is returned.
+
+    """
 
     provides = ["intent"]
 
-    his = ["hello", "hi", "hey"]
+    def __init__(self, intent_keyword_map: Optional[Dict] = None):
+
+        if intent_keyword_map is None:
+            self.intent_keyword_map = {}
+        else:
+            self.intent_keyword_map = intent_keyword_map
 
-    byes = ["bye", "goodbye"]
+    def train(
+        self,
+        training_data: "TrainingData",
+        cfg: Optional["RasaNLUModelConfig"] = None,
+        **kwargs: Any
+    ) -> None:
+        self.intent_keyword_map = {}
+        for intent in training_data.intents:
+            self.intent_keyword_map[intent] = [
+                ex.text
+                for ex in training_data.training_examples
+                if ex.get("intent") == intent
+            ]
 
     def process(self, message: Message, **kwargs: Any) -> None:
+        intent_name = self.map_keyword_to_intent(message.text)
+        if intent_name is not None:
+            intent = {"name": intent_name, "confidence": 1.0}
+            message.set("intent", intent, add_to_output=True)
 
-        intent = {"name": self.parse(message.text), "confidence": 1.0}
-        message.set("intent", intent, add_to_output=True)
+    def map_keyword_to_intent(self, text: Text) -> Optional[Text]:
path: rasa/nlu/classifiers/keyword_intent_classifier.py
text: @user1:
```suggestion
    def _map_keyword_to_intent(self, text: Text) -> Optional[Text]:
```