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

Weiterentwicklung des Vonage Laravel Helpdesk mit OpenAI

Zuletzt aktualisiert am June 20, 2023

Lesedauer: 7 Minuten

Dieser Artikel wurde im März 2025 aktualisiert.

Dies ist der Folgeartikel zu einem Artikel über eine sich entwickelnde Laravel Anwendung, die die Vonage-APIs nutzt, um gängige Anwendungsfälle zu replizieren.

Im ersten Teilhaben wir eine neue Laravel-Anwendung erstellt, die in der vonage-laravel Bibliothek ein und erstellten eine Helpdesk-Ticketansicht. Der Kunde wählt die von ihm gewählte Kommunikationsmethode aus (in diesem Fall war es nur SMS), so dass jede von einem Administrator gepostete Nachricht an das Mobiltelefon des Kunden gesendet werden würde. Die Beantwortung der Nachricht wird dann über eingehende Webhooks hinzugefügt und in die Ticketkonversation geschrieben.

Es ist erwähnenswert, dass dieser Code geschrieben wurde, bevor Laravel die Erstellung von Starter-Templates geändert hat. Um dies von Grund auf zu replizieren, müssen Sie die Option wählen, ein Starter-Kit mit integrierter Authentifizierung und Livewire/Blade zu erstellen.

In diesem Artikel fügen wir die Möglichkeit hinzu, die Text-to-Speech (TTS) Voice-Funktionen von Vonage mithilfe der Voice APImit der Möglichkeit für den Kunden, eine Antwort zu sprechen, die in das Ticketgespräch zurückgeschrieben wird.

Voraussetzungen

Wir gehen davon aus, dass das erste Tutorium abgeschlossen ist, was uns zu einem Ergebnis führt:

  • Der helpdesk Repository, lokal von GitHub geklont

  • Laravel Segel einrichten und ausführen, um die lokale Entwicklungsumgebung zu docken

  • Migrationen laufen

  • Vite Entwicklungsserver läuft zum Erstellen von Laravel Breezedie Boilerplate-Assets

  • Ngrok und eine Vonage-Anwendungsinstanz, die so konfiguriert ist, dass sie Webhooks an sie sendet

Wie macht sie das? Teil 2: Voice

OK, es ist also an der Zeit, die Voice-Funktionen durchzugehen. Der Ablauf von Gesprächen ist hier genau derselbe: Sie erstellen ein neues Ticket als Kunde und die Gesprächsansicht wird geöffnet. Dieses Mal werden wir das Ticket jedoch als Voice-Konversation einrichten.

Aktivieren von Voice

Damit dies funktioniert, benötigen wir eine Voice-aktivierte Anwendungs-ID im Vonage Dashboard. Sie können die vorherige Anwendung aus dem letzten Tutorial bearbeiten oder eine neue Anwendung erstellen. Die Aktivierung der Anwendungs-ID ist alles, was Sie brauchen: Machen Sie sich keine Gedanken über die Verwendung der UI, um Webhooks an die korrekte lokale Route zu senden (wir gehen später darauf ein), da der Code die Webhook-Antwort-URL für Sie konstruiert (dies unterscheidet sich von der Art und Weise, wie wir SMS eingerichtet haben, und ich gehe darauf ein, warum, wenn wir den Code später in diesem Artikel überprüfen).

Was ist OpenAI?

Das häufigste Produkt, von dem Sie in Verbindung mit diesem Namen gehört haben, ist ChatGPT.

ChatGPT ist ein hochmodernes Sprachmodell, das von OpenAIentwickelt wurde, um in natürlicher Sprache mit Nutzern zu kommunizieren. Als KI-basierter Assistent kann ChatGPT Informationen liefern, Fragen beantworten und bei verschiedenen Aufgaben helfen. Es nutzt Deep-Learning-Techniken, um menschenähnlichen Text zu verstehen und zu generieren, wodurch sich die Interaktionen persönlicher anfühlen.

Ja, dieser Absatz wurde von ChatGPT geschrieben. Aber was Sie vielleicht nicht wissen, ist, dass die Firma dahinter, OpenAI, auch mehrere andere Produkte die alle über ihre API angesprochen werden können. Ein solches Produkt ist Whisperdas wir verwenden werden, um zu transkribieren, was ein Kunde als aufgezeichnete Nachricht als Antwort auf einen Ticketeintrag in der Helpdesk-App sagt.

Einrichten der OpenAI-API

Zunächst benötigen Sie einen OpenAI Account. Folgen Sie diesem Link, um einen Account zu erstellenWenn Sie damit fertig sind, müssen Sie in Ihrem Profilmenü oben rechts auf "Account verwalten" gehen. Wenn Sie dies öffnen, sehen Sie den folgenden Bildschirm - gehen Sie zu API-Schlüssel und richten Sie einen neuen Schlüssel ein. Das Endergebnis sollte in etwa so aussehen:

Screenshot of the OpenAI dashboard showing API Key creation

Wenn Sie den Schlüssel erstellen, haben Sie eine Chance, ihn zu kopieren - stellen Sie sicher, dass Sie das tun.

Wir müssen dieses Geheimnis in unsere env Datei hinzufügen. Sie können in der example.env Datei im Repo sehen, dass wir einen Platzhalter dafür haben:

Screenshot of Helpdesk's environment variables example file

Ich habe die anderen in den Screenshot eingefügt, weil es wichtig ist, darauf hinzuweisen, dass diese Funktion nicht funktioniert, wenn nicht alle diese Umgebungsvariablen gesetzt sind:

  • VONAGE_SMS_FROM wird als abgehende Rufnummer wiederverwendet

  • PUBLIC_URL ist Ihr Ngrok (oder ein anderes Werkzeug wie Beyond Codes Expose) öffentlich zugängliche Adresse. Dies ist wichtig, da der Code die Antwort-URL an die API zusammensetzt, wenn ein Aufruf erfolgt

  • VONAGE_APPICATION_ID und VONAGE_PRIVATE_KEY. Im letzten Tutorial hätten wir eine einfache Authentifizierung verwenden können, aber damit Webhooks funktionieren, müssen sie an eine Anwendungs-ID gebunden sein. Für die Verwendung der Vonage Voice API benötigen wir einen privaten Schlüssel und eine Anwendungs-ID, die das PHP-SDK von Vonage verwendet, um die JWT-Autorisierung für uns zu generieren und zu verwalten.

Unter der Haube

Die Funktionalität, die wir uns ansehen werden, befindet sich in der update() Methode in der TicketController. Wir wollen nur dann einen ausgehenden Anruf tätigen, wenn das Ticket von einem Admin-Benutzer (und nicht vom Kunden) aktualisiert wird und der Kunde Voice als bevorzugte Kommunikationsart gewählt hat.

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

Hier ist eine Zusammenfassung dessen, was der Code hier tut:

  • Wir wissen, dass wir in diesem Logikblock einen ausgehenden Anruf tätigen wollen, also erstellen wir einen neuen OutboundCall der die Telefonnummer des Kunden aus dem Ticket und die Absendernummer aus der Konfiguration übernimmt.

  • Jetzt kommt der Clou. Wissen Sie noch, wie wir im ersten Teil dieses Tutorials eine Ngrok-URL im Vonage Dashboard für SMS-Webhooks festgelegt haben? Das haben wir hier nicht getan, denn jeder Anruf mit dem Voice SDK kann so konfiguriert werden, dass eine bestimmte Rückruf-URL für den Anruf zu verwenden, den wir tätigen. Dieser Teil ist wirklich wichtig, weil er erlaubt es uns, den Status zu konfigurieren. In diesem Fall nehmen wir die öffentliche Ngrok-URL von $currentHost (d.h. die Konstante PUBLIC_URL), eine von uns definierte Route für unsere Anwendung (/webhook/answer/) und den Schlüssel, damit dies funktioniert: die ID des Ticketeintrags als Teil der Route. Später, in der WebhookController können wir das übergeordnete Ticket sowie den Besitzer dieses Tickets herausziehen.

Jetzt brauchen wir also einen neuen Controller, um zu verarbeiten, was reinkommt, wenn der Kunde seinen Ticketaufruf abgeschlossen hat. Die beiden Teile dazu sind:

  • Vorlesen einer Antwort, wenn der Kunde den Anruf entgegennimmt (dies wird in dem der Route zugewiesenen Controller eingestellt).

  • Eine Route zum Lesen eingehender Antwort-Ereignisse (die wir beim Einrichten des ausgehenden Anrufs festlegen).

  • Holen Sie aus dem Aufzeichnungsereignis, das nach Beendigung des Anrufs generiert wird, eine Sprachaufzeichnung der Antwort des Kunden, transkribieren Sie sie mit OpenAI und schreiben Sie sie als neue TicketEntry.

Puh! Hier gibt es einiges zu verdauen, also lasst uns mit dem Essen beginnen:

Verwendung von NCCOs für TTS

NCCOs sind JSON-Payloads, die die Vonage-Dienste anweisen, was sie tun sollen, d. h. eine Aktion, etwas aufzeichnen usw. Wenn der Kunde den Hörer abnimmt, möchten wir ihm die letzte Ticketaktualisierung vorlesen, die der Administrator vorgenommen hat, und ihm dann eine Aufforderung zum Beantworten des Tickets geben. Hier ist die Route:

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

Die Route zeigt auf WebhookController::answer(), so dass unsere TTS-Antwort wie folgt aussieht:

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

Jedes Array enthält eine Reihe von Anweisungen, die recht einfach sind, aber der wichtige Klebstoff zur Beantwortung der Frage "Wie erfassen wir die Reaktion des Kunden?" liegt in der record Aktion. Wie Sie sehen können, gibt es eine beepStart Aufforderung, und vor allem wird das Verhalten nach Abschluss des Anrufs festgelegt. Die eventUrl wird mit einem Webhook getroffen, der eine URL dieser Aufzeichnung enthält.

Bearbeitung der Aufzeichnung

Unsere nächste Route enthält einen Link zur Antwort des Kunden in Form einer MP3-Aufnahme sowie die Ticket-ID, damit wir wissen, zu welcher Entität sie gehört. Hier ein Beispiel für eine Nutzlast, die wir erwarten können:

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

Und hier ist unsere Route:

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

Und die recording() Methode, um damit umzugehen:

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

Dies verwendet Route Model Binding, um die relevanten TicketEntry und injiziert es als Abhängigkeit, dann zieht es die recording_url. Das Vonage SDK hat eine nützliche Methode namens getRecording() die eine StreamInterface zurückgibt, die im Body enthalten ist.

Aus Sicherheitsgründen kann der Audiostream nicht direkt in die OpenAI-Anfrage geschrieben werden, die wir für die Transkription senden werden, also müssen wir die Datei vorübergehend speichern. Sobald wir sie gespeichert haben, können wir sie mit der Storage Fassade verwenden, um sie während der Transkriptionsanforderung wieder auszulesen und sie dann zu löschen.

Die transcribeRecording() ist eine benutzerdefinierte Methode in diesem Klassencontroller, zu der wir gleich noch kommen werden, aber unter der Annahme, dass ein String von der Transkription zurückkommt, erstellen wir eine neue TicketEntryund verknüpfen es mit dem Besitzer des Tickets (wir wissen, dass dies der Kunde ist, da es sich um eine eingehende Webhook-Route handelt) und speichern es in der Ticket

OpenAI Transkription

Dies ist der letzte Teil - das Abrufen unserer Transkription. Es gibt Möglichkeiten, dies asynchron zu tun, aber ich habe mich dafür entschieden, dies synchron zu tun, um es einfacher zu halten. Wenn Sie dies auf asynchrone Weise implementieren möchten (schließlich handelt es sich um eine Datenverarbeitung, so dass es eine gute Praxis ist, dies zu tun), können Sie einen Laravel Job Queue Worker verwenden, aber seien Sie gewarnt, dass Sie auch in Race Condition-Probleme laufen könnten (was ich in der Vergangenheit getan habe).

Die Transkription in der Methode controller wird von der transcribeRecording() Funktion gehandhabt, also sehen wir uns das mal an:

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

Dies wurde aus Demonstrationsgründen zusammengeschustert, daher möchte ich zunächst klarstellen, dass, wenn Ihre Anwendung von einer API eines Drittanbieters wie dieser (oder Vonage) abhängig ist, Sie diesen Client und seine Konfiguration als Dienstanbieter verpacken.

Die Methode erstellt eine neue Guzzle Client, und bereitet die Anfrage als MultipartStreamauf, da OpenAI die Anfrage in kodierter Form benötigt. Wir setzen die Basis-URL und holen unsere temporäre Datei, die wir zuvor erstellt haben (call_recording.mp3). Wir können nun mit fopen() verwenden, um die Datei herauszuschreiben und sie dann zu löschen, nachdem die Anfrage abgeschlossen ist.

Wenn alles gut geht, erhalten Sie ein Transkriptions-Array zurück, das den Schlüssel textenthält, der zum Aktualisieren der TicketEntry. Glückwunsch: Wir haben jetzt ein funktionierendes TTS-Ticketing-System!

Schlussfolgerung

Wir freuen uns immer über die Beteiligung der Gemeinschaft. Sie können sich uns gerne auf GitHub und dem Vonage Community Slack. Sie können uns auch eine Nachricht senden auf Twitter.

Teilen Sie:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeSenior PHP Entwickler Advocate

Als ausgebildeter Schauspieler mit einer Dissertation in Standup-Comedy bin ich über die Meetup-Szene zur PHP-Entwicklung gekommen. Man findet mich, wenn ich über Technik spreche oder schreibe, oder wenn ich seltsame Platten aus meiner Vinylsammlung spiele oder kaufe.