Example for setting up a conversational agent based on Rasa Open Source on a Google Compute Engine instance. The conversational agent in this example interacts with people in 5 conversational sessions.
Based on this Github repository as well as the work by Tom Jacobs.
This virtual coach consists of a backend based on Rasa Open Source (backend), a custom action server (actions), a frontend (frontend), a database (db), an SQLTrackerStore, and Nginx.
To run this project on a Google Compute Engine, I followed these steps:
-
Create a Google Compute Engine instance:
- Use Ubuntu 20.04.
- Make sure that the location is in Europe.
- Enable http and https traffic.
- Choose a small instance for the start, since you have to pay more for larger instances. I started with an e2-medium machine type and 100GB for the boot disk. For my experiment, I used an e2-highmem machine type with 2 vCPUs and 16GB memory but got a notification from Google that the instance was underutilized.
- The first 3 months you have some free credit.
- Follow the instructions from here in the sense that you “allow full access to all cloud APIs” on the Google Compute Engine instance. This is shown in this video: https://www.youtube.com/watch?v=qOHszxJsuGs&ab_channel=JiteshGaikwad. Also see this screenshot:
-
Open port 5005 for tcp on the Compute Engine instance:
-
You now need to install Docker and docker-compose on the instance. You only need to do this once when you first set up your instance. You can do this installation via the command line that opens after you click on "SSH":
- Install Docker on the instance:
sudo apt-get update
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update
apt-cache policy docker-ce
sudo apt-get install -y docker-ce
sudo systemctl status docker
- Install docker-compose on the instance:
- I followed the steps described here:
curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
- You might need to add
sudo
in front of the commands to make them work.
- I followed the steps described here:
- Install Docker on the instance:
-
I suggest getting a static IP address for your Google Compute Engine instance:
- Follow the instructions here.
- You have to pay for every month, but it is rather cheap.
-
Make sure you turn off your instance whenever you do not need it, as you are charged for the time that it is up.
-
Create your own version of this example project (e.g., via a fork).
-
This example project uses Nginx. If you are NOT using Nginx, set the IP address of your Google Compute Engine instance in the function
send(message)
in the file frontend/static/js/script.js:url: "http://<your_instance_IP>:5005/webhooks/rest/webhook"
.- When you run the project locally, use
url: "http://localhost:5005/webhooks/rest/webhook"
.
- When you run the project locally, use
-
Clone your project from Github on the Google Compute Engine instance.
-
Navigate to your project folder on the Compute Engine instance (e.g.,
cd rasa_example_project
). -
If your containers are still running and you want to apply changes you made to the containers, run
docker-compose down --volumes
first to stop the containers. -
Start your project with
docker-compose up --build
from inside the project folder. -
You can check if all your containers are running on your Google Compute Engine instance via
docker container ls
. -
You can access the frontend from your browser via
http://<your_instance_IP>/?userid=<some_user_id>&n=1
.- For example, if your instance's IP is 233.23.33.22 and the
userid
you want to use is Bob22, you can use the URLhttp://233.23.33.22/?userid=Bob22&n=1
. n
determines which session is started (1-5). Earlier sessions need to be completed by a user to be able to access later ones.- This project uses Nginx. If you do NOT use Nginx:
- You also need to specify the port number in the URL:
http://<your_instance_IP>:3000/?userid=<some_user_id>&n=1
. - You also need to open port 3000 on your Google Compute Engine instance for tcp.
- You also need to specify the port number in the URL:
- For example, if your instance's IP is 233.23.33.22 and the
-
Open the chat here:
- The button can be very small on your phone.
-
The chat should look something like this:
-
Right now I have set the code in
frontend/static/css/style.css
such that the chat is always opened in fullscreen. See this code:.widget { display: none; width: 98%; right: 1%; left: 1%; height: 98%; bottom: 2%; position: fixed; background: #f7f7f7; border-radius: 10px 10px 10px 10px; box-shadow: 0px 2px 10px 1px #b5b5b5; }
-
The code by Tom Jacobs instead first opens the chat as a smaller window and adds a "fullscreen"-option to the drop-down used in the code by Jitesh Gaikwad. For example, like this in
script.js
://fullscreen function to toggle fullscreen. $("#fullscreen").click(function () { if ($('.widget').width() == 350) { $('.widget').css("width" , "98%"); $('.widget').css("height" , "100%"); } else { $('.widget').css("width" , "350px"); $('.widget').css("height" , "500px"); } });
-
But then you also need to make sure to add the drop-down to the file index.html:
<div class="chat_header"> <!--Add the name of the bot here --> <span class="chat_header_title">Virtual Coach Mel</span> <span class="dropdown-trigger" href='#' data-target='dropdown1'> <i class="material-icons"> more_vert </i> </span> </div>
<!-- Dropdown menu--> <ul id='dropdown1' class='dropdown-content'> <li><a href="#" id="fullscreen">Fullscreen</a></li> </ul>
-
And further adapt script.js by adding code to
(document).ready(function ()
://drop down menu $('.dropdown-trigger').dropdown();
-
This project uses an SQLTrackerStore to store the conversation history in a database:
-
Make sure to change the default password both in
docker-compose.yml
and inbackend/endpoints.yml
. If your database was already previously up and the "data"-folder hence already exists, just changing the password indocker-compose.yml
and inbackend/endpoints.yml
will not work. You could just remove the "data"-folder before changing the password then. -
A nice way to see the contents of this database is using the program DBeaver.
- First also open port 5432 on your Google Compute Engine instance for tcp. There is no need to restart the instance after opening the port.
- To configure DBeaver, add a new database connection:
- Select a "PostgresSQL" connection.
- Enter your instance's IP address as the "Host", keep the "Port" set to 5432, enter the username and password used in docker-compose.yml, and set the "Database" to "rasa".
- After connecting, you can inspect the database content by clicking on the "events" table:
- After clicking on "Data," you can see the table content. The "sender_id" is the "<some_user_id>" you used when accessing your frontend:
- To refresh the view, you can click on File > Refresh in DBeaver.
- You can also export the data in the database:
-
The database is persistent because of the "volumes" we specified in docker-compose.yml for postgres. Read more about this here.
- So you can run
docker-compose down --volumes
anddocker-compose up --build
and the database content is still there. Check for yourself using DBeaver. - To delete the database content, just remove the "data"-folder on your instance. For example, like this:
cd rasa_example_project
sudo rm -r data
- So you can run
The project further uses an MySQL database to store specific data from the conversations:
- The database is also persistent. The folder "data_mysql" is used for this, as set up in docker-compose.yml.
- To inspect the database content content with DBeaver, first open port 3306 on your instance for tcp. Again, there is no need to restart your instance after opening this port.
- When setting up the connection, use "db" for "Database", "root" for "Username", and the password specified in docker-compose.yml. Keep "Port" to 3306. The "Server Host" is the IP address of your instance.
- You might have to set "allowPublicKeyRetrieval" to "true" in "Driver properties."
- To delete the database content, just delete the folder "data_mysql" on your Google Compute Engine instance.
- Make sure to use a secure password. This needs to be set in both docker-compose.yml and actions/definitions.py. For example, see this post.
Some errors I got during the setup:
-
"Couldn't connect to Docker daemon at http+docker://localhost - is it running? If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable“ when running
docker-compose up –-build
.-
I followed the steps suggested here: https://forums.docker.com/t/couldnt-connect-to-docker-daemon-at-http-docker-localhost-is-it-running/87257/2.
-
These 2 steps fixed the issue for me:
- Run
sudo docker-compose up –-build
.
- Run
-
-
When running the project locally on Windows:
- I got an error for the SQLTrackerStore when running
docker-compose up –-build
. Just removing the information onvolumes
in docker-compose.yml helped. This removes the persistence though. - Since adding nginx, nginx does not work out of the box. To just quickly get the project to work locally, I ignored the nginx part. So I accessed the frontend via "localhost:3000/?..." and changed the url in the file script.js to
url: "http://localhost:5005/webhooks/rest/webhook",
.
- I got an error for the SQLTrackerStore when running
-
My tables in the mysql database were not created after first trying to create the tables with a wrong statement. The post here helped: https://stackoverflow.com/questions/38504257/mysql-scripts-in-docker-entrypoint-initdb-are-not-executed. So this boiled down to deleting the "data_mysql"-folder.
Check the file frontend/static/css/style.css to adapt the styling of the frontend:
-
.chats defines the chat area within the window in fullscreen mode. I tuned the height and width of this.
-
.chat_header_title defines the chat header title. I set the color to #f7f7f7 so that the title is not visible in fullscreen mode. Change the margin-left to align the title to the center. Right now I have fully removed the title though. If you want to add the title again, your file frontend/index.html should contain
chat_header_title
:<!--chatbot widget --> <div class="widget"> <div class="chat_header"> <!--Add the name of the bot here --> <span class="chat_header_title">Your Bot Name</span> </div> <!--Chatbot contents goes here --> <div class="chats" id="chats"> <div class="clearfix"></div> </div> <!--keypad for user to type the message --> <div class="keypad"> <textarea id="userInput" placeholder="Type a message..." class="usrInput"></textarea> <div id="sendButton"><i class="fa fa-paper-plane" aria-hidden="true"></i></div> </div> </div>
-
If you want to change the way that buttons are displayed, adapt
.menu
and.menuChips
in the file style.css.-
For example, you may want to display the buttons like this:
-
This can be done with this code:
.menu { padding: 5px; display: flex; flex-wrap: wrap; } .menuChips { display: inline-block; background: #2c53af; color: #fff; padding: 5px; margin-bottom: 5px; cursor: pointer; border-radius: 15px; font-size: 14px; }
-
Important is that
display: flex
andflex-wrap: wrap
in.menu
. -
To further remove the background of the buttons and add a shadow to the individual buttons instead, set
box-shadow: 2px 5px 5px 1px #dbdade
for.menuChips
and use this code for.suggestions
in the file style.css:.suggestions { padding: 5px; width: 80%; border-radius: 10px; background: #f7f7f7; }
-
Then buttons are displayed like this:
-
See this post for some other ideas for displaying buttons next to each other.
-
Note that by default, buttons are displayed like this:
-
The corresponding code in the file style.css looks like this:
.menu { padding: 5px; } .menuChips { display: block; background: #2c53af; color: #fff; text-align: center; padding: 5px; margin-bottom: 5px; cursor: pointer; border-radius: 15px; font-size: 14px; word-wrap: break-word; }
-
The files in frontend/static/img are used to display the chatbot and the user inside the chat, as well as to display the chatbot when the chat is still closed at the start.
You can use "\n" in your utterances in domain.yml to display a single utterance as two (or more) separate messages. The resulting messages are not treated as separate messages when it comes to displaying the typing symbol though.
You might want to allow also for https traffic:
-
I recommend looking at this tutorial. Compared to allowing only http-traffic, you have to make changes in nginx.conf and docker-compose.yml and create an SSL certificate.
-
For example, this is what the entry for nginx in docker-compose.yml may look like when allowing https traffic:
nginx: container_name: "nginx" image: nginx volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./certs:/etc/certs ports: - 80:80 - 443:443 depends_on: - rasa - action-server - chatbotui
- The folder "certs" on the Google compute engine instance stores the SSL certificate files in this example.
-
See this post for how to create a self-signed SSL certificate.
-
See this page for more information on certificates on Google cloud.
- Info on registering a domain: https://cloud.google.com/dns/docs/tutorials/create-domain-tutorial#register-domain.
- Registering a domain for a year is quite cheap (you can get one for about 8 euros).
- Cloud DNS pricing info: https://cloud.google.com/dns/pricing. You need this if you get a domain and want to use it.
- Info on registering a domain: https://cloud.google.com/dns/docs/tutorials/create-domain-tutorial#register-domain.
- It is not a good idea to just get and use the user name as in this example project. This is because many people reply with things such as "Hi, my name is Mary" when being asked about their name.
- So it might be a good idea to not ask for and use the name at all.
- Some steps to try to improve getting the name:
-
Use an entity and an intent for getting the entity in domain.yml:
intents: - user_name_intent entities: - user_name_entity: influence_conversation: false slots: user_name_slot: type: text initial_value: '' influence_conversation: false mappings: - type: from_entity entity: user_name_entity conditions: - active_loop: user_name_form
-
Create some training data for the intent in the file nlu.yml (remember to create a lot of training data):
nlu: - intent: user_name_intent examples: | - "Hi Mel, I'm [PERSON](user_name_entity)" - "My name is [PERSON](user_name_entity)" - "I'm [PERSON](user_name_entity)" - "[PERSON](user_name_entity)"
-
Use spacy in config.yml. See here for different English language models.
language: en pipeline: - name: SpacyNLP model: en_core_web_lg case_sensitive: false - name: SpacyTokenizer - name: SpacyFeaturizer pooling: mean - name: SpacyEntityExtractor dimensions: - PERSON - name: RegexFeaturizer - name: LexicalSyntacticFeaturizer - name: CountVectorsFeaturizer
-
Now that you use spacy, you also need to adapt the Dockerfile for your backend:
USER root COPY requirements.txt . RUN pip install -r requirements.txt # Spacy language model RUN python -m spacy download en_core_web_lg USER 1001
-
And backend/requirements.txt needs to list
spacy
as a requirement. -
When rasa does not succeed in extracting a slot in the
user_name_form
(e.g., when you try a not-so-typical English name such as "Priyanka"), then an ActionExecutionRejected event is thrown. -
A work-around is to then just store the last user utterance as
user_name_slot
. -
To do this, you might create a rule like this one:
- rule: name session 1 failed fallback condition: - active_loop: user_name_form steps: - intent: nlu_fallback - action: action_get_name_from_last_utterance - action: utter_confirm_name - action: action_deactivate_loop - active_loop: null - action: utter_ask_for_mood_session1
-
The action
action_get_name_from_last_utterance
just gets the text from the last user utterance vialast_user_utterance = tracker.latest_message['text']
and stores this in the slotuser_name_slot
. -
Since you are using spacy, you also need to have spacy installed where you train your rasa model. To do this in an anaconda environment, use
conda install -c conda-forge spacy
. -
And then you also need to download the language model you use.
-
I personally got package version conflicts with rasa 3.2.8, so I used rasa 3.5.3 for the training.
- This also means that I updated the Dockerfile for the custom actions to use
FROM rasa/rasa-sdk:3.3.0
and the Dockerfile for the backend to useFROM rasa/rasa:3.5.3-full
.
- This also means that I updated the Dockerfile for the custom actions to use
-
Now this setup MIGHT allow you to correctly handle responses such as "My name is John" or "Priyanka":
- It is quite difficult to get this setup to work well in all cases:
- For example, the DietClassifier may also extract (wrong) entities, in which case you may need to look through all entities in
tracker.latest_message['entities']
in a custom action and choose the entity for whichentity["extractor"] == "SpacyEntityExtractor"
. - Sometimes, rasa recognizes "user_name_intent" for "Priyanka" but cannot extract an entity.
- If the user uses the chatbot name in their message, sometimes the chatbot name is extracted as the user name.
- For example, the DietClassifier may also extract (wrong) entities, in which case you may need to look through all entities in
- It is quite difficult to get this setup to work well in all cases:
-
Unless you are confident that the user has a common English (or whichever language model you use) name and/or types only their name, I would suggest to not use the user name. In my pilot study, 3 out of 8 people typed more than their name.
-
You could of course also play back to the user what you got as their name and ask them for confirmation, but this might make the virtual coach look rather stupid if what they play back as the user name is "My name is Priyanka".
-
- The frontend is not fully cleaned up yet (i.e., still contains quite some components that are not used by this project).
- The repository by Jitesh Gaikwad (https://github.com/AmirStudy/Rasa_Deployment) also contains code for displaying charts, drop-downs, and images in frontend/static/js/script.js (see the function
setBotResponse
for displaying responses from the rasa bot). I have removed this code in this example project, but if you need to send such kinds of messages, take a look. "--debug"
inbackend/Dockerfile
prints a lot of debugging statements (e.g., for the action prediction). This is handy while you are still developing your agent, but can be removed.- The Developer tools in Google Chrome show the logs from script.js (i.e., the result of
console.log()
)if you access the frontend via Google Chrome. - Think carefully about how you deal with timed-out sessions. You may want to customize the
action_session_start
: https://rasa.com/docs/rasa/default-actions#customization. - If you have made changes and they do not reflect on your Google Compute Engine instance, check if you have run
docker-compose down --volumes
anddocker-compose up --build
. - You might want to prevent people from typing while the chatbot is still sending more messages. You can adapt the file script.js to allow for this using statements such as
$('.usrInput').attr("disabled",true);
and$(".usrInput").prop('placeholder', "Wait for Mel's response.");
- Before running the chatbot on a Google Compute Engine instance for your experiment, make sure to get a paid account. Once the trial period ends or you have used up your free credit your instance will stop. And a billing account might also help to prevent Google from stopping your project when it thinks that you are mining crypto currencies (e.g., see here).
- When using the db, pay attention to closing connections. Also pay attention to the kind of cursor you use when you use fetchone(). It may be good to use a buffered cursor then (e.g., see here).
- You might want to get more detailed logs for your mysql database. See here for a useful discussion.
- You can add
- ./mysql_log:/var/log/mysql
to your mysql volumes in docker-compose.yml. - Create a file called mysql.log in /var/log/mysql in your mysql container after running
docker exec -it [mysql_container_id] /bin/bash
(e.g., viacat > mysql.log
). - Give sufficient permissions to this newly created file (e.g., via
chmod
). - Run
SET global general_log = 1;
,SET global general_log_file='/var/log/mysql/mysql.log';
andSET global log_output = 'file';
(e.g., via the console in DBeaver under SQL Editor > Open SQL console). - Now you can see the logs on your Google Compute Engine instance in mysql_log/mysql.log.
- You can add
- Viewing Google activity logs: https://cloud.google.com/compute/docs/logging/activity-logs.
- Listing sessions/active connections on mysql server: https://dataedo.com/kb/query/mysql/list-database-sessions (e.g., can execute a query in DBeaver).
-
The project uses Rasa 3.2.8. It is important that the Rasa model is trained in the same version. If you want to use a different Rasa version, make sure to also update this in
backend/Dockerfile
andactions/Dockerfile
. Changing the Rasa version might also require changes in the way the Rasa training data is specified. -
I just trained the Rasa model locally in an anaconda environment with Rasa 3.2.8 and Python 3.7. But you can also train the model on the Google Compute Engine instance. For this, you need to install Rasa 3.2.8 (or whichever Rasa version your project uses) on the instance:
-
sudo apt-get update
-
sudo apt install python3-pip
-
sudo pip install rasa==3.2.8
-
Navigate to the
backend
-folder of the chatbot on the instance (e.g.,cd rasa_example_project/backend
) -
Try
rasa train
-
I now got this error:
-
I solved this via
pip install websockets==10.0
(as described here) -
Now try again to run
pip install websockets==10.0
-
-
Now again navigate to the
backend
-folder of the chatbot on the instance (e.g.,cd rasa_example_project/backend
) -
Try running
rasa train
again-
I now got this error:
-
I solved this by running
pip install typing_extensions==4.7.1 --upgrade
(as described here)
-
-
Try running
rasa train
again from inside thebackend
-folder of the chatbot. It now worked for me:
-
-
If you do not see the result of retraining your rasa model in the dialog, it can sometimes help to delete all models and retrain from scratch. You might want to add
--force
to the training command.
Copyright (C) 2023 Delft University of Technology.
Licensed under the Apache License, version 2.0. See LICENSE for details.