https://d226lax1qjow5r.cloudfront.net/blog/blogposts/build-a-family-hotline-dr/Blog_Family-Hotline_1200x600.png

Crea una línea directa familiar con Vonage

Publicado el May 4, 2021

Tiempo de lectura: 15 minutos

Cualquier empresa a la que llamo siempre tiene uno de esos sistemas de respuesta automática que me hacen preguntas antes de que pueda hablar con una persona real, y como soy un poco rara, siempre he pensado "¿por qué no puedo...? I tener uno de esos?"

Mi hija empezó la secundaria hace unos meses, y en el formulario que nos mandaron para rellenar sólo había un espacio para un número de teléfono de contacto. ¿Qué pasa si les damos mis datos y no estoy disponible? Por suerte, trabajo para Vonage y sé cómo configurar un número de teléfono capaz de desviar las llamadas a uno de los dos, ¡o a los dos! ¡Vonage al rescate!

Qué vamos a construir

  1. La escuela dispone de un número de Vonage como número de contacto.

  2. Si llaman al número, un mensaje automático les da las siguientes opciones:

    1. Enumere los nombres de los padres, con un dígito a pulsar para que se envíe a cada uno de ellos.

    2. Si la persona que llama no hace nada, reenvía al primer padre de la lista.

    3. Pulse "*" si se trata de una emergencia. Esto establecerá una llamada en conferencia y marcará a ambos padres.

Tenía otro par de requisitos:

  1. Ser sencillo y prácticamente gratuito de alojar y gestionar.

  2. No hay que gestionar ninguna base de datos, para mayor simplicidad.

Cómo vamos a construirlo

He elegido un par de tecnologías con las que estoy más familiarizado: Python 3.6 y Flask. He elegido Python 3.6 porque es la versión que viene con f-strings y ¡son geniales! También estoy usando pipenv para gestionar mis dependencias de Python.

Esta combinación nos permite crear un magnífico sistema IVR (Respuesta de Voz Interactiva) doméstico en poco más de 100 líneas de código.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

¡Vamos!

Primero vamos a configurar nuestro entorno de desarrollo.

Como Vonage necesita llamar al servidor que vas a escribir, y tu portátil está detrás de un bonito y seguro cortafuegos, probablemente querrás usar ngrok para el desarrollo local - permítanme recomendar mi colega Aaron's de mi colega Aaron sobre el uso de ngrok.

Probablemente querrá instalar pipenv globalmente, y a partir de entonces lo utilizaremos para gestionar el entorno virtual de nuestro proyecto:

sudo pip install --upgrade pipenv

Ahora vamos a instalar las dependencias que necesitaremos para este proyecto, y activar nuestro entorno virtual. Ejecuta estos comandos en el directorio de tu nuevo proyecto - yo he llamado al mío hotline:

# Install compatible versions of `vonage`, `flask` and `attrs` pipenv install vonage~=2.5.5 flask~=1.0.2 attrs~=18.2.0 # Activate the virtualenv: pipenv shell

Ahora vamos a empezar a escribir nuestro servidor Flask.

Abra un archivo llamado hotline.pyy escriba lo siguiente:

from flask import Flask, jsonify, url_for as url_for_, request


app = Flask(__name__)


@app.route("/incoming/", methods=["GET", "POST"])
def incoming():
    """
    An HTTP endpoint which handles incoming calls.

    :return: A JSON HTTP response containing the main menu NCCO actions.
    """
    return jsonify(
        [
            {
                "action": "talk",
                "text": "Welcome to the Brockman family hotline",
                "voiceName": "Amy",
            }
        ]
    )

Ahora, en ventanas de terminal separadas querrás ejecutar lo siguiente al mismo tiempo:

# Tunnel requests to local port 5000 (Flask's default port): ngrok http 5000
# Start your Flask server: FLASK_APP=hotline flask run --debugger --reload

Al ejecutar ngrokimprime el nombre de dominio generado que reenviará a su servidor. Se verá algo como esto: https://abcde1234.ngrok.io -> localhost:5000. Ahora puedes probar tu aplicación Flask en https://abcde1234.ngrok.io/incoming (sustituye abcde1234 por lo que se imprimió en tu terminal).

Deberías ver algo como esto:

First NCCO testFirst Test

Llame por teléfono a su servidor

Ahora necesitas configurar un número virtual de Vonage para llamar a tu servidor cuando alguien llame a tu número. Así que ahora necesitas 3 cosas:

  1. A Account de Vonage

  2. Una aplicación Voice configurada

  3. Un número virtual de Vonage configurado.

Crear una cuenta de Vonage

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Instalar la CLI de Vonage

Instala la CLI de Vonage globalmente con este comando:

npm install @vonage/cli -g

Luego, configura la CLI con tu clave y secreto de API de Vonage. Puedes encontrar esta información en el Panel del desarrollador.

vonage config:set --apiKey=VONAGE_API_KEY --apiSecret=VONAGE_API_SECRET

Crear una aplicación Voice

Cree un nuevo directorio para su proyecto y CD en él:

mkdir my_project
CD my_project

Ahora, utiliza la CLI para crear una aplicación de Vonage.

vonage apps:create ✔ Application Name … hotline ✔ Select App Capabilities › Voice ✔ Create voice webhooks? … yes ✔ Answer Webhook - URL … https://ed330676.ngrok.io/incoming/ ✔ Answer Webhook - Method › POST ✔ Event Webhook - URL … https://ed330676.ngrok.io/event/ ✔ Event Webhook - Method › POST ✔ Allow use of data for AI training? Read data collection disclosure … yes Application created: 34abcd12-ef12-40e3-9c6c-4274b3633761

Usted querrá guardar esa identificación que se imprime después de Application created:. Lo necesitarás en el siguiente paso.

Compra un número y vincúlalo a tu aplicación Voice

Ahora necesitas un número para poder recibir llamadas. Puedes alquilar uno utilizando el siguiente comando (sustituyendo el código del país por tu código). Por ejemplo, si estás en EE.UU., sustituye GB por US:

vonage numbers:search US vonage numbers:buy [NUMBER] [COUNTRYCODE]

Ahora vincula el número a tu aplicación:

vonage apps:link --number=VONAGE_NUMBER APP_ID

Vale¡!

Ahora puedes probar todo eso llamando al número de teléfono que acabas de comprar. Si oyes un mensaje que dice que no se ha reconocido el número, o si las cosas no funcionan, abre la página de depuración de ngrok en http://127.0.0.1:4040/ en tu navegador para que puedas ver qué peticiones entraron y si el servidor respondió correctamente.

Respuesta a INPUT

Cambiemos el mensaje y añadamos una acción de entrada a nuestro menú NCCO para que la persona que llama pueda seleccionar la persona con la que desea hablar:

@app.route("/incoming/", methods=["POST", "GET"])
def incoming():
    """
    An HTTP endpoint which handles incoming calls.
    """
    return jsonify(
        [
            {
                "action": "talk",
                "text": """
                    Welcome to the Brockman family hotline.
                    To speak to Pete Brockman, please press 1.
                    To speak to Sue Brockman, please press 2.
                """,
                "voiceName": "Amy",
                "bargeIn": True,
            },
            {
                "action": "input",
                "eventUrl": [url_for("family_selection")],
                "maxDigits": 1,
            },
        ]
    )


@app.route("/family-selection/", methods=["POST"])
def family_selection():
    """
    An HTTP endpoint which handles the DTMF input from the main menu.
    """
    postdata = request.json

    if postdata["timed_out"]:
        index = 0
    else:
        index = int(postdata["dtmf"]) - 1
    return jsonify([{
        "action": "talk",
        "text": f"You selected the {index} option",
    }])

¡No lo llame todavía! Todavía no estamos listos... pero, déjame explicarte lo que esto hace:

He añadido esto a la talk acción:

"bargeIn": True,

Esto significa que la persona que llama puede pulsar un número en su teléfono mientras la acción talk acción sigue leyendo el mensaje. Se activará automáticamente la input acción. También he añadido la siguiente acción:

{
    "action": "input",
    "eventUrl": [url_for("family_selection")],
    "maxDigits": 1,
},

Esta acción de input acción le indica a Vonage que llame a la función family_selection que acabamos de añadir. La función url_for de Flask proporciona la URL para el nombre de la función dada.

Un buen uso departial

url_for genera por defecto un relativa RELATIVA. Vonage Voice no admite URL relativas, por lo que agregué lo siguiente al principio del archivo:

from functools import partial

from flask import url_for as url_for_

# We don't have any use for relative URLs, so hard-code this param:
url_for = partial(url_for_, _external=True)

Este uso de partial convierte la función url_for a algo más útil: es una copia de la función url_for de Flask, pero con el valor por defecto del parámetro _external a True. (Olvidé poner este parámetro tantas veces antes de arreglarlo con esto).

Una vez que lo hayas añadido a tu archivo y te hayas asegurado de que tu servidor se ha recargado, es hora de volver a llamar a tu número. Debería leerte el mensaje, permitirte elegir un número en tu teléfono y, a continuación, leerte ese número con uno menos. Si esperas unos segundos, debería decirte que has seleccionado zero.

¿Por qué se resta 1 al número introducido? Para poder seleccionar a una persona de una lista. Si la prueba anterior salió bien, pongamos a nuestras personas en una lista y hagamos este menú un poco más dinámico.

Desvío de llamadas

Lo primero que realmente necesitamos es una forma mejor de configurar nuestros "padres". Es bueno tener una lista de las personas a las que queremos reenviar las llamadas. Por el momento, vamos a definir sus detalles en el archivo Python. Por favor, ¡no hagas esto con números de teléfono reales y comprométete con un repositorio público!

import attr

@attr.s
class Endpoint:
    """ A data class containing a potential callee's details. """
    name = attr.ib()
    phone_number = attr.ib()

ENDPOINTS = [
    Endpoint(name="Pete Brockman", phone_number="447700900123"),
    Endpoint(name="Sue Brockman", phone_number="447700900456"),
]
VONAGE_NUMBER = "447700900847"

En el código anterior, estoy utilizando el increíble attrs para definir una clase simple, Endpointpara guardar el nombre y el número de teléfono de una persona. Luego estoy creando una lista de Endpoints para las personas a las que queremos desviar las llamadas.

Ahora añado una sencilla función de utilidad para generar talk acciones. Esto hará el código más legible y significa que por defecto usaremos la función Amy Voice. Puedes obtener una lista de todas las voces que soporta Vonage aquí

def talk(message, voice="Amy", barge_in=None):
    """ Utility function to generate a `talk` NCCO action. """
    response = {"action": "talk", "text": message, "voiceName": voice}
    if barge_in is not None:
        response["bargeIn"] = barge_in

    return response

Y ahora He factorizado el código para generar el mensaje del menú principal:

def make_answer_message(message, endpoints):
    """
    Generate a script to be read to the caller, informing them of their options.
    """
    endpoint_options = [
        f"To speak to {endpoint.name}, please press {code}."
        for code, endpoint in enumerate(endpoints, start=1)
    ]

    return " ".join([message, *endpoint_options, "If you're not sure, please hold."])

¿Ves eso *endpoint_options en la última línea del código anterior? ¿Sabías que puedes hacer esto en Python 3 para expandir una lista en otra lista? En este caso, el resultado es que acabamos con una lista de cadenas que podemos unir con espacios.

Ahora puede sustituir la acción talk en el método incoming por la siguiente llamada a la función talk a la función

talk(
    make_answer_message(
        "Welcome to the Brockman family hotline.", ENDPOINTS
    ),
    barge_in=True,
),

Por último, puede modificar la función family_selection para que reenvíe la llamada a un número de teléfono:

@app.route("/family-selection/", methods=["POST"])
def family_selection():
    """
    An HTTP endpoint which handles the DTMF input from the main menu.
    """
    postdata = request.json

    try:
        if postdata["timed_out"]:
            index = 0
        else:
            index = int(postdata["dtmf"]) - 1

        if index < len(ENDPOINTS):
            # They elected to speak to an individual
            endpoint = ENDPOINTS[index]
            return jsonify(
                [
                    talk(f"Connecting to {endpoint.name}"),
                    {
                        "action": "connect",
                        "from": VONAGE_NUMBER,
                        "endpoint": [{"type": "phone", "number": endpoint.phone_number}],
                    },
                ]
            )
    except ValueError:
        pass  # This is raised by `int`, and can be ignored - we just forward on to the following error message...

    return jsonify([talk("I didn't understand that option.")])

El código anterior parece bastante más complicado que antes, pero eso es porque ahora estamos haciendo una comprobación de errores y diciendo a la persona que llama si ha pulsado una tecla que no esperábamos. Si el usuario pulsa 1 o 2, se le leerá un mensaje diciendo que se le está conectando, y entonces se le conectará al número del endpoint.

Bien, ¿qué hemos hecho hasta ahora?

  • La escuela puede llamar a mi número y les leen una lista de personas con las que pueden conectar.

  • Si pulsan 1 o 2, se pondrán en contacto con esa persona. (Por cierto, ninguna de las partes de la llamada puede ver el número de teléfono de la otra, así que es una forma estupenda de anonimizar las llamadas).

  • Si la persona que llama espera, se le conectará al primer punto final de la lista configurada.

Podríamos parar aquí si queremos, pero tenía un tercer requisito: En caso de emergencia, la persona que llamara podría pulsar stary nos llamaría a los dos y nos pondría a todos en conferencia.

Añadir una opción de multiconferencia

Una multiconferencia se crea con una conversation y finaliza (por defecto) cuando no hay más personas en la multiconferencia. Lo único que necesitamos para conectar a una persona a una multiconferencia es el nombre que le dimos a la multiconferencia con la primera conversation acción.

Inicializar un objeto cliente

Vamos a necesitar un objeto Vonage Client para crear llamadas salientes a los padres, así que es una suerte que hayamos instalado la librería Vonage Python al principio de este tutorial, ¿verdad? Pon las siguientes líneas cerca de la parte superior de tu archivo. Si quieres quieres puedes pegar tus application_id y private_key directamente en el archivo, pero creo que es mejor cargarlos desde las variables de entorno. Es demasiado fácil enviarlas a un repositorio público, y cualquiera que las tenga puede pasar su ¡saldo de Vonage!

import vonage

vonage_client = vonage.Client(
    application_id=os.getenv('VONAGE_APPLICATION_ID'),
    private_key=os.getenv('VONAGE_PRIVATE_KEY')
)

Dígale al usuario que puede pulsar "*".

Antes de hacerlo, añade el mensaje "If this is an emergency, please press star." a la cadena devuelta por nuestra función make_answer_message función. Yo lo he añadido justo después del message de la lista.

Crear una multiconferencia

Ahora vamos a crear nuestra multiconferencia. Respira hondo; es un bloque de código razonablemente grande.

Necesitaremos un nuevo punto final HTTP, llamado conference_nccoque proporcionará acciones NCCO para cada uno de los padres que se marquen en la multiconferencia.

He abstraído todo el código para marcar a los padres y generar acciones NCCO para la persona que llama en una función llamada create_conference_call.

Pego las dos funciones juntas para que pueda ver cómo el parámetro conference_id, que se genera en create_conference_call se incrusta en la ruta URL del endpoint conference_ncco endpoint para que sepa el nombre de la conferencia para incrustar en el conversation acción NCCO.

@app.route("/conference/<conference_id>/ncco", methods=["GET", "POST"])
def conference_ncco(conference_id: str):
    """
    An HTTP endpoint which generates the NCCO actions to connect a callee to
    a conference call.
    """
    return jsonify(
        [
            talk("You are being connected to a family hotline conference call."),
            {"action": "conversation", "name": conference_id},
        ]
    )

def create_conference_call(endpoints):
    """
    Generate an NCCO response to connect the caller to all the provided
    `endpoints` in a single conference call.
    """
    # Generate a unique name for our conference:
    conference_name = str(uuid.uuid4())

    # Loop through the endpoints and dial them into the conference call:
    for endpoint in endpoints:
        vonage_client.create_call(
            {
                "to": [{"type": "phone", "number": endpoint.phone_number}],
                "from": {"type": "phone", "number": VONAGE_NUMBER},
                "answer_url": [
                    url_for("conference_ncco", conference_id=conference_name)
                ],
            }
        )
        print(f"Dialing {endpoint.phone_number} into {conference_name}")

    # Connect the inbound leg to the conference call we're creating:
    return jsonify(
        [
            talk("Connecting all parties."),
            {
                "action": "conversation",
                "name": conference_name,
            },
        ]
    )

Manejar una entrada estrella

Ahora, sólo tenemos que modificar family_selection para que sepa qué hacer con un código * código dtmf, poniendo esto cerca de la parte superior de la función:

if postdata["dtmf"] == "*":
    return create_conference_call(ENDPOINTS)

Póngalo a prueba

Llama a tu número de Vonage y sigue cada una de las instrucciones del menú principal para verificar que funcionen. Si todo funciona, ¡es hora de implementarlo!

¡Despliéguelo!

Si consulta el repositorio Git para este proyecto verás que he hecho algunos cambios menores en él, incluyendo la carga de toda la configuración de las variables de entorno, y la adición de un Procfile. Esto debería significar que es relativamente sencillo de desplegar en Heroku - ¡pero lo dejaré como un ejercicio para ti! No olvides actualizar la configuración de tu Aplicación de Vonage para que apunte a la nueva URL de Heroku en lugar de la URL de ngrok que has estado usando para el desarrollo.

Una vez desplegado, es hora de hablar con el colegio de su hijo y pedirle que actualice sus datos de contacto.

¿Qué hemos hecho?

Hemos creado todo un sistema IVR para que la escuela pueda ponerse en contacto con nosotros. Puede desviar llamadas a números individuales o conectar a todo el mundo en una multiconferencia.

Otros créditos

Algunas cosas más que consideré hacer con este servidor incluyen:

  • Reenvío de mensajes SMS enviados al número a ambos padres.

  • Envío de mensajes de texto para informar a los padres si han perdido una llamada de la escuela.

  • Crear una llamada "a la carrera" que llamaría a ambos padres y el primero en contestar coge la llamada. La segunda llamada se cortaría y se enviaría un SMS diciendo que el otro progenitor la había atendido.

  • Si ningún progenitor lo coge, permitir que la escuela grabe un mensaje, que entonces estaría disponible para que ambos progenitores lo recojan.

  • Integración con un Slack familiar, con alertas de llamadas telefónicas entrantes, reenvío de mensajes SMS al Slack, ¡e incluso publicación de mensajes grabados para ser reproducidos dentro del Slack!

Como puedes ver, hay mucho potencial una vez que empiezas. Espero que te hayas divertido siguiendo este tutorial. Soy @judy2k en Twitter - sígueme o hazme preguntas sobre este tutorial.

Compartir:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Mark SmithAntiguos alumnos de Vonage

Mark era nominalmente responsable de las bibliotecas cliente de Nexmo (aunque sólo escribe las bibliotecas Python y Java). Originalmente era un desarrollador Java, ha sido un desarrollador Python durante 18 años, y está incursionando cada vez más en Go y Rust. Le gusta llevar al límite los lenguajes de programación y luego enseñar estas técnicas a otros programadores. Tiene un sombrero vikingo, pero no es un vikingo, y es Judy2k en Twitter por razones que no discutirá.