Introducing entity roles and groups

With Rasa Open Source 1.10 we released an experimental feature called Entity Roles and Groups. In this post I want to briefly explain what it is and how to use it. Feel free to share your feedback here with us.

What are Entity Roles and Groups?

Assigning custom entity labels to words, allow you to define certain concepts in the data. For example, we can define what a city is:

I want to fly from [Berlin](city) to [San Francisco](city).

However, sometimes you want to specify entities even further. Let’s assume we want to build an assistant that should book a flight for us. The assistant needs to know which of the two cities in the above example is the departure city and which is the destination city. “Berlin” and “San Francisco” are still cities, but they play a different role in our example. To distinguish between the different roles, you can assign a role label in addition to the entity label.

I want to fly from [Berlin]{"entity": "city", "role": "departure"} to [San Francisco]{"entity": "city", "role": "destination"}.

You can also group different entities by specifying a group label next to the entity label. The group label can, for example, be used to define different orders. In the following example we use the group label to reference what toppings goes with which pizza and what size which pizza has.

Give me a [small]{"entity": "size", "group": "1"} pizza with [mushrooms]{"entity": "topping", "group": "1"} and a [large]{"entity": "size", "group": "2"} [pepperoni]{"entity": "topping", "group": "2"}

As you may have notice the training data format looks slightly different. Check out the documentation for details on how to define entity roles and groups in your training data.

How to use Entity Roles and Groups?

Upgrade to Rasa 1.10.0 and update your training data to include some entities with role and/or group labels. Your pipeline should contain either the CRFEntityExtractor or the DIETClassifier as only those two entity extractors can detect role and group labels. Most likely you want to fill a certain slot with an entity that has a specific role and/or group. To achieve this we recommend to use forms. However, you can also use a custom action for this.

Forms

Define a custom slot mapping to fill a slot with an entity that has a specific role and/or group label. Let’s go back to the example in which our assistants helped us to book a flight. We want to fill the two slots destination_city and departure_city. To achieve this you can use the function from_entity in your slot mapping:

def slot_mappings(self) -> Dict[Text, Union[Dict, List[Dict]]]:
   return {
       “departure_city": [
           self.from_entity(entity="city", role="departure"),
       ],
       "destination_city": [
           self.from_entity(entity="city", role="destination"),
       ]
   }

If the form is activated and an entity of type city is found that has the role departure the slot departure_city is filled. If the city entity has the role destination it is assigned to the slot destination_city. Once the slots are filled you can use them, for example, in your custom action to book the flight.

Custom Actions

If you want to use a custom action, you can directly obtain the entity from the tracker using the method get_latest_entity_values. You can pass a role label and/or group label next to the entity type to that function. For example:

tracker.get_latest_entity_values(entity_type=”city”, entity_role=”destination”) 

Please, keep in mind that this feature is still experimental. We encourage you to try it out and give us feedback. However, the functionality might be changed or removed in the future.

Happy coding!

12 Likes

Awesome! Many (including I) had difficulty in extracting entities like ‘to’ and ‘from’ city etc. Hope this could address some of those issues. Also I think this could help in dependency parsing? A while ago someone asked this question.

Also, if possible, could you please explain a little bit of maths behind this? or give me some pointers. Thanks!

1 Like

Wow, I love this new feature! Actually I was trying to intergrate Spacy dependency parsing component and hopefully this entity roles and groups can make my life easier, I still need to relabel nearly all the entities in the training data though:joy:

This is awesome! I have found this feature very useful for representing extra information that I required for multiple intent classification. One observation is that it works better(or with fewer training samples) with the DIETClassifier as opposed to CRF.

Great work, thanks for the update! :slight_smile:

1 Like

This a great feature. Our first demo bot in 2018 was Lufthansa info bot answering questions above. At the time all we had were entities, and it was very difficult to have the same words appear as “origin” and “destination”. We could make it work, but this would have been so much easier.

long awaited features. Thanks

The idea behind roles and groups in addition to entity is great. I hope this feature will be kept and not removed from the package. ‘to’ and ‘from’ roles are a great example. Please keep this feature.

@Tanja What are the chances that these two features would not get removed in the future version, because if there is a chance of romoval i would not use them instead i will stick to @BeWe11 solution GitHub - BeWe11/rasa_composite_entities: A Rasa NLU component for composite entities.

@noman I am afraid I cannot answer your question right now. We want to wait for more user feedback before we decide on that. However, as the user feedback so far was quite positive, I guess, it is likely that we gonna keep this feature in some way. But it might be that we change the training data format or the underlying algorithm again. It is simply too early to tell.

1 Like

@saurabh-m523 Sorry, for the late reply. Internally we are using multiple CRFs to predict the entities, roles, and group labels. We use the entity labels as additional input features for the CRFs that predict the role and group labels. Let me know if you have further questions.

2 Likes

Hmmm ok. Thanks.

@Tanja, I was wondering if adding roles would increase significantly the training time for CRFEntityExtractor ? The training for this extractor used to take no more than 2-3 mins and now takes 1h45, although I just added 2 entities with 2 roles ( the roles are the same for both entities), and around 100 sentences with these entities and roles (the extraction is working well, apart from this training time problem…) I haven’t seen anyone complaining about this problem so I’m quite curious.

hey @Tanja

i tried to get the slot in actions but i get this error

i dont get the values of the slot using


> tracker.get_latest_entity_values(entity_type="name1", entity_role="object2)

i get

generator object Tracker.get_latest_entity_values.. at 0x7f66cbfd46d0>

please please PLEASE do not take this feature away!!! It pretty much saved my life! It works perfectly in extracting multiple values of the same entity type in one expression. I use it to differentiate between the 1st part and the 2 part of an order. :raised_hands: :pray: can I have [2]{“entity”: “itemQty”, “group"1”} [coke]{“entity”: “itemName”, “group”: “1”} and [3]{“entity”: “itemQty”, “group"2”} [beers]{“entity”: “itemName”, “group”: “2”} for table 15:

i want 2 coke and 3 beers for table 15

{
  "intent": {
    "name": "multiple_itemName_order",
    "confidence": 1.0
  },
  "entities": [
    {
      "entity": "***itemQty***",
      "start": 7,
  "end": 8,
  "**group**": "**1**",
  "**value**": "**2**",
  "extractor": "DIETClassifier"
},

{
  "entity": "**itemName**",
  "start": 9,
  "end": 13,
  "**group**": "**1**",
  "value": "coke",
  "extractor": "DIETClassifier"
},
{
  "entity": "**itemQty**",
  "start": 18,
  "end": 19,
  "**group**": "**2**",
  "**value**": "**3**",
  "extractor": "DIETClassifier"
},
{
  "entity": "**itemName**",
  "start": 20,
  "end": 25,
  "**group**": "**2**",
  "**value**": "**beers**",
  "extractor": "DIETClassifier"
},
{
  "entity": "**tableNum**",
  "start": 36,
  "end": 38,
  "**value**": "**15**",
  "extractor": "DIETClassifier"
},

], “intent_ranking”: [ { “name”: “multiple_itemName_order”, “confidence”: 1.0 }, { “name”: “deny”, “confidence”: 5.755459650536032e-11 }, { “name”: “bot_challenge”, “confidence”: 2.505445850786714e-11 }, { “name”: “request_restaurant”, “confidence”: 1.0541578027156717e-11 }, { “name”: “thankyou”, “confidence”: 1.3734255703209963e-12 }, { “name”: “affirm”, “confidence”: 2.9411122517454535e-13 }, { “name”: “chitchat”, “confidence”: 1.662231008218995e-13 }, { “name”: “stop”, “confidence”: 3.3230464126248914e-15 }, { “name”: “greet”, “confidence”: 1.1554707305740932e-16 }, { “name”: “single_itemName_order”, “confidence”: 7.348626901551285e-17 } ], “text”: “i want 2 coke and 3 beers for table 15” }

2 Likes

hey , @edubrigham can you tell me what command are you using to track the slots value in actions ?

1 Like

Hi Youssef,

I used the default “formbot” example, and just adapted it:

from typing import Dict, Text, Any, List, Union, Optional

from rasa_sdk import Tracker from rasa_sdk.executor import CollectingDispatcher from rasa_sdk.forms import FormAction

class RestaurantForm(FormAction): “”“Example of a custom form action”""

def name(self) -> Text:
    """Unique identifier of the form"""

    return "restaurant_form"

@staticmethod
def required_slots(tracker: Tracker) -> List[Text]:
    """A list of required slots that the form has to fill"""

    return ["itemName", "itemQty", "tableNum", "seating", "preferences", "feedback"]

def slot_mappings(self) -> Dict[Text, Union[Dict, List[Dict]]]:
    """A dictionary to map required slots to
        - an extracted entity
        - intent: value pairs
        - a whole message
        or a list of them, where a first match will be picked"""

    return {
        "itemName": self.from_entity(entity="itemName", not_intent="chitchat"),
        "itemQty": [
            self.from_entity(
                entity="itemQty", intent=["inform", "request_restaurant"]
            ),
        ],
        "tableNum": [
            self.from_entity(
                entity="tableNum", intent=["inform", "request_restaurant"]
            ),
        ],
        "seating": [
            self.from_entity(entity="seating"),
            self.from_intent(intent="affirm", value=True),
            self.from_intent(intent="deny", value=False),
        ],
        "preferences": [
            self.from_intent(intent="deny", value="no additional preferences"),
            self.from_text(not_intent="affirm"),
        ],
        "feedback": [self.from_entity(entity="feedback"), self.from_text()],
    }

# USED FOR DOCS: do not rename without updating in docs
@staticmethod
def itemName_db() -> List[Text]:
    """Database of supported itemNames"""

    return [
        "coke",
        "cokes",
        "beer",
        "beers",
        "water",
        "waters",
        "mojito",
    ]

@staticmethod
def is_int(string: Text) -> bool:
    """Check if a string is an integer"""

    try:
        int(string)
        return True
    except ValueError:
        return False

# USED FOR DOCS: do not rename without updating in docs
def validate_itemName(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
) -> Dict[Text, Any]:
    """Validate itemName value."""

    if value.lower() in self.itemName_db():
        # validation succeeded, set the value of the "itemName" slot to value
        return {"itemName": value}
    else:
        dispatcher.utter_message(template="utter_wrong_itemName")
        # validation failed, set this slot to None, meaning the
        # user will be asked for the slot again
        return {"itemName": None}

def validate_itemQty(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
) -> Dict[Text, Any]:
    """Validate itemQty value."""

    if self.is_int(value) and int(value) > 0:
        return {"itemQty": value}
    else:
        dispatcher.utter_message(template="utter_wrong_itemQty")
        # validation failed, set slot to None
        return {"itemQty": None}

def validate_tableNum(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
) -> Dict[Text, Any]:
    """Validate itemQty value."""

    if self.is_int(value) and int(value) > 0:
        return {"tableNum": value}
    else:
        dispatcher.utter_message(template="utter_wrong_tableNum")
        # validation failed, set slot to None
        return {"tableNum": None}

def validate_seating(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
) -> Dict[Text, Any]:
    """Validate seating value."""

    if isinstance(value, str):
        if "out" in value:
            # convert "out..." to True
            return {"seating": True}
        elif "in" in value:
            # convert "in..." to False
            return {"seating": False}
        else:
            dispatcher.utter_message(template="utter_wrong_seating")
            # validation failed, set slot to None
            return {"seating": None}

    else:
        # affirm/deny was picked up as T/F
        return {"seating": value}

def submit(
    self,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
) -> List[Dict]:
    """Define what the form has to do
        after all required slots are filled"""

    # utter submit template
    dispatcher.utter_message(template="utter_submit")
    return []

thank you for your reply

but in my case am gonna use a custom actions ./

so i have in my nlu file

  • [date]{“entity”: “attributes”, “value”: “dates”} of events [hosted]{“entity”: “relation”, “value”: “host”} by [Laguana]{“entity”: “name1”, “role”: “object1”}

  • [emails]{“entity”: “attributes”, “value”: “email”} of the [members]{“entity”: “relation”, “value”: “membership”} in [Laguana]{“entity”: “name1”, “role”: “object2”}

here Laguana has two diff roles which are not the same for the two cases in the rasa shell the extracton is made successfully but when it comes to actions i used this command

> tracker.get_latest_entity_values(entity_type="name1", entity_role="object2)

as @Tanja did but no result //:unamused:

the only problem I now face is how to expose the answer of the array [itemQty1, itemQty2] [itemName1, itemName2]:

restaurant_form 1.00 utter_slots_values I am going to run a restaurant search using the following parameters: - item Name: [‘coke’,‘beer’] - item Quantity: [‘3’,‘5’] - Table Number: 9 - preferences: please put lemon in the coke - feedback: None

Current slots: feedback: None, itemName: [‘coke’, ‘beer’], itemQty: [‘3’, ‘5’], preferences: please put lemon in the coke, requested_slot: feedback, seating: None, tableNum: 9

It’s a great feature, thank you ! But when it will be add to Rasa X, because actually Rasa X suppress role in NLU data training ?

1 Like

Yep indeed. Just found that the hard way last week.