Repository: opsdroid/opsdroid

Issue:
Fixing rasanlu parser : dict object has no attribute matchers issue #860
# Description

When using rasanlu parser with no intents file (no need to train the module, already done) an attribute issue error happened in function _get_all_intents(skills)

...
    matchers = [matcher for skill in skills for matcher in skill.matchers]
  File "/Users/IOBreaker/Developments/Bots/opsdroid/opsdroid/parsers/rasanlu.py", line 19, in <listcomp>
    matchers = [matcher for skill in skills for matcher in skill.matchers]
AttributeError: 'dict' object has no attribute 'matchers'

Fixes #860

After discussion with @jacobtomlinson the decision  was to go using intents provided directly by the skill dict.

Beside, an other correction was added to this fixes according to [rasa_nlu](https://github.com/RasaHQ/rasa_nlu/blob/master/docs/http.rst#post-train) documentation

```
The request should always be sent as application/x-yml regardless of wether you use json or md for the data format. Do not send json as application/json for example.
```

## Status
**READY** | **~UNDER DEVELOPMENT~** | **~ON HOLD~**


## Type of change

- Bug fix (non-breaking change which fixes an issue)

# How Has This Been Tested?

I tested the fix on my environment, no issue detected

# Checklist:

- [x] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation (if applicable)
- [x] I have added tests that prove my fix is effective or that my feature works (test file modification)
- [x] New and existing unit tests pass locally with my changes
- [x] Tox ok

rasanlu parser does not manage the new returned response after training request
# Description

This issue was initially detected by @jhofeditz when testing the fix #881.
The new rasanlu trainer instead of returning a json response after a model training, it return a zip file with all files generated by the trainer.

## Steps to Reproduce
1- Download docker image rasa/rasa_nlu:latest-spacy
2- Start docker image
3- Use a skill that interact with rasanlu and that need rasa to be trained using an intents.yml file
4- activate the skill in your opsdroid config file
5- Start opsdroid 

## Expected Functionality
Opsdroid should be capable to manage json response and zip response

## Experienced Functionality

Crash of opsdroid due to parsing error



## Versions
- **Opsdroid version: v0.14.1+37.ge71ea43**
- **Rasanlu version: 0.15.0a1**
- **Python version: 3.7.2**
- **OS/Docker version: Docker version 18.09.2, build 6247962 **

## Additional Details

**From Rasa side :**
```
2019-03-30 11:37:34 INFO     rasa_nlu.data_router  - Logging of requests is disabled. (No 'request_log' directory configured)
2019-03-30 11:37:34 INFO     __main__  - Started http server on port 5000
2019-03-30 11:37:34+0000 [-] Log opened.
2019-03-30 11:37:34+0000 [-] Site starting on 5000
2019-03-30 11:37:34+0000 [-] Starting factory <twisted.web.server.Site object at 0x7f65ffbff470>
2019-03-30 11:50:43+0000 [-] 2019-03-30 11:50:43 DEBUG    rasa_nlu.data_router  - New training queued
Fitting 2 folds for each of 6 candidates, totalling 12 fits
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done  12 out of  12 | elapsed:    0.0s finished
2019-03-30 11:50:59+0000 [-] "172.17.0.1" - - [30/Mar/2019:11:50:58 +0000] "POST /train?project=ergo&fixed_model_name=1c0badaf3eb8c2bf6546465eadfd8492e7d79c1f0f3520d103d4bc39b4bfc&token=sfgz654zfg546qs4fg646rg64efg HTTP/1.1" 200 11767 "-" "Python/3.7 aiohttp/3.5.4"
2019-03-30 11:53:24+0000 [-] "172.17.0.1" - - [30/Mar/2019:11:53:23 +0000] "GET / HTTP/1.1" 200 29 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0"
2019-03-30 11:53:24+0000 [-] "172.17.0.1" - - [30/Mar/2019:11:53:23 +0000] "GET /favicon.ico HTTP/1.1" 404 233 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0"
2019-03-30 11:54:24+0000 [-] Timing out client: IPv4Address(type='TCP', host='172.17.0.1', port=59948)
```


**From Opsdroid side :**
```
INFO opsdroid.parsers.rasanlu: Starting Rasa NLU training.
INFO opsdroid.parsers.rasanlu: Now training the model. This may take a while...
DEBUG asyncio: Using selector: KqueueSelector
Traceback (most recent call last):
...
  File "/Users/hicham/Developments/Bots/opsdroid-iobreaker/opsdroid/opsdroid/parsers/rasanlu.py", line 117, in train_rasanlu
    result = await resp.json()
  File "/Users/hicham/.virtualenvs/opsdroid/lib/python3.7/site-packages/aiohttp/client_reqrep.py", line 1027, in json
    headers=self.headers)
aiohttp.client_exceptions.ContentTypeError: 0, message='Attempt to decode JSON with unexpected mimetype: application/zip'
```



Discussion:
# [Codecov](https://codecov.io/gh/opsdroid/opsdroid/pull/881?src=pr&el=h1) Report
> Merging [#881](https://codecov.io/gh/opsdroid/opsdroid/pull/881?src=pr&el=desc) into [master](https://codecov.io/gh/opsdroid/opsdroid/commit/de8d5ec638f8f32b5b130f2862f01894a17e9ee1?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/opsdroid/opsdroid/pull/881/graphs/tree.svg?width=650&token=9qJfZL4bOO&height=150&src=pr)](https://codecov.io/gh/opsdroid/opsdroid/pull/881?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #881   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          34     34           
  Lines        2200   2200           
=====================================
  Hits         2200   2200
```


| [Impacted Files](https://codecov.io/gh/opsdroid/opsdroid/pull/881?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [opsdroid/parsers/rasanlu.py](https://codecov.io/gh/opsdroid/opsdroid/pull/881/diff?src=pr&el=tree#diff-b3BzZHJvaWQvcGFyc2Vycy9yYXNhbmx1LnB5) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/loader.py](https://codecov.io/gh/opsdroid/opsdroid/pull/881/diff?src=pr&el=tree#diff-b3BzZHJvaWQvbG9hZGVyLnB5) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/opsdroid/opsdroid/pull/881?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/opsdroid/opsdroid/pull/881?src=pr&el=footer). Last update [de8d5ec...a0694e1](https://codecov.io/gh/opsdroid/opsdroid/pull/881?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

I did some other tests about Content Type and bellow what i've got as results :

### Test 1

- Content-Type : Application/x-yml
- file type : intents.md
- File Content :

```yaml
## intent:user_say_iam_back
i am back
i am back again
back again
```

- Result

```json
{
  "error": "Content-Type must be 'application/x-yml' or 'application/json'"
}
```


### Test 2

- Content-Type : Application/json
- file type : intents.json
- File Content :


```json
{
	"rasa_nlu_data": {
		"regex_features": [

		],
		"entity_synonyms": [

		],
		"common_examples": [
			{
				"text": "i am back",
				"intent": "user_is_back",
				"entities": [

				]
			},
			{
				"text": "i am back again",
				"intent": "user_is_back",
				"entities": [

				]
			},
			{
				"text": "back again",
				"intent": "user_is_back",
				"entities": [

				]
			}
		]
	}
}
```

- Result

```json
{
  "info": "new model trained",
  "model": "ergo"
}

```

### Test 3

The same config as Test 2, the only difference here is using

- Content-Type : Application/x-yml


- Result

```json
{
  "error": "argument of type 'NoneType' is not iterable"
}
```


I'd like to have your feedback about that please

Regards

I founded why we have this behaviours. As you can check  [here](https://github.com/RasaHQ/rasa_nlu/blob/master/sample_configs/config_train_server_md.yml) in RasaHQ github docs, the file format has changed.

So i changed the intents.md file format and the training was successful


- Content-Type : Application/x-yml
- File type : intents.md
- File Content :

```yaml
language: "en"
pipeline: "spacy_sklearn"
# data contains the same md, as described in the training data section
data: |
  ## intent:user_say_iam_back
  - i am back
  - i am back again
  - back again
```

- Opsdroid side

```
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 0 seconds.
INFO opsdroid.parsers.rasanlu: Initialising Rasa NLU model.
DEBUG opsdroid.parsers.rasanlu: Rasa NLU response - {"intent": {"name": null, "confidence": 0.0}, "entities": [], "text": "", "project": "ergo", "model": "model_20190320-144251"}
INFO opsdroid.parsers.rasanlu: Initialisation complete in 1 seconds.
DEBUG opsdroid.connector.websocket: Starting Websocket connector
DEBUG opsdroid.connector.telegram: Loaded telegram connector
```

- Rasanlu server side result 

```
2019-03-20 14:42:50+0100 [-] "127.0.0.1" - - [20/Mar/2019:13:42:50 +0000] "GET /status HTTP/1.1" 200 459 "-" "Python/3.7 aiohttp/3.5.4"
2019-03-20 14:42:50+0100 [-] 2019-03-20 14:42:50 DEBUG    rasa_nlu.data_router  - New training queued
2019-03-20 14:42:51+0100 [-] "127.0.0.1" - - [20/Mar/2019:13:42:51 +0000] "POST /train?project=ergo&fixed_model_name=326346356235623562356fdgsdfbgdf324562362356&token=ed6ahthogeexoonahL7gof1maGh1EiSDF5Hi8sd HTTP/1.1" 200 69 "-" "Python/3.7 aiohttp/3.5.4"
2019-03-20 14:42:52+0100 [-] 2019-03-20 14:42:52 INFO     rasa_nlu.components  - Added 'nlp_spacy' to component cache. Key 'nlp_spacy-en'.
2019-03-20 14:42:52+0100 [-] "127.0.0.1" - - [20/Mar/2019:13:42:52 +0000] "POST /parse HTTP/1.1" 200 150 "-" "Python/3.7 aiohttp/3.5.4"
```

- Response :

```
{
  "info": "new model trained",
  "model": "ergo"
}
```

Please can you check from your side and confirm

Next step if it's confirmed is to update the rasanlu.md documentation (about intents.md provided example).

```yaml
## intent:greetings
- Hey
- Hello
- Hi
- Hiya
- hey
- whats up
- wazzup
- heya
```

A nice to have future will be json support ;-)

Regards
This looks great. It's a shame that rasa have broken the format upstream, but I feel like their move to yaml/json is probably a good one. 

We should probably modify the code to stop looking for `intents.md` and instead look for `intents.yaml`, `intents.yml` or `intents.json`. This would mean we can support all the formats above.

Would you be happy to make this change here?
It would be great to support json and yaml intent files.

I am going to change the **def _load_intents(config)** in **loader.py**  function to load **intents.yml** instead of **intents.md** and modify the corresponding docs to indicate the new why to implement the intent file. This will allow us to close the ongoing issue.

When it's done, why not working on json, yml, yaml support :-)
I changed as discussed the code to manage intents.yml file in loader and tests files.
I updated the doc to reflct the new intents.yml file format.
can you please check 

```
SKIPPED:  py35: InterpreterNotFound: python3.5
  py36: commands succeeded
  py37: commands succeeded
  lint: commands succeeded
  docker: commands succeeded
  congratulations :)
```
@iobreaker I am also testing rasa with opsdroid and cloned your repo to test. I just installed the latest Rasa NLU and the API /train endpoint is returning zip content 
https://github.com/RasaHQ/rasa_nlu/blob/master/rasa/nlu/server.py#L277
This makes training fail:
> aiohttp.client_exceptions.ContentTypeError: 0, message='Attempt to decode JSON with unexpected mimetype: application/zip'

I only picked up opsdroid today and rasa yesterday, so I might be missing something...
Hi @jhofeditz,

can you please explain exactly how are you using opsdroid with rasanlu
- The content of your intents.yml
- The cli you are using to start rasa server
- The debug log from opsdroid


From my side i can train my model with no problem, bellow an example :

**From rasanlu side** 
```
2019-03-27 23:36:43+0100 [-] Starting factory <twisted.web.server.Site object at 0x1310bfa90>
2019-03-27 23:39:14+0100 [-] "127.0.0.1" - - [27/Mar/2019:22:39:13 +0000] "GET /status HTTP/1.1" 200 296 "-" "Python/3.7 aiohttp/3.5.4"
2019-03-27 23:39:14+0100 [-] 2019-03-27 23:39:14 DEBUG    rasa_nlu.data_router  - New training queued
2019-03-27 23:39:18+0100 [-] "127.0.0.1" - - [27/Mar/2019:22:39:18 +0000] "POST /train?project=ergo&fixed_model_name=5feade1d4feffab3add74c192b2576aea2c76ae4643f6b58467cb69997177616&token=zgazrg345efgez45rtherthtrh454 HTTP/1.1" 200 69 "-" "Python/3.7 aiohttp/3.5.4"
2019-03-27 23:39:18+0100 [-] 2019-03-27 23:39:18 WARNING  rasa_nlu.project  - Invalid model requested. Using default
2019-03-27 23:39:19+0100 [-] 2019-03-27 23:39:19 INFO     rasa_nlu.components  - Added 'nlp_spacy' to component cache. Key 'nlp_spacy-en'.
2019-03-27 23:39:20+0100 [-] "127.0.0.1" - - [27/Mar/2019:22:39:19 +0000] "POST /parse HTTP/1.1" 200 150 "-" "Python/3.7 aiohttp/3.5.4"
```


**From opsdroid side**
```
DEBUG opsdroid.core: Loaded 2 skills
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 4 seconds.
INFO opsdroid.parsers.rasanlu: Initialising Rasa NLU model.
DEBUG opsdroid.parsers.rasanlu: Rasa NLU response - {"intent": {"name": null, "confidence": 0.0}, "entities": [], "text": "", "project": "ergo", "model": "model_20190327-233918"}
INFO opsdroid.parsers.rasanlu: Initialisation complete in 1 seconds.
DEBUG opsdroid.connector.websocket: Starting Websocket connector
DEBUG opsdroid.connector.telegram: Loaded telegram connector
DEBUG opsdroid.connector.telegram: Connecting to telegram
INFO opsdroid.core: Opsdroid is now running, press ctrl+c to exit.
INFO opsdroid.web: Started web server on http://127.0.0.1:8080
```

**Test sending yes from telegram to opsdroid after training**

```
DEBUG opsdroid.core: Parsing input: yes
DEBUG opsdroid.core: Processing parsers...
DEBUG opsdroid.core: Checking Rasa NLU...
DEBUG opsdroid.parsers.rasanlu: Rasa NLU response - {"intent": {"name": "affirm", "confidence": 0.8618867670286532}, "entities": [], "intent_ranking": [{"name": "affirm", "confidence": 0.8618867670286532}, ], "text": "yes", "project": "ergo", "model": "model_20190327-233918"}
```

Thx


ah, I was using the `rasa/rasa_nlu:latest-spacy` docker image. When I switch to `0.14.4-spacy` (or `pip install rasa-nlu`) I get the expected yml output. 

So, it's either a breaking change in 0.15 or a bug.

Thank you for working on this issue @iobreaker, I am a bit worried about the breaking change of the future versions of rasa nlu, but this is something we will have to tackle when they release the next version.

Jacob seems to be happy with the PR as well so I am going to merge it now to master 😄 👍 
@jhofeditz can you please log an issue and describe exactly how to reproduce it (env, cli, config ...)

@FabioRosado because opsdroid is an opensource project with no commitments about releases (best efforts) i think a best practices will be :

- Indicate in the documentation which versions opsdroid support for each external module or parser ...
- For each code that need to address an external resources, a check of supported version should be done by the developer to avoid crashes or a bad behaviors.
 
I started working on a fix that detect if the returned data is a json one or a zip one.
Now opsdroid know how to deal with it

### Example :

**From Opsdroid Side:**
```
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 16 seconds.
INFO opsdroid.parsers.rasanlu: Initialising Rasa NLU model.
DEBUG opsdroid.parsers.rasanlu: Rasa NLU response - {"intent": {"name": null, "confidence": 0.0}, "entities": [], "text": "", "project": "ergo", "model": "model_20190330-121825"}
INFO opsdroid.parsers.rasanlu: Initialisation complete in 15 seconds.
DEBUG opsdroid.parsers.rasanlu: =====> Model file saved to : /Users/hicham/Desktop/model.zip
```


**From Rasanlu side :**
```
2019-03-30 12:18:09+0000 [-] 2019-03-30 12:18:09 DEBUG    rasa_nlu.data_router  - New training queued
Fitting 2 folds for each of 6 candidates, totalling 12 fits
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done  12 out of  12 | elapsed:    0.0s finished
2019-03-30 12:18:26+0000 [-] "172.17.0.1" - - [30/Mar/2019:12:18:26 +0000] "POST /train?project=ergo&fixed_model_name=1c0badaf3eb8c2bfc535cf8553eadfd8492e7d79c1f0f3520d103d4bc39b4bfc&token=fg31d3f5g4e3b13fds21bh3eth13eg1he5zt HTTP/1.1" 200 11766 "-" "Python/3.7 aiohttp/3.5.4"
```


**Testing the new model (intent:affirm) :**
```
DEBUG opsdroid.connector.telegram: {'update_id': 246644773, 'message': {'message_id': 656, 'from': {'id': 65465465, 'is_bot': False, 'first_name': 'IOBreaker', 'language_code': 'en'}, 'chat': {'id': 452282388, 'first_name': 'IOBreaker', 'type': 'private'}, 'date': 1553948755, 'text': 'yes'}}
DEBUG opsdroid.core: Parsing input: yes
DEBUG opsdroid.core: Processing parsers...
DEBUG opsdroid.core: Checking Rasa NLU...
DEBUG opsdroid.parsers.rasanlu: Rasa NLU response - {"intent": {"name": "affirm", "confidence": 0.8215188398116678}, "entities": [], "intent_ranking": [{"name": "affirm", "confidence": 0.8215188398116678}, {"name": "user_say_iam_back", "confidence": 0.09246078336795119}, {"name": "goodbye", "confidence": 0.08602037682038108}], "text": "yes", "project": "ergo", "model": "model_20190330-121825"}
```


Now still 2 things to address :

- Finding why rasa keep training models even if it is the same as before (already trained)
- What to do with the returned zip
  + Just ignore it
  + Add entry in opsdroid config file to allow user to save it in a location  or ignore it
  + Extract files and do things 

Do not hesitate if you have any ideas :-)

 
really, do not hesitate to comment :-)
ok no answer :-(
So I am going to ignore the returned zip for the moment and deal with that in new version if needed

Sorry! That sounds good.
Ok Thanks :-)
Will apply the changes and submit a PR

Resolving patch:
diff --git a/opsdroid/parsers/rasanlu.py b/opsdroid/parsers/rasanlu.py
--- a/opsdroid/parsers/rasanlu.py
+++ b/opsdroid/parsers/rasanlu.py
@@ -114,17 +114,41 @@ async def train_rasanlu(config, skills):
             return False
 
         if resp.status == 200:
-            result = await resp.json()
-            if "info" in result and "new model trained" in result["info"]:
+            if resp.content_type == "application/json":
+                result = await resp.json()
+                if "info" in result and "new model trained" in result["info"]:
+                    time_taken = (arrow.now() - training_start).total_seconds()
+                    _LOGGER.info(
+                        _("Rasa NLU training completed in %s seconds."), int(time_taken)
+                    )
+                    await _init_model(config)
+                    return True
+
+                _LOGGER.debug(result)
+            if (
+                resp.content_type == "application/zip"
+                and resp.content_disposition.type == "attachment"
+            ):
                 time_taken = (arrow.now() - training_start).total_seconds()
                 _LOGGER.info(
                     _("Rasa NLU training completed in %s seconds."), int(time_taken)
                 )
                 await _init_model(config)
+                """
+                As inditated in the issue #886, returned zip file is ignored, this can be changed
+                This can be changed in future release if needed
+                Saving model.zip file example :
+                try:
+                    output_file = open("/target/directory/model.zip","wb")
+                    data = await resp.read()
+                    output_file.write(data)
+                    output_file.close()
+                    _LOGGER.debug("Rasa taining model file saved to /target/directory/model.zip")
+                except:
+                    _LOGGER.error("Cannot save rasa taining model file to /target/directory/model.zip")
+                """
                 return True
 
-            _LOGGER.debug(result)
-
         _LOGGER.error(_("Bad Rasa NLU response - %s"), await resp.text())
         _LOGGER.error(_("Rasa NLU training failed."))
         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
@@ -419,7 +419,7 @@ async def test__get_existing_models_fails(self):
             models = await rasanlu._get_existing_models({})
             self.assertEqual(models, [])
 
-    async def test_train_rasanlu(self):
+    async def test_train_rasanlu_fails(self):
         result = amock.Mock()
         result.status = 404
         result.text = amock.CoroutineMock()
@@ -462,12 +462,55 @@ async def test_train_rasanlu(self):
             patched_request.return_value.set_result(result)
             self.assertEqual(await rasanlu.train_rasanlu({}, {}), False)
 
-            result.status = 200
+            result.json.return_value = {"info": "error"}
+            patched_request.return_value = asyncio.Future()
+            patched_request.side_effect = None
+            patched_request.return_value.set_result(result)
+            self.assertEqual(await rasanlu.train_rasanlu({}, {}), False)
+
+    async def test_train_rasanlu_succeeded(self):
+        result = amock.Mock()
+        result.text = amock.CoroutineMock()
+        result.json = amock.CoroutineMock()
+        result.status = 200
+        result.json.return_value = {"info": "new model trained: abc1234"}
+
+        with amock.patch(
+            "aiohttp.ClientSession.post"
+        ) as patched_request, amock.patch.object(
+            rasanlu, "_get_all_intents"
+        ) as mock_gai, amock.patch.object(
+            rasanlu, "_init_model"
+        ) as mock_im, amock.patch.object(
+            rasanlu, "_build_training_url"
+        ) as mock_btu, amock.patch.object(
+            rasanlu, "_get_existing_models"
+        ) as mock_gem, amock.patch.object(
+            rasanlu, "_get_intents_fingerprint"
+        ) as mock_gif:
+
+            mock_gem.return_value = ["abc123"]
+            mock_btu.return_value = "http://example.com"
+            mock_gai.return_value = "Hello World"
+            mock_gif.return_value = "abc1234"
+            result.content_type = "application/json"
+
+            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.json.return_value = {"info": "error"}
+            result.json.return_value = {}
+            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)
+
+            result.content_type = "application/zip"
+            result.content_disposition.type = "attachment"
+            result.json.return_value = {"info": "new model trained: abc1234"}
+
+            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)