
Compartir:
Actor de formación con una disertación sobre la comedia, llegué al desarrollo de PHP a través de la escena de las reuniones. Puedes encontrarme hablando y escribiendo sobre tecnología, o tocando/comprando discos raros de mi colección de vinilos.
Evolución del servicio de asistencia de Vonage Laravel con OpenAI
Tiempo de lectura: 8 minutos
Este artículo se actualizó en marzo de 2025
Este es el artículo de seguimiento a uno sobre un Laravel que aprovecha las API de Vonage para reproducir casos de uso comunes en el mundo real.
En la primera partecreamos una nueva aplicación Laravel, tiramos de la base de datos vonage-laravel y creamos una vista de ticket del Helpdesk. El cliente selecciona el método de comunicación elegido (en este caso, sólo SMS) para que cualquier mensaje publicado por un administrador se envíe al teléfono móvil del cliente. La respuesta al mensaje se añadiría mediante webhooks entrantes y se escribiría en la conversación del ticket.
Vale la pena señalar que este código fue escrito antes de que Laravel cambiara su creación de plantillas de inicio, por lo que para replicar esto desde cero tendrías que elegir la opción de crear un kit de inicio con autenticación incorporada, y Livewire/Blade.
En este artículo, vamos a agregar la capacidad de usar las funciones de voz de texto a voz (TTS) de Vonage mediante la Voice APIcon la capacidad de que el cliente diga una respuesta que se transcribe de vuelta a la conversación del ticket.
Requisitos previos
Vamos a suponer que el primer tutorial se ha completado, lo que nos dará:
En helpdesk clonado localmente desde GitHub
Laravel Sail en funcionamiento, para dockerizar el entorno de desarrollo local
Vite servidor de desarrollo en ejecución para construir Laravel Breezede Laravel Breeze
Ngrok ejecutándose localmente, y una instancia de aplicación de Vonage configurada para enviarle webhooks
¿Cómo lo hace? Parte 2: Voice
Bien, es hora de pasar a las capacidades de Voice. El flujo de cómo se producen las conversaciones aquí es exactamente el mismo: se crea un nuevo ticket como cliente y se abre la vista de conversación. Sin embargo, cuando lo creamos esta vez, vamos a configurar el ticket como una conversación de Voz.
Activar Voice
Para que esto funcione, necesitaremos un ID de aplicación habilitada para Voice en el Panel de Vonage. Puedes editar la aplicación anterior del último tutorial o crear una nueva. Habilitar el ID de la aplicación es todo lo que necesitas: no te preocupes por usar la interfaz de usuario para enviar webhooks a la ruta local correcta (revisaremos esto más adelante) ya que el código construye la URL de respuesta del webhook por ti (esto es diferente de cómo hemos configurado SMS, y revisaré por qué cuando revisemos el código más adelante en el artículo).
¿Qué es OpenAI?
El producto más común del que puede haber oído hablar asociado a este nombre es ChatGPT.
ChatGPT es un modelo lingüístico de vanguardia desarrollado por OpenAIdiseñado para entablar conversaciones en lenguaje natural con los usuarios. Como asistente basado en IA, ChatGPT puede proporcionar información, responder preguntas y ayudar con diversas tareas. Utiliza técnicas de aprendizaje profundo para comprender y generar texto similar al humano, lo que hace que las interacciones sean más personalizadas.
Sí, ese párrafo lo escribió ChatGPT. Pero lo que quizá no sepas es que la empresa que está detrás, OpenAI, tiene varios otros productos a los que se puede acceder a través de su API. Uno de ellos es Whisperque vamos a utilizar para transcribir lo que dice un cliente como un mensaje grabado en respuesta a una entrada de ticket de la aplicación Helpdesk.
Configuración de la API OpenAI
En primer lugar, necesitarás una Account de OpenAI. Sigue este enlace para crear una AccountCuando hayas terminado, dirígete a "Gestionar Account" en el menú superior derecho de tu perfil. Al abrirla verás la siguiente pantalla: dirígete a Claves API y configura una nueva clave. El resultado final debería ser algo parecido a esto:

Cuando crees la clave, tendrás una oportunidad de copiarla: asegúrate de hacerlo.
Tenemos que añadir ese secreto a nuestro env archivo. En el archivo example.env archivo en el repositorio que tenemos un marcador de posición para ello:

He incluido las demás en la captura de pantalla porque es importante tener en cuenta que esta función no funcionará si no se establecen todas estas variables de entorno:
VONAGE_SMS_FROMse reutiliza como número de llamada salientePUBLIC_URLes tu Ngrok (o cualquier otra herramienta como Beyond Code's Exponer). Esto es esencial, ya que el código coserá la URL de respuesta a la API al realizar una llamadaVONAGE_APPICATION_IDyVONAGE_PRIVATE_KEY. En el último tutorial, podríamos haber utilizado la autenticación básica, pero para que los webhooks funcionen, deben estar vinculados a un ID de aplicación. Para usar la Voice API de Vonage, debemos tener una clave privada y un ID de aplicación, que el SDK PHP de Vonage usará para generar y manejar la autorización JWT por nosotros.
Bajo el capó
La funcionalidad que vamos a ver se encuentra en el método update() del módulo TicketController. Sólo queremos hacer una llamada saliente si el ticket está siendo actualizado por un usuario administrador (en lugar del cliente), y el cliente ha elegido la voz como su preferencia de comunicación.
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);
}
Aquí tienes una sinopsis de lo que hace el código:
Sabemos que queremos hacer una llamada saliente en este bloque lógico, así que creamos un nuevo bloque
OutboundCallque extrae el número de teléfono del cliente del ticket y el número de envío de la configuración.Esta es la parte interesante. ¿Recuerdas que en la primera parte de este tutorial establecimos una URL de Ngrok en el panel de Vonage para los webhooks de SMS? No hemos hecho eso aquí, porque cada llamada que usa el SDK de Voice puede configurarse para usar una URL de devolución de llamada específica para esta llamada que estamos realizando. Esta parte es realmente importante porque nos permite configurar el estado. En este caso, tomamos la URL pública de Ngrok de
$currentHost(es decir, la constantePUBLIC_URL), una ruta definida por nosotros para nuestra aplicación (/webhook/answer/) y la clave para que esto funcione: el ID de la entrada como parte de la ruta. Más tarde, enWebhookControllerpodemos sacar el ticket padre, además del propietario de ese ticket.
Por lo tanto, ahora necesitamos un nuevo controlador para manejar lo que entra cuando el cliente ha completado su llamada ticket. Las dos partes de esto son:
Leer en voz alta una respuesta cuando el cliente descuelga su llamada telefónica (esto se establecerá en el controlador asignado a la ruta).
Disponer de una ruta para leer los eventos de respuesta entrantes (los configuramos al configurar la llamada saliente).
A partir del evento de grabación generado una vez finalizada la llamada, obtenga una grabación de voz de la respuesta del cliente, transcríbala con OpenAI y escríbala como un nuevo archivo
TicketEntry.
¡Uf! Hay mucho que digerir, así que empecemos a comer:
Utilización de las OCN para el TTS
Las NCCO son cargas útiles JSON que indican a los servicios de Vonage qué "hacer", es decir, una acción, grabar algo, etc. Cuando el cliente levanta el teléfono, queremos leerle la última actualización del ticket realizada por el administrador y luego darle una instrucción para que responda. Esta es la ruta:
Route::post('/webhook/answer/{ticketEntry:id}', [WebhookController::class, 'answer'])->name('voice.answer');
La ruta apunta a WebhookController::answer()por lo que nuestra respuesta TTS tiene este aspecto:
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 da una carga útil de instrucciones que son bastante sencillas, pero el pegamento importante aquí para responder a la pregunta "¿cómo capturamos la respuesta del cliente?" está en la acción record acción. Se puede ver que da un beepStart y, lo que es más importante, definimos el comportamiento una vez finalizada la llamada. El eventUrl recibirá un webhook que contendrá una URL de esta grabación.
Tratamiento de la grabación
Nuestra siguiente ruta es la que contendrá un enlace a la respuesta del cliente como MP3 grabado, así como el ID del ticket para que sepamos a qué entidad pertenece. Este es un ejemplo de payload que podemos esperar recibir:
{
"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"
}Y aquí está nuestra ruta:
Route::post('/webhook/recordings/{ticketEntry:id}', [WebhookController::class, 'recording'])->name('voice.recording');
Y el recording() método para manejarlo:
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);
}
Esto utiliza Route Model Binding para tomar el TicketEntry y lo inyecta como dependencia, y luego extrae el modelo recording_url. El SDK de Vonage tiene un método útil llamado getRecording() que devolverá un StreamInterface contenido en el cuerpo.
Por razones de seguridad, no se puede escribir el flujo del audio directamente a la solicitud de OpenAI que vamos a enviar para la transcripción, por lo que necesitamos guardar el archivo temporalmente. Una vez que lo hayamos guardado, podemos utilizar la fachada Storage para volver a leerlo durante la solicitud de transcripción y luego borrarlo.
El transcribeRecording() es un método personalizado en este controlador de clase al que llegaremos en un momento, pero suponiendo que una cadena vuelve de la transcripción, creamos un nuevo archivo TicketEntryasociándolo con el propietario del ticket (sabemos que es el cliente ya que es una ruta webhook entrante), y lo guardamos en el directorio Ticket
Transcripción OpenAI
Esta es la última parte - obtener nuestra transcripción. Hay maneras de hacer esto de forma asíncrona, pero he optado por hacerlo de forma síncrona para mantenerlo más simple. Si quieres implementar esto de forma asíncrona (después de todo, se trata de procesamiento de datos, por lo que es una buena práctica hacerlo), puedes utilizar un trabajador de Laravel Job Queue, pero ten en cuenta que podrías encontrarte con problemas de race condition (como me ha pasado en el pasado).
La transcripción en el controlador del método es manejada por la función transcribeRecording() así que echémosle un vistazo:
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'];
}
Esto ha sido hackeado por razones de demostración, por lo que en primer lugar me gustaría dejar claro que si su aplicación tiene una dependencia de una API de terceros como éste (o Vonage), debe envolver este cliente y su configuración como un proveedor de servicios.
El método crea un nuevo Guzzle y prepara la petición como un archivo MultipartStreamya que OpenAI requiere que la petición esté codificada. Establecemos la url base y recuperamos nuestro archivo temporal que creamos antes (call_recording.mp3). Ahora podemos usar fopen() para escribir el archivo, y luego borrarlo después de que la solicitud se haya completado.
Si todo va bien, obtendrá de vuelta un array de transcripción, que contendrá la clave textque se devuelve para actualizar el array TicketEntry. Enhorabuena: ¡ya tenemos un sistema de tickets TTS que funciona!
Conclusión
La participación de la comunidad es siempre bienvenida. No dude en unirse a nosotros en GitHub y en Slack de la comunidad de Vonage. También puedes enviarnos un mensaje en Twitter.
Compartir:
Actor de formación con una disertación sobre la comedia, llegué al desarrollo de PHP a través de la escena de las reuniones. Puedes encontrarme hablando y escribiendo sobre tecnología, o tocando/comprando discos raros de mi colección de vinilos.