Repository: opsdroid/opsdroid (language: python)

Issue:
Use pre-trained Rasa model.
<!-- Before you post an issue or if you are unsure about something join our matrix channel https://app.element.io/#/room/#opsdroid-general:matrix.org and ask away! We are more than happy to help you. -->
# Description
There is no option to take a pre-trained Rasa model (perhaps trained on a more powerful machine) and use that for predictions instead of training a new model each time opsdroid is started.


## Expected Functionality
There should be a configuration option under something such as `parsers->rasanlu->no-train` which by default is false, but can be set to true in order to prevent opsdroid sending the request to re-train each time.


## Experienced Functionality
No such option exists.

## Versions
- **Opsdroid version:**0.23.0
- **Python version:**3.9.2
- **OS/Docker version:**Debian 11



<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->



Discussion / hints:
Thank you for raising this issue @DAMO238  is this still the case? I'm just catching up on the convo in our community channel
Yes, this is still an issue. I used a work around by commenting out the line of code responsible for training rasa in my installation, but this is more of a hacky work around than a solution. There seem to already be functions for loading a model and checking if rasa already has a model loaded. I propose that opsdroid should check if rasa has a model loaded, and then, only if there is no model, check the configuration to see if a model should be loaded and then finally, fallback to training the model (current functionality).
Ah gotcha thank you again for raising this and for the clarification 👍 
Can I ask you for an example on your configuration? Are you providing an intentions yaml file with your rasa config?

Looking at the docs it seems that we only train the model if you provide the intents yaml file.
I am not providing an intentions yaml file, as I trained my model on a different computer and then copied it onto my server. Here is the rasa section of my opsdroid config:

```
parsers:
        rasanlu:
                url: http://localhost:5005
                models-path: models
                token: SUPER_SECRET_TOKEN
                min-score: 0.8
```

Then my rasa config:

```
# Configuration for Rasa NLU.
# https://rasa.com/docs/rasa/nlu/components/
language: en

pipeline:
   - name: SpacyNLP
     model: en_core_web_md
     case_sensitive: False
   - name: SpacyTokenizer
   - name: SpacyFeaturizer
   - name: SpacyEntityExtractor
   - name: RegexFeaturizer
     #   - name: RegexEntityExtractor
   - name: LexicalSyntacticFeaturizer
   - name: CountVectorsFeaturizer
   - name: CountVectorsFeaturizer
     analyzer: char_wb
     min_ngram: 1
     max_ngram: 4
   - name: DIETClassifier
     epochs: 100
     constrain_similarities: true
   - name: EntitySynonymMapper
   - name: ResponseSelector
     epochs: 100
     constrain_similarities: true
   - name: FallbackClassifier
     threshold: 0.3
     ambiguity_threshold: 0.1

# Configuration for Rasa Core.
# https://rasa.com/docs/rasa/core/policies/
policies:
# # No configuration for policies was provided. The following default policies were used to train your model.
# # If you'd like to customize them, uncomment and adjust the policies.
# # See https://rasa.com/docs/rasa/policies for more information.
#   - name: MemoizationPolicy
#   - name: RulePolicy
#   - name: UnexpecTEDIntentPolicy
#     max_history: 5
#     epochs: 100
#   - name: TEDPolicy
#     max_history: 5
#     epochs: 100
#     constrain_similarities: true
```

This is an exact clone of the config used to create the model so that rasa can load the model trained on my laptop.

Fix patch:
diff --git a/opsdroid/const.py b/opsdroid/const.py
index 98c7cc5e0..605f7eb5e 100644
--- a/opsdroid/const.py
+++ b/opsdroid/const.py
@@ -33,6 +33,7 @@
 
 RASANLU_DEFAULT_URL = "http://localhost:5000"
 RASANLU_DEFAULT_MODELS_PATH = "models"
+RASANLU_DEFAULT_TRAIN_MODEL = True
 
 LUISAI_DEFAULT_URL = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/"
 
diff --git a/opsdroid/core.py b/opsdroid/core.py
index a6e072bf2..2e95344f7 100644
--- a/opsdroid/core.py
+++ b/opsdroid/core.py
@@ -27,7 +27,11 @@
 from opsdroid.parsers.event_type import parse_event_type
 from opsdroid.parsers.luisai import parse_luisai
 from opsdroid.parsers.parseformat import parse_format
-from opsdroid.parsers.rasanlu import parse_rasanlu, train_rasanlu
+from opsdroid.parsers.rasanlu import (
+    parse_rasanlu,
+    train_rasanlu,
+    has_compatible_version_rasanlu,
+)
 from opsdroid.parsers.regex import parse_regex
 from opsdroid.parsers.sapcai import parse_sapcai
 from opsdroid.parsers.watson import parse_watson
@@ -351,7 +355,12 @@ async def train_parsers(self, skills):
         if "parsers" in self.modules:
             parsers = self.modules.get("parsers", {})
             rasanlu = get_parser_config("rasanlu", parsers)
-            if rasanlu and rasanlu["enabled"] and rasanlu.get("train", True):
+            if rasanlu and rasanlu["enabled"]:
+                rasa_version_is_compatible = await has_compatible_version_rasanlu(
+                    rasanlu
+                )
+                if rasa_version_is_compatible is False:
+                    self.critical("Rasa version is not compatible", 5)
                 await train_rasanlu(rasanlu, skills)
 
     async def setup_connectors(self, connectors):
diff --git a/opsdroid/parsers/rasanlu.py b/opsdroid/parsers/rasanlu.py
index d2c3c97cb..2a896a557 100644
--- a/opsdroid/parsers/rasanlu.py
+++ b/opsdroid/parsers/rasanlu.py
@@ -9,10 +9,20 @@
 import aiohttp
 import arrow
 
-from opsdroid.const import RASANLU_DEFAULT_URL, RASANLU_DEFAULT_MODELS_PATH
+from opsdroid.const import (
+    RASANLU_DEFAULT_URL,
+    RASANLU_DEFAULT_MODELS_PATH,
+    RASANLU_DEFAULT_TRAIN_MODEL,
+)
 
 _LOGGER = logging.getLogger(__name__)
-CONFIG_SCHEMA = {"url": str, "token": str, "models-path": str, "min-score": float}
+CONFIG_SCHEMA = {
+    "url": str,
+    "token": str,
+    "models-path": str,
+    "min-score": float,
+    "train": bool,
+}
 
 
 async def _get_all_intents(skills):
@@ -84,7 +94,7 @@ async def _get_rasa_nlu_version(config):
         return result
 
 
-async def _check_rasanlu_compatibility(config):
+async def has_compatible_version_rasanlu(config):
     """Check if Rasa NLU is compatible with the API we implement"""
     _LOGGER.debug(_("Checking Rasa NLU version."))
     json_object = await _get_rasa_nlu_version(config)
@@ -152,14 +162,17 @@ async def _is_model_loaded(config):
 
 async def train_rasanlu(config, skills):
     """Train a Rasa NLU model based on the loaded skills."""
+    train = config.get("train", RASANLU_DEFAULT_TRAIN_MODEL)
+    if train is False:
+        _LOGGER.info(_("Skipping Rasa NLU model training as specified in the config."))
+        return False
+
     _LOGGER.info(_("Starting Rasa NLU training."))
     intents = await _get_all_intents(skills)
     if intents is None:
         _LOGGER.warning(_("No intents found, skipping training."))
         return False
 
-    await _check_rasanlu_compatibility(config)
-
     """
     TODO: think about how to correlate intent with trained model
           so we can just load the model without training it again if it wasn't changed


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 153cd26e9..acea8417c 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -556,11 +556,23 @@ async def test_start_databases(self):
                 self.assertEqual(1, len(opsdroid.memory.databases))
 
     async def test_train_rasanlu(self):
-        with OpsDroid() as opsdroid:
+        with OpsDroid() as opsdroid, amock.patch(
+            "opsdroid.parsers.rasanlu._get_rasa_nlu_version"
+        ) as mock_crc:
             opsdroid.modules = {
                 "parsers": [{"config": {"name": "rasanlu", "enabled": True}}]
             }
-            with amock.patch("opsdroid.parsers.rasanlu.train_rasanlu"):
+            mock_crc.return_value = {
+                "version": "2.0.0",
+                "minimum_compatible_version": "2.0.0",
+            }
+            await opsdroid.train_parsers({})
+
+            mock_crc.return_value = {
+                "version": "1.0.0",
+                "minimum_compatible_version": "1.0.0",
+            }
+            with self.assertRaises(SystemExit):
                 await opsdroid.train_parsers({})
 
     async def test_watchdog_works(self):
diff --git a/tests/test_parser_rasanlu.py b/tests/test_parser_rasanlu.py
index 50b3bc5bc..5af81bce6 100644
--- a/tests/test_parser_rasanlu.py
+++ b/tests/test_parser_rasanlu.py
@@ -475,25 +475,25 @@ async def test__get_rasa_nlu_version(self):
                 await rasanlu._get_rasa_nlu_version({}), result.text.return_value
             )
 
-    async def test__check_rasanlu_compatibility(self):
+    async def test_has_compatible_version_rasanlu(self):
         with amock.patch.object(rasanlu, "_get_rasa_nlu_version") as mock_crc:
             mock_crc.return_value = {
                 "version": "1.0.0",
                 "minimum_compatible_version": "1.0.0",
             }
-            self.assertEqual(await rasanlu._check_rasanlu_compatibility({}), False)
+            self.assertEqual(await rasanlu.has_compatible_version_rasanlu({}), False)
 
             mock_crc.return_value = {
                 "version": "2.6.2",
                 "minimum_compatible_version": "2.6.0",
             }
-            self.assertEqual(await rasanlu._check_rasanlu_compatibility({}), True)
+            self.assertEqual(await rasanlu.has_compatible_version_rasanlu({}), True)
 
             mock_crc.return_value = {
                 "version": "3.1.2",
                 "minimum_compatible_version": "3.0.0",
             }
-            self.assertEqual(await rasanlu._check_rasanlu_compatibility({}), True)
+            self.assertEqual(await rasanlu.has_compatible_version_rasanlu({}), True)
 
     async def test__load_model(self):
         result = amock.Mock()
@@ -588,7 +588,7 @@ async def test_train_rasanlu_fails(self):
         ) as mock_btu, amock.patch.object(
             rasanlu, "_get_intents_fingerprint"
         ) as mock_gif, amock.patch.object(
-            rasanlu, "_check_rasanlu_compatibility"
+            rasanlu, "has_compatible_version_rasanlu"
         ) as mock_crc, amock.patch.object(
             rasanlu, "_load_model"
         ) as mock_lmo, amock.patch.object(
@@ -662,7 +662,7 @@ async def test_train_rasanlu_succeeded(self):
         ) as mock_btu, amock.patch.object(
             rasanlu, "_get_intents_fingerprint"
         ) as mock_gif, amock.patch.object(
-            rasanlu, "_check_rasanlu_compatibility"
+            rasanlu, "has_compatible_version_rasanlu"
         ) as mock_crc, amock.patch.object(
             rasanlu, "_load_model"
         ) as mock_lmo, amock.patch.object(
@@ -691,13 +691,16 @@ async def test_train_rasanlu_succeeded(self):
             patched_request.return_value.set_result(result)
             self.assertEqual(await rasanlu.train_rasanlu({}, {}), True)
 
-            patched_request.side_effect = None
-            patched_request.return_value = asyncio.Future()
-            patched_request.return_value.set_result(result)
-            self.assertEqual(await rasanlu.train_rasanlu({}, {}), True)
-
             result.status = 500
             patched_request.side_effect = None
             patched_request.return_value = asyncio.Future()
             patched_request.return_value.set_result(result)
             self.assertEqual(await rasanlu.train_rasanlu({}, {}), False)
+
+            config = {
+                "name": "rasanlu",
+                "min-score": 0.3,
+                "token": "12345",
+                "train": False,
+            }
+            self.assertEqual(await rasanlu.train_rasanlu(config, {}), False)