https://d226lax1qjow5r.cloudfront.net/blog/blogposts/pycascades-code-of-conduct-hotline-nexmo-voice-api-dr/PyCascades-CoC-Hotline.png

Amélioration de la ligne d'assistance du code de conduite de PyCascades avec l'API Voice de Nexmo

Publié le May 4, 2021

Temps de lecture : 12 minutes

Bonjour, je m'appelle Mariatta. Je travaille en tant qu'ingénieur de plateforme chez Zapier. Je suis un développeur Python Core, et je participe également à l'organisation de l'événement PyCascades PyCascades.

Chez PyCascades, la diversité dans notre communauté est une priorité, et non une réflexion après coup. L'une des façons d'y parvenir est d'avoir un code de conduite fort et de le faire respecter. Pour faciliter le signalement des problèmes liés au code de conduite, Alan Vezina, l'un de nos organisateurs, a créé une ligne téléphonique d'assistance pour le code de conduite (CoC). ligne d'assistance téléphonique sur le code de conduite. Cette hotline a depuis été adoptée par PyCon US 2018 et DjangoCon 2018.

Voici comment fonctionne la première hotline PyCascades CoC. Lorsque quelqu'un souhaite signaler un problème lié au code de conduite, il peut appeler le numéro de la hotline. À ce moment-là, tous nos organisateurs seront avertis, et l'appelant sera alors connecté au premier organisateur qui répondra. Pour des raisons d'Account, les informations relatives à l'appel sont également postées sur un canal Slack, afin que nous ayons une trace de l'appel.

Depuis le lancement de la ligne d'assistance, j'ai réfléchi à des idées pour l'améliorer au cours de l'année à venir.

Dans ce billet de blog, je vais vous montrer comment j'ai utilisé Nexmo Voice API et Zapier pour améliorer la hotline du code de conduite PyCascades.

Ce sont les caractéristiques de la ligne d'assistance améliorée :

  • L'appelant est accueilli, lui indiquant qu'il a joint la ligne d'assistance du code de conduite PyCascades. Il est important que l'appelant sache qu'il a atteint le bon numéro, le numéro officiel de la ligne d'assistance du code de conduite de PyCascades.

  • Tous les appels sont automatiquement enregistrés. Le rapport sur le code de conduite est une question importante et sensible. L'enregistrement nous aide à rester Account, et nous permet de revenir en arrière et de réécouter l'appel afin de ne manquer aucun détail.

  • La musique d'attente est désormais diffusée pendant que l'appelant attend d'être mis en relation avec l'un de nos collaborateurs.

  • Lorsqu'un organisateur répond à l'appel, l'appelant entend un message identifiant l'organisateur : "Mariatta se joint à cet appel".

  • Nous avons ajouté une alerte pour informer l'organisateur que cet appel concerne le code de conduite de PyCascades. Je filtre mes appels. J'ignore souvent les appels provenant de numéros 1-800 ou d'appels inconnus dont je ne reconnais pas le numéro. Pendant la période de conférence, j'ai besoin de savoir si ces appels concernent des questions relatives au code de conduite (que je devrais prendre) ou s'il s'agit d'un appel de télémarketing m'offrant une croisière gratuite (que j'ignorerai).

  • Un registre de toutes les activités d'appel de la CdC est désormais enregistré dans une feuille de calcul Google.

En outre, la fonctionnalité suivante doit encore fonctionner :

  • Information sur les appels entrants à la hotline du CdC postée sur Slack. Slack est l'un des principaux moyens de communication des organisateurs de PyCascades. Le message Slack sert à la fois de notification et d'enregistrement de l'appel, même si personne ne répond.

Autres informations techniques :

La hotline est écrite en Python, et le cadre web que j'ai choisi est aiohttpun serveur web asynchrone et un framework client pour Python. J'ai utilisé aiohttp pour construire des robots GitHub comme miss-islington et black-out.

Le service web est déployé sur Heroku. La plupart de l'infrastructure web de PSF est hébergée sur Heroku, donc en tant que développeur Python, j'ai été plus familier avec Heroku qu'avec d'autres types d'infrastructure en nuage.

L'une des principales raisons de choisir Nexmo API, pour moi, est que Nexmo a soutenu la communauté Python de nombreuses façons, notamment en sponsorisant PyCon US 2018, DjangoCon 2018 et le tout premier PyCascades ? L'autre raison de choisir l'API Nexmo est que la bibliothèque nexmo-python est disponible en open source, et qu'elle est compatible avec les nouvelles versions de Python et testée contre Python 3.7.

Vous pouvez consulter le code source de la ligne d'assistance améliorée du CdC.

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.

Installation de l'In-App Voice Voice

Tout d'abord, permettez-moi de vous montrer comment mon application Nexmo Voice est configurée.

Nexmo Voice App settings screenshotNexmo voice app settings

Lors de la configuration d'une application Voice dans Nexmo, vous devez configurer deux URL de webhook, une URL d'événement et une URL de réponse.

L'URL de l'événement est toujours nécessaire ; c'est là que Nexmo vous enverra des informations chaque fois qu'il y a un changement dans l'état de l'appel.

Réception du webhook des événements et enregistrement des activités

Voici un exemple de charge utile délivrée par le webhook Event :

{
    "status": "started",
    "direction": "outbound",
    "from": "12025550124",
    "uuid": "80c80c80-80ce-80c8-80c8-80c80c80c80c",
    "conversation_uuid": "CON-be2be2be-a0dd-a0dd-a0dd-34b34b34b34b",
    "timestamp": "2018-10-25T17:42:17.552Z",
    "to": "12025550124"
}

Il contient des informations telles que le numéro de l'appelant, le numéro composé, le statut de l'appel, l'horodatage et les identifiants uniques de l'appel et de la conversation. Il s'agit d'informations utiles qui peuvent être enregistrées afin que nous disposions d'une trace de chaque activité.

Au lieu de créer mon propre service web pour recevoir ces webhooks, j'ai créé une intégration Zapier. L'une des intégrations que vous pouvez utiliser dans Zapier est Webhooks par Zapier. Avec Webhooks by Zapier, vous pouvez recevoir des données de n'importe quel service ou envoyer des requêtes à n'importe quelle URL sans avoir à écrire du code ou à faire fonctionner des serveurs. En d'autres termes, vous pouvez recevoir et donner des webhooks.

Lorsque j'ai créé un nouveau Zap en utilisant Webhooks by Zapier comme action de déclenchement, Zapier a généré une URL "hooks.zapier.com" que je peux utiliser pour recevoir les webhooks. J'ai fourni l'URL hooks.zapier.com en tant qu'URL des Applications dans l'application Nexmo Voice.

Webhook Trigger ScreenshotWebhook Trigger

Maintenant que j'ai configuré Zapier pour recevoir le webhook d'événements de Nexmo, je peux faire beaucoup de choses. Tout d'abord, j'ai ajouté une intégration Slack, de sorte qu'un message est automatiquement posté dans notre canal privé CoC sur les appels entrants à la hotline. Ensuite, j'ai ajouté une intégration Google Sheets, de sorte que toutes les activités liées à la hotline sont automatiquement ajoutées en tant que nouvelle ligne dans Google Sheets.

CoC Events screenshotCoC Events

Répondre aux appels

Lorsqu'un appelant compose le numéro de la hotline, Numbers envoie la charge utile de cet événement à l'URL de réponse. L'URL de réponse doit renvoyer un NCCO (Objet de contrôle d'appel Nexmo) qui régit cet appel.

L'URL de la réponse correspond à l'URL de mon service web. /webhook/answer/ DE MON WEBSERVICE. (code source)

Je voulais que l'appelant soit accueilli et informé qu'il a joint la ligne d'assistance du Code de conduite PyCascades. Par conséquent, le premier NCCO que j'ai renvoyé est une action "parler" :

ncco = [
        {
            "action": "talk",
            "text": "You've reached the PyCascades Code of Conduct Hotline. This call is recorded."
        }
    ]

Ensuite, comme je reçois maintenant des événements lorsqu'un appelant a composé le numéro de la ligne d'assistance, je dois appeler tous les membres de notre personnel et les connecter au même appel. Pour ce faire, je dois ajouter l'appelant et le personnel à une conférence téléphonique.

Pour ajouter des appelants à une conférence téléphonique, j'ajouterai une action NCCO "conversation" portant le même nom.

{
            "action": "conversation",
            "name": conversation name,
}

Dois-je donc inventer un "nom" pour la conversation ? Pas nécessairement. Jetez un coup d'œil au qui a délivré l'URL de la réponse

Voici un exemple de requête GET à l'adresse answer_url :

/webhooks/answer?to=447700900000&from=447700900001&conversation_uuid=CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab&uuid=aaaaaaaa-bbbb-cccc-dddd-0123456789cd

Notez que la charge utile comprend un conversation_uuid. Au lieu d'"inventer" de nouveaux noms pour la conversation, j'ai décidé d'utiliser le même conversation_uuid comme nom de conversation.

J'ai donc récupéré le conversation_uuid de la demande et l'ai utilisé dans le NCCO.

conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
    ...
    {
            "action": "conversation",
            "name": conversation_uuid,
            ...
    }

Pour enregistrer la conversation, je peux spécifier "record": True dans le dictionnaire NCCO de la conversation. Lorsque l'enregistrement se termine, Nexmo envoie également un webhook à l'adresse eventUrlet le payload de ce webhook inclura l'url où l'enregistrement est stocké.

Voici un exemple de charge utile pour l'enregistrement eventUrl webhook :

{
  "start_time": "2020-01-01T12:00:00Z",
  "recording_url": "https://api.nexmo.com/media/download?id=aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
  "size": 12345,
  "recording_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
  "end_time": "2020-01-01T12:01:00Z",
  "conversation_uuid": "bbbbbbbb-cccc-dddd-eeee-0123456789ab",
  "timestamp": "2020-01-01T14:00:00.000Z"
}

Là encore, au lieu de configurer mon propre service web pour recevoir le webhook, j'ai utilisé Webhooks by Zapier. J'ai créé un Zap différent pour recevoir les webhooks d'enregistrement.

Recording Zapier screenshotZapier Recording

Dans ce Zap, j'ai ajouté une intégration Google Spreadsheet, de sorte que les informations provenant de la charge utile, y compris l'élément recording_urlsont automatiquement ajoutées en tant que nouvelle ligne dans Google Spreadsheets. En outre, j'ai ajouté une intégration Slack pour que les membres de notre personnel soient informés du nouvel enregistrement.

À ce stade, la conversation avec le BCN ressemble à ce qui suit :

conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
    ...
    {
            "action": "conversation",
            "name": conversation_uuid,
            "record": True,
            "eventUrl": [os.environ.get("ZAPIER_CATCH_HOOK_RECORDING_FINISHED_URL")],

            ...
    }

À ce stade, j'ai besoin que la ligne d'assistance fasse deux choses. Tout d'abord, il faut qu'elle appelle chaque membre du personnel, afin que je puisse les ajouter à la conversation. Et deuxièmement, j'ai besoin qu'elle joue de la musique pendant que l'appelant attend d'être connecté.

Jouer de la musique pendant que l'appelant attend

Jouer de la musique dans cet appel est assez simple. Ajoutez l'option "musicOnHoldURL" au NCCO, et fournir un url vers la musique à jouer, par exemple :

"musicOnHoldUrl": ["https://..../music.mp3"]

J'utilise la musique de collection de musique de Wistiaet plus particulièrement les sessions "Let 'Em In". Vous pouvez consulter la licence de ces musiques ici.

import random
    
    MUSIC_WHILE_YOU_WAIT = [
        "https://assets.ctfassets.net/j7pfe8y48ry3/530pLnJVZmiUu8mkEgIMm2/dd33d28ab6af9a2d32681ae80004886e/oaklawn-dreams.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/2toXv1xuOsMm0Yku0YEGya/a792ce81a7866fc77f6768d416018012/broken-shovel.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/16VJzaewWsKWg4GsSUiwGi/9b715be5e8c850e46de98b64e6d31141/lennys-song.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/1qApZVYkxaiayA6aysGAOo/8983586c8ab4db8b69490718469a12f5/new-juno.mp3",
        "https://assets.ctfassets.net/j7pfe8y48ry3/6iXXKtJCp2oCMiGmsmAKqu/8163a8fe863405292ba3609193593add/davis-square-shuffle.mp3",
    ]
    
    ncco = {
        ...
        "musicOnHoldUrl": [random.choice(MUSIC_WHILE_YOU_WAIT)],
    }

Appeler les autres membres du personnel à la conférence téléphonique

Je dois maintenant appeler chacun des membres du personnel et les ajouter à cet appel.

Ce n'est pas quelque chose que je peux faire au sein du BCN. J'ai donc utilisé le client Python de Nexmo Python client de Nexmo. Elle peut être installée en utilisant pipJ'ai donc ajouté nexmo à mon fichier requirements.txt.

J'ai créé une fonction d'aide pour l'instanciation du client.

def get_nexmo_client():
        app_id = os.environ.get("NEXMO_APP_ID")
        private_key = os.environ.get("NEXMO_PRIVATE_KEY_VOICE_APP")
    
        client = nexmo.Client(application_id=app_id, private_key=private_key)
        return client

J'ai également créé une fonction d'aide pour récupérer les numéros de téléphone du personnel. Les numéros de téléphone sont stockés en tant que variables d'environnement dans Heroku, au format suivant :

[
            {
                "name": "Mariatta",
                "phone": "12025550124"
            },
            {
                "name": "Miss Islington",
                "phone": "12025550123"
            }
        ]

La fonction d'aide est assez simple :

import json
    
    def get_phone_numbers():
        return json.loads(os.environ.get("PHONE_NUMBERS"))

Maintenant que j'ai les fonctions pour récupérer le client nexmo, ainsi que les numéros de téléphone à composer, je peux composer les numéros.

Pour appeler un numéro en utilisant la bibliothèque client Nexmo Python :

response = client.create_call({
      'to': [{'type': 'phone', 'number': 12025550124}],
      'from': {'type': 'phone', 'number': 12025550123},
      'answer_url': ['https://example.com/answer']
    })

Dans la méthode create_call je devais fournir le numéro de téléphone to ainsi que le numéro de téléphone from numéro de téléphone. Le to est le numéro de téléphone du personnel que je souhaite appeler.

Pour le from au lieu de donner le numéro de téléphone de l'appelant de la hotline, j'ai utilisé le numéro hotline De cette façon, le personnel sait, en lisant l'identification de l'appelant, que l'appel provient de la hotline.

Qu'en est-il de la answer_url? Il s'agit de l'accroche web pour le moment où un membre du personnel répond à cet appel. answer_url est le webhook à utiliser lorsqu'un membre du personnel répond à cet appel. Le comportement souhaité ici est que le personnel qui a répondu à l'appel soit ajouté à la conversation où se trouve l'appelant de la hotline. Par conséquent, en plus de la charge utile fournie par Nexmo au webhook, je dois passer le paramètre conversation_name (qui est le conversation_uuid).

J'ai créé un nouveau point de terminaison dans mon service web pour gérer ce webhook en incluant à la fois l'élément conversation_uuid et l'appel uuid dans l'URL :

@routes.get(
        "/webhook/answer_conference_call/{origin_conversation_uuid}/{origin_call_uuid}/"
    )
    async def answer_conference_call(request):
    
        origin_conversation_uuid = request.match_info["origin_conversation_uuid"]
        origin_call_uuid = request.match_info["origin_call_uuid"]
        ...

Une fois ce point final créé, chaque fois qu'un membre du personnel répondra à l'appel de la ligne d'assistance, j'aurai un moyen de savoir à quelle conversation l'ajouter.

Enfin, le webhook de réponse ressemble à ce qui suit :

@routes.get("/webhook/answer/")
async def answer_call(request):
    conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
    call_uuid = request.rel_url.query["uuid"].strip()

    ncco = [
        {
            "action": "talk",
            "text": "You've reached the PyCascades Code of Conduct Hotline. This call is recorded.",
        },
        {
            "action": "conversation",
            "name": conversation_uuid,
            "record": True,
            "eventMethod": "POST",
            "musicOnHoldUrl": [random.choice(MUSIC_WHILE_YOU_WAIT)],
            "eventUrl": [os.environ.get("ZAPIER_CATCH_HOOK_RECORDING_FINISHED_URL")],
            "endOnExit": False,
            "startOnEnter": False,
        },
    ]

    client = get_nexmo_client()
    phone_numbers = get_phone_numbers()

    for phone_number_dict in phone_numbers:
        client.create_call(
            {
                "to": [{"type": "phone", "number": phone_number_dict["phone"]}],
                "from": {
                    "type": "phone",
                    "number": os.environ.get("NEXMO_HOTLINE_NUMBER"),
                },
                "answer_url": [
                    f"https://mariatta-enhanced-coc.herokuapp.com/webhook/answer_conference_call/{conversation_uuid}/{call_uuid}/"
                ],
            }
        )
    return web.json_response(ncco)

Ajouter des membres du personnel à la conférence téléphonique

Le point d'extrémité answer_conference_call a été créé dans le but d'ajouter les membres du personnel à la conférence téléphonique. Pour ce faire, je devais renvoyer un NCCO au webhook qui contient une action "conversation" et le nom de la conversation. Mais avant qu'ils ne soient ajoutés, j'aimerais saluer le personnel pour qu'ils sachent qu'ils rejoignent la hotline du Code de conduite PyCascades.

Rappelons que la variable d'environnement PHONE_NUMBERS comprend également les noms des propriétaires des numéros de téléphone.

J'ai créé la fonction suivante pour récupérer le nom du propriétaire du numéro de téléphone :

def get_phone_number_owner(phone_number):
        phone_numbers = get_phone_numbers()
        for phone_number_info in phone_numbers:
            if phone_number_info["phone"] == phone_number:
                return phone_number_info["name"]
    
        return None

Grâce à cette fonction, je peux saluer le personnel comme suit :

@routes.get(
        "/webhook/answer_conference_call/{origin_conversation_uuid}/{origin_call_uuid}/"
    )
    async def answer_conference_call(request):
    
        to_phone_number = request.rel_url.query["to"]
        origin_conversation_uuid = request.match_info["origin_conversation_uuid"]
    
        phone_number_owner = get_phone_number_owner(to_phone_number)
    
        ncco = [
            {
                "action": "talk",
                "text": f"Hello {phone_number_owner}, connecting you to PyCascades hotline.",
            },
            {
                "action": "conversation",
                "name": origin_conversation_uuid,
                "startOnEnter": True,
                "endOnExit": True,
            },
        ]
        return web.json_response(ncco)

À ce stade, vous vous demandez peut-être à quoi sert le symbole origin_call_uuid est utilisé. J'ai pensé qu'il serait courtois de faire savoir à l'appelant de la hotline quel membre de l'équipe PyCascades répond à son appel. N'oubliez pas non plus qu'il s'agit d'une conférence téléphonique, et qu'il est donc possible que plusieurs personnes se joignent à l'appel. Au lieu de laisser quelqu'un se joindre silencieusement, je vais avertir tous les participants de l'appel de la personne qui vient de se joindre.

client = get_nexmo_client()

    response = client.send_speech(
        origin_call_uuid, text=f"{phone_number_owner} is joining this call."
    )

Ainsi, le point de terminaison answer_conference_call ressemble à ce qui suit :

@routes.get(
        "/webhook/answer_conference_call/{origin_conversation_uuid}/{origin_call_uuid}/"
    )
    async def answer_conference_call(request):

        to_phone_number = request.rel_url.query["to"]
        origin_conversation_uuid = request.match_info["origin_conversation_uuid"]
        origin_call_uuid = request.match_info["origin_call_uuid"]
    
        phone_number_owner = get_phone_number_owner(to_phone_number)
        client = get_nexmo_client()
    
        try:
            response = client.send_speech(
                origin_call_uuid, text=f"{phone_number_owner} is joining this call."
            )
        except nexmo.Error as er:
            print(
                f"error sending speech to {origin_call_uuid}, owner is {phone_number_owner}"
            )
            print(er)
    
        else:
            print(f"Successfully notified caller. {response}")
    
        ncco = [
            {
                "action": "talk",
                "text": f"Hello {phone_number_owner}, connecting you to PyCascades hotline.",
            },
            {
                "action": "conversation",
                "name": origin_conversation_uuid,
                "startOnEnter": True,
                "endOnExit": True,
            },
        ]
        return web.json_response(ncco)

Le flux d'appel terminé

Le code de conduite amélioré de PyCascades est ainsi complété.

Le flux d'appels complet est le suivant :

  • Un appelant compose le numéro de la ligne d'assistance.

  • Les membres du personnel de PyCascades reçoivent une notification Slack indiquant qu'il y a un appel entrant sur la ligne d'assistance.

  • Les informations relatives à l'appel sont ajoutées à Google Sheets.

  • L'appelant entend un message : "Bienvenue sur la ligne d'assistance téléphonique du code de conduite PyCascades. Cet appel est enregistré."

  • L'appelant entend de la musique pendant qu'il attend d'être connecté.

  • Chaque membre du personnel de PyCascades reçoit un appel de la hotline.

  • Un employé de PyCascades répond à l'appel et entend : "Bonjour {nom de l'employé}, je vous connecte à la hotline de PyCascades".

  • Pendant ce temps, l'appelant entend le message "{nom du personnel} se joint à cet appel".

  • Le personnel et l'appelant poursuivent la conversation.

  • Le personnel raccroche et l'enregistrement de l'appel est terminé.

  • Les membres du personnel reçoivent une notification Slack les informant de l'existence d'un nouvel enregistrement.

  • Les informations sur l'enregistrement sont également ajoutées dans Google Sheets.

Télécharger l'enregistrement

L'enregistrement peut être téléchargé en utilisant le client Nexmo Python, et l'adresse recording_url est l'url reçue dans le webhook des événements d'enregistrement.

client = get_nexmo_client()
    recording = client.get_recording(recording_url)

Les enregistrements d'appels sont stockés dans Nexmo pendant un mois avant d'être automatiquement supprimés. Comme ces appels sont importants et que nous ne voulons pas perdre les enregistrements, j'ai créé un script en ligne de commande qui peut être utilisé pour télécharger les enregistrements.

Le script peut être exécuté comme suit :

python3 -m download_recording url1 url2 url3 ...

Une fois le script exécuté, les enregistrements sont téléchargés et stockés localement dans le répertoire recording répertoire.

Conclusions

Grâce à Nexmo et Zapier, je suis en mesure d'améliorer la hotline du code de conduite PyCascades. La mise en place de cette hotline semble être plus compliquée que avant.

Cependant, je crois que les nouvelles améliorations telles que l'enregistrement automatique, l'enregistrement automatique dans Google Spreadsheets sont utiles à tous les membres de notre personnel, alors je suis prêt à passer le temps supplémentaire pour configurer cela pour PyCascades. De plus, en utilisant Zapier au lieu de le coder en dur, nous pouvons être plus flexibles au cas où nous voudrions ajouter des intégrations supplémentaires.

Merci de m'avoir lu ! Si vous avez d'autres questions, concernant la hotline, PyCascades, ou Zapier, n'hésitez pas à m'envoyer un email à mariatta.wijaya@zapier.com.


Note des relations avec les développeurs de Nexmo : Nous sommes très heureux que Mariatta ait décidé d'utiliser Nexmo pour améliorer les rapports sur le Code de Conduite de PyCascades. Nous pensons qu'avoir un code de conduite est une partie vitale de la création d'un espace accueillant et inclusif. Nous aimerions montrer notre soutien à toute conférence ou rencontre qui souhaiterait mettre en place une hotline Code de Conduite. Si vous êtes un organisateur d'événements et que vous souhaitez utiliser la ligne d'assistance téléphonique de Mariatta pour votre événement, veuillez envoyer un courriel à devrel@nexmo.com et nous vous aiderons volontiers à mettre en place l'application et à obtenir un crédit Nexmo gratuit.

Partager:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Mariatta

I am not open, parts of me are broken