将 Dialogflow 代理集成到 React 应用程序中
已发表: 2022-03-10Dialogflow 是一个平台,可简化创建和设计自然语言处理会话聊天助手的过程,该助手可以在从 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; 您可以在此处下载文件夹并将其从代理设置页面中的导出和导入选项卡中恢复到我们新创建的代理中。
进口代理之前接受过培训,可以根据用户购买一瓶葡萄酒的预算向用户推荐葡萄酒产品。
通过导入的代理,我们将看到它从意图页面创建了三个意图。 一个是后备意图,在代理无法识别用户输入时使用,另一个是在与代理开始对话时使用的欢迎意图,最后一个意图用于根据句子中的数量参数。 我们关心的是get-wine-recommendation
意图
此意图具有来自默认欢迎意图的wine-recommendation
的单一输入上下文,以将对话链接到此意图。
“上下文是代理中的一个系统,用于控制从一个意图到另一个意图的对话流程。”
上下文下方是训练短语,它们是用于训练代理了解用户期望的语句类型的句子。 通过意图中的大量训练短语,代理能够识别用户的句子及其所属的意图。
我们的代理get-wine-recommendation
意图中的训练短语(如下所示)指示了葡萄酒的选择和价格类别:
查看上图,我们可以看到列出的可用训练短语,每个短语的货币数字都以黄色突出显示。 这种突出显示在 Dialogflow 上被称为注释,它会自动完成以从用户的句子中提取被称为实体的识别数据类型。
在与代理的对话中匹配此意图后,将向外部服务发出 HTTP 请求,以根据从用户句子中提取为参数的价格获取推荐的葡萄酒,通过使用在其中找到的启用的 webhook此意向页面底部的履行部分。
我们可以使用位于 Dialogflow 控制台右侧的 Dialogflow 模拟器来测试代理。 为了测试,我们以“嗨”信息开始对话,然后用所需的酒量跟进。 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 格式创建一个:
- 单击新创建的服务帐户以导航到服务帐户页面。
- 滚动到 Keys 部分,然后单击Add Key下拉菜单,然后单击Create new key选项打开一个模式。
- 选择 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
请求 - 响应类似于下图所示的响应:
在上图中,我们可以看到来自 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 结果显示了在发出 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
实用程序将该代理转换回一个数组,并将值存储在组件内的一个变量中。 该数组被进一步迭代以使用映射函数使用数组中的值填充聊天气泡。
现在使用聊天组件,我们可以输入一个句子并等待响应显示在代理中。
录制用户语音输入
默认情况下,所有 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 及其属性。
现在我们可以启动 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