https://d226lax1qjow5r.cloudfront.net/blog/blogposts/evolving-the-vonage-laravel-helpdesk-with-openai/laravel-vonage-helpdesk_v2.png

Faire évoluer le service d'assistance Laravel de Vonage avec OpenAI

Publié le June 20, 2023

Temps de lecture : 8 minutes

Cet article a été mis à jour en mars 2025

Cet article est la suite d'un article sur l'évolution d'un système de gestion de l'information. Laravel qui exploite les Applications de Vonage pour reproduire des cas d'utilisation courants dans le monde réel.

Dans la première partie, nous avons créé une nouvelle application Laravel, tirée dans le fichier vonage-laravel et avons créé une vue de ticket Helpdesk. Le client sélectionne la méthode de communication de son choix (dans ce cas, il s'agissait uniquement de SMS), de sorte que tout message posté par un administrateur soit envoyé sur le téléphone portable du client. La réponse au message serait alors ajoutée à l'aide des webhooks entrants et écrite dans la conversation du ticket.

Il est intéressant de noter que ce code a été écrit avant que Laravel ne modifie la création des modèles de démarrage, donc pour reproduire ceci à partir de zéro, vous devez choisir l'option de création d'un kit de démarrage avec authentification intégrée, et Livewire/Blade.

Dans cet article, nous allons ajouter la possibilité d'utiliser les capacités vocales de synthèse vocale (TTS) de Vonage à l'aide de la fonction Voice APIde Vonage, avec la possibilité pour le client de prononcer une réponse qui sera transcrite dans la conversation du ticket.

Conditions préalables

Nous supposerons que le premier tutoriel a été réalisé, ce qui nous donnera :

Comment fait-il cela ? Partie 2 : Voice

Bon, il est temps de passer en revue les capacités Voice. Le déroulement des conversations est exactement le même : vous créez un nouveau ticket en tant que client et la vue de la conversation s'ouvre. Cependant, cette fois-ci, nous allons configurer le ticket comme une conversation vocale.

Permettre la Voice

Avant que cela ne fonctionne, nous aurons besoin d'un identifiant d'application Voice dans le tableau de bord Vonage. Vous pouvez modifier l'application précédente du dernier tutoriel ou en créer une nouvelle. L'activation de l'ID de l'application est tout ce dont vous avez besoin : ne vous souciez pas d'utiliser l'interface utilisateur pour envoyer les webhooks à la bonne route locale (nous verrons cela plus tard) car le code construit l'URL de réponse du webhook pour vous (c'est différent de la façon dont nous avons configuré les SMS, et je vais expliquer pourquoi lorsque nous examinerons le code plus tard dans l'article).

Qu'est-ce que l'OpenAI ?

Le produit le plus courant dont vous avez peut-être entendu parler en association avec ce nom est le ChatGPT.

ChatGPT est un modèle linguistique de pointe développé par l OpenAIet conçu pour engager des conversations en langage naturel avec les utilisateurs. En tant qu'assistant basé sur l'IA, ChatGPT peut fournir des informations, répondre à des questions et aider à accomplir diverses tâches. Il utilise des techniques d'apprentissage en profondeur pour comprendre et générer des textes semblables à ceux des humains, ce qui rend les interactions plus personnalisées.

Oui, ce paragraphe a été écrit par ChatGPT. Mais ce que vous ne savez peut-être pas, c'est que l'entreprise qui se trouve derrière, OpenAI, a plusieurs autres produits qui sont tous accessibles via leur API. L'un de ces produits est Whisperque nous allons utiliser pour transcrire ce qu'un client dit dans un message enregistré en réponse à une entrée de ticket depuis l'application Helpdesk.

Mise en place de l'API OpenAI

Tout d'abord, vous aurez besoin d'un Account OpenAI. Suivez ce lien pour créer un AccountUne fois que vous avez créé votre compte, vous devez vous rendre dans la section "Manage Account" (Gérer le compte) dans le menu de votre profil en haut à droite. En ouvrant cette fenêtre, vous verrez l'écran suivant - allez dans API Keys et configurez une nouvelle clé. Le résultat final devrait ressembler à ceci :

Screenshot of the OpenAI dashboard showing API Key creation

Lorsque vous créez la clé, vous avez une chance de la copier - assurez-vous de le faire.

Nous devons ajouter ce secret à notre fichier env à notre fichier Vous pouvez voir dans le fichier example.env dans le repo que nous avons un espace réservé pour cela :

Screenshot of Helpdesk's environment variables example file

J'ai inclus les autres dans la capture d'écran car il est important de noter que cette fonctionnalité ne fonctionnera pas si toutes ces variables d'environnement ne sont pas définies :

  • VONAGE_SMS_FROM est réutilisé comme numéro d'appel sortant

  • PUBLIC_URL est votre Ngrok (ou tout autre outil tel que Beyond Code's Expose). C'est essentiel, car le code va assembler l'URL de réponse à l'API lors d'un appel.

  • VONAGE_APPICATION_ID et VONAGE_PRIVATE_KEY. Dans le dernier tutoriel, nous aurions pu utiliser l'authentification de base, mais pour que les webhooks fonctionnent, ils doivent être liés à un identifiant d'application. Pour utiliser l'API Voice de Vonage, nous devons disposer d'une clé privée et d'un identifiant d'application, que le SDK PHP de Vonage utilisera pour générer et gérer l'autorisation JWT pour nous.

Sous le capot

La fonctionnalité que nous allons examiner se trouve dans la méthode update() dans la méthode TicketController. Nous ne voulons passer un appel sortant que si le ticket est mis à jour par un utilisateur administrateur (par opposition au client) et que le client a choisi la voix comme préférence de communication.

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);
}

Voici un résumé de ce que le code fait ici :

  • Nous savons que nous voulons effectuer un appel sortant dans ce bloc logique, nous créons donc un nouveau OutboundCall qui récupère le numéro de téléphone du client dans le ticket et le numéro d'envoi dans la configuration.

  • Voici ce qui est intéressant. Vous savez que dans la première partie de ce tutoriel, nous avons défini une URL Ngrok dans le tableau de bord de Vonage pour les webhooks SMS ? Nous ne l'avons pas fait ici, car chaque appel utilisant le Voice SDK peut être configuré pour utiliser une URL de rappel spécifique pour l'appel que nous effectuons. Cette partie est très importante car elle nous permet de configurer l'état. nous permet de configurer l'état. Dans ce cas, nous prenons l'URL publique Ngrok de $currentHost (c'est-à-dire la constante PUBLIC_URL), une route que nous avons définie pour notre application (/webhook/answer/) et la clé pour que cela fonctionne : l'identifiant de l'entrée du ticket en tant que partie de la route. Plus tard, dans la section WebhookController nous pouvons extraire le ticket parent, ainsi que le propriétaire de ce ticket.

Nous avons donc besoin d'un nouveau contrôleur pour gérer ce qui arrive lorsque le client a terminé son appel. Les deux parties de ce contrôleur sont les suivantes :

  • Lire une réponse lorsque le client décroche son appel téléphonique (ce paramètre est défini dans le contrôleur affecté à l'itinéraire).

  • Disposer d'une route pour lire les événements de réponse entrante (nous les définissons lors de l'établissement de l'appel sortant).

  • À partir de l'événement d'enregistrement généré après la fin de l'appel, récupérez un enregistrement vocal de la réponse des clients, transcrivez-le à l'aide d'OpenAI et écrivez-le en tant que nouveau fichier TicketEntry.

Ouf ! Il y a pas mal de choses à digérer, alors commençons à manger :

Utilisation des NCCO pour le TTS

Les NCCO sont des charges utiles JSON qui indiquent aux services Vonage ce qu'ils doivent faire, c'est-à-dire une action, un enregistrement, etc. Lorsque le client décroche le téléphone, nous voulons lui lire la dernière mise à jour du ticket effectuée par l'administrateur, puis l'inviter à y répondre. Voici l'itinéraire :

Route::post('/webhook/answer/{ticketEntry:id}', [WebhookController::class, 'answer'])->name('voice.answer');

L'itinéraire pointe vers WebhookController::answer()Notre réponse TTS ressemble donc à ceci :

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.',  
        ]    ]);}

Chaque tableau contient une série d'instructions assez simples, mais la colle importante pour répondre à la question "comment capturer la réponse du client ? record action. Vous pouvez voir qu'elle donne une beepStart et, plus important encore, nous définissons le comportement à adopter une fois l'appel terminé. Le eventUrl recevra un webhook qui contiendra l'URL de cet enregistrement.

Traitement de l'enregistrement

Notre prochaine route est celle qui contiendra un lien vers la réponse du client sous la forme d'un MP3 enregistré, ainsi que l'identifiant du ticket afin que nous sachions à quelle entité il appartient. Voici un exemple de charge utile que nous pouvons nous attendre à recevoir :

{
  "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"
}

Et voici notre itinéraire :

Route::post('/webhook/recordings/{ticketEntry:id}', [WebhookController::class, 'recording'])->name('voice.recording');

Et la recording() méthode pour la traiter :

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);
}

Ceci utilise Route Model Binding pour prendre le modèle pertinent et l'injecter en tant que dépendance. TicketEntry et l'injecter en tant que dépendance, puis extrait l'élément recording_url. Le SDK de Vonage dispose d'une méthode utile nommée getRecording() qui renvoie un StreamInterface contenu dans le corps.

Pour des raisons de sécurité, vous ne pouvez pas écrire le flux audio directement dans la requête OpenAI que nous allons envoyer pour la transcription, nous devons donc enregistrer le fichier temporairement. Une fois que nous l'avons sauvegardé, nous pouvons utiliser la façade Storage pour le relire lors de la demande de transcription, puis le supprimer.

La méthode transcribeRecording() est une méthode personnalisée dans cette classe de contrôleur que nous aborderons dans un instant, mais en supposant qu'une chaîne revienne de la transcription, nous créons un nouvel élément TicketEntrynous l'associons au propriétaire du ticket (nous savons qu'il s'agit du client puisqu'il s'agit d'une route webhook entrante), et nous le sauvegardons dans le répertoire Ticket

Transcription OpenAI

C'est la dernière partie - obtenir notre transcription. Il existe des moyens de le faire de manière asynchrone, mais j'ai choisi de le faire de manière synchrone pour simplifier les choses. Si vous voulez implémenter ceci de manière asynchrone (après tout, il s'agit de traitement de données, c'est donc une bonne pratique), vous pouvez utiliser un worker Laravel Job Queue, mais soyez averti que vous risquez de rencontrer des problèmes de race condition (ce qui m'est arrivé dans le passé).

La transcription dans le contrôleur de méthode est gérée par la fonction transcribeRecording() Nous allons donc y jeter un coup d'œil :

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'];  
}

Ceci a été bricolé pour des raisons de démonstration, donc tout d'abord j'aimerais préciser que si votre application dépend d'une API tierce telle que celle-ci (ou Vonage), vous devriez envelopper ce client et sa configuration comme un fournisseur de service.

La méthode crée un nouveau Guzzle et prépare la requête en tant que MultipartStreamcar OpenAI exige que la requête soit encodée. Nous définissons l'url de base et récupérons le fichier temporaire que nous avons créé plus tôt (call_recording.mp3). Nous pouvons maintenant utiliser fopen() pour écrire le fichier, et le supprimer une fois la requête terminée.

Si tout se passe bien, vous obtiendrez en retour un tableau de transcription contenant la clé textqui est renvoyée pour la mise à jour du TicketEntry. Félicitations : nous avons maintenant un système de billetterie TTS qui fonctionne !

Conclusion

La participation de la communauté est toujours la bienvenue. N'hésitez pas à nous rejoindre sur GitHub et sur Communauté Vonage Slack. Vous pouvez également nous envoyer un message sur sur Twitter.

Partager:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeDéveloppeur PHP senior Advocate

Acteur de formation avec une thèse sur la comédie, je suis venu au développement PHP par le biais de la scène des rencontres. Vous pouvez me trouver en train de parler et d'écrire sur la technologie, ou de jouer/acheter des disques bizarres de ma collection de vinyles.