Repository: opsdroid/opsdroid

Issue:
Unhandled exception in wit.ai matcher (wrong wit.ai response parsing)
# Description
Opsdroid doesn't understand wit.ai time range response


## Steps to Reproduce
Please also include relevant information and steps to reproduce the bug/issue.
1. Connect with wit.ai app
2. Create new indent
3. Train indent to understand `wit/datetime`
4. Send message like `aws cost since december`
5. Opsdroid will fail to understand wit.ai api response

```
INFO opsdroid.parsers.witai.call_witai(): wit.ai response - {"_text": "aws cost since december", "entities": {"intent": [{"confidence": 0.99986626506986, "value": "aws_cost"}], "datetime": [{"confidence": 0.9995, "type": "interval", "from": {"grain": "month", "value": "2022-12-01T00:00:00.000-08:00"}, "values": [{"type": "interval", "from": {"grain": "month", "value": "2022-12-01T00:00:00.000-08:00"}}, {"type": "interval", "from": {"grain": "month", "value": "2023-12-01T00:00:00.000-08:00"}}, {"type": "interval", "from": {"grain": "month", "value": "2024-12-01T00:00:00.000-08:00"}}]}]}, "WARNING": "DEPRECATED", "msg_id": "0eanvH01TmiwU0Era"}.
ERROR aiohttp.server.log_exception(): Error handling request
Traceback (most recent call last):
  File "C:\Users\jakub\Documents\venvs\chatops\lib\site-packages\aiohttp\web_protocol.py", line 435, in _handle_request
    resp = await request_handler(request)
  File "C:\Users\jakub\Documents\venvs\chatops\lib\site-packages\aiohttp\web_app.py", line 504, in _handle
    resp = await handler(request)
  File "C:\Users\jakub\Documents\venvs\chatops\lib\site-packages\opsdroid\connector\websocket\__init__.py", line 99, in websocket_handler
    await self.opsdroid.parse(message)
  File "C:\Users\jakub\Documents\venvs\chatops\lib\site-packages\opsdroid\core.py", line 627, in parse
    ranked_skills = await self.get_ranked_skills(unconstrained_skills, event)
  File "C:\Users\jakub\Documents\venvs\chatops\lib\site-packages\opsdroid\core.py", line 516, in get_ranked_skills
    ranked_skills += await parse_witai(self, skills, message, witai)
  File "C:\Users\jakub\Documents\venvs\chatops\lib\site-packages\opsdroid\parsers\witai.py", line 72, in parse_witai
    key, entity[0]["value"], entity[0]["confidence"]
KeyError: 'value'
```

## Expected Functionality
There should be no exception


## Experienced Functionality
Thrown exception

## Versions
- **Opsdroid version:** 0.25.0
- **Python version:** 3.9.5 
- **OS/Docker version:** Windows 10

## Configuration File
Please include your version of the configuration file below.

```yaml
# Your code goes here.
parsers:
  - name: witai
    access-token: [REDACTED]
    min-score: 0.6
```

## Additional Details

Raw wit.ai response for `aws cost since december`

```json
{
  "_text": "aws cost since december",
  "entities": {
    "intent": [
      {
        "confidence": 0.99965322126667,
        "value": "aws_cost"
      }
    ],
    "datetime": [
      {
        "confidence": 0.9995,
        "type": "interval",
        "from": {
          "grain": "month",
          "value": "2022-12-01T00:00:00.000-08:00"
        },
        "values": [
          {
            "type": "interval",
            "from": {
              "grain": "month",
              "value": "2022-12-01T00:00:00.000-08:00"
            }
          },
          {
            "type": "interval",
            "from": {
              "grain": "month",
              "value": "2023-12-01T00:00:00.000-08:00"
            }
          },
          {
            "type": "interval",
            "from": {
              "grain": "month",
              "value": "2024-12-01T00:00:00.000-08:00"
            }
          }
        ]
      }
    ]
  },
  "WARNING": "DEPRECATED",
  "msg_id": "051qg0BBGn4O7xZDj"
}

```

Here we can see that wit.ai sends `values` in `datetime[0]` dict. 

Opsdroid expects it to be `value` (without **s**):

https://github.com/opsdroid/opsdroid/blob/c5dad210fe3d9068c75cd4fac9762fcc353335d3/opsdroid/parsers/witai.py#L69-L73

Which is fine for simple response without any matched intents (excluded by `if` in L70):

```json
{
  "_text": "aws",
  "entities": {
    "intent": [
      {
        "confidence": 0.99692494474705,
        "value": "aws_cost"
      }
    ]
  },
  "WARNING": "DEPRECATED",
  "msg_id": "0lbTZJcwDL5RoT2Wi"
}
```

Simple query `aws cost today`. If there is only one field in `values` wit.ai will rewrite it to short version to `values`

```json
{
  "_text": "aws cost today",
  "entities": {
    "intent": [
      {
        "confidence": 0.99965536553564,
        "value": "aws_cost"
      }
    ],
    "datetime": [
      {
        "confidence": 0.9995,
        "type": "value",
        "grain": "day",
        "value": "2022-04-16T00:00:00.000-07:00",
        "values": [
          {
            "type": "value",
            "grain": "day",
            "value": "2022-04-16T00:00:00.000-07:00"
          }
        ]
      }
    ]
  },
  "WARNING": "DEPRECATED",
  "msg_id": "05vACT9WHhDUmAy9u"
}
```




Discussion:

Resolving patch:
diff --git a/opsdroid/parsers/witai.py b/opsdroid/parsers/witai.py
--- a/opsdroid/parsers/witai.py
+++ b/opsdroid/parsers/witai.py
@@ -68,8 +68,16 @@ async def parse_witai(opsdroid, skills, message, config):
                             message.witai = result
                             for key, entity in result["entities"].items():
                                 if key != "intent":
+                                    witai_entity_value = ""
+                                    if "value" in entity[0]:
+                                        witai_entity_value = entity[0]["value"]
+                                    elif "values" in entity[0]:
+                                        # we never know which data are important for user,
+                                        # so we return list with all values
+                                        witai_entity_value = entity[0]["values"]
+
                                     message.update_entity(
-                                        key, entity[0]["value"], entity[0]["confidence"]
+                                        key, witai_entity_value, entity[0]["confidence"]
                                     )
                             matched_skills.append(
                                 {


Test patch:
diff --git a/tests/test_parser_witai.py b/tests/test_parser_witai.py
--- a/tests/test_parser_witai.py
+++ b/tests/test_parser_witai.py
@@ -256,7 +256,7 @@ async def test_parse_witai_raise_ClientOSError(self):
             self.assertFalse(mock_skill.called)
             self.assertTrue(mocked_call.called)
 
-    async def test_parse_witai_entities(self):
+    async def test_parse_witai_entities_single_value(self):
         with OpsDroid() as opsdroid:
             opsdroid.config["parsers"] = [
                 {"name": "witai", "token": "test", "min-score": 0.3}
@@ -343,3 +343,96 @@ async def test_parse_witai_entities(self):
             self.assertEqual(len(skill["message"].entities.keys()), 1)
             self.assertTrue("location" in skill["message"].entities.keys())
             self.assertEqual(skill["message"].entities["location"]["value"], "london")
+
+    async def test_parse_witai_entities_multiple_values(self):
+        with OpsDroid() as opsdroid:
+            opsdroid.config["parsers"] = [
+                {"name": "witai", "token": "test", "min-score": 0.3}
+            ]
+            mock_skill = await self.getMockSkill()
+            opsdroid.skills.append(match_witai("aws_cost")(mock_skill))
+
+            mock_connector = amock.CoroutineMock()
+            message = Message(
+                text="aws cost since december",
+                user="user",
+                target="default",
+                connector=mock_connector,
+            )
+
+            with amock.patch.object(witai, "call_witai") as mocked_call_witai:
+                mocked_call_witai.return_value = {
+                    "_text": "aws cost since december",
+                    "entities": {
+                        "intent": [
+                            {"confidence": 0.99965322126667, "value": "aws_cost"}
+                        ],
+                        "datetime": [
+                            {
+                                "confidence": 0.9995,
+                                "type": "interval",
+                                "from": {
+                                    "grain": "month",
+                                    "value": "2022-12-01T00:00:00.000-08:00",
+                                },
+                                "values": [
+                                    {
+                                        "type": "interval",
+                                        "from": {
+                                            "grain": "month",
+                                            "value": "2022-12-01T00:00:00.000-08:00",
+                                        },
+                                    },
+                                    {
+                                        "type": "interval",
+                                        "from": {
+                                            "grain": "month",
+                                            "value": "2023-12-01T00:00:00.000-08:00",
+                                        },
+                                    },
+                                    {
+                                        "type": "interval",
+                                        "from": {
+                                            "grain": "month",
+                                            "value": "2024-12-01T00:00:00.000-08:00",
+                                        },
+                                    },
+                                ],
+                            }
+                        ],
+                    },
+                    "WARNING": "DEPRECATED",
+                    "msg_id": "051qg0BBGn4O7xZDj",
+                }
+                [skill] = await witai.parse_witai(
+                    opsdroid, opsdroid.skills, message, opsdroid.config["parsers"][0]
+                )
+
+            self.assertEqual(len(skill["message"].entities.keys()), 1)
+            self.assertTrue("datetime" in skill["message"].entities.keys())
+            self.assertEqual(
+                skill["message"].entities["datetime"]["value"],
+                [
+                    {
+                        "type": "interval",
+                        "from": {
+                            "grain": "month",
+                            "value": "2022-12-01T00:00:00.000-08:00",
+                        },
+                    },
+                    {
+                        "type": "interval",
+                        "from": {
+                            "grain": "month",
+                            "value": "2023-12-01T00:00:00.000-08:00",
+                        },
+                    },
+                    {
+                        "type": "interval",
+                        "from": {
+                            "grain": "month",
+                            "value": "2024-12-01T00:00:00.000-08:00",
+                        },
+                    },
+                ],
+            )