Building contextual assistants with Rasa Forms

A contextual assistant that goes beyond simple FAQ-style interactions requires more than just an algorithm and a prayer. A conversational assistant needs to have collected important details needed to answer user questions in the right context. Otherwise, no happy path. Simple enough, and known as slot filling. But how do you gather and define the details that matter before taking action, or providing a response?

Slot filling is made easy with our new addition of FormPolicy. This is a fresh feature that implements slot filling in an easy and effective way. How? FormPolicy allows you to cover all the happy paths with a single story. Forms also let you alter logic within a happy path, without needing to change the training data.

So, how do you implement this new technique? Glad you asked. Here’s how to get FormPolicy working for you.

Outline:

  1. Introduction to Slot Filling
  2. Building a restaurant search assistant using the new Forms:
    Step 1: Extracting details from user inputs using Rasa NLU
    Step 2: Training the dialogue model: handling the happy path with forms
    Step 3: Defining the domain
    Step 4: Defining the FormAction
    Step 5: Expanding the FormAction to handle more advanced cases
    Step 6: Handling the deviations from the happy path
    Step 7: Testing the restaurant search assistant
  3. Summary
  4. Useful resources

Introduction to Slot Filling

Slot filing is a process of collecting important pieces of information in order to fulfil the user's request. It’s all about having relevant data on hand that may be useful in a given conversation.

Let’s take a restaurant search assistant as an example to illustrate it. Before the assistant can actually perform a restaurant search action, it has to know users preferences like cuisine, price range, location, etc, in order to make a useful suggestion.

To store such information, Rasa Core uses slots. In simple cases, you can implement slot filling by only using slots, but things quickly become complicated once the complexity of the conversations grows. With more details comes more possible dialogue turns, which creates larger and larger training data requirements. This is where FormAction comes to the rescue. It allows you to enforce strict logic on the information collection process, and considerably reduce the amount of training data needed to build a good dialogue model.

Building a restaurant search assistant using Rasa Forms

In the remaining part of this tutorial, you will learn how to use the new FormPolicy in practice. This tutorial is based on a restaurant search assistant called formbot. You can find all the code inside the Rasa Core GitHub repository:

git clone https://github.com/RasaHQ/rasa_core.git
cd rasa_core/examples/formbot

By following this tutorial and using the code inside the repo, you will build a fun assistant, capable of suggesting restaurants based on user preferences like cuisine, number of people, preferred seating, and other possible requirements. An example conversation between the user and the assistant will look as follows:

typo_fix

Step 1: Extracting details from user inputs using Rasa NLU

Before storing important information as slots, the assistant has to extract them from user inputs.

To enable your assistant to do that, it is necessary to train the NLU model which will classify the intent of the user inputs and extract important details as entities.

Formbot example already comes with training examples which you can use to train the NLU model. Using the provided training examples you can teach your assistant to understand inputs like greeting, restaurant search queries, inputs to supply requested information, etc, and then extract entities like cuisine, number of people, additional requirements, etc. You can find the NLU training examples inside the data/nlu_data.md file of the formbot example.

To train the model, run the command below. This command is a shortcut shell command which will call the Rasa NLU train function, pass the training data and model configuration files, and save the model inside the models/nlu/current directory of your working directory:

make train-nlu

Note: if you are new to Rasa NLU and would like to learn more about it, make sure to check out the Rasa NLU documentation.

Step 2: Training the dialogue model: handling the happy path with forms

Once the assistant is capable of understanding the user inputs, it’s time to build a dialogue management model. When using Forms for Slot Filling, it’s best to get started by training the model to handle the happy paths - situations where the user provides all the required information and lets the assistant drive the conversation.

The best part about the new forms is that the assistant learns to handle all the happy paths from one single training story. Check out the video below for the illustration:

How does it work?

Once the form action restaurant_form gets predicted, the assistant keeps asking for necessary details until all required slots are set. There are no restrictions on how the user should provide the details - if a user specifies all preferences in the initial restaurant request, for example, ‘Book me a table for two at the Chinese restaurant’, the assistant will skip the questions about the cuisine and number of people.

If the user does not specify any of these details in an initial request, then the assistant asks all the details in follow-up questions until all of them are provided. Both of these situations represent two different conversations (there can be more than two), but by using FormsPolicy they both will be learned using the same, single story. Below is a snippet of the training story used to model all the happy paths in a formbot example:

## happy path
* greet
    - utter_greet
* request_restaurant
    - restaurant_form
    - form{"name": "restaurant_form"}
    - form{"name": null}
    - utter_slots_values
* thankyou
    - utter_noworries

Check out the data/stories.md file of the Formbot example to investigate the training stories in more detail.

Step 3: Defining the domain

In order to train the dialogue management model with Rasa Core, you also need to define the domain. This is where you can specify which extracted details should be stored as slots.

There are three important things you should consider when defining the domain for an assistant with slot filling:

  1. In Rasa Core, different slot types have a different influence on the predictions of the next action. When using FormAction to fill the slots, you are enforcing strict rules which tell your assistant what information it should ask for next. This way, you allow FormAction to handle all the happy paths with on single story as it checks which requested slots are populated and which ones are still missing. For this to work, the requested slots in your domain file should be defined as unfeaturized.

  2. The names of the templates which will be used to ask for the missing required slots should follow the format utter_ask_{slotname}. This is important for FormAction to know which template to use for which slot.

  3. In addition to all the usual parts of the domain (intents, entities, templates, actions and slots), you will have to include one additional section called forms. This section has to include the names of all form actions your assistant should be able to predict based on the stories defined in a training data file.

Below is a snippet of the domain used in a formbot example:

entities:
  - cuisine
  - num_people
  - number
  - feedback
  - seating

slots:
  cuisine:
    type: unfeaturized
    auto_fill: false
  num_people:
    type: unfeaturized
    auto_fill: false
  outdoor_seating:
    type: unfeaturized
    auto_fill: false
  preferences:
    type: unfeaturized
    auto_fill: false
  feedback:
    type: unfeaturized
    auto_fill: false
  requested_slot:
    type: unfeaturized
    
forms:
    - restaurant_form

Step 4: Defining the FormAction

The next step is to actually implement the FormAction which, once predicted, will handle the slot filling. You can implement all FormActions in the actions.py file where you would implement any other custom action you would like your assistant to handle. Let’s define the FormAction for the slot filling of the restaurant assistant step-by-step (check out the actions.py file of the formbot example to see a full code):

  • First, define the form action class. Note, that form actions, in contrast to regular custom actions, inherit from FormAction class:
class RestaurantForm(FormAction):
    """Example of a custom form action"""
  • The first function which you should define in a form action class is called name which simply defines the name of the action (the same one you defined in the domain file). In a restaurant search example it is defined as restaurant_form:
class RestaurantForm(FormAction):
    """Example of a custom form action"""

    def name(self):
        """Unique identifier of the form"""
        return "restaurant_form"
  • Next, a function called required_slots is used to define a list of slots which the assistant needs to fill in before responding to the user’s request:
class RestaurantForm(FormAction):
    """Example of a custom form action"""

    def name(self):
        """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 ["cuisine", "num_people", "outdoor_seating",
                "preferences", "feedback"]

Pro tip: required_slots function is a great place to introduce some custom slot logic. For example, it might make sense to include the outdoors seating option only for the restaurants of a specific cuisine. You can achieve this by introducing simple logic like in the example below, where outdoor_seating slot will only be required when the users ask for restaurants which serve Greek food:

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

    if tracker.get_slot('cuisine') == 'greek':
        return ["cuisine", "num_people", "outdoor_seating",
             	"preferences", "feedback"]
    else:
        return ["cuisine", "num_people",
             	"preferences", "feedback"]
  • The final building block of a simple form action is a function called submit which defines what should happen once all required slots are populated. In a restaurant search assistant case, once all the slots are populated, the assistant will execute the template utter_submit which, based on how it’s defined in a domain.yml file, will confirm that the assistant has all the information it needs to proceed:
class RestaurantForm(FormAction):
    """Example of a custom form action"""

    def name(self):
        """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 ["cuisine", "num_people", "outdoor_seating",
                "preferences", "feedback"]

    def submit(self):
        """Define what the form has to do
            	after all required slots are filled"""

       	dispatcher.utter_template('utter_submit', tracker)
        return []

And there you have it - a simple implementation of the FormAction. There is so much more you can add to enable your assistant to handle even more advanced cases. Let’s look into that in the next step of this tutorial.

Step 5: Handling the advanced cases with FormAction

Some necessary slots can come from very different user inputs. For example, the user could answer the question ‘Would you like to sit outside?’ with the following possible answers:

  • ‘Yes’
  • ‘No’
  • ‘I prefer sitting indoors’ (or similar direct answer)

Each of these responses corresponds to different intents or has different important entities, but since they provide a viable answer to the question, an assistant must be able to accept it and set a slot to move forward.

This is where the slot_mappings function in a FormAction comes in play - it defines how to extract slot values from possible user responses and maps them to a specific slot. Below is an example of the slot_mappings function for the previously discussed outdoor_seating slot. Based on the defined logic, the outdoor_seating slot will be populated using either:

  • value ‘True’ if the user responds with ‘affirm’ intent to the question
  • value ‘False’ if the user responds with ‘deny’ intent to the question
  • value of the extracted entity seating
def slot_mappings(self):
    # type: () -> 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 { "outdoor_seating": [self.from_entity(entity="seating"),
                      self.from_intent(intent='affirm',
                                                 value=True),
                      self.from_intent(intent='deny',
                                                 value=False)]}

You can find more examples of slot mapping inside the slot_mappings function of the actions.py file of the formbot example.

Another useful thing you can do with FormArction is slot validation. For example, before allowing your assistant to move on with questions, you may want to check the slot value against possible values in your database or check if the value is in the right format. You can achieve that by creating a function called validate in your FormAction class. By default, it checks if the requested slot was extracted, but you can add as much additional logic as you need. Below is an example of the validation function which first checks if the requested slot was populated and later checks if the provided number of people is in the right format: if the number is an integer, the assistant will use the provided value, if not it will respond with a message that the format of the slot value is invalid, set the slot to None and ask for it again.

def validate(self,
                 dispatcher: CollectingDispatcher,
                 tracker: Tracker,
                 domain: Dict[Text, Any]) -> List[Dict]:
    """Validate extracted requested slot
            else reject the execution of the form action
    """
    # extract other slots that were not requested
    # but set by corresponding entity
    slot_values = self.extract_other_slots(dispatcher, tracker, domain)

    # extract requested slot
    slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
    if slot_to_fill:
        slot_values.update(self.extract_requested_slot(dispatcher,
                                                       tracker, domain))
        if not slot_values:
            # reject form action execution
            # if some slot was requested but nothing was extracted
            # it will allow other policies to predict another action
            raise ActionExecutionRejection(self.name(),
                                           "Failed to validate slot {0}"
                                           "with action {1}"
                                           "".format(slot_to_fill,
                                                         self.name()))

    # we'll check when validation failed in order
    # to add appropriate utterances
    for slot, value in slot_values.items():

        if slot == 'num_people':
            if not self.is_int(value) or int(value) <= 0:
                dispatcher.utter_template('utter_wrong_num_people',
                                              tracker)
                # validation failed, set slot to None
                slot_values[slot] = None

You can find more examples of the slot validation (checking against the database, string matching) in the actions.py file of the formbot example.

Step 6: Handling the deviations from the happy path

The main idea behind the slot filling with FormAction is that strict logic is applied to collect the important pieces of information and handle the happy path while the regular machine learning is used to gracefully handle the deviations from the happy path. These deviations could be some chit-chat messages in the middle of the form action session or the situations where the users refuse to provide all the necessary details. In order to handle situations like this, you have to write the stories which represent such conversation turns.

For example, the story below represents the situation where the user decided to stop providing the information in the middle of the form action session but later came back to provide the required details:

## stop but continue path
* request_restaurant
    - restaurant_form
    - form{"name": "restaurant_form"}
* stop
    - utter_ask_continue
* affirm
    - restaurant_form
    - form{"name": null}
    - utter_slots_values
* thankyou
    - utter_noworries

To enable your assistant to handle even more advanced cases, you need to collect more stories like this to cover different dialogue turns. In some situations, you might want to handle unhappy paths differently depending on what slot is currently being asked for from the user. To achieve that, inside the domain file you should set requested_slot to categorical. This will allow the predictions of the next response to be influenced depending on which slot is currently requested.

Check out the file data/stories.md of the formbot example to see more possible unhappy path stories.

Step 7: Testing the restaurant search assistant

By now you have learned a lot about implementing the FormAction and have defined all the necessary bits of the dialogue management model. Now it’s time for the most exciting part - testing the assistant!

First, train the dialogue management model using the following command which will call the Rasa Core train function, pass the domain and data files to it, and store the trained model inside the models/dialogue directory of your working directory:

make train-core

Once the model is trained, time to load it alongside the previously trained NLU model, and test how the restaurant search bot performs!

First, in a new terminal, start a duckling server by running the following command:

docker run -p 8000:8000 rasa/duckling

Launch your assistant by executing the following command which will spin up the local server for custom actions and load the assistant for you to chat:

make run

To best see how the FormAction works, spend some time testing how the assistant performs on different happy and unhappy paths.

Summary

Building good contextual assistants is not easy. Combining FormAction with traditional ML allows you to build assistants that can handle deeper conversations without requiring you to write loads of training stories to handle the happy path. In addition to this, FormAction makes it a lot easier to make changes to the code and dialogue of your assistant as requested slots are not used in the training stories directly.

Test the new forms on your own datasets and share your feedback with us by joining the discussion on this thread of the Rasa Community Forum!

Resources