Build John Mayer's Customer Service Line with Twilio Voice and Python

January 26, 2021
Written by
Chris Hranj
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

header - Build John Mayer's Customer Service Line with Twilio Voice and Python

A while back, John Mayer tweeted an oddly brilliant idea.

John Mayer tweet saying "They should let everyone on hold with customer service talk to one another."

I recently came across that tweet again and realized that I could build this system using Twilio. In this blog post, we are going to build a simple call center with Twilio Programmable Voice and Python that allows callers to talk to one another until connected with an agent.

If you’d just like to see the finished code, it can be found on Github here.

Get Started

NOTE: If you are confused about anything regarding setting up a Python environment for Twilio apps, refer to this Twilio guide.

This call center is going to use the Flask microframework, which means you’ll need to have Python installed locally. Instructions to download Python can be found here. Once installed, create a new directory and install the required dependencies by running the following command in your terminal:

pip install twilio flask

We’ll be using ngrok to test our app later on, so make sure that is also installed. Instructions to download ngrok can be found here.

Create a new file called app.py and paste the following code into the file:

from flask import Flask, Response, request
from twilio.rest import Client

app = Flask(__name__)
client = Client()

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

The code above will serve as boilerplate for the app. It starts by importing a few modules and creating a Twilio Client object. The Client will be used to interact with the Twilio API later on, and can implicitly use your Twilio API Credentials with the help of environment variables. Your Twilio credentials can be found on the Twilio dashboard and should never be shared or committed to source control. The Client() constructor will check for TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN environment variables by default.

Set these environment variables by running the following commands, replacing the placeholder with your actual credentials:

export TWILIO_ACCOUNT_SID="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
export TWILIO_AUTH_TOKEN="YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"

Start the app by running python app.py in the terminal and navigate to http://127.0.0.1:5000/. If the page displays Hello World!, then everything is configured properly and you’re ready to build a call center.

Butt-head from Beavis and Butt-head talking on a device

Build a Social Call Center

Building the call center can be separated into two basic tasks (shoutout to Twilio’s own Devin Rader for this helpful StackOverflow answer):

  1. When a customer calls your Twilio number, your endpoint must return some TwiML which places the callers into a <Conference>.
  2. When an agent calls in, connect them to a customer using a customer’s CallSid to redirect the live call to a new experience.

Hande Agents and Customers

In order to tackle the two tasks listed above, we need to add a few variables to keep track of the application's state and manage incoming callers and agents. We need to create and manage our own queue because the TwiML <Enqueue> verb’s waitUrl attribute does not currently support the <Conference> verb.

For simplicity, these variables will be added at the top of the module in the app.py file so that they can be accessed from anywhere in our code. In a robust, production-ready application, there are better implementations of this.

from collections import OrderedDict

CUSTOMER_QUEUE = OrderedDict()
AGENT_QUEUE = OrderedDict()

Python's OrderedDict collection is being used here so that a caller's phone number and CallSid can be grouped together and accessed in first-in-first-out (FIFO) order later on.

Queue Customers

characters from Mrs. Maisel wiring up a telecommunications system

The next step is to add incoming callers, both customers and agents, to the appropriate queue and return some TwiML for customers.

Import the Twilio VoiceResponse and Dial objects by adding the following line at the top of your app.py file:

from twilio.twiml.voice_response import Dial, VoiceResponse

Remove the code for the hello() route from earlier and replace it with a new Flask route that looks as such:

@app.route("/incoming", methods=["POST"])
def incoming():
    from_number = request.form["From"]
    call_sid = request.form["CallSid"]

    if from_number in AGENT_NUMBERS:
        return handle_agent(from_number, call_sid)

    response = VoiceResponse()
    dial = Dial()

    try:
        # If an agent is available immediately, connect the customer to them.
        available_agent = AGENT_QUEUE.popitem(last=False)        
        dial.conference(available_agent[1])
    except KeyError:
        # Otherwise, place them in a conference called `Waiting Room` with the
        # other customers currently on hold.
        CUSTOMER_QUEUE[from_number] = call_sid
        dial.conference('Waiting Room')
        
    response.append(dial)
    return Response(str(response), 200, mimetype="application/xml")

The route above will handle an incoming call, determine if the caller is an agent or a customer, and place the caller in the appropriate queue. If there is an agent already available when a customer calls in, then the caller will be connected immediately. Otherwise, they will be dialed into a conference call named 'Waiting Room'.

Optionally, update the code at the bottom of your app to check and make sure the app is configured with at least one agent number, otherwise the customers will never be able to connect with anyone.

if __name__ == "__main__":
    if not AGENT_NUMBERS:
        raise Exception("At least one agent phone number must be provided.")
    app.run()

For the purposes of this tutorial, we'll add your agent phone numbers to an  AGENT_NUMBERS list at the top of your app. This number should be your own phone number or the number of a friend (not a Twilio number, since Twilio will not be calling in as an agent).

Queue Agents

dogs speaking to each other over the phone

Notice above that the handle_agent function being called in the /incoming route does not yet exist. The next step is to add that function.

Create a new function below the /incoming route and add the following code:

def handle_agent(agent_number, agent_call_sid):
    response = VoiceResponse()
    response.dial().conference(agent_call_sid)

    try:
        # If any callers are in the conference, redirect whoever has been
        # waiting the longest by popping the first caller off the queue.
        oldest_customer = CUSTOMER_QUEUE.popitem(last=False)
        redirect_thread = CustomerRedirect(oldest_customer[0], oldest_customer[1], agent_call_sid)
        redirect_thread.start()
    except KeyError:
        # If no callers are in the conference, add the agent to the agent queue
        # so that the next caller will immediately be connected to an agent.
        AGENT_QUEUE[agent_number] = agent_call_sid
    return Response(str(response), 200, mimetype="application/xml")

This is where things start to get a little weird. The function above starts by checking the customer queue to see if there's a customer that the agent can connect to immediately. If so, that customer is popped off the queue and a new Thread is created to handle redirecting that customer. This will be discussed more in the next section.

If there are no customers in the customer queue, then the agent is added to the agent queue to wait for the next incoming customer.

Redirect Live Calls

The next step is to implement the CustomerRedirect Thread being instantiated above.

Start by adding the following two imports at the top of the file:

import time
from threading import Thread

Then add the following new class:

class CustomerRedirect(Thread):
    def __init__(self, customer_num, customer_sid, agent_sid):
        Thread.__init__(self)
        self.customer_num = customer_num
        self.customer_sid = customer_sid
        self.agent_sid = agent_sid

    def run(self):
        time.sleep(4) # Hacks
        client.calls(self.customer_sid).update(
            twiml=f"<Response><Dial><Conference>{self.agent_sid}</Conference></Dial></Response>"
        )

The reason we need to use a Thread in this fashion is to avoid redirecting a customer to a non-existent call. The handle_agent() function needs to return TwiML to the incoming agent call, but it also needs to redirect the customer to that same agent.

If we try to redirect the customer before the agent is connected, the customer will encounter an error and the call will be dropped. The customer redirect needs to be deferred until after we know the agent is connected. If we were using JavaScript, a callback would be the ideal solution, but Python and Flask do not natively support such a concept.

A new Thread with an explicit sleep() is the quick and dirty way to do accomplish this. This is a little bit hacky (what great Twilio app isn't?), but it works! It's important to note that this can still be prone to race conditions in the event that it takes longer than 4 seconds for the agent to connect to Twilio. Feel free to play around with it or explore alternate solutions.

Handle Early Hangups

woman from Saints and Sinners hanging up the phone

The last step in this journey is to account for the case that a customer (or an agent) hangs up their call before being connected to anybody. These callers need to be removed from the appropriate queue or else the next caller will be connected to nobody and Twilio will start throwing errors.

Luckily, Twilio has us covered with its Call Status Callbacks which can be configured to hit our application when a call is completed. Add a new route at the bottom of the app that looks as such:

@app.route("/status", methods=["POST"])
def status():
    if request.form["CallStatus"].lower() == "completed":
        incoming_caller = request.form["From"]
        try:
            if incoming_caller in AGENT_NUMBERS:
                AGENT_QUEUE.pop(incoming_caller)
            else:
                CUSTOMER_QUEUE.pop(incoming_caller)
        except KeyError:
            # We don't really care if this fails.
            pass

    return Response('', 200)

This route will grab the phone number of the caller that just hung up and use it to determine which queue to remove them from.

Test John Mayer's Customer Service Line with Twilio Voice and Python

gif of the Simpsons talking on the phone

At this point all of the code for the call center is complete. The last step is testing.

In order to test this app, your locally running Python app will need to be accessible to the public so that Twilio can reach it. The easiest way to achieve this is with ngrok. In a separate terminal window from the one running your Python app, run the following:

ngrok http 5000

Then copy the first Forwarding URL that ngrok displays and either export it as an environment variable named BASE_URL or set it manually to the BASE_URL variable in the Python code.

Here's an example of what you should put under the client = Client() line:

BASE_URL = os.environ.get("BASE_URL", "http://XXXXXXXXXXXX.ngrok.io")

Replace the URL with your Forwarding URL.

Navigate to the Phone Numbers tab on the Twilio Console to find an active number for your call center. Configure this number to point to your ngrok URL. Make sure to append "/incoming" to the end of the URL for when "A Call Comes In" and append "/status" at the end of the URL for "Call Status Changes". You can see the webhooks below:

Adding ngrok webhooks to Twilio Phone Number Voice and Fax settings page

Don't forget to click Save!

Restart the app by running python app.py.

The last step is to actually call and test out the app. You’ll need at least two friends to help out with this step, as the conference will not be created until at least two participants join.

Have your two friends, aka the customers, call your Twilio number. They should hear some elevator music as they wait to be connected to each other. You, the agent, can call the Twilio number when you're ready to speak with one of the customers. Once you're connected to a customer, the other one will be left waiting for another customer or agent to speak to.

When I tested this during development, I had my mobile phone, a house phone, and three friends for a total of 5 phones. We were able to iterate through a bunch of test cases with two agents and three customers and everything worked great.

Wrap Up

Congratulations! Thanks to this app, the miserable task of sitting on hold has been transformed into a fun, social experience! Twilio conference calls support up to 250 people, but it might be a good idea to hire more agents before letting your call center get that crowded.

cute cats talking on the phone in an office setting

If you run into any issues throughout this post or you have questions, please reach out to me on Twitter @brodan_, or on Github, and stay tuned for more posts in the future!