Basic Forms in Rasa Open Source 2.x
Note: This is material for Rasa 2.x. The syntax has updated slightly in Rasa 3.0 so we recommend new users to check the new course on Custom Actions found here.
Video
There are many use-cases to collect information from a user before taking an action on their behalf. If we'd like to, for example, order a pizza then we'd need to know the kind and the size of the pizza. To make the collection of information easy, Rasa has a "Forms"-feature which we dive into in this segment.
How does a Rasa-Form work?
Conceptually, a form can be seen as a loop that keeps on asking the user for information until enough slot values have been filled. An example of such a flow is shown in the diagram below.
The idea behind a Rasa form is that we don't need to ask a question if the user has already given us the required information. If the user tells us they want to buy a "veggie pizza", then we don't need to ask about the kind of pizza they're interested in anymore.
Code
Github
We will share code snippets below. But we also have a Github repository with the entire Rasa project. If you'd prefer to explore the code that way, simply go to the repository here.
That said, to set up a basic form, you'll need to change your domain.yml
file to configure
the form and slots but you'll also need to update your rules.yml
file so that Rasa
understands when you trigger a form loop.
Example domain.yml
The example below demonstrates how to set up a simple for with two slots to fill.
# This is the form definition.# Note that we refer to slots defined below.forms: simple_pizza_form: required_slots: pizza_size: - entity: pizza_size type: from_entity pizza_type: - entity: pizza_type type: from_entity
# These are slot definitions.# Note that we also have entities with the same name.slots: pizza_size: type: text influence_conversation: true pizza_type: type: text influence_conversation: true
# These are simple entity definitionsentities:- pizza_size- pizza_type
# We also need to add relevant intents intents:- buy_pizza- inform
# We also need to add responses for our form.responses: utter_submit: - text: I will now order a pizza for you! utter_pizza_slots: - text: I will order a {pizza_size} {pizza_type} pizza. utter_ask_pizza_size: - text: What size would you like your pizza to be? utter_ask_pizza_type: - text: What kind of pizza would you like to buy?
You may notice that there are a few responses that follow the utter_ask_<slot_name>
naming convention. These reponses are picked up by our form whenever we'd like the
user to give us the information required to fill in a slot name.
Example rules.yml
Given that we have a form defined, we also need Rasa to be able to trigger the loop at the appropriate time in the conversation. You can use rules to define this.
- rule: Activate Pizza Form steps: - intent: buy_pizza - action: simple_pizza_form - active_loop: simple_pizza_form
- rule: Submit Pizza Form # Ensure that we're starting from an active loop condition: - active_loop: simple_pizza_form steps: - action: simple_pizza_form # If there are no more requested slots - active_loop: null - slot_was_set: - requested_slot: null # Then submit the form - action: utter_submit - action: utter_pizza_slots
Don't forget nlu.yml
Since we're dealing with entities, we shouldn't forget to add a few examples to our
nlu.yml
file. We need examples of the entities but also for the intents that we're
using in our rules.yml
file.
- intent: buy_pizza examples: | - i'd like to buy a pizza - i want a pizza - can i buy a pizza - I'm interested in a savory round flattened bread of Italian origin - i want to buy a pizza- intent: inform examples: | - i'd like a [large](pizza_size) pizza - i want to order a [xl](pizza_size) [hawai](pizza_type) pizza - [medium](pizza_size) pizza - [xl](pizza_size) - [small](pizza_size) - [s](pizza_size) - [pepperoni](pizza_type)
Invalid Data
It might be the case that the user gives us information that isn't valid. A user might ask us to make a "fruit" pizza, which is something that we cannot provide.
In these situations we'd like to validate and catch this invalid utterances so that we can ask the user to try again.
To validate data, we can use Custom Actions.
Adding a Custom Action
If we want to add a custom action, we shouldn't forget to add it to our
domain.yml
file.
actions: - validate_simple_pizza_form
Note the naming convention here. We need to adhere to the validate_<form_name>
convention.
Python Code
An example validator for our form can be seen below.
from typing import Text, List, Any, Dict
from rasa_sdk import Tracker, FormValidationAction, Actionfrom rasa_sdk.events import EventTypefrom rasa_sdk.executor import CollectingDispatcherfrom rasa_sdk.types import DomainDict
ALLOWED_PIZZA_SIZES = [ "small", "medium", "large", "extra-large", "extra large", "s", "m", "l", "xl"]ALLOWED_PIZZA_TYPES = [ "mozzarella", "fungi", "veggie", "pepperoni", "hawaii"]
class ValidateSimplePizzaForm(FormValidationAction): def name(self) -> Text: return "validate_simple_pizza_form"
def validate_pizza_size( self, slot_value: Any, dispatcher: CollectingDispatcher, tracker: Tracker, domain: DomainDict, ) -> Dict[Text, Any]: """Validate `pizza_size` value."""
if slot_value.lower() not in ALLOWED_PIZZA_SIZES: dispatcher.utter_message(text=f"We only accept pizza sizes: s/m/l/xl.") return {"pizza_size": None} dispatcher.utter_message(text=f"OK! You want to have a {slot_value} pizza.") return {"pizza_size": slot_value}
def validate_pizza_type( self, slot_value: Any, dispatcher: CollectingDispatcher, tracker: Tracker, domain: DomainDict, ) -> Dict[Text, Any]: """Validate `pizza_type` value."""
if slot_value not in ALLOWED_PIZZA_TYPES: msg = "I don't recognize that pizza." msg += f"We only serve {'/'.join(ALLOWED_PIZZA_TYPES)}." dispatcher.utter_message(msg) return {"pizza_type": None} dispatcher.utter_message(text=f"OK! You want to have a {slot_value} pizza.") return {"pizza_type": slot_value}
There's a few things to note.
- Note that our
ValidateSimplePizzaForm
class inherits fromFormValidationAction
, which is different from the standardAction
class that custom actions inherit from. - The
name
method defines the name of the validator, which needs to correspond with the name in ourdomain.yml
file and needs to adhere to thevalidate_<form_name>
naming convention. - For each slot we have a
validate_<slot_name>
method. Again, we need to follow a naming convention. - Note that each validator method returns a dictionary. You can set a slot to
None
which will invalidate it.
Exploring Forms
If you want to explore your forms, we recommend using rasa interactive
. This gives
you an interactive shell that also shows the status of the slots, which is very useful
when you're debugging a form.
Links
Exercises
Try to answer the following questions to test your knowledge.
- In what ways is a form validator different from a custom action?
- When would you use a form to set slots? When might a custom action be preferable?