Tutorial: Building Assistants

Tutorial: Building Assistants

After following the basics of setting up an assistant in the Rasa Tutorial, we’ll now walk through building a basic FAQ chatbot and then build a bot that can handle contextual conversations.

Building a simple FAQ assistant

FAQ assistants are the simplest assistants to build and a good place to get started. These assistants allow the user to ask a simple question and get a response. We’re going to build a basic FAQ assistant using features of Rasa designed specifically for this type of assistant.

In this section we’re going to cover the following topics:

You should first install Rasa using the Step-by-step Installation Guide and then follow the Rasa Tutorial to make sure you know the basics.

Setting Up the Assistant

To prepare for this tutorial, we’re going to create a new directory and start a new Rasa project.

mkdir rasa-assistant
rasa init

Let’s remove the default content from this bot, so that the nlu.md, stories.md and domain.yml files are empty.

Memoization Policy

The MemoizationPolicy remembers examples from training stories for up to a max_history of turns. The number of “turns” includes messages the user sent, and actions the assistant performed. For the purpose of a simple, context-less FAQ bot, we only need to pay attention to the last message the user sent, and therefore we’ll set that to 1.

You can do this by editing your config.yml file as follows:

policies:
- name: MemoizationPolicy
  max_history: 1
- name: MappingPolicy

Note: The MappingPolicy is there because it handles the logic of the /restart intent, which allows you to clear the conversation history and start fresh.

Now that we’ve defined our policies, we can add some stories for the goodbye, thank and greet intents to the stories.md file:

## greet
* greet
  - utter_greet

## thank
* thank
  - utter_noworries

## goodbye
* bye
  - utter_bye

We’ll also need to add the intents, actions and responses to our domain.yml file in the following sections:

intents:
  - greet
  - bye
  - thank

responses:
  utter_noworries:
    - text: No worries!
  utter_greet:
    - text: Hi
  utter_bye:
    - text: Bye!

Finally, we’ll copy over some NLU data from Sara into our nlu.md file (more can be found here):

## intent:greet
- Hi
- Hey
- Hi bot
- Hey bot
- Hello
- Good morning
- hi again
- hi folks

## intent:bye
- goodbye
- goodnight
- good bye
- good night
- see ya
- toodle-oo
- bye bye
- gotta go
- farewell

## intent:thank
- Thanks
- Thank you
- Thank you so much
- Thanks bot
- Thanks for that
- cheers

You can now train a first model and test the bot, by running the following commands:

rasa train
rasa shell

This bot should now be able to reply to the intents we defined consistently, and in any order.

Response Selectors

The ResponseSelector NLU component is designed to make it easier to handle dialogue elements like Small Talk and FAQ messages in a simple manner. By using the ResponseSelector, you only need one story to handle all FAQs, instead of adding new stories every time you want to increase your bot’s scope.

People often ask Sara different questions surrounding the Rasa products, so let’s start with three intents: ask_channels, ask_languages, and ask_rasax. We’re going to copy over some NLU data from the Sara training data into our nlu.md. It’s important that these intents have an faq/ prefix, so they’re recognised as the faq intent by the ResponseSelector:

## intent: faq/ask_channels
- What channels of communication does rasa support?
- what channels do you support?

## intent: faq/ask_languages
- what language does rasa support?

## intent: faq/ask_rasax
- I want information about rasa x

Next, we’ll need to define the responses associated with these FAQs in a new file called responses.md in the data/ directory:

## ask channels
* faq/ask_channels
  - We have a comprehensive list of [supported connectors](/content/docs/core/connectors/index.html), but if you don't see the one you're looking for, you can always create a custom connector by following [this guide](/content/docs/rasa/user-guide/connectors/custom-connectors/index.html).

## ask languages
* faq/ask_languages
  - You can use Rasa to build assistants in any language you want!

## ask rasa x
* faq/ask_rasax
 - Rasa X is a tool to learn from real conversations and improve your assistant. Read more [here](/content/docs/rasa-x/index.html)

The ResponseSelector should already be at the end of the NLU pipeline in our config.yml:

language: en
pipeline:
  - name: WhitespaceTokenizer
  - name: RegexFeaturizer
  - name: LexicalSyntacticFeaturizer
  - name: CountVectorsFeaturizer
  - name: CountVectorsFeaturizer
    analyzer: "char_wb"
    min_ngram: 1
    max_ngram: 4
  - name: DIETClassifier
    epochs: 100
  - name: EntitySynonymMapper
  - name: ResponseSelector
    epochs: 100

Now that we’ve defined the NLU side, we need to make Core aware of these changes. Open your domain.yml file and add the faq intent:

intents:
  - greet
  - bye
  - thank
  - faq

We’ll also need to add a retrieval action, which takes care of sending the response predicted from the ResponseSelector back to the user:

actions:
  - respond_faq

Next we’ll write a story so that Core knows which action to predict:

## Some question from FAQ
* faq
    - respond_faq

After all of the changes are done, train a new model and test the modified FAQs:

rasa train
rasa shell

At this stage it makes sense to add a few test cases to your test_stories.md file again:

## ask channels
* faq: What messaging channels does Rasa support?
  - respond_faq

## ask languages
* faq: Which languages can I build assistants in?
  - respond_faq

## ask rasa x
* faq: What’s Rasa X?
  - respond_faq

Using the features we described in this tutorial, you can easily build a context-less assistant. When you’re ready to enhance your assistant with context, check out Building a contextual assistant.

Handling business logic

A lot of conversational assistants have user goals that involve collecting a bunch of information from the user before being able to do something for them. This is called slot filling. For example, in the banking industry, you may have a user goal of transferring money, where you need to collect information about which account to transfer from, whom to transfer to and the amount to transfer. This type of behavior can and should be handled in a rule-based way, as it is clear how this information should be collected.

For this type of use case, we can use Forms and our FormPolicy. The FormPolicy works by predicting the form as the next action until all information is gathered from the user.

Defining the Form

We will start by defining the SalesForm as a new class in the file called actions.py. The first method we need to define is the name, which like in a regular Action returns the name that will be used in our stories:

from rasa_sdk.forms import FormAction

class SalesForm(FormAction):
    """Collects sales information and adds it to the spreadsheet"""

def name(self):
        return "sales_form"

Next we have to define the required_slots method which specifies which pieces of information to ask for, i.e. which slots to fill.

@staticmethod
def required_slots(tracker):
    return [
        "job_function",
        "use_case",
        "budget",
        "person_name",
        "company",
        "business_email",
    ]

Note: You can customize the required slots function not to be static. E.g. if the job_function is a developer, you could add a required_slot about the user's experience level with Rasa.

Once you’ve done that, you’ll need to specify how the bot should ask for this information. This is done by specifying utter_ask_{slotname} responses in your domain.yml file. For the above we’ll need to specify the following:

utter_ask_business_email:
  - text: What's your business email?
utter_ask_company:
  - text: What company do you work for?
utter_ask_budget:
  - text: "What's your annual budget for conversational AI? 💸"
utter_ask_job_function:
  - text: "What's your job? 🕴"
utter_ask_person_name:
  - text: What's your name?
utter_ask_use_case:
  - text: What's your use case?

We’ll also need to define all these slots in our domain.yml file:

slots:
  company:
    type: unfeaturized
  job_function:
    type: unfeaturized
  person_name:
    type: unfeaturized
  budget:
    type: unfeaturized
  business_email:
    type: unfeaturized
  use_case:
    type: unfeaturized

Going back to our Form definition, we need to define the submit method as well, which will do something with the information the user has provided once the form is complete:

def submit(
        self,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> List[Dict]:

dispatcher.utter_message("Thanks for getting in touch, we’ll contact you soon")
    return []

In this case, we only tell the user that we’ll be in touch with them, however usually you would send this information to an API or a database. See the rasa-demo for an example of how to store this information in a spreadsheet.

We’ll need to add the form we just created to a new section in our domain.yml file:

forms:
  - sales_form

We also need to create an intent to activate the form, as well as an intent for providing all the information the form asks the user for. For the form activation intent, we can create an intent called contact_sales. Add the following training data to your nlu file:

## intent:contact_sales
- I wanna talk to your sales people.
- I want to talk to your sales people
- I want to speak with sales
- Sales
- Please schedule a sales call
- Please connect me to someone from sales
- I want to get in touch with your sales guys
- I would like to talk to someone from your sales team
- sales please

You can view the full intent here)

We will also create an intent called inform which covers any sort of information the user provides to the bot. The reason we put all this under one intent is because there is no real intent behind providing information; only the entity is important. Add the following data to your NLU file:

## intent:inform
- [100k](budget)
- [100k](budget)
- [240k/year](budget)
- [150,000 USD](budget)
- I work for [Rasa](company)
- The name of the company is [ACME](company)
- company: [Rasa Technologies](company)
- it's a small company from the US, the name is [Hooli](company)
- it's a tech company, [Rasa](company)
- [ACME](company)
- [Rasa Technologies](company)
- [maxmeier@firma.de](business_email)
- [bot-fan@bots.com](business_email)
- [maxmeier@firma.de](business_email)
- [bot-fan@bots.com](business_email)
- [my email is email@rasa.com](business_email)
- [engineer](job_function)
- [brand manager](job_function)
- [marketing](job_function)
- [sales manager](job_function)
- [growth manager](job_function)
- [CTO](job_function)
- [CEO](job_function)
- [COO](job_function)
- [John Doe](person_name)
- [Jane Doe](person_name)
- [Max Mustermann](person_name)
- [Max Meier](person_name)
- We plan to build a [sales bot](use_case) to increase our sales by 500%.
- We plan to build a [sales bot](use_case) to increase our revenue by 100%.
- A [insurance tool](use_case) that consults potential customers on the best life insurance to choose.
- We're building a [conversational assistant](use_case) for our employees to book meeting rooms.

Note: Entities like business_email and budget would usually be handled by pretrained entity extractors (e.g. DucklingHTTPExtractor or SpacyEntityExtractor), but for this tutorial we want to avoid any additional setup.

The intents and entities will need to be added to your domain.yml file as well:

intents:
  - greet
  - bye
  - thank
  - faq
  - contact_sales
  - inform

entities:
  - company
  - job_function
  - person_name
  - budget
  - business_email
  - use_case

A story for a form is very simple, as all the slot collection form happens inside the form, and therefore doesn’t need to be covered in your stories. You just need to write a single story showing when the form should be activated. For the sales form, add this story to your stories.md file:

## sales form
* contact_sales
    - sales_form                   <!--Run the sales_form action-->
    - form{"name": "sales_form"}   <!--Activate the form-->
    - form{"name": null}           <!--Deactivate the form-->

As a final step, let’s add the FormPolicy to our config file:

policies:
  - name: MemoizationPolicy
  - name: KerasPolicy
  - name: MappingPolicy
  - name: FormPolicy

At this point, you already have a working form, so let’s try it out. Make sure to uncomment the action_endpoint in your endpoints.yml to make Rasa aware of the action server that will run our form:

action_endpoint:
 url: "http://localhost:5055/webhook"

Then start the action server in a new terminal window:

rasa run actions

Then you can retrain and talk to your bot:

rasa train
rasa shell

This simple form will work out of the box, however you will likely want to add a bit more capability to handle different situations. One example is validating slots, to make sure the user provided information correctly (read more about it here).

Another example of defining slots

You can use the slot_mappings method to describe what your entities should be extracted from. This can be useful if you expect the user to complete questions where entities don’t match perfectly, and allows for flexibility in how the slot data is collected:

def slot_mappings(self) -> Dict[Text, Union[Dict, List[Dict[Text, Any]]]]:
    return {
        "use_case": self.from_text(intent="inform")
    }

Now our bot will extract the full user message when asking for the use case slot, and we don’t need to use the use_case entity defined before.

All of the methods within a form can be customized to handle different branches in your business logic. Read more about this here. However, you should make sure not to handle any unhappy paths inside the form. These should be handled by writing regular stories, so your model can learn this behavior.

Handling unexpected user input

All expected user inputs should be handled by the form we defined above, i.e. if the user provides the information the bot asks for. However, in real situations, the user will often behave differently. In this section we’ll go through various forms of “interjections” and how to handle them within Rasa.

Out of scope intent

It is good practice to also handle questions you know your users may ask, but for which you haven’t necessarily implemented a user goal yet.

You can define an out_of_scope intent to handle generic out of scope requests, like “I’m hungry” and have the bot respond with a default message like “Sorry, I can’t handle that request”:

* out_of_scope
  utter_out_of_scope

We also need to add NLU data for the out_of_scope intent:

## intent:out_of_scope
- I want to order food
- What is 2 + 2?
- Who’s the US President?
- I need a job

And finally, we’ll add a response to our domain.yml file:

responses:
  utter_out_of_scope:
  - text: Sorry, I can’t handle that request.

Now you can retrain and test this addition:

rasa train
rasa shell

Failing gracefully

Even if you design your bot perfectly, users will inevitably say things to your assistant that you did not anticipate. In these cases, your assistant will fail, and it’s important you ensure it does so gracefully.

Wrap Up

Make sure to add end-to-end stories and test cases to ensure the reliability of your assistant. Getting your structure right will enable you to smoothly scale your assistant’s capabilities without losing the thread of your conversation.

By following the outlined steps in building an FAQ assistant and handling contextual conversations, you will have a robust assistant ready to accommodate user input efficiently.