Repository: opsdroid/opsdroid (language: python)

Issue:
Extracting multiple entities with same name from user message.
<!-- 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
Hi, I am trying to build chatbot with opsdroid, matrix and rasanlu. When I try to extract entities from user message, if message contains multiple entities with same name we are able to extract single entity with highest confidence. 

For example: 
user_message: I want to travel from Berlin to  San Francisco.

## Expected Functionality
Desired output: extract both entities i.e. {'Berlin' & 'San Francisco'}

The RASA NLU is able to extract all entities correctly, but opsdroid returns single entity only.
Rasa nlu response: 
`{"text": "i want to travel from Berlin to San Fransisco", "intent": {"name": "knowledge", "confidence": 0.9999788999557495}, "entities": [{"entity": "city", "start": 22, "end": 28, "confidence_entity": 0.9633104801177979, "role": "departure", "confidence_role": 0.9610307812690735, "value": "Berlin", "extractor": "DIETClassifier"}, {"entity": "city", "start": 32, "end": 45, "confidence_entity": 0.7566294074058533, "role": "destination", "confidence_role": 0.8198645114898682, "value": "San Fransisco", "extractor": "DIETClassifier"}], "text_tokens": [[0, 1], [2, 6], [7, 9], [10, 16], [17, 21], [22, 28], [29, 31], [32, 35], [36, 45]] }}`



## Experienced Functionality
on `message.entities`  am able extract only single value for entity city.
`{"city":{"value":"San Francisco", "confidence": 0.9977127313613892}}`

## Versions
- **Opsdroid version: v0.28.0**
- **Python version:3.9.15**
- **OS/Docker version: 20.10.21**

## Configuration File
Please include your version of the configuration file below.

```yaml
welcome-message: false

logging:
  level: debug
  path: false
  rich: false # to get rich formated logs but does not work in the docker container
  console: true #true - remove all colours and rich styles
  timestamp: true # to keep or remove timestamp in the logs
  extended: true # to enable/disable log location


connectors:
  test-bot:
    repo: $TEST_REPO_URL
    homeserver: $TEST_HOMESERVER_URL
    mxid: $MXID
    access_token: $ACCESS_TOKEN
    rooms:
      main: $ROOM_MAIN
    bot_name: $BOT_NAME

parsers:
  regex:
    enabled: true
  rasanlu:
    url: $RASA_URL
    token: $RASA_TOKEN 
    min-score: 0.6
    models-path: /rasa_model/path
    model_filename: $RASA_MODEL_NAME
    train: False

skills:
  common:
    path: /path/to/skills/__init__.py

```

## Additional Details
intents.yml
```
version: "3.1"

intents:
  - greetings
  - bye
  - help
  - travel

entities:
  - city:
    roles:
    - departure
    - destination

- intent: travel
  examples: |
    - I want to fly from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
    - I want to fly from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
    - I want to fly from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
    - I want to fly from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
    - I want to fly from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
```

<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->



Discussion / hints:
I checked the rasanlu matcher and found the issue.

That's what happening:
* The entity extraction in rasanlu matcher is happening [here](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/parsers/rasanlu.py#L293)
* which calls the [entity update function](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/events.py#L171)
* which overwrites the key `city` in `message.entities`

This is common to Opsdroid in general, that`message.entities` is a dictionary. A dictionary cannot contain duplicate entries.

Any ideas how to fix this without breaking every skill depending on it?

Fix patch:
diff --git a/docs/skills/matchers/rasanlu.md b/docs/skills/matchers/rasanlu.md
index d13a2f79e..75cdf045b 100644
--- a/docs/skills/matchers/rasanlu.md
+++ b/docs/skills/matchers/rasanlu.md
@@ -171,3 +171,84 @@ The example skill will print the following .
   }
 }
 ```
+
+### Using entities with [roles](https://learning.rasa.com/conversational-ai-with-rasa/entities/#roles-and-groups) (Rasa 3.X)
+
+```eval_rst
+.. warning::
+   When using Rasa roles the entity name is used multiple times (for departure and for destination).
+   To be able to provided it via `message.entities` Opsdroid rasanlu matcher will create a new name `city` with both roles `departure` and `destination` appended (with `_`).
+
+   So the entities will be stored in `message.entities` with keys `city_departure` and `city_destination`.
+```
+
+Intents file (`intents.yml`).
+```YAML
+version: "3.1"
+
+intents:
+  - greetings
+  - bye
+  - travel
+
+entities:
+  - city:
+    roles:
+    - departure
+    - destination
+
+nlu:
+  - intent: greetings
+    examples: |
+      - Hey
+      - Hi
+      - hey there
+      - hello
+  - intent: bye
+    examples: |
+      - googbye
+      - bye
+      - ciao
+      - see you
+  - intent: travel
+    examples: |
+      - I want to fly from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
+      - I want to go from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
+      - I want to travel from [Berlin]{"entity": "city","role":"departure"} to [San Francisco]{"entity": "city","role":"destination"}
+```
+
+Skill file (`__init__.py`).
+```python
+from opsdroid.skill import Skill
+from opsdroid.matchers import match_rasanlu
+
+class MySkill(Skill):
+    @match_rasanlu('travel')
+    async def hello(self, message):
+        if "city_departure" in message.entities and "city_destination" in message.entities:
+            source = message.entities['city_departure']['value']
+            destination = message.entities['city_destination']['value']
+            await message.respond("👌 Searching for routes from {} to {}".format(source, destination))
+        else:
+            await message.respond("😕 I couldn't understand you. Maybe try to rephrase your query.")
+```
+
+This skill will result the following structure in `message.entities`:
+
+* Input: `I want to travel from Berlin to San Francisco`
+* `message.entities`:
+
+```JSON
+{
+  "city_departure":
+  {
+    "value": "Berlin",
+    "confidence": 0.9992508292198181
+  },
+  "city_destination":
+  {
+    "value": "San Francisco",
+    "confidence": 0.9521082639694214
+  }
+}
+```
\ No newline at end of file
diff --git a/opsdroid/parsers/rasanlu.py b/opsdroid/parsers/rasanlu.py
index b8784f2f4..6fb6b86b9 100644
--- a/opsdroid/parsers/rasanlu.py
+++ b/opsdroid/parsers/rasanlu.py
@@ -292,15 +292,18 @@ async def parse_rasanlu(opsdroid, skills, message, config):
                     if matcher["rasanlu_intent"] == result["intent"]["name"]:
                         message.rasanlu = result
                         for entity in result["entities"]:
+                            entity_name = entity["entity"]
+                            if "role" in entity:
+                                entity_name = entity["entity"] + "_" + entity["role"]
                             if "confidence_entity" in entity:
                                 message.update_entity(
-                                    entity["entity"],
+                                    entity_name,
                                     entity["value"],
                                     entity["confidence_entity"],
                                 )
                             elif "extractor" in entity:
                                 message.update_entity(
-                                    entity["entity"],
+                                    entity_name,
                                     entity["value"],
                                     entity["extractor"],
                                 )


Test patch:
diff --git a/tests/test_parser_rasanlu.py b/tests/test_parser_rasanlu.py
index d60292ad5..32ddd0190 100644
--- a/tests/test_parser_rasanlu.py
+++ b/tests/test_parser_rasanlu.py
@@ -214,6 +214,74 @@ async def test_parse_rasanlu_entities(self):
                     skill["message"].entities["cuisine"]["value"], "chinese"
                 )
 
+    async def test_parse_rasanlu_multiple_entities_with_same_name(self):
+        with OpsDroid() as opsdroid:
+            opsdroid.config["parsers"] = [
+                {"name": "rasanlu", "token": "test", "min-score": 0.3}
+            ]
+            mock_skill = await self.getMockSkill()
+            opsdroid.skills.append(match_rasanlu("knowledge")(mock_skill))
+
+        mock_connector = amock.CoroutineMock()
+        message = Message(
+            text="i want to travel from Berlin to San Fransisco",
+            user="user",
+            target="default",
+            connector=mock_connector,
+        )
+        with amock.patch.object(rasanlu, "call_rasanlu") as mocked_call_rasanlu:
+            mocked_call_rasanlu.return_value = {
+                "text": "i want to travel from Berlin to San Fransisco",
+                "intent": {"name": "knowledge", "confidence": 0.9999788999557495},
+                "entities": [
+                    {
+                        "entity": "city",
+                        "start": 22,
+                        "end": 28,
+                        "confidence_entity": 0.9633104801177979,
+                        "role": "departure",
+                        "confidence_role": 0.9610307812690735,
+                        "value": "Berlin",
+                        "extractor": "DIETClassifier",
+                    },
+                    {
+                        "entity": "city",
+                        "start": 32,
+                        "end": 45,
+                        "confidence_entity": 0.7566294074058533,
+                        "role": "destination",
+                        "confidence_role": 0.8198645114898682,
+                        "value": "San Fransisco",
+                        "extractor": "DIETClassifier",
+                    },
+                ],
+                "text_tokens": [
+                    [0, 1],
+                    [2, 6],
+                    [7, 9],
+                    [10, 16],
+                    [17, 21],
+                    [22, 28],
+                    [29, 31],
+                    [32, 35],
+                    [36, 45],
+                ],
+            }
+
+            [skill] = await rasanlu.parse_rasanlu(
+                opsdroid, opsdroid.skills, message, opsdroid.config["parsers"][0]
+            )
+
+            self.assertEqual(len(skill["message"].entities.keys()), 2)
+            self.assertTrue("city_departure" in skill["message"].entities.keys())
+            self.assertTrue("city_destination" in skill["message"].entities.keys())
+            self.assertEqual(
+                skill["message"].entities["city_departure"]["value"], "Berlin"
+            )
+            self.assertEqual(
+                skill["message"].entities["city_destination"]["value"], "San Fransisco"
+            )
+
     async def test_parse_rasanlu_raises(self):
         with OpsDroid() as opsdroid:
             opsdroid.config["parsers"] = [