Build a Live Traffic WhatsApp Chatbot with Python, Flask, Folium and Twilio

June 02, 2020
Written by
Contributor
Opinions expressed by Twilio contributors are their own

Build a Live Traffic WhatsApp Chatbot with Python, Flask, Folium and Twilio

Like most people I am endlessly frustrated by sitting in slow-moving traffic and I often wonder if I just got unlucky or is it always like this?  In this tutorial I’m going to show you how to answer that question by building a basic chatbot for WhatsApp using the Twilio API for WhatsApp and the Flask framework for Python.

The chatbot will allow users to share their current location and get back a live traffic report and a link to an interactive map.  Here’s an example showing how it works:

traffic chatbot demo

Tutorial requirements

To follow this tutorial you will need the following:

  • Python 3.6 or newer.  You can download an installer from python.org.
  • ngrok. We will use this free utility to connect our Flask application running on our local system to a public URL that Twilio can connect to from the Internet.  Installation instructions for your operating system can be found here.
  • A smartphone with an active phone number and WhatsApp installed.
  • A Twilio account.  If you are a new user, you can create a free account here to get $10 credit when you upgrade.  You can review the features and limitations of a free Twilio account.  When you sign up, ensure you use the same phone number as the one you will use to test this application.

Configure the Twilio WhatsApp sandbox

Twilio provides a WhatsApp sandbox allowing you to easily develop and test your application.  Once you are happy with your application and want to put it into production, you can request access for your Twilio phone number, which requires approval by WhatsApp.

Let’s start by connecting your smartphone to the sandbox.  From your Twilio Console, open the Dock by clicking on the three dots on the left-hand side of the page.  From there, select Programmable SMS and then click on WhatsApp on the left-hand menu.  You should now see the sandbox phone number assigned to your account as below.

twilio sandbox for whatsapp

You will also see a code that starts with join followed by two random words.  To enable the WhatsApp sandbox for your smartphone, send a WhatsApp message with this code to the number assigned to your account.  After a moment, you should receive a reply from Twilio confirming your mobile number is connected and can start sending and receiving messages.

You can also connect additional phone numbers to this sandbox by repeating the same process.

Creating a Python virtual environment

We are now going to start developing our chatbot application.  We will create a separate directory for this project and create a virtual environment using Python’s inbuilt venv module.  We will then install the packages we require inside of it.

If you are using a Unix or Mac OS system, open a terminal and enter the following commands to do the tasks described above:

$ mkdir traffic-bot
$ cd traffic-bot
$ python3 -m venv traffic-bot-venv
$ source traffic-bot-venv/bin/activate
(traffic-bot-venv) $ pip install twilio flask requests folium

Seeing (traffic-bot-venv) prefixed at the start of every command in your terminal confirms you are inside of the virtual environment.

For those of you following the tutorial on Windows, enter the following commands in a command prompt window:

$ md traffic-bot
$ cd traffic-bot
$ python -m venv traffic-bot-venv
$ traffic-bot-venv\Scripts\activate
(traffic-bot-venv) $ pip install twilio flask requests folium

The last command uses pip, the Python package installer, to install the four packages that we are going to use in this project, which are:

For your reference, at the time this tutorial was released these were the versions of the above packages and their dependencies tested:

branca==0.4.1
certifi==2020.4.5.1
chardet==3.0.4
click==7.1.2
Flask==1.1.2
folium==0.11.0
idna==2.9
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
numpy==1.18.4
PyJWT==1.7.1
pytz==2020.1
requests==2.23.0
six==1.14.0
twilio==6.40.0
urllib3==1.25.9
Werkzeug==1.0.1

Create a Flask chatbot service

Now that we have set up our development environment, we can start building our chatbot.

For this tutorial the chatbot will be very simple.  There will be two types of responses depending on the user input.  When the bot receives a normal text-based message, it will reply with a standard greeting.  If it receives a location message, the bot will process this location and reply with traffic information for this location.  We will see later on how we recognise WhatsApp location messages in our application.

Webhook

The Twilio API for WhatsApp uses webhooks in order to interact with users.  A webhook delivers data (in our application this includes the incoming message).  Our application will configure a URL, also referred to as an endpoint, so that Twilio can communicate with this webhook.

The Flask framework makes it easy to define a webhook.  Let’s create a file called app.py in the project directory and start writing some code in this file.  I will go through and explain each section of the code first, then the final script will be available at the end for you to copy if required.  You will be able to run the code snippets as we go along to give you an idea of how we develop the application.

from flask import Flask

app = Flask(__name__)

@app.route('/bot', methods=['POST'])
def bot():
    # all of our application logic goes here
    pass

What we’ve done here is instantiated our Flask application instance and defined a route at the /bot endpoint which supports POST requests.  Directly under that, we’ve written a bot() function (can be named anything) which will process the message sent by the user via WhatsApp and return a response.

In your terminal run flask run, ensuring you are within the virtual environment.  You should see something like this:

(traffic-bot-venv) $ flask run
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

The application is currently running locally on your computer at http://127.0.0.1 (also known as localhost) on port 5000 by default.

In your browser go to http://127.0.0.1:5000/.  You should see a “Not Found” message since we haven’t defined anything for the / route. If you go to http://127.0.0.1:5000/bot you will get a different error: “Method Not Allowed”.  This is because your browser makes a GET request, but we’ve defined the /bot route to only accept POST requests. Press Ctrl-C to stop the Flask application.

More information on the Flask framework can be found in the quick start section of its documentation which should bring you up to speed quickly.

Messages and responses

How does our application recognize the message sent by the user?  It’s included in the payload of the POST request with a key of Body.  In Flask we can access it through the request object.

Once we have the user’s message and applied our logic to formulate a response, we need to send that response back to them.  Twilio expects this response to be written in a particular format known as TwiML or Twilio Markup Language.  This is an XML-based language but we’re not required to create XML directly.  Instead Twilio provides a Python library to make it easy to create our response in the right format.  Here’s an example of the library in use which you can put in the app.py file:

from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse

app = Flask(__name__)

@app.route('/bot', methods=['POST'])
def bot():
    incoming_msg = request.values.get('Body', '')
    print(incoming_msg)

    resp = MessagingResponse()
    msg = resp.message()
    msg.body('the response text goes here')
    return str(msg)

To test this out we can use the curl command to make a POST request to the /bot route (using the -X flag) along with some data as a key-value pair to simulate the incoming message (using the -d flag).  Run the flask run command as before, and then in another terminal run:

curl -X POST -d "Body=incoming test message" http://127.0.0.1:5000/bot

The response to the curl command should be XML output, which is what will be sent to Twilio to tell it how to handle various events.

<?xml version="1.0" encoding="UTF-8"?><Message><Body>the response text goes here</Body></Message>

In the first terminal you should see the value for the Body key printed out as a string.

Data source

We will use the TomTom API as our source of live traffic data.  You can register for an account here which grants you 2500 free API calls per day. This will be plenty for the purposes of testing our application.

After you sign up, you should be taken to the dashboard where you will see a random sequence of letters and numbers which represents your API key.

tomtom api dashboard

You should keep this secure otherwise this will allow anyone to make API requests under your account.

TomTom provides a whole suite of API’s but we will be using the Flow Segment Data endpoint of the Traffic Flow API for this tutorial - full documentation can be found here.

The URL for this endpoint is https://api.tomtom.com/traffic/services/4/flowSegmentData/absolute/10/json.  The documentation states that there are two required parameters, key for the API key and point for the geographic coordinates, given as latitude and longitude separated by a comma.  We can add these parameters in the query string of the URL, so for example, a query for traffic in London could be obtained by sending a request to   https://api.tomtom.com/traffic/services/4/flowSegmentData/absolute/10/json?key=XXXXX&point=51.501084,-0.126459 (you’ll need to enter your own API key here for the key parameter).

You can navigate to this URL in a browser and it will return data in JSON format as a response.  This shows that the key and point parameters are the minimum we need since the remaining parameters have default values. One of the optional parameters is unit, which defaults to kmph for kilometers per hour. If you prefer to receive data in miles per hour, add unit=mph to the query string of the URL.

Since the API key is sensitive we will refrain from hard-coding it, instead saving it as an environment variable and accessing it using os.environ.  Run this command in the terminal:

export TOMTOM_API_KEY="XXXXX"

We’ll use the requests library to access this data in our application.  Let’s test this out by running the following lines in a Python shell (you can open a shell by running python in the terminal):

import os
import requests

lat = 51.508015
lon = -0.128076
params = {'point': f'{lat},{lon}',  'unit': 'mph', 'thickness': 14, 'key': os.environ['TOMTOM_API_KEY']}

base_url = 'https://api.tomtom.com/traffic/services/4/flowSegmentData/absolute/10/json'
data = requests.get(base_url, params=params).json()
print(data)

This makes a GET request to the API with the request parameters defined in a dictionary.  The lat and lon variables hold the latitude and longitude values.  f'{lat},{lon}' uses python string formatting to ensure the coordinates are comma-separated for the point parameter as is required by the API.  Feel free to try out different values for lat, lon and the request parameters.

Here’s a sample version of the outputted JSON data which we’ll be using to construct a reply:

{
    "flowSegmentData": {
        "frc": "FRC3",
        "currentSpeed": 27,
        "freeFlowSpeed": 27,
        "currentTravelTime": 18,
        "freeFlowTravelTime": 18,
        "confidence": 0.9700000286102295,
        "roadClosure": false,
        "coordinates": {
            "coordinate": [
                {
                    "latitude": 51.501427642968466,
                    "longitude": -0.1269413205943124
                },
                {
                    "latitude": 51.5014692096109,
                    "longitude": -0.12678605307615953
                }
            ]
        },
        "@version": "traffic-service 3.2.028"
    }
}

The Response Data section of the documentation explains what all the fields mean.  All the relevant data is nested under the flowSegmentData key.  For example, the frc field indicates the road type, which we can define in a dictionary to obtain a human-readable output.  The coordinates field includes the coordinates to describe the shape of the road segment which we will use when creating a map later on.  In the same Python shell, you can try running these lines of code which uses the same data variable as before:

road_types = {'FRC0': 'Motorway', 'FRC1': 'Major road', 'FRC2': 'Other major road', 'FRC3': 'Secondary road',
              'FRC4': 'Local connecting road', 'FRC5': 'Local road of high importance', 'FRC6': 'Local road'}

if data['flowSegmentData']['roadClosure']:
    reply = 'Unfortunately this road is closed!'

else:
    reply = (f"Your nearest road is classified as a _{road_types[data['flowSegmentData']['frc']]}_.  "
             f"The current average speed is *{data['flowSegmentData']['currentSpeed']} mph* and "
             f"would take *{data['flowSegmentData']['currentTravelTime']} seconds* to pass this section of road.  "
             f"With no traffic, the speed would be *{data['flowSegmentData']['freeFlowSpeed']} mph* and would "
             f"take *{data['flowSegmentData']['freeFlowTravelTime']} seconds*.")

print(reply)

In the code above we have taken advantage of the fact that the roadClosure field returns a boolean indicating whether the road is closed or not.  If this is false, we construct a reply using the frc (via the road_type dictionary), currentSpeed, currentTravelTime, freeFlowSpeed and freeFlowTravelTime fields.

You may have noticed that the reply includes formatting symbols like asterisks and underscores to make some text bold or italic.  This is an example of WhatsApp message formatting supported by Twilio.

This reply string will be included alongside a link to a map in the final response which we will learn how to create in the next section.

Creating a map

The folium package for Python makes it easy to create interactive maps with custom objects overlaid on top.

We will use the coordinates extracted from the user’s location message to center the map on this point using the location argument of the folium.Map function.  As we saw from the JSON snippet above, the TomTom API returns a list of coordinates that define the outline of the road segment nearest to the input location.  We will use the folium.PolyLine function to generate this shape and plot it on the map.  We use a list comprehension to transform the coordinates section of the JSON data into a list of 2-element tuples required by this function as an argument.  Below is an example output of the folium.PolyLine function overlaid on a map.

interactive map

In the same Python shell as before, let’s run the following lines of code:

import folium

points = [(i['latitude'], i['longitude']) for i in data['flowSegmentData']['coordinates']['coordinate']]
print(points)

m = folium.Map(location=(lat, lon), zoom_start=15)
folium.PolyLine(points, color='green', weight=10).add_to(m)

m.save('map.html')

The code above shows that I’ve tweaked the zoom_start and weight arguments to change how the map looks - feel free to play around with these parameters.  You can open the map.html file that was saved to the current directory in your browser to see the interactive map.

In the final application, we will output the map as a HTML file to serve to the user.  This will be done under a separate /map route.

WhatsApp location messages

As a reminder, our simple chatbot will do one of two things: respond with a standard reply to any normal text-based message, otherwise it will generate a reply based on the user’s location.

Location-based messages sent through WhatsApp can be recognized by our application since they will contain ’Latitude’ and ’Longitude’ keys within the payload (in the same way text-based messages contain the ’Body’ key as explained earlier).

Let’s update the bot() function in our app.py file as follows:

from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse

app = Flask(__name__)

@app.route('/bot', methods=['POST'])
def bot():
    resp = MessagingResponse()
    msg = resp.message()

    if 'Latitude' in request.values.keys() and 'Longitude' in request.values.keys():
        lat = request.values.get('Latitude')
        lon = request.values.get('Longitude')

        ### reply logic code based on the `lat` and `lon` values will go here

        msg.body(f'We received these coordinates: ({lat}, {lon})')

    else:
        msg.body('Hello!  This is the Twilio Traffic Bot.  Please share your location to get a live traffic report.')

    return str(resp)

The code above shows that we first check for the existence of these keys in the request payload.  If they do exist we respond with the reply logic defined earlier, otherwise we provide a generic greeting.

We can test this out by using the curl command in the terminal as before.  If flask run is still live then you need to stop and rerun it.  In another terminal run:

curl -X POST -d "Latitude=51.1&Longitude=0.5" http://127.0.0.1:5000/bot 

This should return the following XML:

<?xml version="1.0" encoding="UTF-8"?><Response><Message><Body>We received these coordinates: (51.1, 0.5)</Body></Message></Response>

We can try sending a request without any coordinates:

curl -X POST http://127.0.0.1:5000/bot

This will return:

<?xml version="1.0" encoding="UTF-8"?><Response><Message><Body>Hello!  This is the Twilio Traffic Bot.  Please share your location to get a live traffic report.</Body></Message></Response>

This tests the two possible types of responses that our chatbot will have.

Everything together

We’ve seen all aspects of our chatbot implementation and we are now ready to put it all together in our app.py file.  I’ve created functions for the various sections of the application to make it easier to follow:

  • get_traffic_data - takes location coordinates as arguments (lat and lon) and outputs the JSON data from the TomTom API.
  • create_reply - also takes lat and lon as arguments, calls the get_traffic_data function and generates a reply string based on the data.
  • create_map - this function defines our /map route which outputs the map generated by folium.  The function has no arguments, instead gets the location coordinates from the URL parameters which can be obtained using Flask’s request.args dictionary.  The URL will look something like this: http://127.0.0.1:5000/map?lat=50.84&lon=-1.72

    Then, the get_traffic_data function will be called once again to get the points to draw the road segment as explained previously.  Finally, the function will return the contents of the map file using Folium’s _repr_html_ function.  This allows the map to be displayed to the user interactively.
  • bot - this function defines the /bot route which is the webhook used by the Twilio API as explained previously.  If a location-based message is sent by the user, a reply is generated using the create_reply function and a URL for the map file is created to go alongside the reply.  We use Flask’s url_root function to obtain the relevant URL root e.g. http://127.0.0.1:5000/ when running locally on our computer or https://66d812ba.ngrok.io/ when we deploy our application to the Internet.

The full script is as follows:

import os
import requests
import folium

from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse

app = Flask(__name__)

def get_traffic_data(lat, lon):
    params = {'point': f'{lat},{lon}', 'unit': 'mph', 'thickness': 14, 'key': os.environ['TOMTOM_API_KEY']}
    base_url = 'https://api.tomtom.com/traffic/services/4/flowSegmentData/absolute/10/json'
    data = requests.get(base_url, params=params).json()
    return data

def create_reply(lat, lon):
    data = get_traffic_data(lat, lon)

    road_types = {'FRC0': 'Motorway', 'FRC1': 'Major road', 'FRC2': 'Other major road', 'FRC3': 'Secondary road',
                  'FRC4': 'Local connecting road', 'FRC5': 'Local road of high importance', 'FRC6': 'Local road'}

    if data['flowSegmentData']['roadClosure']:
        reply = 'Unfortunately this road is closed!'

    else:
        reply = (f"Your nearest road is classified as a _{road_types[data['flowSegmentData']['frc']]}_.  "
                 f"The current average speed is *{data['flowSegmentData']['currentSpeed']} mph* and "
                 f"would take *{data['flowSegmentData']['currentTravelTime']} seconds* to pass this section of road.  "
                 f"With no traffic, the speed would be *{data['flowSegmentData']['freeFlowSpeed']} mph* and would "
                 f"take *{data['flowSegmentData']['freeFlowTravelTime']} seconds*.")

    return reply

@app.route('/map')
def create_map():
    lat = request.args.get('lat')
    lon = request.args.get('lon')

    data = get_traffic_data(lat, lon)

    points = [(i['latitude'], i['longitude']) for i in data['flowSegmentData']['coordinates']['coordinate']]

    m = folium.Map(location=(lat, lon), zoom_start=15)
    folium.PolyLine(points, color='green', weight=10).add_to(m)

    return m._repr_html_()

@app.route('/bot', methods=['POST'])
def bot():
    resp = MessagingResponse()
    msg = resp.message()

    if 'Latitude' in request.values.keys() and 'Longitude' in request.values.keys():
        lat = request.values.get('Latitude')
        lon = request.values.get('Longitude')

        reply = create_reply(lat, lon)
        url = f'{request.url_root}map?lat={lat}&lon={lon}'
        msg.body(f'{reply}\n\nCheck out the interactive map here:\n{url}')

    else:
        msg.body('Hello!  This is the Twilio Traffic Bot.  Please share your location to get a live traffic report.')

    return str(resp)

Running the chatbot

We’re now in a position to run our chatbot!   Remember to set your TomTom API key as an environment variable using export TOMTOM_API_KEY="XXXXX".  Start the chatbot by running (or re-running) flask run in your terminal, ensuring your Python virtual environment is still activated.

The application is only running locally on your computer for now.  We can use ngrok to make it reachable from the Internet.  In a new terminal window, run ngrok http 5000 where 5000 corresponds to the default port number for Flask applications.  In your terminal you should see something like this:

ngrok screenshot

The URLs after “Forwarding” are what ngrok uses to redirect requests into our application.  The https:// URL appended with /bot becomes the URL of our webhook which is what Twilio needs to know about.  In my case this URL would be https://19749e81.ngrok.io/bot. The first part of the ngrok URL is different every time you run ngrok.

Let’s go back to the Twilio Sandbox for WhatsApp page on the Twilio Console and paste this URL on the “WHEN A MESSAGE COMES IN” field.  Don’t forget to append /bot at the end of the ngrok URL and to click “Save” at the bottom of the page.

configure twilio whatsapp webhook

Let’s try our chatbot out by sending messages to it from our smartphone.  We can provide different text-based and location-based messages to ensure our application logic works as intended.  Here’s an example session I had with the chatbot:

traffic chatbot demo

Conclusion

In this tutorial we have created a simple but fairly powerful chatbot that returns information about live traffic and an interactive map based on a user’s location.  This was implemented using Flask, Requests, Folium and the Twilio API for WhatsApp.  Our data was obtained from a free traffic API and there is certainly scope to extend the capabilities of our chatbot further.

You could display traffic levels for a greater number of nearby roads by making more calls to the TomTom API for each map.  You could customize the objects on the map by using different colours based on the traffic level or adding labels and icons.  To make the chatbot more expressive, you could use TomTom’s other API’s to display traffic incidents, suggest routes between locations or search for nearby places.

All the code for this tutorial can be found on GitHub here.

I hope you learnt something from this tutorial and are inspired to build some chatbots of your own!

Imran Khan

https://imrankhan17.github.io/