DialogflowエージェントをReactアプリケーションに統合する

公開: 2022-03-10
簡単な要約↬小規模または企業レベルで使用できる会話型チャットアシスタントを構築する場合、Dialogflowは検索リストに表示される最初のオプションの1つである可能性が高く、なぜそうではないのでしょうか。 音声とテキストの入力を処理する機能、カスタムWebhookを使用して動的な応答を提供する機能、Googleアシスタントを使用して数百万のGoogle対応デバイスに接続する機能など、いくつかの機能を提供します。 しかし、エージェントを設計および管理するために提供されるコンソールとは別に、構築されたWebアプリケーション内でも使用できるチャットアシスタントを作成するにはどうすればよいでしょうか。

Dialogflowは、Dialogflowコンソールまたは統合Webアプリケーションのいずれかから使用する場合に、音声またはテキスト入力を処理できる自然言語処理会話型チャットアシスタントの作成および設計プロセスを簡素化するプラットフォームです。

この記事では、統合されたDialogflow Agentについて簡単に説明しますが、Node.jsとDialogflowについて理解している必要があります。 Dialogflowについて初めて学習する場合は、この記事でDialogflowとは何かとその概念について明確に説明します。

この記事は、React.jsWebアプリケーションとエージェント間のリンクとしてExpress.jsバックエンドアプリケーションを使用してWebアプリケーションに統合できる音声とチャットをサポートするDialogflowエージェントを構築する方法に関するガイドです。 Dialogflow自体について。 記事の終わりまでに、独自のDialogflowエージェントを好みのWebアプリケーションに接続できるようになるはずです。

このガイドを簡単に理解できるように、チュートリアルの最も興味のある部分にスキップするか、次の順序でそれらをたどることができます。

  • Dialogflowエージェントの設定
  • Dialogflowエージェントの統合
  • NodeExpressアプリケーションのセットアップ
    • Dialogflowによる認証
    • サービスアカウントとは何ですか?
    • 音声入力の処理
  • Webアプリケーションへの統合
    • チャットインターフェイスの作成
    • ユーザーの音声入力の録音
  • 結論
  • 参考文献

1.Dialogflowエージェントの設定

この記事で説明したように、Dialogflowのチャットアシスタントはエージェントと呼ばれ、インテント、フルフィルメント、ナレッジベースなどの小さなコンポーネントで構成されています。 Dialogflowは、ユーザーがエージェントの会話フローを作成、トレーニング、および設計するためのコンソールを提供します。 このユースケースでは、エージェントのエクスポートとインポート機能を使用して、トレーニング後にZIPフォルダーにエクスポートされたエージェントを復元します。

インポートを実行する前に、復元しようとしているエージェントとマージされる新しいエージェントを作成する必要があります。 コンソールから新しいエージェントを作成するには、一意の名前と、エージェントをリンクするGoogleCloud上のプロジェクトが必要です。 リンクするGoogleCloudに既存のプロジェクトがない場合は、ここで新しいプロジェクトを作成できます。

エージェントは以前に作成され、予算に基づいてユーザーにワイン製品を推奨するようにトレーニングされています。 このエージェントはZIPにエクスポートされています。 ここからフォルダをダウンロードし、エージェントの[設定]ページにある[エクスポートとインポート]タブから、新しく作成したエージェントに復元できます。

以前にエクスポートしたエージェントをZIPフォルダーから復元する
以前にエクスポートしたエージェントをZIPフォルダーから復元します。 (大プレビュー)

輸入されたエージェントは、ワインのボトルを購入するためのユーザーの予算に基づいて、ユーザーにワイン製品を推奨するように以前に訓練されています。

インポートされたエージェントを調べると、インテントページから3つのインテントが作成されていることがわかります。 1つはフォールバックインテントであり、エージェントがユーザーからの入力を認識しない場合に使用され、もう1つはエージェントとの会話が開始されるときに使用されるウェルカムインテントであり、最後のインテントはに基づいてユーザーにワインを推奨するために使用されます。文内の量パラメータ。 私たちが懸念しているのは、 get-wine-recommendation意図です

このインテントには、会話をこのインテントにリンクするためのデフォルトのウェルカムインテントからのwine-recommendationの単一の入力コンテキストがあります。

「コンテキストは、あるインテントから別のインテントへの会話の流れを制御するために使用されるエージェント内のシステムです。」

コンテキストの下にはトレーニングフレーズがあります。これは、ユーザーに期待するステートメントのタイプについてエージェントをトレーニングするために使用される文です。 インテント内の多種多様なトレーニングフレーズを通じて、エージェントはユーザーの文とそれが含まれるインテントを認識することができます。

エージェントget-wine-recommendationインテント内のトレーニングフレーズ(以下に示す)は、ワインの選択と価格カテゴリを示しています。

get-wine-recommendationを目的とした利用可能なトレーニングフレーズのリスト。
利用可能なトレーニングフレーズを示すGet-wine-recommendationインテントページ。 (大プレビュー)

上の画像を見ると、利用可能なトレーニングフレーズがリストされており、それぞれの通貨の数値が黄色で強調表示されています。 この強調表示はDialogflowの注釈と呼ばれ、ユーザーの文からエンティティと呼ばれる認識されたデータ型を抽出するために自動的に行われます。

エージェントとの会話でこのインテントが一致した後、外部サービスに対してHTTPリクエストが行われ、ユーザーの文章からパラメーターとして抽出された価格に基づいて、内部にある有効なWebhookを使用して推奨ワインを取得します。このインテントページの下部にある[フルフィルメント]セクション。

Dialogflowコンソールの右側のセクションにあるDialogflowエミュレーターを使用してエージェントをテストできます。 テストするために、「こんにちは」というメッセージで会話を開始し、希望する量のワインをフォローアップします。 Webhookがすぐに呼び出され、以下のような豊富な応答がエージェントによって表示されます。

インポートされたエージェントエージェントのWebhookをテストします。
コンソールのエージェントエミュレーターを使用して、インポートされたエージェントのフルフィルメントWebhookをテストします。 (大プレビュー)

上の画像から、Ngrokを使用して生成されたWebhook URLと、右側にユーザ​​ーが入力した20ドルの価格帯のワインを示すエージェントの応答を確認できます。

この時点で、Dialogflowエージェントは完全にセットアップされています。 これで、このエージェントをWebアプリケーションに統合して、他のユーザーがDialogflowコンソールにアクセスせずにエージェントにアクセスして対話できるようにすることができます。

ジャンプした後もっと! 以下を読み続けてください↓

Dialogflowエージェントの統合

RESTエンドポイントにHTTPリクエストを送信するなど、Dialogflow Agentに接続する方法は他にもありますが、Dialogflowに接続するための推奨される方法は、いくつかのプログラミング言語で利用可能な公式クライアントライブラリを使用することです。 JavaScriptの場合、@ google-cloud/dialogflowパッケージをNPMからインストールできます。

内部的には、@ google-cloud / dialogflowパッケージはネットワーク接続にgRPCを使用します。これにより、webpackを使用してパッチを適用した場合を除き、ブラウザ環境内でパッケージがサポートされなくなります。このパッケージを使用するための推奨される方法は、ノード環境からです。 これを行うには、このパッケージを使用するようにExpress.jsバックエンドアプリケーションを設定し、APIエンドポイントを介してWebアプリケーションにデータを提供します。これが次に行うことです。

NodeExpressアプリケーションのセットアップ

Expressアプリケーションをセットアップするには、新しいプロジェクトディレクトリを作成し、開いたコマンドラインターミナルからyarnを使用して必要な依存関係を取得します。

 # create a new directory and ( && ) move into directory mkdir dialogflow-server && cd dialogflow-server # create a new Node project yarn init -y # Install needed packages yarn add express cors dotenv uuid

必要な依存関係をインストールしたら、WebアプリでCORSサポートを有効にして、指定したポートでの接続を処理する非常に無駄のないExpress.jsサーバーのセットアップに進むことができます。

 // index.js const express = require("express") const dotenv = require("dotenv") const cors = require("cors") dotenv.config(); const app = express(); const PORT = process.env.PORT || 5000; app.use(cors()); app.listen(PORT, () => console.log(` server running on port ${PORT}`));

上記のスニペットのコードが実行されると、指定されたPORTExpress.jsで接続をリッスンするHTTPサーバーが起動します。 また、CorsパッケージをExpressミドルウェアとして使用するすべてのリクエストでクロスオリジンリソースシェアリング(CORS)が有効になっています。 今のところ、このサーバーは接続のみをリッスンし、ルートが作成されていないためリクエストに応答できないので、これを作成しましょう。

次に、2つの新しいルートを追加する必要があります。1つはテキストデータを送信するためのもので、もう1つは録音された音声入力を送信するためのものです。 どちらもPOSTリクエストを受け入れ、リクエスト本文に含まれるデータを後でDialogflowエージェントに送信します。

 const express = require("express") const app = express() app.post("/text-input", (req, res) => { res.status(200).send({ data : "TEXT ENDPOINT CONNECTION SUCCESSFUL" }) }); app.post("/voice-input", (req, res) => { res.status(200).send({ data : "VOICE ENDPOINT CONNECTION SUCCESSFUL" }) }); module.exports = app

上記では、作成された2つのPOSTルート用に個別のルーターインスタンスを作成しました。現時点では、 200ステータスコードとハードコードされたダミー応答でのみ応答します。 Dialogflowによる認証が完了したら、戻ってこれらのエンドポイント内にDialogflowへの実際の接続を実装できます。

バックエンドアプリケーションのセットアップの最後のステップでは、app.useとルートのベースパスを使用して、以前に作成したルーターインスタンスをExpressアプリケーションにマウントします。

 // agentRoutes.js const express = require("express") const dotenv = require("dotenv") const cors = require("cors") const Routes = require("./routes") dotenv.config(); const app = express(); const PORT = process.env.PORT || 5000; app.use(cors()); app.use("/api/agent", Routes); app.listen(PORT, () => console.log(` server running on port ${PORT}`));

上記では、2つのルートにベースパスを追加しました。2つは、コマンドラインからcURLを使用してPOSTリクエストを介してテストできます。これは、以下の空のリクエスト本文で実行されます。

 curl -X https://localhost:5000/api/agent/text-response

上記のリクエストが正常に完了すると、オブジェクトデータを含む応答がコンソールに出力されることが期待できます。

これで、Dialogflowとの実際の接続を確立する必要があります。これには、認証の処理、@ google-cloud/dialogflowパッケージを使用したDialogflow上のエージェントからのデータの送信と受信が含まれます。

Dialogflowによる認証

作成されたすべてのDialogflowエージェントは、GoogleCloud上のプロジェクトにリンクされています。 Dialogflowエージェントに外部で接続するには、Googleクラウド上のプロジェクトで認証し、プロジェクトのリソースの1つとしてDialogflowを使用します。 google-cloud上のプロジェクトに接続するために利用できる6つの方法のうち、クライアントライブラリを介してgoogle cloud上の特定のサービスに接続する場合は、[サービスアカウント]オプションを使用するのが最も便利です。

本番環境に対応したアプリケーションの場合、サービスアカウントキーが悪用されるリスクを減らすために、サービスアカウントキーよりも有効期間の短いAPIキーを使用することをお勧めします。

サービスアカウントとは何ですか?

サービスアカウントは、Google Cloudの特別なタイプのアカウントであり、主に外部APIを介して人間以外の操作のために作成されます。 このアプリケーションでは、Dialogflowクライアントライブラリによって生成されたキーを介してサービスアカウントにアクセスし、GoogleCloudで認証します。

サービスアカウントの作成と管理に関するクラウドドキュメントは、サービスアカウントを作成するための優れたガイドを提供します。 サービスアカウントを作成するときは、最後の手順に示すように、DialogflowAPI管理者ロールを作成したサービスアカウントに割り当てる必要があります。 この役割により、サービスアカウントはリンクされたDialogflowエージェントを管理できます。

サービスアカウントを使用するには、サービスアカウントキーを作成する必要があります。 以下の手順は、JSON形式で作成する方法の概要を示しています。

  1. 新しく作成したサービスアカウントをクリックして、サービスアカウントページに移動します。
  2. [キー]セクションまでスクロールして[キーの追加]ドロップダウンをクリックし、[新しいキーの作成]オプションをクリックしてモーダルを開きます。
  3. JSONファイル形式を選択し、モーダルの右下にある[作成]ボタンをクリックします。

注:サービスアカウントキーは非公開にして、バージョン管理システムにコミットしないことをお勧めします。これには、GoogleCloud上のプロジェクトに関する機密性の高いデータが含まれているためです。 これは、ファイルを.gitignoreファイルに追加することで実行できます。

作成されたサービスアカウントとプロジェクトのディレクトリ内で使用可能なサービスアカウントキーを使用して、Dialogflowクライアントライブラリを使用して、Dialogflowエージェントとの間でデータを送受信できます。

 // agentRoute.js require("dotenv").config(); const express = require("express") const Dialogflow = require("@google-cloud/dialogflow") const { v4 as uuid } = require("uuid") const Path = require("path") const app = express(); app.post("/text-input", async (req, res) => { const { message } = req.body; // Create a new session const sessionClient = new Dialogflow.SessionsClient({ keyFilename: Path.join(__dirname, "./key.json"), }); const sessionPath = sessionClient.projectAgentSessionPath( process.env.PROJECT_ID, uuid() ); // The dialogflow request object const request = { session: sessionPath, queryInput: { text: { // The query to send to the dialogflow agent text: message, }, }, }; // Sends data from the agent as a response try { const responses = await sessionClient.detectIntent(request); res.status(200).send({ data: responses }); } catch (e) { console.log(e); res.status(422).send({ e }); } }); module.exports = app;

上記のルート全体がDialogflowエージェントにデータを送信し、次の手順で応答を受信します。

  • 初め
    Google Cloudで認証し、DialogflowエージェントにリンクされたGoogle CloudプロジェクトのprojectIDと、作成されたセッションを識別するためのランダムIDを使用してDialogflowとのセッションを作成します。 このアプリケーションでは、JavaScriptUUIDパッケージを使用して作成された各セッションでUUID識別子を作成しています。 これは、Dialogflowエージェントによって処理されるすべての会話をログに記録またはトレースするときに非常に役立ちます。
  • 2番
    Dialogflowドキュメントで指定された形式に従ってリクエストオブジェクトデータを作成します。 このリクエストオブジェクトには、作成されたセッションと、Dialogflowエージェントに渡されるリクエスト本文から取得したメッセージデータが含まれます。
  • 三番
    DialogflowセッションからdetectIntentメソッドを使用して、リクエストオブジェクトを非同期に送信し、try-catchブロックでES6 async / await構文を使用してエージェントの応答を待機しdetectIntentメソッドが例外を返した場合、エラーをキャッチして返すことができます。アプリケーション全体をクラッシュさせるよりも。 エージェントから返された応答オブジェクトのサンプルは、Dialogflowのドキュメントに記載されており、オブジェクトからデータを抽出する方法を知るために検査できます。

Postmanを利用して、 dialogflow-responseルートで上記で実装されたDialogflow接続をテストできます。 Postmanは、開発段階または本番段階で構築されたAPIをテストする機能を備えたAPI開発用のコラボレーションプラットフォームです。

注:まだインストールされていない場合、APIをテストするためにPostmanデスクトップアプリケーションは必要ありません。 2020年9月以降、PostmanのWebクライアントはGenerally Available(GA)状態に移行し、ブラウザから直接使用できるようになりました。

Postman Webクライアントを使用して、新しいワークスペースを作成するか、既存のワークスペースを使用して、 https://localhost:5000/api/agent/text-inputにあるAPIエンドポイントへのPOSTリクエストを作成し、キーがmessageと「 HiThere 」の値をクエリパラメータに入力します。

[送信]ボタンをクリックすると、実行中のExpressサーバーにPOSTリクエストが送信され、次の画像に示すような応答が返されます。

Postmanを使用してテキスト入力APIエンドポイントをテストします。
Postmanを使用してテキスト入力APIエンドポイントをテストします。 (大プレビュー)

上の画像では、Expressサーバーを介したDialogflowエージェントからのきれいな応答データを見ることができます。 返されるデータは、DialogflowWebhookのドキュメントに記載されているサンプルの応答定義に従ってフォーマットされます。

音声入力の処理

デフォルトでは、すべてのDialogflowエージェントは、テキストデータと音声データの両方を処理し、テキスト形式または音声形式のいずれかで応答を返すことができます。 ただし、オーディオ入力または出力データの操作は、テキストデータよりも少し複雑になる可能性があります。

音声入力を処理および処理するために、オーディオファイルを受信し、エージェントからの応答と引き換えにそれらをDialogflowに送信するために、以前に作成した/voice-inputエンドポイントの実装を開始します。

 // agentRoutes.js import { pipeline, Transform } from "stream"; import busboy from "connect-busboy"; import util from "promisfy" import Dialogflow from "@google-cloud/dialogflow" const app = express(); app.use( busboy({ immediate: true, }) ); app.post("/voice-input", (req, res) => { const sessionClient = new Dialogflow.SessionsClient({ keyFilename: Path.join(__dirname, "./recommender-key.json"), }); const sessionPath = sessionClient.projectAgentSessionPath( process.env.PROJECT_ID, uuid() ); // transform into a promise const pump = util.promisify(pipeline); const audioRequest = { session: sessionPath, queryInput: { audioConfig: { audioEncoding: "AUDIO_ENCODING_OGG_OPUS", sampleRateHertz: "16000", languageCode: "en-US", }, singleUtterance: true, }, }; const streamData = null; const detectStream = sessionClient .streamingDetectIntent() .on("error", (error) => console.log(error)) .on("data", (data) => { streamData = data.queryResult }) .on("end", (data) => { res.status(200).send({ data : streamData.fulfillmentText }} }) detectStream.write(audioRequest); try { req.busboy.on("file", (_, file, filename) => { pump( file, new Transform({ objectMode: true, transform: (obj, _, next) => { next(null, { inputAudio: obj }); }, }), detectStream ); }); } catch (e) { console.log(`error : ${e}`); } });

大まかに言えば、上記の/voice-inputルートは、チャットアシスタントに話しかけられているメッセージを含むファイルとしてユーザーの音声入力を受け取り、それをDialogflowエージェントに送信します。 このプロセスをよりよく理解するために、次の小さなステップに分解できます。

  • まず、connect-busboyを追加して、Webアプリケーションからのリクエストで送信されるフォームデータを解析するためのExpressミドルウェアとして使用します。 その後、前のルートで行ったのと同じ方法で、サービスキーを使用してDialogflowで認証し、セッションを作成します。
    次に、組み込みのNode.js utilモジュールのpromisifyメソッドを使用して、後で複数のストリームをパイプし、ストリームの完了後にクリーンアップを実行するために使用されるStreamパイプラインメソッドと同等のpromiseを取得して保存します。
  • 次に、Dialogflow認証セッションとDialogflowに送信されるオーディオファイルの構成を含むリクエストオブジェクトを作成します。 ネストされたオーディオ構成オブジェクトにより、Dialogflowエージェントは送信されたオーディオファイルに対して音声からテキストへの変換を実行できます。
  • 次に、作成されたセッションとリクエストオブジェクトを使用して、Dialogflowエージェントからバックエンドアプリケーションへの新しいデータストリームを開くdetectStreamingIntentメソッドを使用して、オーディオファイルからユーザーのインテントを検出します。 データはこのストリームを介して小さなビットで返送され、読み取り可能なストリームからのデータ「イベント」を使用して、後で使用するためにデータをstreamData変数に格納します。 ストリームが閉じられた後、「 end 」イベントが発生し、 streamData変数に格納されているDialogflowエージェントからの応答をWebアプリケーションに送信します。
  • 最後に、connect-busboyからのファイルストリームイベントを使用して、リクエスト本文で送信されたオーディオファイルのストリームを受信し、それを以前に作成したPipelineと同等のpromiseに渡します。 この機能は、リクエストから入ってくるオーディオファイルストリームをDialogflowストリームにパイプすることです。オーディオファイルストリームを、上記のdetectStreamingIntentメソッドによって開かれたストリームにパイプします。

上記の手順がレイアウトどおりに機能していることをテストおよび確認するために、Postmanを使用して、リクエスト本文にオーディオファイルを含むテストリクエストを/voice-inputエンドポイントに送信できます。

Postmanを使用して音声入力APIエンドポイントをテストします。
録音された音声ファイルでpostmanを使用して音声入力APIエンドポイントをテストします。 (大プレビュー)

上記のPostmanの結果は、POSTリクエストを行った後に取得された応答を示しています。リクエストの本文には、「こんにちは」という録音された音声メモメッセージのフォームデータが含まれています。

この時点で、Dialogflowとの間でデータを送受信する機能的なExpress.jsアプリケーションができました。この記事の2つの部分は完了です。 ここで、Reactjsアプリケーションからここで作成されたAPIを使用して、このエージェントをWebアプリケーションに統合する必要があります。

Webアプリケーションへの統合

構築されたRESTAPIを使用するために、この既存のReact.jsアプリケーションを拡張します。このアプリケーションには、APIからフェッチされたワインのリストと、babelプロポーザルデコレータープラグインを使用したデコレーターのサポートを示すホームページが既にあります。 状態管理用のMobxと、Express.jsアプリケーションから追加されたREST APIエンドポイントを使用してチャットコンポーネントからワインを推奨する新機能を導入することで、少しリファクタリングします。

まず、MobXを使用してアプリケーションの状態の管理を開始し、いくつかの監視可能な値といくつかのメソッドをアクションとして使用してMobxストアを作成します。

 // store.js import Axios from "axios"; import { action, observable, makeObservable, configure } from "mobx"; const ENDPOINT = process.env.REACT_APP_DATA_API_URL; class ApplicationStore { constructor() { makeObservable(this); } @observable isChatWindowOpen = false; @observable isLoadingChatMessages = false; @observable agentMessages = []; @action setChatWindow = (state) => { this.isChatWindowOpen = state; }; @action handleConversation = (message) => { this.isLoadingChatMessages = true; this.agentMessages.push({ userMessage: message }); Axios.post(`${ENDPOINT}/dialogflow-response`, { message: message || "Hi", }) .then((res) => { this.agentMessages.push(res.data.data[0].queryResult); this.isLoadingChatMessages = false; }) .catch((e) => { this.isLoadingChatMessages = false; console.log(e); }); }; } export const store = new ApplicationStore();

上記では、次の値を持つアプリケーション内にチャットコンポーネント機能のストアを作成しました。

  • isChatWindowOpen
    ここに保存されている値は、Dialogflowのメッセージが表示されるチャットコンポーネントの可視性を制御します。
  • isLoadingChatMessages
    これは、Dialogflowエージェントからの応答をフェッチする要求が行われたときにロードインジケーターを表示するために使用されます。
  • agentMessages
    この配列は、Dialogflowエージェントからの応答を取得するために行われた要求からのすべての応答を格納します。 配列内のデータは、後でコンポーネントに表示されます。
  • handleConversation
    アクションとして装飾されたこのメソッドは、 agentMessages配列にデータを追加します。 まず、引数として渡されたユーザーのメッセージを追加し、Axiosを使用してバックエンドアプリケーションにリクエストを送信し、Dialogflowから応答を取得します。 リクエストが解決されると、リクエストからのレスポンスがagentMessages配列に追加されます。

注:アプリケーションにデコレーターサポートがない場合、MobXはターゲットストアクラスのコンストラクターで使用できるmakeObservableを提供します。 こちらの例をご覧ください。

ストアのセットアップでは、 index.jsファイルのルートコンポーネントから始まるMobXプロバイダーの上位コンポーネントでアプリケーションツリー全体をラップする必要があります。

 import React from "react"; import { Provider } from "mobx-react"; import { store } from "./state/"; import Home from "./pages/home"; function App() { return ( <Provider ApplicationStore={store}> <div className="App"> <Home /> </div> </Provider> ); } export default App;

上記では、ルートアプリコンポーネントをMobXプロバイダーでラップし、以前に作成したストアをプロバイダーの値の1つとして渡します。 これで、ストアに接続されているコンポーネント内でストアからの読み取りに進むことができます。

チャットインターフェイスの作成

APIリクエストから送受信されたメッセージを表示するには、リストされたメッセージを表示するチャットインターフェイスを備えた新しいコンポーネントが必要です。 これを行うには、最初にハードコードされたメッセージを表示する新しいコンポーネントを作成し、後でメッセージを順序付きリストに表示します。

 // ./chatComponent.js import React, { useState } from "react"; import { FiSend, FiX } from "react-icons/fi"; import "../styles/chat-window.css"; const center = { display: "flex", jusitfyContent: "center", alignItems: "center", }; const ChatComponent = (props) => { const { closeChatwindow, isOpen } = props; const [Message, setMessage] = useState(""); return ( <div className="chat-container"> <div className="chat-head"> <div style={{ ...center }}> <h5> Zara </h5> </div> <div style={{ ...center }} className="hover"> <FiX onClick={() => closeChatwindow()} /> </div> </div> <div className="chat-body"> <ul className="chat-window"> <li> <div className="chat-card"> <p>Hi there, welcome to our Agent</p> </div> </li> </ul> <hr style={{ background: "#fff" }} /> <form onSubmit={(e) => {}} className="input-container"> <input className="input" type="text" onChange={(e) => setMessage(e.target.value)} value={Message} placeholder="Begin a conversation with our agent" /> <div className="send-btn-ctn"> <div className="hover" onClick={() => {}}> <FiSend style={{ transform: "rotate(50deg)" }} /> </div> </div> </form> </div> </div> ); }; export default ChatComponent

上記のコンポーネントには、チャットアプリケーションに必要な基本的なHTMLマークアップがあります。 エージェントの名前を示すヘッダーとチャットウィンドウを閉じるためのアイコン、リストタグにハードコードされたテキストを含むメッセージバブル、最後に入力フィールドに入力されたテキストを格納するためのonChangeイベントハンドラーが付加された入力フィールドがあります。 ReactのuseStateを使用したコンポーネントのローカル状態。

チャットエージェントからのハードコードされたメッセージを含むチャットコンポーネントのプレビュー
チャットエージェントからのハードコードされたメッセージを含むチャットコンポーネントのプレビュー。 (大プレビュー)

上の画像から、チャットコンポーネントは正常に機能し、単一のチャットメッセージと下部に入力フィールドがあるスタイル付きチャットウィンドウを示しています。 ただし、表示されるメッセージは、ハードコードされたテキストではなく、APIリクエストから取得した実際の応答である必要があります。

チャットコンポーネントのリファクタリングに進みます。今回は、コンポーネント内のMobXストアの値を接続して利用します。

 // ./components/chatComponent.js import React, { useState, useEffect } from "react"; import { FiSend, FiX } from "react-icons/fi"; import { observer, inject } from "mobx-react"; import { toJS } from "mobx"; import "../styles/chat-window.css"; const center = { display: "flex", jusitfyContent: "center", alignItems: "center", }; const ChatComponent = (props) => { const { closeChatwindow, isOpen } = props; const [Message, setMessage] = useState(""); const { handleConversation, agentMessages, isLoadingChatMessages, } = props.ApplicationStore; useEffect(() => { handleConversation(); return () => handleConversation() }, []); const data = toJS(agentMessages); return ( <div className="chat-container"> <div className="chat-head"> <div style={{ ...center }}> <h5> Zara {isLoadingChatMessages && "is typing ..."} </h5> </div> <div style={{ ...center }} className="hover"> <FiX onClick={(_) => closeChatwindow()} /> </div> </div> <div className="chat-body"> <ul className="chat-window"> {data.map(({ fulfillmentText, userMessage }) => ( <li> {userMessage && ( <div style={{ display: "flex", justifyContent: "space-between", }} > <p style={{ opacity: 0 }}> . </p> <div key={userMessage} style={{ background: "red", color: "white", }} className="chat-card" > <p>{userMessage}</p> </div> </div> )} {fulfillmentText && ( <div style={{ display: "flex", justifyContent: "space-between", }} > <div key={fulfillmentText} className="chat-card"> <p>{fulfillmentText}</p> </div> <p style={{ opacity: 0 }}> . </p> </div> )} </li> ))} </ul> <hr style={{ background: "#fff" }} /> <form onSubmit={(e) => { e.preventDefault(); handleConversation(Message); }} className="input-container" > <input className="input" type="text" onChange={(e) => setMessage(e.target.value)} value={Message} placeholder="Begin a conversation with our agent" /> <div className="send-btn-ctn"> <div className="hover" onClick={() => handleConversation(Message)} > <FiSend style={{ transform: "rotate(50deg)" }} /> </div> </div> </form> </div> </div> ); }; export default inject("ApplicationStore")(observer(ChatComponent));

上記のコードの強調表示された部分から、チャットコンポーネント全体が次の新しい操作を実行するように変更されていることがわかります。

  • ApplicationStore値を挿入した後、MobXストア値にアクセスできます。 コンポーネントはこれらのストア値のオブザーバーにもなっているため、値の1つが変更されたときに再レンダリングされます。
  • チャットコンポーネントが開かれた直後に、 useEffectフック内でhandleConversationメソッドを呼び出して、コンポーネントがレンダリングされるとすぐにリクエストを行うことにより、エージェントとの会話を開始します。
  • 現在、チャットコンポーネントヘッダー内のisLoadingMessages値を利用しています。 エージェントからの応答を取得するリクエストが実行中の場合、 isLoadingMessages値をtrueに設定し、ヘッダーをZaraistypeingに更新します…
  • ストア内のagentMessages配列は、値が設定された後、MobXによってプロキシに更新されます。 このコンポーネントから、MobXのtoJSユーティリティを使用してそのプロキシを配列に変換し直し、コンポーネント内の変数に値を格納します。 その配列はさらに繰り返され、マップ関数を使用してチャットバブルに配列内の値を入力します。

チャットコンポーネントを使用して、文を入力し、エージェントに応答が表示されるのを待つことができます。

HTTPリクエストからエクスプレスアプリケーションに返されるリストデータを表示するチャットコンポーネント。
HTTPリクエストからエクスプレスアプリケーションに返されるリストデータを表示するチャットコンポーネント。 (大プレビュー)

ユーザーの音声入力の録音

デフォルトでは、すべてのDialogflowエージェントは、ユーザーからの指定された言語での音声またはテキストベースの入力を受け入れることができます。 ただし、ユーザーのマイクにアクセスして音声入力を録音するには、Webアプリケーションからいくつかの調整が必要です。

これを実現するために、MobXストアを変更してHTML MediaStream Recording APIを使用し、MobXストアの2つの新しいメソッド内でユーザーの音声を録音します。

 // store.js import Axios from "axios"; import { action, observable, makeObservable } from "mobx"; class ApplicationStore { constructor() { makeObservable(this); } @observable isRecording = false; recorder = null; recordedBits = []; @action startAudioConversation = () => { navigator.mediaDevices .getUserMedia({ audio: true, }) .then((stream) => { this.isRecording = true; this.recorder = new MediaRecorder(stream); this.recorder.start(50); this.recorder.ondataavailable = (e) => { this.recordedBits.push(e.data); }; }) .catch((e) => console.log(`error recording : ${e}`)); }; };

チャットコンポーネントから記録アイコンをクリックすると、上記のMobXストアのstartAudioConversationメソッドが呼び出され、監視可能なisRecordingプロパティがtrueになるメソッドが設定されます。これにより、チャットコンポーネントは、記録が進行中であることを示す視覚的なフィードバックを提供します。

ブラウザのナビゲータインターフェイスを使用して、メディアデバイスオブジェクトにアクセスし、ユーザーのデバイスマイクを要求します。 getUserMediaリクエストに権限が付与されると、MediaStreamデータを使用してその約束が解決されます。MediaStreamデータはさらにMediaRecorderコンストラクターに渡され、ユーザーのデバイスマイクから返されたストリームのメディアトラックを使用してレコーダーを作成します。 次に、後で別のメソッドからアクセスするため、Mediarecorderインスタンスをストアのrecorderプロパティに格納します。

次に、レコーダーインスタンスでstartメソッドを呼び出し、記録セッションが終了した後、 recordedBits配列プロパティに格納するBlobに記録されたストリームを含むイベント引数を使用してondataavailable関数を起動します。

起動されたondataavailableイベントに渡されたevent引数のデータをログアウトすると、ブラウザーコンソールでBlobとそのプロパティを確認できます。

記録が終了した後にMediaRecorderによって作成されたログアウトされたBlobを表示するブラウザーDevtoolsコンソール
記録が終了した後にMediaRecorderによって作成されたログアウトされたBlobを表示するブラウザーDevtoolsコンソール。 (大プレビュー)

MediaRecorderストリームを開始できるようになったので、ユーザーが音声入力の録音を完了したときにMediaRecorderストリームを停止し、生成されたオーディオファイルをExpress.jsアプリケーションに送信できるようにする必要があります。

以下のストアに追加された新しいメソッドは、ストリームを停止し、録音された音声入力を含むPOSTリクエストを作成します。

 //store.js import Axios from "axios"; import { action, observable, makeObservable, configure } from "mobx"; const ENDPOINT = process.env.REACT_APP_DATA_API_URL; class ApplicationStore { constructor() { makeObservable(this); } @observable isRecording = false; recorder = null; recordedBits = []; @action closeStream = () => { this.isRecording = false; this.recorder.stop(); this.recorder.onstop = () => { if (this.recorder.state === "inactive") { const recordBlob = new Blob(this.recordedBits, { type: "audio/mp3", }); const inputFile = new File([recordBlob], "input.mp3", { type: "audio/mp3", }); const formData = new FormData(); formData.append("voiceInput", inputFile); Axios.post(`${ENDPOINT}/api/agent/voice-input`, formData, { headers: { "Content-Type": "multipart/formdata", }, }) .then((data) => {}) .catch((e) => console.log(`error uploading audio file : ${e}`)); } }; }; } export const store = new ApplicationStore();

The method above executes the MediaRecorder's stop method to stop an active stream. Within the onstop event fired after the MediaRecorder is stopped, we create a new Blob with a music type and append it into a created FormData.

As the last step., we make POST request with the created Blob added to the request body and a Content-Type: multipart/formdata added to the request's headers so the file can be parsed by the connect-busboy middleware from the backend-service application.

With the recording being performed from the MobX store, all we need to add to the chat-component is a button to execute the MobX actions to start and stop the recording of the user's voice and also a text to show when a recording session is active.

 import React from 'react' const ChatComponent = ({ ApplicationStore }) => { const { startAudiConversation, isRecording, handleConversation, endAudioConversation, isLoadingChatMessages } = ApplicationStore const [ Message, setMessage ] = useState("") return ( <div> <div className="chat-head"> <div style={{ ...center }}> <h5> Zara {} {isRecording && "is listening ..."} </h5> </div> <div style={{ ...center }} className="hover"> <FiX onClick={(_) => closeChatwindow()} /> </div> </div> <form onSubmit={(e) => { e.preventDefault(); handleConversation(Message); }} className="input-container" > <input className="input" type="text" onChange={(e) => setMessage(e.target.value)} value={Message} placeholder="Begin a conversation with our agent" /> <div className="send-btn-ctn"> {Message.length > 0 ? ( <div className="hover" onClick={() => handleConversation(Message)} > <FiSend style={{ transform: "rotate(50deg)" }} /> </div> ) : ( <div className="hover" onClick={() => handleAudioInput()} > <FiMic /> </div> )} </div> </form> </div> ) } export default ChatComponent

From the highlighted part in the chat component header above, we use the ES6 ternary operators to switch the text to “ Zara is listening …. ” whenever a voice input is being recorded and sent to the backend application. This gives the user feedback on what is being done.

Also, besides the text input, we added a microphone icon to inform the user of the text and voice input options available when using the chat assistant. If a user decides to use the text input, we switch the microphone button to a Send button by counting the length of the text stored and using a ternary operator to make the switch.

We can test the newly connected chat assistant a couple of times by using both voice and text inputs and watch it respond exactly like it would when using the Dialogflow console!

結論

In the coming years, the use of language processing chat assistants in public services will have become mainstream. This article has provided a basic guide on how one of these chat assistants built with Dialogflow can be integrated into your own web application through the use of a backend application.

The built application has been deployed using Netlify and can be found here. Feel free to explore the Github repository of the backend express application here and the React.js web application here. They both contain a detailed README to guide you on the files within the two projects.

参考文献

  • Dialogflow Documentation
  • Building A Conversational NLP Enabled Chatbot Using Google's Dialogflow by Nwani Victory
  • MobX
  • https://web.postman.com
  • Dialogflow API: Node.js Client
  • Using the MediaStream Recording API