Create a Question-Answering Chatbot with Fetchable and Rasa

Andrew Murtagh
Fetchable
Published in
12 min readSep 10, 2019

--

In this tutorial, we will be building a chatbot that can answer questions from users in much the same way that Amazon Alexa or Google Assistant does. We will be using Rasa as the conversational AI framework and Fetchable as the data provider.

TL;DR: finished code is available at https://github.com/fetchableai/example-rasa-chatbot.

The intended audience for this tutorial are developers who are already familiar with Rasa and are searching for a way to add information to their chatbots. We will be developing in Python 3.6 on Ubuntu 18.04 and prior knowledge of both will be assumed.

Shell code is denoted with a $, Python is denoted with a shebang and all other code snippets are in the Rasa markdown format.

1. Architecture 📐

The chatbot we will develop throughout this tutorial will be able to perform the following functions:

  1. Say hello and goodbye to users.
  2. Tell jokes.
  3. Give random fun facts.
  4. Give inspirational quotes.
  5. Provide definitions for words in the English language.
  6. Answer questions on random trivia, e.g. the length of the Amazon river, the height of the Empire State Building, the population of London, etc.

Fetchable is an information retrieval API for AI; we will be using their free Alpha version as the source of information for the above functions. People can make requests against the Fetchable API for all kinds of information and receive just that data back in a JSON or XML format.

In order to call their API, we will need to write an action server. This will define classes for each of the above functions — except hello and goodbye which Rasa will handle itself without the need for our code. Within each class, we will write a function to extract any entities from the intent received from the user, construct an API request for Fetchable and handle the response we get back from it. This function will be invoked by Rasa after receiving the relevant intent from the user.

For the trivia functionality of our chatbot (e.g. getting the length of the Amazon river, the height of the Empire State Building, etc., etc.) we will use the Entity-Attribute endpoint of the Fetchable API. This terminology borrows from Object Oriented Programming where everything is modeled as an object (entity) with properties (attributes). So for the examples above, the Amazon river entity has a length attribute which we can get the value of by making a GET request to the endpoint: /amazon/length; with the same applying for the height of the Empire State Building: /empire_state_building/height.

The general architecture, represented as stages in the chatbot pipeline, is given below:

  1. Free-text statements entered into the command line are read by the Rasa shell program (which also runs the Rasa Core module — visit the Rasa docs for more information on the Rasa architecture).
  2. The statement is mapped to one of the pre-defined intents (performed by the Rasa NLU module).
  3. Any entities within the statement are extracted (Rasa NLU).
  4. Based on the intent received, and the current state of the conversation, the most fitting action to take next is selected and the action server is called to run the corresponding function (Rasa Core).
  5. Our custom code on the action server makes a request to Fetchable, interprets the response and returns a text answer to the Rasa shell program.
  6. The output text is displayed in the command line.
  7. The process repeats as the conversation goes on.

2. Requirements 📋

  • Python 3.6
  • Rasa 1.2.6
  • Fetchable account using version v0.1 of the API.
  • Fetchable Python client-side SDK 0.1.5

3. Installation 📦

3.1 Rasa

Let’s get Rasa installed, full instructions can be found here but these commands should work on most systems; they should also work with pip.

$ pip3 install rasa
$ pip3 install rasa_sdk

We must also install a language model so that Rasa can understand the language we are telling it to work with.

$ pip3 install rasa[spacy]
$ python3 -m spacy download en_core_web_md
$ python3 -m spacy link en_core_web_md en

Easy! Let’s get Fetchable installed next.

3.2 Fetchable

First step is to set up an account here. Once you log in, navigate to ‘My account’ on the left (Fig. 1) and under the ‘download authentication file’ title hit the download button; note the location the file was downloaded to.

Fig. 1: ‘My account’ page on the Fetchable console.

The authentication file contains your API keys, which identify and authenticate your requests with the API. There are a few ways to bring your API keys into your application but the easiest way is to set the absolute path to the file as an environment variable with the code below. The Fetchable SDK will automatically attempt to read them upon initialization.

$ export FETCHABLE_AUTH_FILE=/path/to/fetchable_auth_keys.json

Note: Be sure to keep your authentication file secure, and be sure to regenerate new API keys if it becomes comprised.

After this, we can install the Fetchable Python client-side SDK to simplify interacting with the API. It can be installed from source at https://github.com/fetchableai/fetchable-python; the recommended way is by using pip3 (or pip):

$ pip3 install fetchable

4. Setting up Rasa 💯

With the following commands, we’ll create a workspace, cd into it and use the Rasa CLI to create the default chatbot provided out-of-the-box by Rasa.

$ mkdir workspace
$ cd workspace
$ rasa init --no-prompt

The default chatbot they provide contains some boilerplate code that will ask you how you’re feeling and send you a cute picture if you’re feeling sad. We can start the chatbot and interact with it by running the command below and entering our statements into the command line once it has loaded.

$ rasa shell

A sample interaction with the chabot is shown in Fig. 2.

Fig. 2: Sample interaction with the default Rasa chatbot.

5. Modifying the Rasa Chatbot ✒️

Now that we have the default Rasa chatbot set up, we’ll start modifying the code to make our own custom chatbot.

Note: We’ll only give snippets here for succinctness; but the full, commented code is available on the Github page for this tutorial at https://github.com/fetchableai/example-rasa-chatbot.

5.1 Intents

In data/nlu.md we’ll remove all but the greet and goodbye intents and add in the ones relevant to us. As an example, the intent describing how a user asks for trivia (i.e. the attribute of an entity) looks like:

## request_trivia
- what is the [height](attribute) of [everest](entity)
- what is the [population](attribute) of [england](entity)
- what is the [elevation](attribute) of [mount everest](entity)
- what is the [length](attribute) of the [amazon](entity)
- what is the [length](attribute) of the [amazon river](entity)

This will map any user input resembling those above to the request_trivia intent. Also in this definition is an example of entity extraction, Rasa will automatically extract whatever appears in the square brackets and allow us to access it later in our action server under the name provided in the round brackets. Using this feature allows us to determine what entity, and what attribute of said entity, the user is requesting; and therefore allow us to construct an API request to fetch that information from Fetchable.

Note: If you really want to improve the performance of your chatbot, using a tool like https://rodrigopivi.github.io/Chatito/ to help create many variants of each intent can yield great results.

5.2 Stories

From the training stories defined in data/stories.md, the chatbot learns which action to take in response to the intent that was inferred during the first stage of the Rasa pipeline.

Given below are two example stories for the above intent; two possible paths the interaction could take are given. The ## denotes the name of the story, the * denotes the intent received from the user and the - denotes the corresponding action to take.

In the first path the user immediately asks the chatbot for the attribute of an entity and the chatbot responds by fetching the value from Fetchable and returning it. In the second path: the user says hello with the chatbot responding in kind, the user then asks for the attribute of an entity and the chatbot provides the value, the user then says goodbye and the chatbot utters goodbye back to the user. Providing multiple paths the interaction could possibly take helps improve the accuracy and flexibility of your chatbot.

## trivia_path_one
* request_trivia
- action_fetch_trivia
## trivia_path_two
* greet
- utter_hello
* request_trivia
- action_fetch_trivia
* goodbye
- utter_goodbye

Note: Using Rasa X to interactively train and test your chatbot saves a lot of time manually codifying every path the conversation could take and greatly simplifies the training process.

5.3 Domain

The domain file is where we put everything together. In this file, we declare all intents, entities, slots (not used), actions, and utterance templates with Rasa. The domain file acts much like a header file in other programming languages where variables/functions are only declared and their implementation is defined elsewhere.

The intents are simply transferred over from the data/intents.md file. There is one intent for ‘hello’, one for ‘goodbye’, and the remaining intents each relate to a request for a different piece of information, e.g. a joke, the definition of a word, a fun fact, etc.

Below this, three entities are declared. These allow us to extract from intents the exact word the user is looking for the meaning of; as well as the entity (e.g. the Amazon river) and attribute (e.g. length) the user is requesting the value of.

Actions are declared below this. There are two types of actions: those beginning with utter_ are simple pre-defined text statements that will be returned to the user automatically by Rasa, there is no code needed to perform this action. Those beginning with action_ require code to be executed first before a text statement can be constructed and returned to the user. The action server will be called to handle these types of actions and after executing the code to fetch the data from Fetchable, will return the full text statement back to Rasa for display on the command line.

Lastly, templates are defined. There is one template for each of utter_ actions declared above. When these actions are invoked, Rasa will automatically choose a random statement from the corresponding template to send back to the user. Entering multiple choices helps your chatbot sound more natural and less robotic.

intents:
- hello
- goodbye
- request_joke
...
entities:
- word
- entity
- attribute

actions:
- utter_hello
- utter_goodbye
- action_fetch_joke
...
templates:
utter_hello:
- text: "Hello. What would you like to know?"
- text: "Hi there, ask me a question."
...

5.4 Adding Actions

In this section, we will write the action server code that will be executed once an action is called. As with the above sections, we will only discuss the trivia functionality in this tutorial but the full, commented code covering the other actions is available at https://github.com/fetchableai/example-rasa-chatbot. We will also remove function annotations here for the sake of readability.

For now, our code will just return a placeholder message of “Here’s some trivia” back to the user and in the next section we’ll integrate Fetchable to return actual data back to the user.

In the actions.py file we will add the boilerplate code listed below to handle the action for fetching trivia. Let’s firstly import the Rasa dependencies. We’ll then define a class to handle a specific action (this will need to be repeated for each action). Within the class, we expose a function that will return the exact name of the action the class will handle. Below that, we define a run function that will be invoked once Rasa calls this action.

The first parameter to this function (dispatcher) is an object that will help construct and format the JSON response that will be sent back to Rasa Core. We will use it's utter_message function to send back a simple string literal — but we could use it to do more complex things like utter a template. The second parameter (tracker) is an object containing information about the current state of the conversation. In the next section, we will use it to access the value of any entities that Rasa NLU extracted from the intent. The third parameter is not used in this example.

#!/usr/bin/python3from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
class ActionFetchTrivia(Action): def name(self):
return "action_fetch_trivia"
def run(self, dispatcher, tracker, domain):
dispatcher.utter_message("Here's some trivia")
return []

Before we can use this, we need to tell Rasa Core how to communicate with the action server we have just built. Let’s comment out these two lines from the endpoints.yml file to do so.

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

5.5 Training Rasa

With all this is in place we can now train our new chatbot with the command:

$ rasa train

To run our new chatbot, we’ll need to open two terminals. In one terminal we’ll run rasa run actions and in the other we’ll run rasa shell --endpoint endpoints.yml.

A sample interaction in which we ask our new chatbot for the height (attribute) of the Empire State Building (entity) is given in Fig. 3. We can see the placeholder text statement being returned to the user. Let’s now look at how to integrate Fetchable so we can fill that message with actual data.

Fig. 3: Output of interaction with placeholder response.

6. Integrating Fetchable 🔥

Now that we’ve handled the Rasa side, we’ll modify the code to fetch real data from Fetchable.

We’ll return to actions.py and make the following changes: we’ll import the Fetchable dependencies at the top of the file and below that, initialize a client object which simplifies making requests to the Fetchable API. This object exposes convenience methods and handles authentication and options behind the scenes. Upon initialization, the client object will search for the environment variable we exported in step 3.2; if it cannot find this, the object will throw an exception, so we’ll surround it with a try/except statement to catch and handle the exception.

In this step, we will also configure it to use the latest version of the API by importing the configuration class and setting the API version in the constructor of the client object to the latest version, as defined in the configuration class. In future versions, this class will also allow you to easily set the desired response type (JSON, XML, Plain Text, etc.), the desired language, units, and plenty of other options to customize your interaction with Fetchable.

#!/usr/bin/python3...from fetchable import FetchableClient
from fetchable import configuration
try:
client = FetchableClient(api_version = configuration.api_version.latest)
except:
print("something went wrong initialising the Fetchable client")

Below this, we’ll modify the class we defined in section 5.4. In the run function, we’ll firstly use the tracker parameter to fill an array with whatever entities were extracted from the intent. We’ll iterate through the array, look for the entities relevant to this particular action and assign their value to variables.

Next, we’ll perform a quick sanity check to ensure both the variables were set properly. If this is the case, we’ll call the fetch_entity_attribute method from the Fetchable client object and pass in the entities that were extracted. We’ll assign whatever response we get back to a variable.

In every case the response object from the Fetchable library will contain a status_code attribute. We’ll check it’s value for a successful status code (200) and if that is the case, we’ll go ahead and construct a message to send back to the user containing the answer in a natural manner, e.g “The height of the Empire State Building is 381 meters”. If the response was not successful, we’ll return an error message back to the user; the Github code contains more error checking here.

#!/usr/bin/python3...class ActionFetchTrivia(Action):    def name(self):
return "action_fetch_trivia"
def run(self, dispatcher, tracker, domain):

entities = tracker.latest_message['entities']
for e in entities:
if e['entity'] == "entity":
entity = e['value']
elif e['entity'] == "attribute":
attribute = e['value']

if entity and attribute:
response = client.fetch_entity_attribute(entity, attribute) if response['status_code']==200: dispatcher.utter_message("The {} of {} is {} {}.".format(attribute, entity, response['value'], response['unit'])) else:
dispatcher.utter_message("Something has gone wrong.")

Note: Make sure the FETCHABLE_AUTH_FILE environment variable has been exported before running this code.

Running the chabot with the same commands as before outputs something like:

Fig. 4: Output of final chatbot once Fetchable is integrated.

We then repeat this for every other action and we have ourselves a working chatbot! The code for the other actions, as well as everything else, can be found on the Github page for this tutorial at https://github.com/fetchableai/example-rasa-chatbot.

7. Conclusion 🎁

In this tutorial, we have built a chatbot to tell jokes, give fun facts, inspirational quotes, word definitions and trivia. Rasa was used as the chatbot framework and Fetchable for the data source. Using both technologies made it incredibly easy to build something respectable with little development effort.

The possibilities opened up by putting powerful AI, massive compute power, cheap storage, and petabytes of data into the hands of millions of developers are amazing; and we’re excited to see what great things people will invent with these kinds of technologies in the future.

Written by Andrew Murtagh on Sept 10, 2019.

Building Artificial Intelligence?

Fetchable is the search engine for Artificial Intelligence.

Use the Fetchable API in any language to get the world’s data in JSON format — weather, traffic, people, places, trivia, recipes, news, sports results and a lot more.

We released our Alpha version in September 2019 and it’s completely free! 🎉

Start building at https://fetchable.ai.

--

--