Building an end-to-end Conversational Assistant with Rasa
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_product
will 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:
- An excellent tutorial from Justina on how to build a simple Chatbot using Rasa
- Some interesting tips while using Rasa.
- Demystifying Rasa NLU.
- Conditional Random Field (CRF) introduction.
- A Practitioner’s Guide to Natural Language Processing here.
- Illustrated Guide to LSTM.
- More about Rasa Core Policies.