2

I've done some searching around this topic and have consulted the relevant API references, but I'm still struggling to implement this completely.

I'm attempting to build, in Python/Flask with the python-twilio library, a system which will do the following:

  1. User calls in, hears a message
  2. App simultaneously dials out to first of n groups, where a group is a list of numbers with meta information (like a name). Represented in code as a dict with values of lists of numbers. (k[v] = [0,1,n])
  3. If somebody (an agent) in the group answers, ask them to input a confirmation digit to continue. On confirmation, connect them to the user.
  4. If an agent in the group did not pick up, try the next agent in the group (ring their numbers) and go back to step 3.

Right, so on to the code. This has been stripped down to the basic logic. Unfortunately, I haven't been able to quite get to grips with Flask application state, so I've been using Python global variable access. I'd like to move away from that, as the app will likely work incorrectly if two calls are placed to the app.

# excluding flask import/setup
from twilio import twiml
from twilio.rest import TwilioRestClient

agents = OrderedDict()
agents['Foo'] = ["+440000000000", "+440000000001"]
agents['Bar'] = ["+440000000002", "+440000000003"]

caller = None
@app.route('/notify', methods=['POST'])
def notify():
    """Log all call status callbacks"""
    global caller
    called = request.values.get('Called')
    call_status = request.values.get('CallStatus') 
    timestamp = request.values.get('Timestamp') # e.g. Wed, 02 Mar 2016 15:42:30 +0000
    call_statuses = {
        "initiated": "[%s@%s] INITIATED",
        "ringing": "[%s@%s] RINGING",
        "in-progress": "[%s@%s] IN-PROGRESS",
        "answered": "[%s@%s] ANSWERED",
        "failed": "[%s@%s] CALL FAILED",
        "no-answer": "[%s@%s] NO ANSWER",
        "busy": "[%s@%s] BUSY",
        "completed": "[%s@%s] COMPLETED"
        }
    called_name = [i for i in agents.items() if called in i][0][0]
    print("--%s-- %s" % (timestamp, call_statuses[call_status] % (called_name, called)))

    return jsonify({"status": 200})

def new_twilio_client():
    _ = TwilioRestClient(app.config['TWILIO_ACCOUNT_SID'], app.config['TWILIO_AUTH_TOKEN'])
    return _

@app.route('/welcome', methods=['GET'])
def welcome():
    global caller
    response = twiml.Response()
    response.say("Welcome message", voice="alice")
    caller = request.values.get("Caller")
    # log the call/caller here
    response.redirect(url_for(".start"), _external=True, method="GET")
    return str(response)

@app.route('/start', methods=['GET'])
def start():
    response = twiml.Response()
    response.say("Trying first agent")
    response.enqueue("q")
    client = new_twilio_client()
    for number in agents['Foo']:
        client.calls.create(to=number, Timeout=12, ifMachine="Hangup", statusCallbackEvent="initiated ringing answered completed", StatusCallback=url_for(".notify", _external=True), from_=app.config['TWILIO_CALLER_ID'], url=url_for(".whisper", _external=True), method="GET")        
    return str(response)

@app.route("/whisper", methods=['GET'])
def whisper():
    """Ask agent if they want to accept the call"""
    global call_sid
    response = twiml.Response()
    client = new_twilio_client()
    response.say("Press 1 to accept this call")
    response.gather(action=url_for(".did_agent_accept", _external=True), numDigits=1, timeout=3)
    return str(response)

@app.route("/did_agent_accept", methods=['POST'])
def did_agent_accept():
    """Determine if the agent accepted the call"""
    response = twiml.Response()
    digit = request.values.get('Digits', None)
    if digit == "1":
        with response.dial() as dial:
            # drop the agent into the queue we made earlier
            dial.queue("q", url=url_for(".thanks_for_waiting", _external=True))
    else:
        response.reject()
    return str(response)

@app.route("/thanks_for_waiting", methods=['GET'])
def thanks_for_waiting():
    """Thank the user"""
    response = twiml.Response()
    response.say("Thanks for waiting. You will now be connected.")
    return str(response)

@app.route('/sorry', methods=['GET'])
def sorry():
    response = twiml.Response()
    response.say("Sorry, trying again")
    response.redirect(url=url_for(".start", _external=True), method="GET")
    return str(response)

So, I've used the queueing functionality of Twilio here to place the caller into a queue, and then I use the REST API to place calls to the first agent's associated numbers. When they pick up and respond to the gather verb, they are then dropped into the queue.

A roadblock I've run into:

  • Managing busy or no-answer (or other non-answer) statuses to determine if the application should try the next group of numbers is difficult due to the async nature of the callback feature. I have tried a solution where I maintain a global list which is updated whenever notify() is called, and the last number which gave a non-answer status is added to said list. I then compare the length of the agents[agent] list with this global list and if it matches, I modify the call to try the next group, however this ended up with phantom/incorrect calls.

My question is, how do I go about doing this live call modifying, such that I clean up any existing ringing calls to the previous group? How do I improve on the design of this code? Is there something obvious that I've missed? Any pointers would be appreciated.

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152

0 Answers0