Rasa Quizbot - Adding Custom Actions for Form Validation (Part 3)
Custom actions in Rasa allow developers to run code to enable the bot do more complex tasks. These actions can connect with databases or APIs to retrieve values based on user input. They can deliver specific pieces of conversation. In the context of our Addy quizbot, we need to use custom actions to validate our form responses. We will use the custom actions to determine whether an answer is correct or not and to keep score. We will also manage the state of our variables.
This article assumes familiarity with the steps outlined in previous articles. You may want to view them before reading this article.
1. Rasa Development: Introduction to Rasa
2. Rasa Quizbot: Setting Up Responses (Part 1)
3. Rasa Quizbot - Adding interaction with forms (Part 2)
If you get stuck or want to preview what we’ll do, take a look at the source code for this article.
Custom actions live in the actions.py in the actions folder. As the file extension suggests, you need to use Python in this file. If you need to import special packages from Python, you can add them to your project and then use the in actions.py.
To get started, we will import a few dependencies.
from typing import Text, List, Any, Dict
from rasa_sdk import Tracker, FormValidationAction, Action
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.types import DomainDict
from rasa_sdk.events import SlotSet, AllSlotsReset
Then, we will build a class of functions to validate the user’s responses to our quiz questions. The name of this form is important. It must start with “validate” and then include the form name. “beginner_quiz_form” becomes “BeginnerQuizForm.” We use put FormValidationAction as an argument for the class to get access to Rasa’s tools for form validation.
class ValidateBeginnerQuizForm(FormValidationAction):
Now we need to set up two things. First, when we initialize the class, we need to set a score variable. This will keep track of the total number of correct answers the user has provided. Second, we need to connect the class to a special action.
class ValidateBeginnerQuizForm(FormValidationAction):
def __init__(self):
self.score = 0
def name(self) -> Text:
return "validate_beginner_quiz_form"
We need to add a slot and an action to domain.yml. Add the score slot to the slots section. This slot doesn’t need to be connected to any form because we are going to set this value using a custom action.
score:
type: rasa.shared.core.slots.TextSlot
initial_value: null
auto_fill: true
influence_conversation: false
Next, add an actions section in domain.yml. Preserve the naming convention that we have used so far – validate + form name.
actions:
- validate_beginner_quiz_form
With our domain.yml set up, we can go back to actions.py to continue building our form validation. First, we need a list of possible answers to our quiz questions. We will have a separate list of answers for each question. In our case, there is only one right answer, but we still may want to account for the various ways users might try to answer. Add the following after the name function.
@staticmethod
def beginnerQ1_db() -> List[Text]:
"""Database of supported responses for beginner Q1"""
return ["2", "two"]
@staticmethod
def beginnerQ2_db() -> List[Text]:
"""Database of supported responses for beginner Q2"""
return ["11", "eleven"]
Now we need to compare the user’s response to the database. Each question needs its own validation function, so we have validate_beginnerQ1 and validate_beginnerQ2. Since each question requires the same validation method, we can put the actual validation method into its own function – common_validation – to avoid duplicate code. However, you can write individual validations for each question as your chatbot project requires.
In the case of Addy, the validation function will always set the value of the slot to whatever the user inputs, regardless of whether it’s right or not. The only difference in common_validation is that a valid response triggers the addition of a point to score. We return the slot values we want to update.
def common_validation(self, slot, val, db, score):
if val.lower() in db:
self.score += 1
return {slot: val, "score": self.score}
else:
return {slot: val}
def validate_beginnerQ1(
self,
slot_value: Any,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: DomainDict,
) -> Dict[Text, Any]:
db = self.beginnerQ1_db()
slot = "beginnerQ1"
update = self.common_validation(slot, slot_value, db, self.score)
return update
def validate_beginnerQ2(
self,
slot_value: Any,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: DomainDict,
) -> Dict[Text, Any]:
db = self.beginnerQ2_db()
slot = "beginnerQ2"
update = self.common_validation(slot, slot_value, db, self.score)
return update
Since we are tracking the score now, we should show users their score after finishing the quiz. We need to add a new utterance. In domain.yml, add utter_quiz_results with the slot value.
utter_quiz_results:
- text: "You scored {score} out of 2 points."
We need to update stories.yml with this utterance. Let’s put it after utter_quiz_finished. Below you can see an example of how a story should look at this point. However, remember, you will need to add the utterance in each story.
stories:
- story: happy path beginner quiz
steps:
- intent: greet
- action: utter_greet
- action: utter_purpose
- action: name_form
- active_loop: name_form
- active_loop: null
- action: utter_ask_quiz_level
- intent: quiz_level
- slot_was_set:
- quiz_level: "beginner"
- action: beginner_quiz_form
- active_loop: beginner_quiz_form
- active_loop: null
- action: utter_quiz_finished
- action: utter_quiz_results
- action: quiz_retake_form
- active_loop: quiz_retake_form
- active_loop: null
- slot_was_set:
- quiz_retake: "false"
- action: utter_goodbye
We’ve created our form validation for the beginner quiz and made the necessary updates to the domain.yml. Now we need to make sure we can run our custom actions. So far, we have used rasa shell to start up our chatbot. However, custom actions run on a separate action server. We need to run our chatbot and action server simultaneously to work together. The action server setting is in endpoints.yml.
Uncomment the action_endpoint entry.
action_endpoint:
url: "http://localhost:5055/webhook"
Now, open another terminal. Navigate to the project directory and start your rasaenv. Type rasa run actions. Your action server will start.
It’s important to note that actions.py is not included in this training. If you make changes to actions.py, you need to restart your action server. If you make changes to the other files, you need to retrain and restart your bot.
The new additions we are trying to include are in domain.yml. So, in the original terminal, type rasa train to update our model with all the work we’ve done. When training has finished, type in rasa shell.
Addy now validates the input, but if you opt to do another quiz, you may notice a couple problems. Addy automatically skips to the end if you select the beginner quiz again. All the slots we filled have retained their values, so Addy thinks we don’t need to respond to them. Also, as we take more quizzes, the score does not reset to zero. To fix these, we need to add two more classes to actions.py.
In each of the classes below, we are getting specific slots and setting the values. In ResetForms, we want the name, quiz_level, and score to stay the same. That way, we can use those slot values in messages to the user. We are resetting all the other slot values we’ve defined to avoid having to write out a reset for each quiz item for both quizzes. In ResetScore, we want to set the score to 0 at the beginning of a quiz. That way, if a user decides to take another quiz, score will be reset to 0.
class ResetForms(Action):
def name(self):
return "reset_forms"
def run(self, dispatcher, tracker, domain):
name = tracker.get_slot('name')
quiz_level = tracker.get_slot('quiz_level')
score = tracker.get_slot('score')
return [AllSlotsReset(), SlotSet("name", name), SlotSet("quiz_level", quiz_level), SlotSet("score", score)]
class ResetScore(Action):
def name(self):
return "reset_score"
def run(self, dispatcher, tracker, domain):
score = tracker.get_slot('score')
quiz_retake = tracker.get_slot('quiz_retake')
return[SlotSet("score", 0), SlotSet("quiz_retake", '')]
Since we’ve added two more actions, we need to update domain.yml. Add the reset_form and reset_score actions to the list in the actions section.
actions:
- validate_beginner_quiz_form
- validate_advanced_quiz_form
- reset_forms
- reset_score
Unlike with our forms, we need to manually trigger these custom actions in our stories. Reset_forms can go after a user completes a quiz because their responses aren’t needed anymore. Reset_score needs to go before a user takes a quiz to erase their previous score and response to the quiz_retake_form (if they had taken a quiz already). The example below only shows the addition of these two functions for the happy path. However, remember that you would need to add these custom actions to each story. Otherwise, they will not activate.
stories:
- story: happy path beginner quiz
steps:
- intent: greet
- action: utter_greet
- action: utter_purpose
- action: name_form
- active_loop: name_form
- active_loop: null
- action: utter_ask_quiz_level
- intent: quiz_level
- slot_was_set:
- quiz_level: "beginner"
- action: reset_score
- action: beginner_quiz_form
- active_loop: beginner_quiz_form
- active_loop: null
- action: reset_forms
- action: utter_quiz_finished
- action: quiz_retake_form
- active_loop: quiz_retake_form
- active_loop: null
- slot_was_set:
- quiz_retake: "false"
- action: utter_goodbye
In one terminal window, restart your action server with rasa run actions. In the other, retrain your model with rasa train and then start Addy with rasa shell. Now Addy will keep accurate scores and will not skip through quizzes on subsequent attempts.
We’ve added the custom action for the beginner quiz. To practice, try to set up the intermediate quiz validation yourself. Test out your bot to see if it works as you expect. You can review the full project code if you get stuck.
We’ve done a lot! Great job with your work on this bot so far. There’s still so much more we can do to improve the bot, though. Check back later for more articles where we refine Addy more.
Project Code: Addy Quizbot 3