Building an end-to-end Conversational Assistant with Rasa

Jagadish Hiremath
8 min readFeb 10, 2021

--

What is Rasa

Rasa is an open-source Conversational AI framework. What I like about Rasa is you are not tied to a pre-built model or use case (Dialogflow etc.). So you can customize it to your use case which can be a market differentiator. Rasa is not a rule-based framework (e.g. Botkit) and you don’t need to worry about putting your data in someone else’s cloud as in Dialogflow, Microsoft LUIS or Amazon Lex.

Rasa has two main components — Rasa NLU and Rasa Core.

NLU is Natural Language Understanding. Suppose the user says “I want to order a book”. NLU's job is to take this input, understand the intent of the user and find the entities in the input. For example, in the above sentence, the intent is ordering and the entity is a book. Rasa NLU internally uses Bag-of-Word (BoW) an algorithm to find intent and Conditional Random Field (CRF) to find entities. Although you can use other algorithms for finding intent and entities using Rasa. You have to create a custom pipeline to do that. Details about the custom pipeline is out of the scope of this post. If you are interested, check out this link.

The job of Rasa Core is to essentially generate the reply message for the chatbot. It takes the output of Rasa NLU (intent and entities) and applies Machine Learning models to generate a reply. We will discuss more the possible Machine Learning models that you can use later during the API discussion.

As seen in the above diagram, the input message is interpreted by a Interpreter extract intent and entity. It is then passed to the Tracker that keeps track of the current state of the conversation. The Policy applies a Machine Learning algorithm to determine what should be the reply and chooses Action accordingly. Action updates the Tracker to reflect the current state.

Building a Chatbot

Suppose we are building a customer service chatbot. Create a directory called customer_bot and create a directory named data inside it. Also, create a configuration file called config_spacy.yml

$mkdir customer_bot
$cd customer_bot
$mkdir data
$vi config_spacy.yml

Copy the contents for the config_spacy.yml file from here. In the config file, we are specifying what type of pipeline we are using to build various models. In this case, we are using Spacy and Scikit-learn to build the pipeline and support only English language as specified in the config like below:

language: "en" 
pipeline: "spacy_sklearn"

You can use other types of pipelines supported by Rasa or you can create your customized model pipeline and specify it in the config.

Let’s understand files that are used to build the project using the Rasa framework.

Building the NLU Model

First, we need to build the NLU model. Rasa NLU works using a supervised learning model. Therefore you need training data to train the NLU model. In the training data, we need to specify what is intent and entity for that data. For example — if the input text to the bot is ‘hi’ you can define the intent as ‘greet’. And there is no entity in this case. So the training dataset looks like below

{        
"text": "hi",
"intent": "greet",
"entities": []
}

You can use my GitHub to copy-paste the entire training data to be used in the code. Name it data.json and put it inside the data folder.

Creating these training data manually is very time-consuming. So instead, you can use this web UI to create data to train the Rasa NLU.

Now using the data.json and config_spacy.yml, we need to train an NLU model. Create nlu_model.py and put it under customer_bot the folder. The NLU model training code below:

We can call the above train_nlu method like below:

if __name__ == '__main__': 
model_directory = train_nlu('./data/data.json', 'config_spacy.yml', './models/nlu')

Let’s see what is going on here. We are loading the training data using load_data. The load_data the function reads the training data and returns a TrainingData object. Then we are creating a Trainer object using the configuration passed through config_spacy.yml. Now using that trainer object, we can actually train the data to create a Machine Learning model — in this case Rasa NLU model which is shown in trainer.train(training_data). Once the model is trained, we need to save the model in a directory. We do that by calling the persist method of the Trainer class. As you see above, in the trainer.persist we are specifying the directory to save the model and assigning our model a name — customernlu.

Now that we have the model trained and save, we can run the NLU model like below:

def run_nlu(model_directory): 
interpreter = Interpreter.load(model_directory)

Building the Dialogue Management Model

Dialogue management is the job of Rasa Core. Before we build the dialogue model, we need to define how we want the conversation to flow. Essentially we are creating a set of training examples for the dialogue model. We will create a file stories.md and put it in the data folder. Let’s look at a sample set of data for stories.md.

Training the dialogue model using stories.md

## story_001
* greet
- utter_greet
* order_product
- utter_ask_product_name
* order_product[router=829]
- slot{"router": "829"}
- action_order_product
* goodbye
- utter_goodbye

Suppose we want a conversation that starts when the user says Hi. We can define the intent as greet. When the bot finds a greet intent, it replies with utter_greet. The content of utter_greet will be defined in a domain file later.

Slots and actions

Let’s take another example. Suppose the user says “I want to order an 829 router”. The bot understands that it is order_product intent and the entity is router. The value of the entity is 829 — meaning there are routers of different product ID but the user is only interested in 829. Therefore router and its value 829 is defined as part of a slot here slot{“router”: “829”}. Slots are essentially your bots memory and defined as a key-value pair. More about slots can be found here. Sometimes the reply message from the bot is not a static message. Rather the bot might need to call some service or perform some other computation to create the content of the reply message. Those are defined through action as seen on action_order_product above. What actually happens in action_order_productwill be defined in actions.py.

The stories.md file for this project can be found here.

Details about stories data format is here.

Defining the domain

We will create an yml file for domain named customer_domain.yml and put it inside customer_bot directory. The domain file specifies slots, intent, entities, actions and templates (sample reply for different utterances such at utter_greet). The domain file for this project can be found here.

actions.py

If the reply messages of your bot are all static messages, you do not need any actions. But most likely that is not the case. Any realistic bot application will communicate with some other services or compute something realtime for at least some of its reply. In our project, once the product is ordered, the bot should reply with a confirmation number. This confirmation number will be different for different user/product. So that product ordering process will be part of the action. For simplicity, in our current code, we are showing a hardcoded confirmation number assuming the product order is successful.

How to write an action in Rasa

You can find actions.py file for this project here.

Training the dialogue model

To train the dialogue model, we will write a function train_dialogue. The function needs 3 parameters — domain file, stories file and a path where you want to save your dialogue model after training. In Rasa, we use Agent class to train a dialogue model. You create an agent object by passing the domain file and specifying a policy. Policies are essentially models. For example KerasPolicy internally uses LSTM network. MemoizationPolicy memorizes the conversations in your training data. Depending on which version of Rasa Core you are using, you might have different types of policies available. More about policies here.

We have to use the train method of the agent object to train using stories.md file. You can specify epochs, batch_size and validation_split as seen above. We will use agent.persist to save the model.

To run the dialogue model, we need an interpreter object as shown in run_customer_bot method. An interpreter does what it says — interprets the text input to the bot. Intuitively, you can probably understand that you need the NLU model that you created earlier to create an interpreter object. Because it is NLU’s job to interpret the incoming text — understand intent and entity. Then you need to create an agent object that takes the interpreter object and the dialogue model that you just created through train_dialogue method. Once you have the agent object, you use agent.handle_channel() to run the bot that will do the conversation. In this case, we are using a Mac terminal as the input/output gateway so we are passing ConsoleInputChannel() as a parameter of agent.handle_channel. Rasa Core supports a few other input channel or you can create you own custom channels. More about interpreter can be found here.

Some of you might have noticed that the dialogue_management_model.py is not 100% reflective of the figure 2. For example, there is no use of Tracker object in the dialogue_management_model.py. This is because figure 2 is reflective of what happens internally, not necessarily what you write in code. You can still use tracker functionalities to know about the current state of the conversation. More about tracker is here.

How to Run

This tutorial was done on a MacBook. Following Rasa documentation install Rasa NLU and Rasa Core. For this tutorial, I have used NLU version 0.12.3 and Core version 0.9.0a6

Download the entire code from my github.

Create the NLU model

$python nlu_model.py

Create the dialogue model. Comment out the run_customer_bot() method inside the __main__. Then run below command.

$python dialogue_management_model.py

Now uncomment the run_customer_bot() method at __main__ and comment out the train_dialogue() method. Then run below again.

$python dialogue_management_model.py

You should now have the bot running in the terminal.

Using the Models as a Service

Once you have the NLU and dialogue model ready, you can run the Rasa Core as a server and communicate with it from a server application

$cd customer_bot
$python -m rasa_core.server -d models/dialogue -u models/nlu/default/customernlu/ --debug -o out.log --cors *

Rasa Core server runs on port 5005 by default. Suppose the user says hello. This user input comes from the front-end to the backend in a NodeJS server. The NodeJS server wants to communicate with the Rasa Core server that is running on port 5005 and wants to get a reply from the Rasa bot. Below sample NodeJS code to do it.

var messageToRasa = 'hello'
request.post(
'http://localhost:5005/conversations/default/respond',
{ json: { 'query': messageToRasa} },
function (error, response, body) {
if (!error && response.statusCode == 200) { //body[0].text is the reply from Rasa
console.log(body[0].text)
}
else{
console.log(`Error: \n${error}`);
}
}
);

Reference:

  1. An excellent tutorial from Justina on how to build a simple Chatbot using Rasa
  2. Some interesting tips while using Rasa.
  3. Demystifying Rasa NLU.
  4. Conditional Random Field (CRF) introduction.
  5. A Practitioner’s Guide to Natural Language Processing here.
  6. Illustrated Guide to LSTM.
  7. More about Rasa Core Policies.

--

--