
Aprimorando a linha direta do Código de Conduta da PyCascades com a Voice API da Nexmo
Tempo de leitura: 11 minutos
Olá, meu nome é Mariatta. Trabalho como engenheira de plataforma na Zapier. Sou desenvolvedora principal de Python e também ajudo a organizar o conferência PyCascades .
Na PyCascades, a diversidade em nossa comunidade é uma prioridade, não algo secundário. Uma das maneiras pelas quais tentamos alcançar isso é por meio de um código de conduta rigoroso e de sua aplicação. Para facilitar a denúncia de problemas relacionados ao código de conduta, Alan Vezina, um de nossos organizadores, criou uma linha direta do código de conduta (CoC). Desde então, a linha direta foi adotada pela PyCon US 2018 e pela DjangoCon 2018.
Veja como funciona a primeira linha direta do Código de Conduta (CoC) do PyCascades. Quando alguém quiser relatar um problema relacionado ao código de conduta, pode ligar para o número da linha direta. Nesse momento, todos os nossos organizadores serão notificados, e a pessoa que ligou será então conectada ao primeiro organizador que responder. Para garantir a transparência, as informações sobre a ligação também são publicadas em um canal do Slack, para que tenhamos um registro da mesma.
Desde que a linha direta foi lançada, venho pensando em ideias para aprimorá-la no próximo ano.
Nesta postagem do blog, vou mostrar a vocês como utilizei a Voice API da Nexmo e o Zapier para aprimorar a linha direta do Código de Conduta da PyCascades.
Estas são as funcionalidades da linha direta aprimorada:
A pessoa que liga é recebida com uma mensagem informando que entrou em contato com a Linha Direta do Código de Conduta do PyCascades. É importante que a pessoa saiba que ligou para o número correto, a Linha Direta oficial do Código de Conduta do PyCascades.
Todas as chamadas são gravadas automaticamente. As denúncias relacionadas ao Código de Conduta são um assunto importante e delicado. Ter uma gravação nos ajuda a prestar contas, além de nos permitir rever a chamada para não perdermos nenhum detalhe.
Agora, uma música de espera é reproduzida enquanto o chamador aguarda para ser atendido por um de nossos funcionários.
Quando um organizador atender a chamada, a pessoa que ligou ouvirá uma mensagem identificando o organizador: “Mariatta está entrando nesta chamada.”
Adicionamos um alerta para informar ao organizador que esta ligação é sobre o Código de Conduta do PyCascades. Eu filtro minhas ligações. Costumo ignorar chamadas de números 1-800 ou chamadas desconhecidas cujo número não reconheço. Durante o período da conferência, preciso saber se essas chamadas são relacionadas a questões do Código de Conduta (que devo atender) ou se são chamadas de telemarketing oferecendo um cruzeiro grátis (que vou ignorar).
Um registro de todas as atividades de chamadas do CoC agora é mantido em uma planilha do Google.
Além disso, o recurso a seguir ainda precisa funcionar:
Informações sobre chamadas recebidas na linha direta do CoC são publicadas no Slack. O Slack é um dos principais meios de comunicação entre os organizadores do PyCascades. A mensagem no Slack serve tanto como notificação quanto como registro de que uma chamada ocorreu, mesmo que ninguém tenha atendido.
Outras informações técnicas:
A linha direta foi desenvolvida em Python, e a estrutura web que escolhi é aiohttp, uma estrutura assíncrona de servidor e cliente web para Python. Usei o aiohttp para criar bots do GitHub como miss-islington e black-out.
O serviço web está implantado no Heroku. A maior parte da infraestrutura web da PSF está hospedada no Heroku; por isso, como desenvolvedor do núcleo do Python, estou mais familiarizado com o Heroku do que com outros tipos de infraestrutura em nuvem.
Para mim, uma das principais razões para escolher a API da Nexmo é que a Nexmo tem apoiado a comunidade Python de várias maneiras, incluindo o patrocínio da PyCon US 2018, da DjangoCon 2018 e da primeira edição da PyCascades ? A outra razão para escolher a API da Nexmo é que a biblioteca nexmo-python está disponível como código aberto, é compatível com as versões mais recentes do Python e foi testada com o Python 3.7.
Você pode visualizar o código-fonte da Linha Direta CoC Aprimorada.
Configuração do aplicativo Nexmo Voice
Primeiro, gostaria de explicar como meu aplicativo Nexmo Voice está configurado.
Nexmo voice app settings
Ao configurar um aplicativo de Voice no Nexmo, é necessário definir duas URLs de webhook: uma URL de evento e uma URL de resposta.
A URL do evento é sempre obrigatória; é para lá que a Nexmo enviará informações sempre que houver uma mudança no status da chamada.
Recebendo o webhook de eventos e registrando as atividades
A seguir, apresentamos um exemplo de carga útil enviada pelo webhook de eventos:
{
"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"
}Ele contém informações como o número de quem ligou, o número discado, o status da chamada, a data e a hora, além de identificadores exclusivos da chamada e da conversa. Todas essas são informações úteis que podem ser registradas para que tenhamos registros de cada atividade.
Em vez de criar meu próprio serviço web para receber esses webhooks, criei uma integração no Zapier. Uma das integrações que você pode usar no Zapier é Webhooks do Zapier. Com o Webhooks by Zapier, você pode receber dados de qualquer serviço ou enviar solicitações para qualquer URL sem precisar escrever código ou operar servidores. Em outras palavras, você pode receber e enviar webhooks.
Quando criei um novo Zap usando o Webhooks by Zapier como ação de acionamento, o Zapier gerou uma URL “hooks.zapier.com” que posso usar para receber os webhooks. Inseri a URL hooks.zapier.com como URL de eventos no Nexmo Voice Application.
Webhook Trigger
Agora que configurei o Zapier para receber o webhook de eventos do Nexmo, posso fazer várias coisas. Primeiro, adicionei uma integração com o Slack, de modo que uma mensagem é publicada automaticamente em nosso canal privado do CoC sobre as chamadas recebidas na linha direta. Em seguida, adicionei uma integração com o Google Sheets, de modo que todas as atividades relacionadas à linha direta sejam automaticamente adicionadas como uma nova linha no Google Sheets.
CoC Events
Atender chamadas
Quando um chamador disca o número da linha direta, a Nexmo enviará a carga útil desse evento para a URL de resposta. A URL de resposta precisa retornar um NCCO (Objeto de Controle de Chamada da Nexmo) que controla essa chamada.
A URL de resposta está definida como o /webhook/answer/ . (código-fonte)
Eu queria que a pessoa que ligasse fosse recebida e informada de que havia entrado em contato com a Linha Direta do Código de Conduta do PyCascades. Portanto, o primeiro NCCO que retornei é uma ação do tipo “talk”:
ncco = [
{
"action": "talk",
"text": "You've reached the PyCascades Code of Conduct Hotline. This call is recorded."
}
]Em seguida, como agora estou recebendo notificações quando alguém liga para a linha direta, preciso ligar para todos os nossos funcionários e conectá-los à mesma chamada. Para isso, preciso adicionar o chamador e os funcionários a uma teleconferência.
Para adicionar participantes a uma teleconferência, vou criar uma ação NCCO do tipo “conversa” com o mesmo nome.
{
"action": "conversation",
"name": conversation name,
}Então, preciso inventar um “nome” para a conversa? Não necessariamente. Dê uma olhada na payload entregue na URL da resposta
Um exemplo de solicitação GET para o answer_url é o seguinte:
/webhooks/answer?to=447700900000&from=447700900001&conversation_uuid=CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab&uuid=aaaaaaaa-bbbb-cccc-dddd-0123456789cd
Observe que a carga útil inclui um conversation_uuid. Em vez de “inventar” novos nomes para a conversa, decidi usar o mesmo conversation_uuid que o nome da conversa.
Então, recuperei o conversation_uuid da solicitação e o utilizei no NCCO.
conversation_uuid = request.rel_url.query["conversation_uuid"].strip()
...
{
"action": "conversation",
"name": conversation_uuid,
...
}Para gravar a conversa, posso especificar "record": True no dicionário NCCO da conversa. Quando a gravação terminar, a Nexmo também enviará um webhook para o eventUrl, e a carga útil desse webhook incluirá a URL onde a gravação está armazenada.
A seguir, um exemplo de carga útil para o webhook de gravação eventUrl webhook de gravação:
{
"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"
}Mais uma vez, em vez de configurar meu próprio serviço web para receber o webhook, utilizei o recurso Webhooks do Zapier. Criei um Zap diferente para receber os webhooks de gravação.
Zapier Recording
Neste Zap, adicionei uma integração com o Google Spreadsheets, para que as informações da carga útil, incluindo o recording_url, sejam automaticamente adicionadas como uma nova linha no Google Spreadsheets. Além disso, adicionei uma integração com o Slack para que os membros da nossa equipe sejam notificados sobre a nova gravação.
Neste momento, a conversa no NCCO se apresenta da seguinte forma:
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")],
...
}Neste momento, há duas coisas que preciso que a linha direta faça. Primeiro, preciso que ela ligue para cada funcionário, para que eu possa adicioná-los à conversa. E, segundo, preciso que ela toque alguma música enquanto a pessoa que está ligando aguarda a conexão.
Reproduzir música enquanto o chamador aguarda
Reproduzir música nesta chamada é bastante simples. Adicione o "musicOnHoldURL" ao NCCO e forneça uma URL para a música a ser reproduzida, por exemplo:
"musicOnHoldUrl": ["https://..../music.mp3"]Estou usando a música da coleção de músicas da Wistia, mais especificamente do álbum “The Let ‘Em In Sessions”. Você pode consultar a licença dessas músicas aqui.
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)],
} Convoque os demais membros da equipe para a teleconferência
Agora preciso ligar para cada um dos funcionários e incluí-los nesta chamada.
Isso não é algo que eu consiga fazer no NCCO. Por isso, recorri ao biblioteca do cliente Python da Nexmo . Ela pode ser instalada usando pip, então adicionei nexmo ao meu arquivo requirements.txt.
Criei uma função auxiliar para instanciar o cliente.
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 clientTambém criei uma função auxiliar para recuperar os números de telefone dos funcionários. Os números de telefone são armazenados como variáveis de ambiente no Heroku, no seguinte formato:
[
{
"name": "Mariatta",
"phone": "12025550124"
},
{
"name": "Miss Islington",
"phone": "12025550123"
}
]A função auxiliar é bastante simples:
import json
def get_phone_numbers():
return json.loads(os.environ.get("PHONE_NUMBERS"))Agora que tenho funções para recuperar o cliente Nexmo, bem como os números de telefone a serem discados, posso discar esses números.
Para ligar para um número usando a biblioteca cliente do Nexmo para Python:
response = client.create_call({
'to': [{'type': 'phone', 'number': 12025550124}],
'from': {'type': 'phone', 'number': 12025550123},
'answer_url': ['https://example.com/answer']
})No create_call método de chamada, precisei fornecer o to número de telefone, bem como o from número de telefone. O to número de telefone é o número do funcionário para quem eu gostaria de ligar.
No lugar do from número, em vez de fornecer o número de telefone da pessoa que ligou para a linha direta, usei o hotline próprio número; assim, a equipe sabe, ao ler o identificador de chamadas, que essa ligação é da linha direta.
E quanto ao answer_url? O answer_url é o webhook acionado quando um funcionário atende essa chamada. O comportamento desejado aqui é que o funcionário que atendeu a chamada seja adicionado à conversa em que o usuário da linha direta está. Portanto, além da carga útil fornecida pela Nexmo ao webhook, preciso passar o conversation_name (que é o conversation_uuid).
Criei um novo endpoint no meu serviço web para lidar com esse webhook, incluindo tanto o conversation_uuid e a chamada uuid para na 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"]
...Com esse endpoint criado, sempre que um funcionário atender a ligação da linha direta, terei como saber a qual conversa devo adicioná-lo.
Por fim, o webhook de resposta tem a seguinte aparência:
@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) Adicionando participantes à teleconferência
O answer_conference_call endpoint foi criado com o objetivo de adicionar os membros da equipe à teleconferência. Para isso, precisei retornar um NCCO ao webhook que contivesse uma ação “conversation” e o nome da conversa. Mas, antes que eles sejam adicionados, gostaria de dar as boas-vindas à equipe para que saibam que estão participando da Linha Direta do Código de Conduta da PyCascades.
Lembre-se de que a PHONE_NUMBERS variável de ambiente também inclui os nomes dos titulares dos números de telefone.
Criei a seguinte função para recuperar o nome do titular do número de telefone:
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 NoneCom essa função, posso cumprimentar a equipe da seguinte maneira:
@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)Neste momento, você deve estar se perguntando para que origin_call_uuid é usado para isso. Achei que seria uma boa cortesia informar à pessoa que ligou para a linha de atendimento qual membro da equipe do PyCascades está atendendo a ligação. Além disso, lembre-se de que se trata de uma teleconferência, portanto, é possível que mais de uma pessoa participe. Em vez de deixar alguém entrar sem avisar, estou avisando a todos na chamada quem acabou de entrar.
client = get_nexmo_client()
response = client.send_speech(
origin_call_uuid, text=f"{phone_number_owner} is joining this call."
)Então, agora o answer_conference_call ponto final fica assim:
@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) O fluxo da chamada concluída
Com isso, o Código de Conduta aprimorado do PyCascades está concluído.
O fluxo completo da chamada é o seguinte:
Um ouvinte liga para a linha direta.
Os funcionários da PyCascades recebem uma notificação no Slack informando que há uma chamada recebida na linha direta.
As informações sobre a chamada são adicionadas ao Google Sheets.
O chamador ouve a seguinte mensagem: “Bem-vindo à Linha Direta do Código de Conduta da PyCascades. Esta ligação está sendo gravada.”
A pessoa que liga ouve música enquanto espera para ser atendida.
Cada funcionário da PyCascades recebe uma ligação da linha direta.
Um funcionário da PyCascades atende a ligação e ouve: “Alô, {nome do funcionário}, transferindo sua ligação para a linha de atendimento da PyCascades.”
Enquanto isso, quem está ligando ouve a mensagem “{staffname} está entrando nesta chamada”.
O funcionário e a pessoa que ligou continuam a conversa.
O atendente desliga o telefone, momento em que a gravação da ligação é concluída.
Os funcionários recebem uma notificação no Slack informando que há uma nova gravação.
As informações sobre a gravação também são adicionadas ao Google Sheets.
Baixando a gravação
A gravação pode ser baixada usando o cliente Python da Nexmo, e a recording_url é a URL recebida no webhook de eventos de gravação.
client = get_nexmo_client()
recording = client.get_recording(recording_url)As gravações de chamadas ficam armazenadas no Nexmo por um mês antes de serem excluídas automaticamente. Como essas chamadas são importantes e não queremos perder as gravações, criei um script de linha de comando que pode ser usado para baixar as gravações.
O script pode ser executado da seguinte maneira:
python3 -m download_recording url1 url2 url3 ...Assim que o script for executado, as gravações serão baixadas e armazenadas localmente no recording diretório.
Conclusões
Graças à Nexmo e ao Zapier, estou conseguindo aprimorar a linha direta do Código de Conduta da PyCascades. Configurar essa linha direta parece ser mais complicado do que antes.
No entanto, acredito que os novos recursos, como a gravação automática e o registro automático no Google Spreadsheets, sejam úteis para todos os membros da nossa equipe; por isso, estou disposto a dedicar um tempo extra para configurar isso para o PyCascades. Além disso, ao usar o Zapier em vez de codificar manualmente, podemos ter mais flexibilidade caso queiramos adicionar outras integrações.
Obrigado pela leitura! Se tiver mais dúvidas sobre a linha direta, o PyCascades ou o Zapier, não hesite em me enviar um e-mail para mariatta.wijaya@zapier.com
Nota da Equipe de Relações com Desenvolvedores da Nexmo: Estamos muito felizes que a Mariatta tenha decidido usar a Nexmo para ajudar a melhorar o sistema de denúncias do Código de Conduta do PyCascades. Acreditamos que ter um Código de Conduta é fundamental para criar um espaço acolhedor e inclusivo. Gostaríamos de demonstrar nosso apoio a qualquer conferência ou encontro que queira implementar uma linha direta de denúncias relacionadas ao Código de Conduta. Se você é organizador de eventos e gostaria de utilizar a linha direta de denúncias do Código de Conduta da Mariatta para o seu evento, por favor, envie um e-mail para devrel@nexmo.com e teremos o maior prazer em ajudá-lo a configurar o aplicativo e oferecer um crédito gratuito da Nexmo.