Repository: opsdroid/opsdroid

Issue:
Compatibility with Rasa >= 3.0
<!-- 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

When running Opsdroid agains Rasa >= 3.x the model training fails.

## Steps to Reproduce

1. Run the latest Rasa (3.3.3) on some host `docker run --rm -ti -p 5005:5005 --name rasa rasa/rasa:3.3.3-full run --enable-api --auth-token very-secure-rasa-auth-token -vv`
2. Create a skill with the provided configuration files (see configuration section)
3. Run Opsdroid with this skill


## Expected Functionality

Opsdroid will trigger Rasa to train and load the model provided in `intents.yml`.


## Experienced Functionality

The model training is triggered and the model is loaded but Opsdroid terminates with the stacktrace:

```
DEBUG opsdroid.parsers.rasanlu Checking Rasa NLU version.
DEBUG opsdroid.parsers.rasanlu Rasa NLU response - {"version": "3.3.3", "minimum_compatible_version": "3.0.0"}.
DEBUG opsdroid.parsers.rasanlu Rasa NLU version 3.3.3.
INFO opsdroid.parsers.rasanlu Starting Rasa NLU training.
INFO opsdroid.parsers.rasanlu Now training the model. This may take a while...
INFO opsdroid.parsers.rasanlu Rasa NLU training completed in 9 seconds.
DEBUG asyncio Using selector: EpollSelector
Traceback (most recent call last):
  File "/usr/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/__main__.py", line 12, in <module>
    init()
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/__main__.py", line 9, in init
    opsdroid.cli.cli()
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/venv/lib/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/venv/lib/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/venv/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/venv/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/venv/lib/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/cli/start.py", line 41, in start
    opsdroid.run()
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/core.py", line 170, in run
    self.sync_load()
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/core.py", line 219, in sync_load
    self.eventloop.run_until_complete(self.load())
  File "/usr/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/core.py", line 232, in load
    await self.train_parsers(self.modules["skills"])
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/core.py", line 365, in train_parsers
    await train_rasanlu(rasanlu, skills)
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/parsers/rasanlu.py", line 221, in train_rasanlu
    await _load_model(config)
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/opsdroid/parsers/rasanlu.py", line 137, in _load_model
    result = await resp.json()
  File "/home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/venv/lib/python3.8/site-packages/aiohttp/client_reqrep.py", line 1104, in json
    raise ContentTypeError(
aiohttp.client_exceptions.ContentTypeError: 0, message='Attempt to decode JSON with unexpected mimetype: ', url=URL('http://rasa-host:5005/model?token=very-secure-rasa-auth-token')
```

## Versions
- **Opsdroid version: 73da764ca72581a506f05117ec9690ca34b15400**
- **Python version: 3.8.5**
- **OS/Docker version: Linux Docker 20.10.6 (for Rasa)**

## Configuration File

`configuration.yaml`:
```:YAML
welcome-message: false
logging:
  level: debug
connectors:
  websocket:
parsers:
  rasanlu:
    url: http://rasa-host:5005
    token: very-secure-rasa-auth-token
skills:
  rasa-test:
    path: /home/user/PyCharm/opsdroid_rasa_3.3.3_compatibility/rasa_test_skill
```

`__init__.py`:
```:python
from opsdroid.skill import Skill
from opsdroid.matchers import match_rasanlu

class MySkill(Skill):
    @match_rasanlu('greet')
    async def hello(self, message):
        await message.respond("Hello there!")

    @match_rasanlu('bye')
    async def bye(self, message):
        await message.respond("See ya!")
```

`intents.yml`:
```:YAML
version: "3.1"

intents:
  - greet
  - bye

nlu:
- intent: greet
  examples: |
    - Hey
    - Hi
    - hey there
    - hello
- intent: bye
  examples: |
    - googbye
    - bye
    - ciao
    - see you
```




Discussion:
Seem that Rasa 3.X doesn't return any content (and we are expecting it) when loading a model. Just HTTP code 204.
https://rasa.com/docs/rasa/pages/http-api#operation/replaceModel

Resolving patch:
diff --git a/opsdroid/parsers/rasanlu.py b/opsdroid/parsers/rasanlu.py
--- a/opsdroid/parsers/rasanlu.py
+++ b/opsdroid/parsers/rasanlu.py
@@ -134,7 +134,10 @@ async def _load_model(config):
             _LOGGER.error(_("Unable to connect to Rasa NLU."))
             return None
         if resp.status == 204:
-            result = await resp.json()
+            try:
+                result = await resp.json()
+            except aiohttp.client_exceptions.ContentTypeError:
+                return {}
         else:
             result = await resp.text()
             _LOGGER.error(_("Bad Rasa NLU response - %s."), result)
@@ -155,7 +158,7 @@ async def _is_model_loaded(config):
             return None
         if resp.status == 200:
             result = await resp.json()
-            if result["model_file"].find(config["model_filename"]):
+            if config["model_filename"] in result["model_file"]:
                 return True
         return False
 


Test patch:
diff --git a/tests/test_parser_rasanlu.py b/tests/test_parser_rasanlu.py
--- a/tests/test_parser_rasanlu.py
+++ b/tests/test_parser_rasanlu.py
@@ -520,12 +520,11 @@ async def test_has_compatible_version_rasanlu(self):
             self.assertEqual(await rasanlu.has_compatible_version_rasanlu({}), True)
 
     async def test__load_model(self):
-        result = amock.Mock()
-        result.status = 204
-        result.text = amock.CoroutineMock()
-        result.json = amock.CoroutineMock()
-
         with amock.patch("aiohttp.ClientSession.put") as patched_request:
+            result = amock.Mock()
+            result.status = 204
+            result.text = amock.CoroutineMock()
+            result.json = amock.CoroutineMock()
             patched_request.side_effect = None
             result.json.return_value = {}
             patched_request.return_value = asyncio.Future()
@@ -554,6 +553,24 @@ async def test__load_model(self):
                 await rasanlu._load_model({"model_filename": "model.tar.gz"}), None
             )
 
+        with amock.patch("aiohttp.ClientSession.put") as patched_request:
+            result = amock.Mock()
+            result.status = 204
+            result.content = aiohttp.streams.EmptyStreamReader()
+            result.reason = "No Content"
+            result.text = amock.CoroutineMock(
+                side_effect=aiohttp.ContentTypeError(None, None)
+            )
+            result.json = amock.CoroutineMock(
+                side_effect=aiohttp.ContentTypeError(None, None)
+            )
+            patched_request.return_value = asyncio.Future()
+            patched_request.return_value.set_result(result)
+            patched_request.side_effect = None
+            self.assertEqual(
+                await rasanlu._load_model({"model_filename": "model.tar.gz"}), {}
+            )
+
     async def test__is_model_loaded(self):
         result = amock.Mock()
         result.status = 200