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

VonageのLaravelヘルプデスクをOpenAIで進化させる

最終更新日 June 20, 2023

所要時間:1 分

この記事は2025年3月に更新されました。

この記事は、進化する Laravelアプリケーションの続編です。

最初の部分ではでは、新しいLaravelアプリケーションを作成し、そのアプリケーションを vonage-laravelライブラリを取り込み、ヘルプデスクのチケットビューを作成しました。顧客は選択した通信方法(この場合はSMSのみ)を選択し、管理者によって投稿されたメッセージは顧客の携帯電話に送信されます。メッセージへの返信は、着信ウェブフックを使用して追加され、チケットの会話に書き込まれます。

このコードは、Laravelがスターターテンプレートの作成を変更する前に書かれたものなので、ゼロからこれを再現するには、ビルトイン認証とLivewire/Bladeでスターターキットを作成するオプションを選択する必要があります。

この記事では、Vonageの音声合成(TTS)機能を使うために Voice APIを使用して、顧客がチケットの会話に書き戻される応答を話す機能を追加します。

前提条件

最初のチュートリアルが終了したと仮定する:

どうやるの?パート2:声

Voiceの機能について説明しましょう。顧客として新しいチケットを作成し、会話ビューを開きます。しかし、今回作成するときは、チケットをVoice会話として設定します。

イネーブル・ヴォイス

これが機能する前に、Voice対応のアプリケーションIDが必要です。 Vonageダッシュボード.前回のチュートリアルで使用したアプリケーションを編集するか、新しいアプリケーションを作成してください。アプリケーションIDを有効にするだけです。UIを使用して正しいローカルルートにWebhookを送信することについては心配しないでください(これについては後で説明します)。

OpenAIとは?

この名前から連想される最も一般的な製品は、次のとおりです。 ChatGPT.

ChatGPTは、OpenAIが開発した最先端の言語モデルです。 オープンAIユーザーと自然言語で会話するように設計されています。AIベースのアシスタントとして、ChatGPTは情報を提供し、質問に答え、様々なタスクを支援することができます。ディープラーニング技術を使用して人間のようなテキストを理解し、生成することで、よりパーソナライズされたインタラクションを実現します。

そう、このパラグラフはChatGPTが書いたものだ。しかし、あなたが知らないかもしれないのは、その背後にあるOpenAIという会社には、他にもいくつかの製品があるということです。 他の製品API経由でアクセスすることができます。その一つが ウィスパーWhisperは、ヘルプデスクアプリからのチケット入力に応答して、顧客の発言を録音メッセージとして書き起こすために使用します。

OpenAI APIのセットアップ

まず、OpenAIのアカウントが必要です。 アカウントを作成するには、このリンクをたどってください。アカウント作成が完了したら、右上のプロフィールメニューの下にある'Manage Account'に向かう必要があります。これを開くと、以下の画面が表示されます - API Keysに向かい、新しいキーを設定します。最終的にはこのようになります:

Screenshot of the OpenAI dashboard showing API Key creation

キーを作成する際、1度だけコピーするチャンスがあります。

その秘密を envファイルに追加する必要があります。リポジトリの example.envファイルにプレースホルダーがあります:

Screenshot of Helpdesk's environment variables example file

これらの環境変数がすべて設定されていないと、この機能は動作しないことに注意することが重要なので、スクリーンショットには他の環境変数も含めてある:

  • VONAGE_SMS_FROMは発信番号として再利用される。

  • PUBLIC_URLはあなたの ングロック(またはBeyond Codeの Exposeなど)の公開アドレスです。このコードは、APIを呼び出すときにレスポンスURLをつなぎ合わせるので、必須です。

  • VONAGE_APPICATION_IDそして VONAGE_PRIVATE_KEY.前回のチュートリアルでは、基本的な認証を使用することもできましたが、Webhook を動作させるためには、アプリケーション ID と結びつける必要があります。Vonage Voice API を使うには、秘密鍵とアプリケーション ID が必要です。Vonage PHP SDK が JWT 認証を生成してくれます。

ボンネットの下

これから見ていく機能は update()メソッドにある。 TicketController.チケットの更新が (顧客ではなく) 管理ユーザーによって行われ、顧客が通信の優先順位として音声を選択した場合にのみ、発信を行いたいと思います。

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

ここでコードが行っていることの概要は以下の通りだ:

  • このロジックブロックでアウトバウンドコールをかけたいことがわかっているので、チケットから顧客の電話番号を、コンフィグから送信番号を取り込む新しい OutboundCallを作成し、チケットから顧客の電話番号を、コンフィグから送信番号を取り込みます。

  • これが面白いところです。このチュートリアルのパート1で、SMSウェブフック用にVonageダッシュボードにNgrok URLを設定したのをご存知ですか?Voice SDKを使用する各通話は、この通話の特定のコールバックURLを使用するように設定できます。 特定のコールバックURLを使用するように設定できるからです。.この部分は本当に重要です。 ステートを設定できるからです。この場合、NgrokのパブリックURLを $currentHost(つまり、定数 PUBLIC_URL)、私たちのアプリケーション用に定義されたルート(/webhook/answer/)、そしてこれを動作させるためのキー: ルートの一部としてのチケットエントリIDです。後で WebhookControllerで、親チケットとそのチケットの所有者を取り出すことができます。

そこで、顧客がチケットのコールを完了したときに来るものを処理する新しいコントローラが必要です。これには2つの部分があります:

  • 顧客が電話に出たときに応答を読み上げる(これはルートに割り当てられたコントローラに設定される)。

  • 着信応答イベントを読み取るルートを持つ(アウトバウンドコールの設定時に設定する)。

  • 通話終了後に生成される録音イベントから、顧客の応答を録音したVoiceを取得し、OpenAIを使って書き起こし、それを新しい TicketEntry.

ふぅ!消化すべきことがたくさんある:

NCCOのTTSへの活用

NCCOは、Vonageサービスに「何をすべきか」を指示するJSONペイロードです。顧客が電話に出たとき、管理者によって更新された最新のチケットを読み上げ、それに応答するプロンプトを与えたい。これがそのルートです:

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

ルートは WebhookController::answer()したがって、TTSの応答は次のようになる:

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

各配列は、かなり単純な命令のペイロードを与えるが、「どのように顧客の反応を捕らえるのか」という質問に答えるための重要なグルーは、ここでは recordアクションにあります。これは beepStartそして最も重要なのは、通話が完了した後の動作を定義することです。顧客は eventUrlには、この録音のURLを含むWebhookが送られます。

録音を処理する

次のルートは、録音されたMP3としての顧客の応答へのリンクと、それがどのエンティティのものかを知るためのチケットIDを含むものです。受け取るペイロードの例を示します:

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

そして、これが我々のルートだ:

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

そして recording()それを処理する方法:

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

これは、Route Model Bindingを使って、関連する TicketEntryを取り出し、依存関係としてインジェクトします。 recording_url.Vonage SDKには getRecording()という便利なメソッドがあります。 StreamInterfaceを返す便利なメソッドがあります。

セキュリティ上の理由から、音声からのストリームをそのままOpenAIのリクエストに書き出すことはできないので、一時的にファイルを保存する必要があります。いったん保存してしまえば、あとは Storageファサードを使って、文字起こしリクエストの間にそれを読み出し、削除することができます。

は、このクラスコントローラのカスタムメソッドです。 transcribeRecording()はこのクラスコントローラのカスタムメソッドで、後で説明しますが、文字列がトランスクリプションから戻ってきたと仮定して、新しい TicketEntryを作成し、それをチケットの所有者 (ウェブフックの受信ルートなので、これは顧客であることがわかっています) に関連付け、それを Ticket

OpenAIテープ起こし

これが最後の部分だ。これを非同期で行う方法もありますが、よりシンプルにするために同期で行うことにしました。非同期で実装したい場合(結局のところ、これはデータ処理なので、そうするのが良い習慣です)、Laravelのジョブキューワーカーを使うことができますが、レースコンディションの問題に遭遇する可能性があるので注意してください(過去に私もそうでした)。

メソッド・コントローラの転記は transcribeRecording()関数で処理されます:

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

これはデモンストレーションのためにハックしたものなので、まずはっきりさせておきたいのは、もしあなたのアプリケーションがこのようなサードパーティAPI(あるいはVonage)に依存しているのであれば、このクライアントとそのコンフィギュレーションを をサービス・プロバイダー.

このメソッドは、新しい を作成しクライアントを作成し、リクエストを MultipartStreamとしてリクエストを準備します。ベース URL を設定し、先ほど作成した一時ファイル (call_recording.mp3).これで fopen()を使ってファイルを書き出し、リクエストが完了したら削除します。

うまくいけば、転写配列が返ってくる。 textを更新するために送り返されます。 TicketEntry.おめでとうございます。これでTTS発券システムが動作するようになりました!

結論

私たちは常に地域社会の参加を歓迎しています。お気軽に GitHubおよび Vonage コミュニティ Slack.また ツイッター.

シェア:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeシニアPHPデベロッパー

スタンダップ・コメディーの学位論文を持つ俳優の訓練を受け、ミートアップ・シーンを経てPHP開発に携わるようになった。技術について話したり書いたり、レコード・コレクションから変わったレコードを再生したり買ったりしています。