將 Dialogflow 代理集成到 React 應用程序中

已發表: 2022-03-10
快速總結 ↬在構建可在小型或企業級別使用的對話式聊天助手時,Dialogflow 很可能是您的搜索列表中出現的第一個選項 - 為什麼不呢? 它提供了多種功能,例如處理音頻和文本輸入的能力、使用自定義 webhook 提供動態響應、使用 Google 助手連接到數百萬支持 Google 的設備等等。 但是除了提供用於設計和管理代理的控制台之外,我們如何創建一個也可以在我們構建的 Web 應用程序中使用的聊天助手?

Dialogflow 是一個平台,可簡化創建和設計自然語言處理會話聊天助手的過程,該助手可以在從 Dialogflow 控制台或集成 Web 應用程序使用時處理語音或文本輸入。

雖然本文對集成的 Dialogflow Agent 進行了簡要說明,但希望您對 Node.js 和 Dialogflow 有所了解。 如果您是第一次學習 Dialogflow,本文對 Dialogflow 是什麼及其概念進行了清晰的解釋。

本文是關於如何構建具有語音和聊天支持的 Dialogflow 代理的指南,該代理可以在 Express.js 後端應用程序的幫助下集成到 Web 應用程序中,作為 React.js Web 應用程序和代理之間的鏈接在 Dialogflow 本身上。 在本文結束時,您應該能夠將自己的 Dialogflow 代理連接到您首選的 Web 應用程序。

為了使本指南易於理解,您可以跳到您最感興趣的教程的任何部分,或者按照它們出現的以下順序進行操作:

  • 設置 Dialogflow 代理
  • 集成 Dialogflow 代理
  • 設置 Node Express 應用程序
    • 使用 Dialogflow 進行身份驗證
    • 什麼是服務帳戶?
    • 處理語音輸入
  • 集成到 Web 應用程序中
    • 創建聊天界面
    • 錄製用戶語音輸入
  • 結論
  • 參考

1. 設置 Dialogflow 代理

如本文所述,Dialogflow 上的聊天助手稱為代理,它由更小的組件組成,例如意圖、履行、知識庫等等。 Dialogflow 為用戶提供了一個控制台來創建、訓練和設計代理的對話流。 在我們的用例中,我們將使用代理導出和導入功能恢復訓練後導出到 ZIP 文件夾中的代理。

在執行導入之前,我們需要創建一個新代理,該代理將與即將恢復的代理合併。 要從控制台創建新代理,需要一個唯一的名稱,還需要一個 Google Cloud 上的項目來鏈接代理。 如果 Google Cloud 上沒有可鏈接的現有項目,則可以在此處創建一個新項目。

之前已經創建並訓練了一個代理,以根據用戶的預算向用戶推薦葡萄酒產品。 此代理已導出為 ZIP; 您可以在此處下載文件夾並將其從代理設置頁面中的導出和導入選項卡中恢復到我們新創建的代理中。

從 ZIP 文件夾恢復以前導出的代理
從 ZIP 文件夾恢復以前導出的代理。 (大預覽)

進口代理之前接受過培訓,可以根據用戶購買一瓶葡萄酒的預算向用戶推薦葡萄酒產品。

通過導入的代理,我們將看到它從意圖頁面創建了三個意圖。 一個是後備意圖,在代理無法識別用戶輸入時使用,另一個是在與代理開始對話時使用的歡迎意圖,最後一個意圖用於根據句子中的數量參數。 我們關心的是get-wine-recommendation意圖

此意圖具有來自默認歡迎意圖的wine-recommendation的單一輸入上下文,以將對話鏈接到此意圖。

“上下文是代理中的一個系統,用於控制從一個意圖到另一個意圖的對話流程。”

上下文下方是訓練短語,它們是用於訓練代理了解用戶期望的語句類型的句子。 通過意圖中的大量訓練短語,代理能夠識別用戶的句子及其所屬的意圖。

我們的代理get-wine-recommendation意圖中的訓練短語(如下所示)指示了葡萄酒的選擇和價格類別:

具有 get-wine-recommendation 意圖的可用訓練短語列表。
Get-wine-recommendation intent 頁面顯示可用的培訓短語。 (大預覽)

查看上圖,我們可以看到列出的可用訓練短語,每個短語的貨幣數字都以黃色突出顯示。 這種突出顯示在 Dialogflow 上被稱為註釋,它會自動完成以從用戶的句子中提取被稱為實體的識別數據類型。

在與代理的對話中匹配此意圖後,將向外部服務發出 HTTP 請求,以根據從用戶句子中提取為參數的價格獲取推薦的葡萄酒,通過使用在其中找到的啟用的 webhook此意向頁面底部的履行部分。

我們可以使用位於 Dialogflow 控制台右側的 Dialogflow 模擬器來測試代理。 為了測試,我們以“”信息開始對話,然後用所需的酒量跟進。 webhook 將立即被調用,並且代理將顯示類似於下面的豐富響應。

測試導入的代理代理 webhook。
使用控制台中的代理模擬器測試導入的代理的履行 webhook。 (大預覽)

從上圖中,我們可以看到使用 Ngrok 生成的 webhook URL,右側的代理響應顯示了用戶輸入的價格在 20 美元範圍內的葡萄酒。

至此,Dialogflow 代理已完全設置完畢。 我們現在可以開始將此代理集成到 Web 應用程序中,以使其他用戶無需訪問我們的 Dialogflow 控制台即可訪問該代理並與之交互。

跳躍後更多! 繼續往下看↓

集成 Dialogflow 代理

雖然還有其他方法可以連接到 Dialogflow 代理,例如向其 REST 端點發出 HTTP 請求,但推薦的連接到 Dialogflow 的方法是使用其以多種編程語言提供的官方客戶端庫。 對於 JavaScript,可以從 NPM 安裝 @google-cloud/dialogflow 包。

在內部,@google-cloud/dialogflow 包使用 gRPC 進行網絡連接,這使得該包在瀏覽器環境中不受支持,除非使用 webpack 進行修補,使用此包的推薦方法是來自 Node 環境。 我們可以通過設置 Express.js 後端應用程序來使用這個包,然後通過其 API 端點向 Web 應用程序提供數據來做到這一點,這就是我們接下來要做的事情。

設置 Node 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

安裝了所需的依賴項後,我們可以繼續設置一個非常精簡的 Express.js 服務器,該服務器處理指定端口上的連接,並為 Web 應用程序啟用了 CORS 支持。

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

執行時,上面代碼片段中的代碼將啟動一個 HTTP 服務器,該服務器偵聽指定 PORT Express.js 上的連接。 它還對使用 cors 包作為 Express 中間件的所有請求啟用了跨域資源共享 (CORS)。 目前,這個服務器只監聽連接,它無法響應請求,因為它沒有創建路由,所以讓我們創建這個。

我們現在需要添加兩條新路由:一條用於發送文本數據,另一條用於發送錄製的語音輸入。 它們都將接受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

上面我們為兩個創建的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}`));

上面,我們添加了兩個路由的基本路徑,我們可以通過命令行使用 cURL 的POST請求來測試它們中的任何一個,因為它在下面使用空請求正文完成;

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

成功完成上述請求後,我們可以看到一個包含對像數據的響應被打印到控制台。

現在我們要與 Dialogflow 建立實際連接,其中包括使用 @google-cloud/dialogflow 包在 Dialogflow 上處理身份驗證、發送和接收來自代理的數據。

使用 Dialogflow 進行身份驗證

創建的每個 Dialogflow 代理都鏈接到 Google Cloud 上的一個項目。 為了從外部連接到 Dialogflow 代理,我們在 Google 雲上對項目進行身份驗證,並將 Dialogflow 作為項目的資源之一。 在連接到 google-cloud 上的項目的六種可用方法中,當通過其客戶端庫連接到 google-cloud 上的特定服務時,使用服務帳戶選項是最方便的。

注意對於生產就緒的應用程序,建議使用短期 API 密鑰而不是服務帳戶密鑰,以降低服務帳戶密鑰落入壞人手中的風險。

什麼是服務帳戶?

服務帳戶是 Google Cloud 上的一種特殊類型的帳戶,主要通過外部 API 為非人工交互而創建。 在我們的應用程序中,將通過 Dialogflow 客戶端庫生成的密鑰訪問服務帳戶,以向 Google Cloud 進行身份驗證。

有關創建和管理服務帳戶的雲文檔為創建服務帳戶提供了極好的指南。 創建服務帳戶時,應將 Dialogflow API 管理員角色分配給創建的服務帳戶,如上一步所示。 此角色為服務帳戶提供對鏈接的 Dialogflow 代理的管理控制權。

要使用服務帳戶,我們需要創建一個服務帳戶密鑰。 以下步驟概述瞭如何以 JSON 格式創建一個:

  1. 單擊新創建的服務帳戶以導航到服務帳戶頁面。
  2. 滾動到 Keys 部分,然後單擊Add Key下拉菜單,然後單擊Create new key選項打開一個模式。
  3. 選擇 JSON 文件格式,然後單擊模式右下角的創建按鈕。

注意:建議將服務帳戶密鑰保密,不要將其提交給任何版本控制系統,因為它包含有關 Google Cloud 上項目的高度敏感數據。 這可以通過將文件添加到.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 雲進行身份驗證,然後使用鏈接到 Dialogflow 代理的 Google 雲項目的 projectID 和一個隨機 ID 創建與 Dialogflow 的會話,以識別創建的會話。 在我們的應用程序中,我們在使用 JavaScript UUID 包創建的每個會話上創建一個 UUID 標識符。 這在記錄或跟踪 Dialogflow 代理處理的所有對話時非常有用。
  • 第二
    我們按照 Dialogflow 文檔中的指定格式創建請求對像數據。 此請求對象包含創建的會話和從要傳遞給 Dialogflow 代理的請求正文中獲取的消息數據。
  • 第三
    使用 Dialogflow 會話中的detectIntent方法,我們異步發送請求對象並在 try-catch 塊中使用 ES6 async / await 語法等待 Agent 的響應,如果detectIntent方法返回異常,我們可以捕獲錯誤並返回它,而不是而不是使整個應用程序崩潰。 Dialogflow 文檔中提供了從代理返回的響應對象的示例,可以對其進行檢查以了解如何從對像中提取數據。

我們可以利用 Postman 來測試上面在dialogflow-response路由中實現的 Dialogflow 連接。 Postman 是一個用於 API 開發的協作平台,具有用於測試在開發或生產階段構建的 API 的功能。

注意:如果尚未安裝,則不需要 Postman 桌面應用程序來測試 API。 從 2020 年 9 月開始,Postman 的 Web 客戶端進入通用 (GA) 狀態,可以直接從瀏覽器使用。

使用 Postman Web 客戶端,我們可以創建一個新的工作空間或使用現有的工作空間在https://localhost:5000/api/agent/text-input向我們的 API 端點創建一個POST請求,並使用密鑰添加數據message和“ Hi There ”的值到查詢參數中。

單擊“發送”按鈕後,將向正在運行的 Express 服務器發出POST請求 - 響應類似於下圖所示的響應:

使用 Postman 測試文本輸入 API 端點。
使用 Postman 測試文本輸入 API 端點。 (大預覽)

在上圖中,我們可以看到來自 Dialogflow 代理通過 Express 服務器的美化響應數據。 返回的數據根據 Dialogflow Webhook 文檔中給出的示例響應定義進行格式化。

處理語音輸入

默認情況下,所有 Dialogflow 代理都可以處理文本和音頻數據,並以文本或音頻格式返迴響應。 但是,處理音頻輸入或輸出數據可能比文本數據複雜一些。

為了處理和處理語音輸入,我們將開始實現我們之前創建的/voice-input端點,以便接收音頻文件並將它們發送到 Dialogflow 以換取代理的響應:

 // 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 作為 Express 中間件,用於解析 Web 應用程序請求中發送的表單數據。 之後,我們使用服務密鑰向 Dialogflow 進行身份驗證並創建一個會話,就像我們在之前的路由中所做的那樣。
    然後使用內置 Node.js util 模塊中的 promisify 方法,我們獲取並保存與 Stream 管道方法等效的 promise,以便稍後用於管道多個流,並在流完成後執行清理。
  • 接下來,我們創建一個請求對象,其中包含 Dialogflow 身份驗證會話和將要發送到 Dialogflow 的音頻文件的配置。 嵌套的音頻配置對象使 Dialogflow 代理能夠對發送的音頻文件執行語音到文本的轉換。
  • 接下來,使用創建的會話和請求對象,我們使用detectStreamingIntent方法從音頻文件中檢測用戶的意圖,該方法會打開一個從 Dialogflow 代理到後端應用程序的新數據流。 數據將通過該流以小比特發送回,並使用來自可讀流的數據“事件”,我們將數據存儲在streamData變量中以供以後使用。 流關閉後,“ end ”事件被觸發,我們將存儲在streamData變量中的 Dialogflow 代理的響應發送到 Web 應用程序。
  • 最後使用來自 connect-busboy 的文件流事件,我們接收到請求正文中發送的音頻文件流,然後我們將其進一步傳遞給我們之前創建的 Pipeline 的 Promise 等價物。 其作用是將request傳入的音頻文件流通過管道傳遞給Dialogflow流,我們將音頻文件流傳遞給上面detectStreamingIntent方法打開的流。

為了測試並確認上述步驟是否按規定工作,我們可以使用 Postman 向/voice-input端點發出一個包含請求正文中的音頻文件的測試請求。

使用 Postman 測試語音輸入 API 端點。
使用 postman 和錄製的語音文件測試語音輸入 API 端點。 (大預覽)

上面的 Postman 結果顯示了在發出 POST 請求後得到的響應,其中包含在請求正文中的錄製的語音註釋消息的表單數據,其中包含“ Hi ”。

至此,我們現在有了一個功能性的 Express.js 應用程序,它可以從 Dialogflow 發送和接收數據,本文的兩部分就完成了。 現在剩下的就是通過使用從 Reactjs 應用程序創建的 API 來將此代理集成到 Web 應用程序中。

集成到 Web 應用程序中

為了使用我們構建的 REST API,我們將擴展這個現有的 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可以在目標商店類的構造函數中使用。 請參閱此處的示例

通過 store 設置,我們需要從index.js文件中的根組件開始用 MobX Provider 高階組件包裝整個應用程序樹。

 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 Provider 包裝了根 App 組件,並將之前創建的 store 作為 Provider 的值之一傳入。 現在我們可以繼續從連接到存儲的組件中的存儲中讀取。

創建聊天界面

要顯示從 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 請求獲得的實際響應,而不是硬編碼文本。

我們繼續重構 Chat 組件,這次連接和使用組件內 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 存儲值。 該組件也已成為這些存儲值的觀察者,因此當其中一個值更改時它會重新呈現。
  • 在聊天組件打開後,我們通過調用useEffect鉤子中的handleConversation方法立即開始與代理的對話,以在組件被渲染後立即發出請求。
  • 我們現在使用 Chat 組件標題中的isLoadingMessages值。 當從代理獲取響應的請求正在進行時,我們將isLoadingMessages值設置為true並將標頭更新為Zara 正在輸入...
  • 存儲中的agentMessages數組在其值設置後被 MobX 更新為代理。 從這個組件中,我們使用 MobX 中的toJS實用程序將該代理轉換回一個數組,並將值存儲在組件內的一個變量中。 該數組被進一步迭代以使用映射函數使用數組中的值填充聊天氣泡。

現在使用聊天組件,我們可以輸入一個句子並等待響應顯示在代理中。

聊天組件顯示從 HTTP 請求返回到 express 應用程序的列表數據。
聊天組件顯示從 HTTP 請求返回到 express 應用程序的列表數據。 (大預覽)

錄製用戶語音輸入

默認情況下,所有 Dialogflow 代理都可以接受來自用戶的任何指定語言的語音或基於文本的輸入。 但是,它需要對 Web 應用程序進行一些調整才能訪問用戶的麥克風並記錄語音輸入。

為了實現這一點,我們修改了 MobX 商店以使用 HTML MediaStream Recording API 在 MobX 商店中的兩個新方法中錄製用戶的聲音。

 // 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 數據解析其承諾,我們進一步將其傳遞給 MediaRecorder 構造函數,以使用從用戶設備麥克風返回的流中的媒體軌道創建記錄器。 然後我們將媒體記錄器實例存儲在商店的recorder屬性中,因為稍後我們將從另一個方法訪問它。

接下來,我們在記錄器實例上調用 start 方法,並在記錄會話結束後,使用一個事件參數觸發ondataavailable函數,該事件參數包含一個 Blob 中的記錄流,我們將其存儲在recordedBits數組屬性中。

註銷傳遞給觸發的ondataavailable事件的事件參數中的數據,我們可以在瀏覽器控制台中看到 Blob 及其屬性。

瀏覽器 Devtools 控制台顯示記錄結束後由媒體記錄器創建的註銷 Blob。拉引號
瀏覽器 Devtools 控制台顯示記錄結束後由媒體記錄器創建的註銷 Blob。 (大預覽)

現在我們可以啟動 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