Looking for a more recent example? Read about how we upgraded the bot from this tutorial to Rasa Open Source 2.0.
One skill nearly every AI assistant needs is the ability to collect information from the user. Whether your assistant is conducting a survey, taking an order, or updating a customer's account, chances are it needs to ask the user a series of questions and collect specific pieces of required information.
That's where forms come in. Forms are an essential building block for developing interactive AI assistants, and as a new Rasa developer, forms are one of the first things you'll want to master. Once you've gotten the hang of forms, you can start to add some really interesting and useful functionality to your assistant.
This post is a friendly introduction to using forms in Rasa. It's leveled for developers who are brand new to Rasa or want to brush up on the fundamentals. This blog post is divided into two main sections. In the first section, we'll take it slow and cover the most important concepts related to forms. In the second section we'll practice what we've learned with a step-by-step tutorial for building a simple wellness tracker bot.
How Forms Work
First things first-what does a form do?
When a form is active, a Rasa assistant goes into information collecting mode. That is, it asks the user questions one by one until all of the information has been collected. If a user doesn't provide information that matches what the assistant is looking for, it asks again. You can think of a form as a while loop that repeats until all of the information has been collected.
In order to execute this behavior, the assistant needs to know:
- Which pieces of information (also called slots) it needs to collect.
- Which questions it should ask the user to prompt them for the next piece of information.
- How to know if what the user says matches the requested information.
- When to activate and deactivate the form.
In the next sections, we'll go through each of these points, one by one.
Which information to collect...
Each piece of information a form collects is stored in a slot. A slot saves information to the assistant's memory in a key-value format, like "name":"Susan"
or "birthday":"June 18"
. We can think of slots as the basic unit of a form.
Let's discuss a few of the valid ways you can fill slots in a form.
When you start thinking about slot filling and forms, the first examples that come to mind are probably slots that are filled by entities. Entities are important keywords that are extracted from a user's message; for example, a name, a city, a vendor name, a product, or a date. If a user says "My name is Michael," you probably just want to save the entity-Michael-instead of the full text of the user message. Entities-like names, dates, or places, which are extracted from the user's message by the NLU model-are one way you can fill slots in a form.
With some types of entities, you might want to accept an almost unlimited range of values, as with names. In other cases, there might be just a few valid choices. For example, you might ask the user what size t-shirt they want to order, and the only valid options are small, medium, and large. In cases where you want to enforce a limited set of entity values, you can offer the user buttons to choose. Like entities extracted from freeform text, button values can also be used to set slots.
Another way a slot can be filled is with a value mapped to an intent. An intent is the goal or intention behind a user's message. Every time a user sends a message to the assistant, the NLU model decides which intent the message matches out of the list of intents it's been trained to recognize. When might you want to fill a form slot using an intent? One common example is a yes/no question, for example, "Do you want to receive special offers and promotions?". If the model predicts intent:affirm, we know the user said yes and we can save the slot value to True; if the model predicts intent:deny, we know they've said no and save the slot to False. Then, we can use these boolean values to create conditional logic in custom actions, like saving the user's information to a mailing list if their answer is yes.
But a slot doesn't have to be just a keyword-you can also save the full text of the user's message.
The form we'll build later in this post uses a combination of these slot filling strategies: extracting just the entity, both from freeform text and from button values, detecting the intent and mapping it to a value, and saving the full text of the user's message.
Defining Slots
Now that we've covered what slots are and a few ways they can be filled, let's talk about how to tell your assistant which slots it should recognize.
Slots are defined in the domain.yml file. At a minimum, you need to provide the name of the slot key (e.g. name, email, etc.) and the type of slot. (There are other optional parameters you can read about in the documentation, but we'll keep things simple in this tutorial).
When you're building a form, each slot you define in the domain.yml file maps to a piece of information you want your assistant to collect:
slots:
user_name:
type: unfeaturized
We'll show you a detailed example of this, later in this post.
A Note on Featurized vs Unfeaturized Slots
When choosing your slot types, you'll need to decide whether your slots should be featurized or unfeaturized. A featurized slot can affect the predictions made by the Rasa dialogue management model, meaning the model considers whether or not the slot has been filled when deciding which action to take next. For some slot types, like text slots, the actual value of the slot doesn't matter, only whether it's been filled. For other slot types, like categorical slots, the value does matter. You can see the full list of featurized slot types in the documentation.
Unfeaturized slots don't affect the flow of the conversation. The best practice is to use unfeaturized slots in your forms, unless you have a reason why the slot should be considered by the dialogue management model. In this tutorial, we'll stick to unfeaturized slots.
Which questions to ask...
Once you've told the assistant which slots it should recognize and try to fill, you'll need a way to ask the user for the information. We do this by creating a response template corresponding to each form slot. The response template defines the message the assistant sends to the user, when trying to fill a slot.
Response templates are defined in the domain.yml file, and there's a special naming convention used when the response template is prompting the user for a slot value:
utter_ask__slotname_
For example, if we want to collect the user's name and save it to the user_name
slot, we'd create a response template like this to ask "What is your name?":
utter_ask_user_name:
- text: What is your name?
How to fill each slot...
By default, Rasa Open Source fills a slot with an entity that has the same name. So if you've defined a name
slot in your domain file, and you also have a name
entity defined in your domain and training data, when the NLU model extracts the name entity, it'll be saved to the name slot automatically.
But what if you want to save a slot value that isn't an entity? Remember, you can also save slot values based on intent-either a mapped value like True or the full text of the user message. If a slot should be filled by anything other than an entity of the same name, you'll need to map the slot. Slot mapping creates rules around how a slot should be filled.
Slot mapping is configured by the slot_mappings
function, within the FormAction method. In the next section, we'll discuss how FormAction lets you configure a form's behavior.
FormAction
A form's logic is defined in using an action, which relies on the FormAction method imported from the Rasa SDK.
To use the FormAction method, you'll need to include an import statement at the top of your actions.py file:
from rasa_sdk.forms import FormAction
And then for each form you create, you'll define a new class and pass in FormAction as an argument, as seen here:
class HealthForm(FormAction):
# your form functions go here
Within the class, you'll create several important functions that control the form's behavior:
- Name: gives the form a name,
- Required_slots: specifies which slots are required
- Slot_mappings: tells the assistant how to fill the slots
- Submit: tells the assistant what to do once all of the slots have been filled.
Later in this post, we'll walk through how to create each of these functions.
When to activate and deactivate the form...
When a form is active, the assistant continues to try filling form slots, and when a form is deactivated, the normal conversation flow resumes. A Rasa assistant learns when to activate and deactivate a form the same way it learns how to handle other types of conversation patterns: using stories.
Stories are training data for the dialogue model; they teach the assistant which next action to take based on previous actions. The story data format is like an example conversation script that shows the assistant what action it should take based on what the user says.
From the story training data, the assistant learns to activate or deactivate a form based on user intent. You might have a specific intent to trigger the form like update_account
, where the user asks to update their account. Or, you might want to activate the form based on a conversation pattern, like the user expressing an affirm
intent when the assistant asks "Would you like to take our survey?"
Similarly, stories indicate which intent should trigger a form deactivation. For example, you might want to deactivate the form if the user says something that matches the out-of_scope
intent, like "Why do you want to know that?"
To provide a good user experience, you'll need to create stories for both the happy path, or situations where the user is cooperating and providing all of the requested information, and the unhappy path, situations where the form is interrupted before it's complete. In the case of the unhappy path, you'd want to confirm whether the user wants to continue and deactivate the form if they say no, or resume the form if they do want to continue. Later in the post, we'll add these stories to our wellness-check assistant.
FormPolicy
Another component that plays an important role in activating and deactivating the form is the FormPolicy.
Policies are used by the dialogue management model to determine the assistant's next action. When a form is active, the FormPolicy simply selects the same next action every time: the FormAction. The FormPolicy keeps predicting the FormAction until the form is deactivated. To use forms, you will need to include the FormPolicy in your config.yml file-we'll show you how in the next sections.
Putting it all together: building the Wellness Check bot
We've covered the important background information, and now it's time to build your first Rasa form. Our project is a simple bot that logs a daily wellness check. It asks the user a series of questions about their health to measure how well they're exercising, eating, sleeping, and managing stress.
Here's what a conversation with this assistant looks like:
User: hi there
Bot: Hi! It's time for your daily wellness check. Tracking healthy habits is a great way to measure your progress over time. Would you like to answer a few questions about your health?
User: ok, sure
Did you exercise yesterday? Don't sweat it if you didn't run a marathon - walks count!
User: yes, I went for a run
Bot: How much sleep did you get 💤 last night?
User: about 8 hours
Bot: Did you stick to a healthy diet 🥦 yesterday?
User: Too much junk food, not enough veggies
Bot: What is your stress level right now 🧘 ?
User: Low
Bot: Setting goals - even small ones - is a great way to focus your day. What do you want to accomplish today 🥇 ?
User: Try to get my to-do list done
Thanks, great job!
You've submitted the following answers:
- Exercised?: True
- Type of exercise: run
- Sleep: 8 hours
- Stuck to a healthy diet?: Too much junk food, not enough veggies
- Stress level: low
- Goal: Try to get my to-do list done
Here, we're simply printing the user's replies when the form is complete, but you can imagine a slightly more advanced bot might push the data to a spreadsheet or database, to keep a log of changes over time.
If you want to skip straight to the code, you can find it on GitHub. Let's get started!
Creating a new Rasa project
We'll base the wellness check bot on moodbot, the Rasa starter project. Follow this tutorial to install Rasa Open Source, and then create moodbot using the rasa init
command.
The purpose of moodbot is to provide a scaffold for building a Rasa assistant; it creates the Rasa project structure and files. The files we'll be working with are:
- config.yml
- domain.yml
- data/nlu.md
- data/stories.md
- actions.py
- endpoints.yml
You'll find domain.yml and actions.py in the root of the project directory, and nlu.md and stories.md within the data folder.
Moodbot comes with a few pre-built intents and stories, but we don't really need these in our project. Before you begin, you can head to your nlu.md file and remove 2 intents: ## intent:mood_unhappy
and ## intent:mood_great
, along with all of the training examples below each intent.
Then, in stories.md, remove 3 stories: ## happy path
, ## sad path 1
, and ## sad path 2
.
In the domain file, remove the following from your list of intents at the top of the file:
- mood_great
- mood_unhappy
And go ahead and remove these response templates:
utter_cheer_up:
- text: "Here is something to cheer you up:"
image: "https://i.imgur.com/nGF1K8f.jpg"
utter_did_that_help:
- text: "Did that help you?"
utter_happy:
- text: "Great, carry on!"
Okay - that's it for preparing our project. Now, we're ready to start building the wellness check bot. We'll start by opening the config.yml file.
Config.yml
Locate the list of policies in your config.yml file and add the FormPolicy to the list. Then, you can save and close the file.
policies:
...
- name: FormPolicy
Domain.yml
The next file we'll work with is domain.yml. Domain.yml is a configuration file that describes your assistant's entire "world," that is, all of the intents, entities, actions, slots, responses, etc. that the assistant knows. There are four things we need to do in the domain.yml file:
- Register the intents and entities we'll be creating
- Define our slots
- Create response templates to prompt the user for slot values
- Register the name of the form action we'll be creating
Let's start with intents and entities.
At the very top of the domain.yml file, add three new items to the list of intents:
intents:
...
- inform
- thankyou
- out_of_scope
Below the list of intents, add the list of entities:
entities:
- exercise
- sleep
- stress
Given that our form asks the user six questions, you might be surprised that we're only creating three intents. We'll explain these intents in greater detail when we create our NLU data, but let's focus on the inform intent for a moment. We can think of the inform intent as a general purpose data collection intent. It encompasses all of the things a user might say when they're simply providing information. The out_of_scope
intent encompasses any user messages where the user wishes to exit the form or goes off-topic.
We also have 3 entities. The exercise
entity will be the type of activity the user did for exercise, e.g. running, yoga, or basketball. Sleep
is the amount of sleep the user got, e.g. 8 hours. And stress
will be the user's current stress level, expressed as low, medium, or high.
Next, add a new section below the entities, to define our slots:
slots:
confirm_exercise:
type: unfeaturized
exercise:
type: unfeaturized
sleep:
type: unfeaturized
diet:
type: unfeaturized
stress:
type: unfeaturized
goal:
type: unfeaturized
Each slot corresponds with a form question, six in all. You can see we have three slots with the same names as the entities we just created: exercise
, sleep
, and stress
. As we discussed earlier in this post, these slots will fill automatically when the entity of the same name is extracted by the NLU model. For our other slots, confirm_exercise
, diet
, and goal
, we'll create slot mappings in a later step, to tell the form how to fill them. All slot types here are unfeaturized, meaning that whether they are set or not does not influence the dialogue management predictions.
Next, we'll add response templates for each form question, as well as a few other templates so the bot can properly respond when the form is complete or when the user interrupts the form. Notice that while most of the responses are simple text strings, for stress level
, we're providing buttons. We're also creating a template called utter_slots_values
that echoes all of the slot values back to the user.
utter_ask_confirm_exercise:
- text: Did you exercise yesterday? Don't sweat it if you didn't run a marathon - walks count!
utter_ask_exercise:
- text: What kind of exercise did you do 💪 ?
utter_ask_sleep:
- text: "How much sleep did you get 💤 last night?"
utter_ask_diet:
- text: "Did you stick to a healthy diet 🥦 yesterday?"
utter_ask_stress:
- text: "What is your stress level right now 🧘 ?"
buttons:
- title: "low"
payload: '/inform{"stress": "low"}'
- title: "medium"
payload: '/inform{"stress": "medium"}'
- title: "high"
payload: '/inform{"stress": "high"}'
utter_ask_goal:
- text: "Setting goals - even small ones - is a great way to focus your day. What do you want to accomplish today 🥇 ?"
utter_slots_values:
- text: "You've submitted the following answers:\n
- Exercised?: {confirm_exercise}\n
- Type of exercise: {exercise}\n
- Sleep: {sleep}\n
- Stuck to a healthy diet?: {diet}\n
- Stress level: {stress}\n
- Goal: {goal}"
utter_no_worries:
- text: "No problem :)"
utter_ask_continue:
- text: "Sorry, I don't quite understand. Do you want to continue?"
Lastly, scroll to the end of the file and create a new block below the actions section. This registers the name of the form we'll create in our actions.py file:
forms:
- health_form
NLU.md
Now we'll move on to the nlu.md file, to create training data for the new intents and entities. Recall that we have three new intents we'll need to create, and three new entities.
We'll start with the inform
intent, the intent that includes everything a user might say in response to the form. Add the following to your nlu.md file:
## intent:inform
- a full [8 hours](sleep)
- only [four hours](sleep)
- about [six hours](sleep)
- [low](stress)
- [medium](stress)
- [high](stress) stress right now
- Yes, I went for a [run](exercise)
- uh huh an hour of [yoga](exercise)
- yep, I went for a [hike](exercise)
- Yes I took the dog for a [walk](exercise)
- Yeah, I played [tennis](exercise)
- I went [swimming](exercise)
- Took a [spin class](exercise)
- Did some [boxing](exercise)
- [rock climbing](exercise)
- played some [basketball](exercise)
- played [football](exercise)
- I did a [workout](exercise) video
- I want to get up early and go for a run
- Be more patient with my family
- Try to be a better friend
- Make sure to eat better tomorrow
- eat less junk food
- eat better
- spend less time on my phone
- don't procrastinate
- Go to bed earlier
- Don't stay up so late
- try to make some time to meditate
- i ate pretty healthy overall
- lots of fruits and vegetables
- I ate a salad
- too many snacks
- too many sweets
- too much junk food
- a lot of carbs
- too much fatty food
- Get more exercise
Here, we're providing training examples for all of the questions in the form, and labeling the exercise, sleep, and stress entities using brackets.
The out_of_scope
intent includes all of the things a user could say that indicate they don't want to continue the form, including off topic interjections:
## intent:out_of_scope
- that's not what I want to do
- wait stop
- you're no help
- this is no help at all
- how old are you
- I want to order a pizza
- tell me the weather
- this isn't working
- I already told you that
- don't like that
- I don't want to tell you that
- none of your business
- that's not right
- stop asking
- nevermind
- I want to do something else
- I changed my mind
Add a few more training examples to the intent:affirm
and intent:deny
:
## intent:deny
...
- none
- absolutely not
- no thanks
- didn't do any
- no I didn't
- could have been better
- not great
- nope
## intent:affirm
...
- sure
- ok
- why not
- ok great
- yes I did
- yeah, great
- pretty good
- fine
- You bet
- I sure did
- more or less
And finally, add a thankyou
intent:
## intent:thankyou
- okay great thanks
- thank you
- sure thanks
- thanks bye
Stories.md
Now that we have our NLU data covered, let's provide some training data for the dialogue management model. This shows the model when to trigger the form and what to do once the form has activated, depending on what the user says.
The first story is our happy path. Here, the user affirms they want to take the survey, answers all of the questions, and the conversation ends in goodbye.
## survey happy path
* greet
- utter_greet
* affirm
- health_form
- form{"name": "health_form"}
- form{"name": null}
- utter_slots_values
* thankyou
- utter_no_worries
- utter_goodbye
Let's zoom in on this section, line by line:
* affirm => establishes the intent that should trigger the form
- health_form => calls the form action
- form{"name": "health_form"} => activates the form
- form{"name": null} => deactivates the form
We also need to be able to handle situations where the user declines to take the survey, by adding this story to the file:
## no survey
* greet
- utter_greet
* deny
- utter_goodbye
And we'll add another story for situations where the user starts the form and stops partway through:
## survey stop
* greet
- utter_greet
* affirm
- health_form
- form{"name": "health_form"}
* out_of_scope
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": null}
- utter_goodbye
But what if the user wants to go back to filling out the survey, instead of quitting? We'll add one more story to cover that scenario:
## survey continue
* greet
- utter_greet
* affirm
- health_form
- form{"name": "health_form"}
* out_of_scope
- utter_ask_continue
* affirm
- health_form
- form{"name": null}
- utter_slots_values
With just 4 stories, we've covered most scenarios users might run into when filling the form.
Actions.py
Now, we'll create the health_form
action in the actions.py file.
At the top of the file, import these modules, including the FormAction method from the Rasa SDK:
from typing import Any, Text, Dict, List, Union
from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.forms import FormAction
Below that, define a new class for the form and pass in the FormAction method. Then, we define our first function, which returns the name of the form: health_form
. The name
function is required every time you create a form. You'll recall health_form is the same form name we registered in the domain.yml file, and the action name we used in our stories-it's important that the name matches in all of the places it's referenced.
class HealthForm(FormAction):
def name(self):
return "health_form"
Below the name
function, we define a second function: required_slots
. Like the name function, the required_slots
function is required too. As the name suggests, it configures which slots are required by the form. In addition to specifying the required slots, we're also introducing a bit of conditional logic. The first question we ask the user, "Did you exercise yesterday?," is a yes or no question. If the user answers no, we don't want to ask them what kind of exercise they did. Here, we're telling the form that it should only require the exercise slot if confirm_exercise
evaluates to True, otherwise, we'll skip it.
@staticmethod
def required_slots(tracker):
if tracker.get_slot('confirm_exercise') == True:
return ["confirm_exercise", "exercise", "sleep",
"diet", "stress", "goal"]
else:
return ["confirm_exercise", "sleep",
"diet", "stress", "goal"]
Next, create a third function: slot_mappings
. This function is optional when creating a form. You need to map slots only if you don't want to automatically fill slots with entities of the same name, and want to enforce some other logic.
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 {
"confirm_exercise": [
self.from_intent(intent="affirm", value=True),
self.from_intent(intent="deny", value=False),
self.from_intent(intent="inform", value=True),
],
"sleep": [
self.from_entity(entity="sleep"),
self.from_intent(intent="deny", value="None"),
],
"diet": [
self.from_text(intent="inform"),
self.from_text(intent="affirm"),
self.from_text(intent="deny"),
],
"goal": [
self.from_text(intent="inform"),
],
}
As you can see, we can provide multiple methods for filling a single slot. Take our first slot: confirm_exercise
. There are a few ways a user could respond to "Did you exercise yesterday?" They could say "yes," they could say "no," or they could say "I went for a run," which is essentially an affirmation. This gives us three possible intents that could fill the slot: affirm, intent, and inform. We also map the affirm/deny intents to values: True or False. This lets us create a boolean slot and use the value for conditional logic in our required_slots
function. And as a side note, if the user says "I went for a run," Rasa Open Source will extract the exercise entity and fill the exercise slot at the same time, and skip the follow-up question asking what kind of exercise they did.
With sleep, we could extract the sleep entity (e.g. "8 hours"), or the user could say None, which would match the deny intent. We map the deny intent to the value None, which is printed back to the user on form submission.
Another thing to note is the self.from_text()
method. This method saves the full text of the user's message to the slot. So if we ask "Did you stick to a healthy diet yesterday?" to fill the diet
slot, we can capture a response like "way too much junk food." We take the same approach for the goals slot, where we expect the user to enter a full statement like "I want to go to bed before 10pm."
Finally, we create one last function: the submit
function. This tells the assistant what to do when all of the slots in the form have been filled. In a more advanced assistant, you might want to do something like post data to a database or make an API call. Here, we're going to keep things simple and utter a message: "Thanks, great job!"
def submit(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict]:
dispatcher.utter_message("Thanks, great job!")
return []
Endpoints.yml
Last but not least, we need to make sure the endpoint for the action server is exposed in endpoints.yml. Add the following to your file:
action_endpoint:
url: "http://localhost:5055/webhook"
Testing the Assistant
Time to save all files, and start up the assistant on the command line.
First, train the model by running this command in your terminal:
rasa train
When training finishes, start the action server. Open a second terminal window and run this command:
rasa run actions
Return to the first terminal window and run the following command:
rasa shell
Once the bot has loaded, try having a conversation with the wellness check assistant!
Conclusion
Building AI assistants requires a toolbox of techniques, and forms are one of the most powerful. Forms help you collect multiple pieces of information from the user which you can use to take an action, whether it's posting data to a database, logging the answers to a spreadsheet, or making an API call.
You can find the complete code for this tutorial here.
Now that you've built your first Rasa form, we hope you'll take what you've learned and adapt it to new use cases. As a next step, try modifying some of the slots and response templates in the wellness-check bot to give the bot some personality and collect different pieces of information. Looking for ideas? Try building a contact form, a customer satisfaction survey, or a fun quiz bot.
When you're ready to take the next step and build more complex forms, we have a few resources we recommend:
- Building Contextual Assistants with Rasa Forms (tutorial, restaurant search assistant)
- Tutorial: Building Assistants (tutorial, going from a basic to more advanced assistant)
- Helpdesk Starter Pack (example assistant, form with data validation and API integration)
- Financial Services Starter Pack (example assistant, multiple forms)