
Compartilhar:
Sou ator formado, com uma dissertação sobre stand-up comedy, e comecei a me dedicar ao desenvolvimento em PHP por meio dos encontros da comunidade. Você pode me encontrar dando palestras e escrevendo sobre tecnologia, ou ouvindo e comprando discos curiosos da minha coleção de vinil.
A evolução do Helpdesk da Vonage em Laravel com a OpenAI
Tempo de leitura: 8 minutos
Este artigo foi atualizado em março de 2025
Este é o artigo que dá continuidade a outro sobre um Laravel que utiliza as APIs da Vonage para reproduzir casos de uso comuns do mundo real.
Na primeira parte, criamos uma nova aplicação Laravel e importamos o biblioteca vonage-laravel e criamos uma visualização de ticket do Helpdesk. O cliente seleciona o método de comunicação de sua preferência (neste caso, apenas SMS) para que qualquer mensagem enviada por um administrador seja encaminhada para o celular do cliente. A resposta à mensagem seria então adicionada por meio de webhooks de entrada e registrada na conversa do ticket.
Vale ressaltar que esse código foi escrito antes de o Laravel alterar a forma de criação de modelos iniciais; portanto, para reproduzi-lo do zero, você deve escolher a opção de criar um kit inicial com autenticação integrada e Livewire/Blade.
Neste artigo, vamos adicionar a funcionalidade de usar os recursos de conversão de texto em fala (TTS) da Vonage por meio da Voice API, permitindo que o cliente fale uma resposta que será transcrita de volta para a conversa do ticket.
Pré-requisitos
Vamos partir do princípio de que o primeiro tutorial já foi concluído, o que nos dará:
O serviço de suporte repositório do helpdesk, clonado localmente do GitHub
Laravel Sail em funcionamento, para migrar o ambiente de desenvolvimento local para o Docker
Vite servidor de desenvolvimento em execução para compilar os recursos padrão do Laravel Breezeseus recursos padrão
Ngrok em execução localmente e uma instância do aplicativo Vonage configurada para enviar webhooks para ele
Como isso funciona? Parte 2: Voice
OK, então é hora de vermos os recursos de Voice. O fluxo de como as conversas acontecem aqui é exatamente o mesmo: você cria um novo ticket como cliente e a visualização da conversa é aberta. No entanto, ao criá-lo desta vez, vamos configurar o ticket como uma conversa por Voice.
Ativação da função de Voice
Para que isso funcione, precisaremos de um ID de aplicativo habilitado para Voice no Painel da Vonage. Você pode editar o aplicativo anterior, do último tutorial, ou criar um novo. Basta habilitar o ID do aplicativo: não se preocupe em usar a interface do usuário para enviar webhooks para a rota local correta (verificaremos isso mais tarde), pois o código gera a URL de resposta do webhook para você (isso é diferente de como configuramos o SMS, e explicarei o motivo quando analisarmos o código mais adiante neste artigo).
O que é a OpenAI?
O produto mais conhecido que você talvez já tenha ouvido falar em relação a esse nome é ChatGPT.
O ChatGPT é um modelo de linguagem de última geração desenvolvido pela OpenAI, projetado para manter conversas em linguagem natural com os usuários. Como um assistente baseado em IA, o ChatGPT pode fornecer informações, responder a perguntas e auxiliar em diversas tarefas. Ele utiliza técnicas de aprendizado profundo para compreender e gerar textos semelhantes aos humanos, tornando as interações mais personalizadas.
Sim, esse parágrafo foi escrito pelo ChatGPT. Mas o que talvez você não saiba é que a empresa por trás dele, a OpenAI, tem vários outros produtos que podem ser acessados por meio de sua API. Um desses produtos é o Whisper, que vamos usar para transcrever o que um cliente diz em uma mensagem gravada em resposta a um ticket aberto no aplicativo Helpdesk.
Configurando a API da OpenAI
Primeiro, você precisará de um Account na OpenAI. Siga este link para criar um Accounte, quando terminar, acesse “Gerenciar Account” no menu do seu perfil, no canto superior direito. Ao abrir essa página, você verá a tela a seguir — vá até “Chaves de API” e configure uma nova chave. O resultado final deve ficar mais ou menos assim:

Ao criar a chave, você terá uma única oportunidade de copiá-la — certifique-se de fazer isso.
Precisamos adicionar esse segredo ao nosso env arquivo. Você pode ver no example.env arquivo no repositório que temos um espaço reservado para ele:

Incluí as outras na captura de tela porque é importante observar que esse recurso não funcionará se todas essas variáveis de ambiente não estiverem definidas:
VONAGE_SMS_FROMé reutilizado como o número de origem da chamadaPUBLIC_URLé o seu Ngrok (ou qualquer outra ferramenta, como o Exposeda Beyond Code). Isso é essencial, pois o código irá montar a URL de resposta da API ao fazer uma chamadaVONAGE_APPICATION_IDeVONAGE_PRIVATE_KEY. No último tutorial, poderíamos ter usado a autenticação básica, mas, para que os webhooks funcionem, eles precisam estar vinculados a um ID de aplicativo. Para usar a Voice API da Vonage, precisamos ter uma chave privada e um ID de aplicativo, que o SDK PHP da Vonage usará para gerar e gerenciar a autorização JWT para nós.
Por trás dos bastidores
A funcionalidade que vamos analisar está no update() método em TicketController. Queremos fazer uma chamada de saída apenas se o ticket estiver sendo atualizado por um usuário administrador (em vez do cliente) e se o cliente tiver escolhido Voice como sua preferência de comunicação.
if ($userTicket->notification_method === 'voice') {
$currentHost = config('helpdesk.public_url');
$outboundCall = new OutboundCall(
new Phone($userTicket->phone_number),
new Phone(config('vonage.sms_from'))
);
$outboundCall
->setAnswerWebhook(
new Webhook($currentHost . '/webhook/answer/' . $ticketEntry->id, Webhook::METHOD_GET)
)
->setEventWebhook(
new Webhook($currentHost . '/webhook/event/' . $ticketEntry->id, Webhook::METHOD_POST)
);
Vonage::voice()->createOutboundCall($outboundCall);
}
Aqui está um resumo do que o código está fazendo neste trecho:
Sabemos que queremos fazer uma chamada de saída neste bloco lógico; por isso, criamos um novo
OutboundCallque obtém o número de telefone do cliente a partir do ticket e o número de origem a partir da configuração.Essa é a parte legal. Lembra que, na primeira parte deste tutorial, definimos uma URL do Ngrok no Painel do Vonage para os webhooks de SMS? Aqui não fizemos isso, porque cada chamada que usa o SDK de Voice pode ser configurada para usar uma URL de retorno de chamada específica para essa chamada que estamos fazendo. Essa parte é muito importante, pois nos permite configurar o estado. Nesse caso, pegamos a URL pública do Ngrok em
$currentHost(ou seja, a constantePUBLIC_URL), uma rota definida por nós para nosso aplicativo (/webhook/answer/) e o elemento-chave para que isso funcione: o ID do ticket como parte da rota. Mais tarde, noWebhookControllerpodemos extrair o ticket pai, além do proprietário desse ticket.
Então, agora precisamos de um novo controlador para lidar com o que ocorre quando o cliente conclui sua chamada de ticket. As duas partes envolvidas são:
Reproduza uma resposta quando o cliente atender a ligação (essa configuração será definida no controlador atribuído à rota).
Tenha uma rota para ler os eventos de resposta recebidos (nós os definimos ao configurar a chamada de saída).
A partir do evento de gravação gerado após o término da chamada, obtenha a gravação de voz da resposta do cliente, transcreva-a usando o OpenAI e salve-a como um novo
TicketEntry.
Ufa! Tem bastante coisa pra assimilar aqui, então vamos começar a nos aprofundar:
Utilização de NCCOs para TTS
Os NCCOs são cargas JSON que instruem os serviços da Vonage sobre o que “fazer”, ou seja, uma ação, registrar algo etc. Quando o cliente atende o telefone, queremos ler para ele a última atualização do ticket feita pelo administrador e, em seguida, solicitar que ele responda. Aqui está a rota:
Route::post('/webhook/answer/{ticketEntry:id}', [WebhookController::class, 'answer'])->name('voice.answer');
A rota aponta para WebhookController::answer(), então nossa resposta do TTS fica assim:
public function answer(TicketEntry $ticketEntry): JsonResponse
{
if (!$ticketEntry->exists) {
return response()->json([
[ 'action' => 'talk',
'text' => 'Sorry, there has been an error fetching your ticket information'
]
]); }
return response()->json([
[ 'action' => 'talk',
'text' => 'This is a message from the Vonage Helpdesk'
],
[ 'action' => 'talk',
'text' => $ticketEntry->content,
], [ 'action' => 'talk',
'text' => 'To add a reply, please leave a message after the beep, then press the pound key',
], [ 'action' => 'record',
'endOnKey' => '#',
'beepStart' => true,
'eventUrl' => [config('helpdesk.public_url') . '/webhook/recordings/' . $ticketEntry->id]
], [ 'action' => 'talk',
'text' => 'Thank you, your ticket has been updated.',
] ]);}
Cada matriz fornece um conjunto de instruções bastante simples, mas o elemento-chave aqui para responder à pergunta “como capturamos a resposta do cliente?” está na record ação. Você pode ver que ela fornece um beepStart solicitação e, mais importante ainda, definimos o comportamento após a conclusão da chamada. O eventUrl será acionado por um webhook que conterá a URL dessa gravação.
Processamento da gravação
Nossa próxima rota é aquela que conterá um link para a resposta do cliente na forma de um arquivo MP3 gravado, bem como o ID do ticket, para que saibamos a qual entidade ela pertence. Aqui está um exemplo de payload que podemos esperar receber:
{
"start_time": "2020-01-01T12:00:00.000Z",
"recording_url": "https://api.nexmo.com/v1/files/bbbbbbbb-aaaa-cccc-dddd-0123456789ab",
"size": 12222,
"recording_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
"end_time": "2020-01-01T12:00:00.000Z",
"conversation_uuid": "CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
"timestamp": "2020-01-01T12:00:00.000Z"
}E aqui está o nosso trajeto:
Route::post('/webhook/recordings/{ticketEntry:id}', [WebhookController::class, 'recording'])->name('voice.recording');
E o recording() método para lidar com isso:
public function recording(TicketEntry $ticketEntry, Request $request): Response|Application|ResponseFactory
{
$params = $request->all();
Log::info('Recording event', $params);
$audio = Vonage::voice()->getRecording($params['recording_url']);
Storage::put('call_recording.mp3', $audio);
$ticketContent = $this->transcribeRecordingOpenAi();
$newTicketEntry = new TicketEntry([
'content' => $ticketContent,
'channel' => 'voice',
]);
$parentTicket = $ticketEntry->ticket()->get()->first();
$newTicketEntryUser = $parentTicket->user()->get()->first();
$newTicketEntry->user()->associate($newTicketEntryUser);
$newTicketEntry->ticket()->associate($parentTicket);
$newTicketEntry->save();
return response('', 204);
}
Isso utiliza o Route Model Binding para extrair o TicketEntry e injetá-lo como uma dependência; em seguida, extrai o recording_url. O SDK da Vonage possui um método útil chamado getRecording() que retorna um StreamInterface contido no corpo.
Por motivos de segurança, não é possível enviar o fluxo de áudio diretamente para a solicitação da OpenAI que vamos enviar para transcrição; por isso, precisamos salvar o arquivo temporariamente. Depois de salvá-lo, podemos usar a Storage fachada para lê-lo novamente durante a solicitação de transcrição e, em seguida, excluí-lo.
O transcribeRecording() é um método personalizado neste controlador de classe que abordaremos em breve, mas, supondo que uma string seja retornada pela transcrição, criamos um novo TicketEntry, associamos-a ao titular do ticket (sabemos que se trata do cliente, pois é uma rota de webhook de entrada) e a salvamos no Ticket
Transcrição da OpenAI
Esta é a última parte: obter nossa transcrição. Existem maneiras de fazer isso de forma assíncrona, mas optei por fazê-lo de forma síncrona para simplificar. Se você quiser implementar isso de forma assíncrona (afinal, trata-se de processamento de dados, então é uma boa prática fazê-lo assim), pode usar um worker da Fila de Tarefas do Laravel, mas esteja ciente de que você pode muito bem se deparar com problemas de condição de corrida (o que já aconteceu comigo no passado).
A transcrição no controlador de método é tratada pela transcribeRecording() função, então vamos dar uma olhada nela:
public function transcribeRecordingOpenAi(): string
{
$client = new Client([
'base_uri' => 'https://api.openai.com/v1/',
]);
$audioPath = Storage::path('call_recording.mp3');
$multipart = new MultipartStream([
[ 'name' => 'file',
'contents' => fopen($audioPath, 'rb'),
'filename' => basename($audioPath),
], [ 'name' => 'model',
'contents' => 'whisper-1',
], ]);
$response = $client->request('POST', 'audio/transcriptions', [
'headers' => [
'Authorization' => 'Bearer ' . config('helpdesk.open_ai_secret'),
'Content-Type' => 'multipart/form-data; boundary=' . $multipart->getBoundary(),
], 'body' => $multipart,
]);
Storage::delete('call_recording.mp3');
$responseBody = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
return $responseBody['text'];
}
Isso foi montado às pressas apenas para fins de demonstração; portanto, em primeiro lugar, gostaria de deixar claro que, se o seu aplicativo tiver uma dependência de uma API de terceiros como esta (ou a Vonage), você deve encapsular esse cliente e sua configuração como um provedor de serviço.
O método cria um novo Guzzle Client e prepara a solicitação como um MultipartStream, já que a OpenAI exige que a solicitação seja um formulário codificado. Definimos a URL base e buscamos nosso arquivo temporário criado anteriormente (call_recording.mp3). Agora podemos usar fopen() para gravar o arquivo e, em seguida, excluí-lo após a conclusão da solicitação.
Se tudo correr bem, você receberá um array de transcrição, que conterá a chave text, que é enviada de volta para atualizar o TicketEntry. Parabéns: agora temos um sistema de emissão de bilhetes TTS em funcionamento!
Conclusão
Sempre incentivamos a participação da comunidade. Fique à vontade para se juntar a nós no GitHub e no Slack da Comunidade Vonage. Você também pode nos enviar uma mensagem no Twitter.
Compartilhar:
Sou ator formado, com uma dissertação sobre stand-up comedy, e comecei a me dedicar ao desenvolvimento em PHP por meio dos encontros da comunidade. Você pode me encontrar dando palestras e escrevendo sobre tecnologia, ou ouvindo e comprando discos curiosos da minha coleção de vinil.