Forms

Usage

To use forms with Rasa you need to make sure that the Rule Policy is added to your policy configuration. For example:

policies:
  - name: RulePolicy

Defining a Form

Define a form by adding it to the forms section in your domain. The name of the form is also the name of the action which you can use in stories or rules to handle form executions. You will need to specify a list of slot names to the mandatory required_slots key.

The following example form restaurant_form will fill the slot cuisine and slot num_people.

entities:
  - cuisine
  - number
slots:
  cuisine:
    type: text
    mappings:
      - type: from_entity
        entity: cuisine
  num_people:
    type: any
    mappings:
      - type: from_entity
        entity: number
forms:
  restaurant_form:
    required_slots:
      - cuisine
      - num_people

You can define a list of intents to ignore for the whole form under the ignored_intents key. Intents listed under ignored_intents will be added to the not_intent key of each slot mapping.

For example, if you do not want any of the required slots of a form to be filled when the intent is chitchat, then you would need to define the following:

entities:
  - cuisine
  - number
slots:
  cuisine:
    type: text
    mappings:
      - type: from_entity
        entity: cuisine
  num_people:
    type: any
    mappings:
      - type: from_entity
        entity: number
forms:
  restaurant_form:
    ignored_intents:
      - chitchat
    required_slots:
      - cuisine
      - num_people

Once the form action gets called for the first time, the form gets activated and will prompt the user for the next required slot value. It does this by looking for a response called utter_ask_<form_name>_<slot_name> or utter_ask_<slot_name> if the former isn't found. Make sure to define these responses in your domain file for each required slot.

Activating a Form

To activate a form you need to add a story or rule, which describes when the assistant should run the form. In the case a specific intent triggering a form, you can for example use the following rule:

rules:
  - rule: Activate form
    steps:
      - intent: request_restaurant
      - action: restaurant_form
      - active_loop: restaurant_form
note

The active_loop: restaurant_form step indicates that the form should be activated after restaurant_form was run.

Deactivating a Form

A form will automatically deactivate itself once all required slots are filled. You can describe your assistant's behavior for the end of a form with a rule or a story. If you don't add an applicable story or rule, the assistant will automatically listen for the next user message after the form is finished. The following example runs the utterances utter_submit and utter_slots_values as soon as the form your_form filled all required slots.

rules:
  - rule: Submit form
    condition:
      - active_loop: restaurant_form
    steps:
      - action: restaurant_form
        active_loop: null
      - slot_was_set:
          - requested_slot: null
      - action: utter_submit
      - action: utter_slots_values

Users might want to break out of a form early. Please see Writing Stories / Rules for Unhappy Form Paths on how to write stories or rules for this case.

Slot Mappings

Changed in 3.0

As of 3.0, slot mappings are defined in the slots section of the domain. This change allows the same slot mapping to be reused across multiple forms, removing any unnecessary duplication. Please follow the migration guide to update your assistant.

Note specifically the role of Mapping Conditions and the unique entity mapping constraint.

Writing Stories / Rules for Unhappy Form Paths

Your users will not always respond with the information you ask of them. Typically, users will ask questions, make chitchat, change their mind, or otherwise stray from the happy path.

While a form is active, if a user's input does not fill the requested slot, the execution of the form action will be rejected i.e. the form will automatically raise an ActionExecutionRejection. These are the specific scenarios in which a form will raise an ActionExecutionRejection:

To intentionally reject the form execution, you can also return an ActionExecutionRejected event as part of your custom validations or slot mappings.

To handle situations that might cause a form's execution to be rejected, you can write rules or stories that include the expected interruptions. For example, if you expect your users to chitchat with your bot, you could add a rule to handle this:

rules:
  - rule: Example of an unhappy path
    condition:
      - active_loop: restaurant_form
    steps:
      - intent: chitchat
      - action: utter_chitchat
      - action: restaurant_form
        active_loop: restaurant_form

In some situations, users may change their mind in the middle of the form action and decide not to go forward with their initial request. In cases like this, the assistant should stop asking for the requested slots.

You can handle such situations gracefully using a default action action_deactivate_loop which will deactivate the form and reset the requested slot. An example story of such conversation could look as follows:

stories:
  - story: User interrupts the form and doesn't want to continue
    steps:
      - intent: request_restaurant
      - action: restaurant_form
        active_loop: restaurant_form
      - intent: stop
      - action: utter_ask_continue
      - intent: stop
      - action: action_deactivate_loop
        active_loop: null

It is strongly recommended that you build these rules or stories using interactive learning. If you write these rules / stories by hand you will likely miss important things.

Advanced Usage

Forms are fully customizable using Custom Actions.

Validating Form Input

After extracting a slot value from user input, you can validate the extracted slots. By default Rasa only validates if any slot was filled after requesting a slot.

You can implement a Custom Actionvalidate_<form_name> to validate any extracted slots. Make sure to add this action to the actions section of your domain:

actions:
  - validate_restaurant_form

When the form is executed it will run your custom action after every user turn to validate the latest filled slots.

This custom action can extend FormValidationAction class to simplify the process of validating extracted slots. In this case, you need to write functions named validate_<slot_name> for every extracted slot.

The following example shows the implementation of a custom action which validates that the slot named cuisine is valid.

from typing import Text, List, Any, Dict
from rasa_sdk import Tracker, FormValidationAction
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.types import DomainDict

class ValidateRestaurantForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_restaurant_form"

@staticmethod
    def cuisine_db() -> List[Text]:
        return ["caribbean", "chinese", "french"]

def validate_cuisine(
            self,
            slot_value: Any,
            dispatcher: CollectingDispatcher,
            tracker: Tracker,
            domain: DomainDict,
    ) -> Dict[Text, Any]:
        if slot_value.lower() in self.cuisine_db():
            return {"cuisine": slot_value}
        else:
            return {"cuisine": None}

You can also extend the Action class and retrieve extracted slots with tracker.slots_to_validate to fully customize the validation process.

Custom Slot Mappings

Changed in 3.0

The slots_mapped_in_domain argument provided to the required_slots method of FormValidationAction has been replaced by the domain_slots argument, please update your custom actions to the new argument name.

If none of the predefined Slot Mappings fit your use case, you can use the Custom Actionvalidate_<form_name> to write your own extraction code. Rasa will trigger this action when the form is run.

If you're using the Rasa SDK we recommend you to extend the provided FormValidationAction. When using the FormValidationAction, three steps are required to extract customs slots:

  1. Define a method extract_<slot_name> for every slot that should be mapped in a custom way.
  2. In your domain file, for your form's required_slots, list all required slots, with both predefined and custom mappings.
  3. In addition, you can override the required_slots method to add dynamically requested slots.
note

If you have added a slot with a custom mapping in the slots section of the domain file which you only want to be validated within the context of a form by a custom action extending FormValidationAction, please make sure that this slot has a mapping of type custom and that the slot name is included in the form's required_slots.

The following example shows the implementation of a form which extracts a slot outdoor_seating in a custom way, in addition to the slots which use predefined mappings.

from typing import Dict, Text, List, Optional, Any
from rasa_sdk import Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.forms import FormValidationAction

class ValidateRestaurantForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_restaurant_form"

async def extract_outdoor_seating(self,
            dispatcher: CollectingDispatcher,
            tracker: Tracker,
            domain: Dict
    ) -> Dict[Text, Any]:
        text_of_last_user_message = tracker.latest_message.get("text")
        sit_outside = "outdoor" in text_of_last_user_message
        return {"outdoor_seating": sit_outside}

By default the FormValidationAction will automatically set the requested_slot to the first slot specified in required_slots which is not filled.

Dynamic Form Behavior

By default, Rasa will ask for the next empty slot from the slots listed for your form in the domain file. If you use custom slot mappings and the FormValidationAction, it will ask for the first empty slot returned by the required_slots method. If all slots in required_slots are filled the form will be deactivated.

You can update the required slots of your form dynamically. This is, for example, useful when you need to fill additional slots based on how a previous slot was filled or when you want to change the order in which slots are requested.

If you are using the Rasa SDK, we strongly recommend that you use the FormValidationAction and override required_slots to fit your dynamic behavior. You must implement a method extract_<slot name> for every slot which doesn't use a predefined mapping, as described in Custom Slot Mappings.

The example below will ask the user if they want to sit in the shade or in the sun in case they said they want to sit outside.

from typing import Text, List, Optional
from rasa_sdk.forms import FormValidationAction

class ValidateRestaurantForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_restaurant_form"

async def required_slots(
            self,
            domain_slots: List[Text],
            dispatcher: "CollectingDispatcher",
            tracker: "Tracker",
            domain: "DomainDict",
    ) -> List[Text]:
        additional_slots = ["outdoor_seating"]
        if tracker.slots.get("outdoor_seating") is True:
            additional_slots.append("shade_or_sun")
        return additional_slots + domain_slots

If conversely, you want to remove a slot from the form's required_slots defined in the domain file under certain conditions, you should copy the domain_slots over to a new variable and apply changes to that new variable instead of directly modifying domain_slots. Directly modifying domain_slots can cause unexpected behavior.

from typing import Text, List, Optional
from rasa_sdk.forms import FormValidationAction

class ValidateBookingForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_booking_form"

async def required_slots(
            self,
            domain_slots: List[Text],
            dispatcher: "CollectingDispatcher",
            tracker: "Tracker",
            domain: "DomainDict",
    ) -> List[Text]:
        updated_slots = domain_slots.copy()
        if tracker.slots.get("existing_customer") is True:
            updated_slots.remove("email_address")
        return updated_slots

The requested_slot slot

The slot requested_slot is automatically added to the domain as a slot of type text. The value of the requested_slot will be ignored during conversations. If you want to change this behavior, you need to add the requested_slot to your domain file as a categorical slot with influence_conversation set to true. You might want to do this if you want to handle your unhappy paths differently, depending on what slot is currently being asked from the user. For example, if your users respond to one of the bot's questions with another question, like why do you need to know that?

The response to this explain intent depends on where we are in the story. In the restaurant case, your stories would look something like this:

stories:
  - story: explain cuisine slot
    steps:
      - intent: request_restaurant
      - action: restaurant_form
        active_loop: restaurant
      - slot_was_set:
          - requested_slot: cuisine
      - intent: explain
      - action: utter_explain_cuisine
      - action: restaurant_form
        active_loop: null

- story: explain num_people slot
    steps:
      - intent: request_restaurant
      - action: restaurant_form
        active_loop: restaurant
      - slot_was_set:
          - requested_slot: cuisine
      - slot_was_set:
          - requested_slot: num_people
      - intent: explain
      - action: utter_explain_num_people
      - action: restaurant_form
        active_loop: null

Again, it is strongly recommended that you use interactive learning to build these stories.

Using a Custom Action to Ask For the Next Slot

As soon as the form determines which slot has to be filled next by the user, it will execute the action utter_ask_<form_name>_<slot_name> or utter_ask_<slot_name> to ask the user to provide the necessary information. If a regular utterance is not enough, you can also use a custom action action_ask_<form_name>_<slot_name> or action_ask_<slot_name> to ask for the next slot.

from typing import Dict, Text, List
from rasa_sdk import Tracker
from rasa_sdk.events import EventType
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk import Action

class AskForSlotAction(Action):
    def name(self) -> Text:
        return "action_ask_cuisine"

def run(
            self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict
    ) -> List[EventType]:
        dispatcher.utter_message(text="What cuisine?")
        return []

If there is more than one asking option for the slot, Rasa prioritizes in the following order:

  1. action_ask_<form_name>_<slot_name>
  2. utter_ask_<form_name>_<slot_name>
  3. action_ask_<slot_name>
  4. utter_ask_<slot_name>