Integración de un agente de Dialogflow en una aplicación React
Publicado: 2022-03-10Dialogflow es una plataforma que simplifica el proceso de creación y diseño de un asistente de chat conversacional de procesamiento de lenguaje natural que puede procesar entradas de voz o texto cuando se usa desde la consola de Dialogflow o desde una aplicación web integrada.
Aunque el Dialogflow Agent integrado se explica brevemente en este artículo, se espera que tenga conocimientos de Node.js y Dialogflow. Si está aprendiendo sobre Dialogflow por primera vez, este artículo brinda una explicación clara de qué es Dialogflow y sus conceptos.
Este artículo es una guía sobre cómo se creó un agente de Dialogflow con soporte de voz y chat que se puede integrar en una aplicación web con la ayuda de una aplicación de back-end Express.js como enlace entre una aplicación web React.js y el agente. en Dialogflow mismo. Al final del artículo, debería poder conectar su propio agente de Dialogflow a su aplicación web preferida.
Para que esta guía sea fácil de seguir, puede saltar a la parte del tutorial que más le interese o seguirla en el siguiente orden en que aparece:
- Configuración de un agente de Dialogflow
- Integración de un agente de Dialogflow
- Configuración de una aplicación Node Express
- Autenticación con Dialogflow
- ¿Qué son las cuentas de servicio?
- Manejo de entradas de voz
- Integración en una aplicación web
- Creación de una interfaz de chat
- Grabación de la entrada de voz del usuario
- Conclusión
- Referencias
1. Configuración de un agente de Dialogflow
Como se explica en este artículo, un asistente de chat en Dialogflow se llama Agente y se compone de componentes más pequeños, como intenciones, cumplimiento, base de conocimiento y mucho más. Dialogflow proporciona una consola para que los usuarios creen, entrenen y diseñen el flujo de conversación de un agente. En nuestro caso de uso, restauraremos un agente que se exportó a una carpeta ZIP después de ser entrenado, utilizando la función Exportar e Importar agente.
Antes de realizar la importación, debemos crear un nuevo agente que se fusionará con el agente que está a punto de restaurarse. Para crear un nuevo Agente desde la consola, se necesita un nombre único y también un proyecto en Google Cloud para vincular al agente. Si no hay un proyecto existente en Google Cloud para vincular, se puede crear uno nuevo aquí.
Previamente se ha creado y formado un agente para recomendar productos vitivinícolas a un usuario en función de su presupuesto. Este agente ha sido exportado a un ZIP; puede descargar la carpeta aquí y restaurarla en nuestro agente recién creado desde la pestaña Exportar e Importar que se encuentra en la página Configuración del agente.
El agente importado ha sido previamente capacitado para recomendar un producto vitivinícola al usuario en base al presupuesto del usuario para la compra de una botella de vino.
Pasando por el agente importado, veremos que tiene tres intentos creados desde la página de intentos. Una es una intención alternativa, que se usa cuando el agente no reconoce la entrada de un usuario, la otra es una intención de bienvenida que se usa cuando se inicia una conversación con el agente y la última intención se usa para recomendar un vino al usuario en función de la parámetro de cantidad dentro de la oración. Nos preocupa la intención get-wine-recommendation
Esta intención tiene un único contexto de entrada de wine-recommendation
proviene de la intención de bienvenida predeterminada para vincular la conversación a esta intención.
“Un contexto es un sistema dentro de un agente que se utiliza para controlar el flujo de una conversación de un intento a otro”.
Debajo de los contextos se encuentran las frases de entrenamiento, que son oraciones que se usan para entrenar a un agente sobre qué tipo de declaraciones esperar de un usuario. A través de una gran variedad de frases de entrenamiento dentro de una intención, un agente puede reconocer la oración de un usuario y la intención en la que se encuentra.
Las frases de entrenamiento dentro de la intención get-wine-recommendation
nuestros agentes (como se muestra a continuación) indican la elección del vino y la categoría de precio:
Mirando la imagen de arriba, podemos ver las frases de entrenamiento disponibles enumeradas, y la cifra de la moneda está resaltada en color amarillo para cada una de ellas. Este resaltado se conoce como anotación en Dialogflow y se realiza automáticamente para extraer los tipos de datos reconocidos conocidos como una entidad de la oración de un usuario.
Después de que esta intención coincida en una conversación con el agente, se realizará una solicitud HTTP a un servicio externo para obtener el vino recomendado en función del precio extraído como parámetro de la oración de un usuario, mediante el uso del webhook habilitado que se encuentra dentro la sección Cumplimiento en la parte inferior de esta página de intenciones.
Podemos probar el agente usando el emulador de Dialogflow ubicado en la sección derecha de la consola de Dialogflow. Para probar, comenzamos la conversación con un mensaje de " Hola " y seguimos con la cantidad deseada de vino. Se llamará inmediatamente al webhook y el agente mostrará una respuesta enriquecida similar a la siguiente.
En la imagen de arriba podemos ver la URL del webhook generada con Ngrok y la respuesta del agente en el lado derecho que muestra un vino dentro del rango de precio de $20 ingresado por el usuario.
En este punto, el agente de Dialogflow se ha configurado por completo. Ahora podemos comenzar a integrar este agente en una aplicación web para permitir que otros usuarios accedan e interactúen con el agente sin acceso a nuestra consola Dialogflow.
Integración de un agente de Dialogflow
Si bien existen otros medios para conectarse a un agente de Dialogflow, como realizar solicitudes HTTP a sus extremos REST, la forma recomendada de conectarse a Dialogflow es mediante el uso de su biblioteca de cliente oficial disponible en varios lenguajes de programación. Para JavaScript, el paquete @google-cloud/dialogflow está disponible para su instalación desde NPM.
Internamente, el paquete @google-cloud/dialogflow usa gRPC para sus conexiones de red y esto hace que el paquete no sea compatible con un entorno de navegador, excepto cuando se parchea con un paquete web, la forma recomendada de usar este paquete es desde un entorno de nodo. Podemos hacer esto configurando una aplicación de back-end Express.js para usar este paquete y luego enviar datos a la aplicación web a través de sus puntos finales de API y esto es lo que haremos a continuación.
Configuración de una aplicación Node Express
Para configurar una aplicación rápida, creamos un nuevo directorio de proyectos y luego tomamos las dependencias necesarias usando yarn
desde una terminal de línea de comando abierta.
# 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
Con las dependencias necesarias instaladas, podemos proceder a configurar un servidor Express.js muy eficiente que maneje las conexiones en un puerto específico con la compatibilidad con CORS habilitada para la aplicación web.
// 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}`));
Cuando se ejecuta, el código del fragmento anterior inicia un servidor HTTP que escucha las conexiones en un PORT Express.js especificado. También tiene habilitado el uso compartido de recursos de origen cruzado (CORS) en todas las solicitudes que utilizan el paquete cors como un middleware Express. Por ahora, este servidor solo escucha las conexiones, no puede responder a una solicitud porque no tiene una ruta creada, así que creemos esto.
Ahora necesitamos agregar dos nuevas rutas: una para enviar datos de texto y la otra para enviar una entrada de voz grabada. Ambos aceptarán una solicitud POST
y enviarán los datos contenidos en el cuerpo de la solicitud al agente de Dialogflow más adelante.
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
Arriba, creamos una instancia de enrutador separada para las dos rutas POST
creadas que, por ahora, solo responden con un código de estado 200
y una respuesta ficticia codificada. Cuando terminemos de autenticarnos con Dialogflow, podemos regresar para implementar una conexión real a Dialogflow dentro de estos puntos finales.
Para el último paso en la configuración de nuestra aplicación de back-end, montamos la instancia de enrutador creada previamente en la aplicación Express usando app.use y una ruta base para la ruta.
// 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}`));
Arriba, hemos agregado una ruta base a las dos rutas, dos podemos probar cualquiera de ellas a través de una solicitud POST
usando cURL desde una línea de comando como se hace a continuación con un cuerpo de solicitud vacío;
curl -X https://localhost:5000/api/agent/text-response
Después de completar con éxito la solicitud anterior, podemos esperar ver una respuesta que contiene datos de objetos que se imprimen en la consola.
Ahora nos queda hacer una conexión real con Dialogflow, que incluye el manejo de la autenticación, el envío y la recepción de datos del Agente en Dialogflow mediante el paquete @google-cloud/dialogflow.
Autenticación con Dialogflow
Cada agente de Dialogflow creado está vinculado a un proyecto en Google Cloud. Para conectarnos externamente al agente de Dialogflow, nos autenticamos con el proyecto en la nube de Google y usamos Dialogflow como uno de los recursos del proyecto. De las seis formas disponibles para conectarse a un proyecto en la nube de Google, usar la opción Cuentas de servicio es la más conveniente cuando se conecta a un servicio en particular en la nube de Google a través de su biblioteca de clientes.
Nota : Para aplicaciones listas para producción, se recomienda el uso de claves de API de corta duración en lugar de claves de cuenta de servicio para reducir el riesgo de que una clave de cuenta de servicio caiga en las manos equivocadas.
¿Qué son las cuentas de servicio?
Las cuentas de servicio son un tipo especial de cuenta en Google Cloud, creada para la interacción no humana, principalmente a través de API externas. En nuestra aplicación, se accederá a la cuenta de servicio a través de una clave generada por la biblioteca del cliente de Dialogflow para autenticarse con Google Cloud.
La documentación en la nube sobre la creación y administración de cuentas de servicio proporciona una excelente guía para crear una cuenta de servicio. Al crear la cuenta de servicio, la función de administrador de la API de Dialogflow debe asignarse a la cuenta de servicio creada, como se muestra en el último paso. Esta función otorga a la cuenta de servicio control administrativo sobre el agente de Dialogflow vinculado.
Para usar la cuenta de servicio, necesitamos crear una clave de cuenta de servicio. Los siguientes pasos a continuación describen cómo crear uno en formato JSON:
- Haga clic en la cuenta de servicio recién creada para navegar a la página de la cuenta de servicio.
- Desplácese a la sección Claves y haga clic en el menú desplegable Agregar clave y haga clic en la opción Crear nueva clave que abre un modal.
- Seleccione un formato de archivo JSON y haga clic en el botón Crear en la parte inferior derecha del modal.
Nota: Se recomienda mantener privada la clave de una cuenta de servicio y no comprometerla con ningún sistema de control de versiones, ya que contiene datos altamente confidenciales sobre un proyecto en Google Cloud. Esto se puede hacer agregando el archivo al archivo .gitignore
.
Con una cuenta de servicio creada y una clave de cuenta de servicio disponible dentro del directorio de nuestro proyecto, podemos usar la biblioteca del cliente de Dialogflow para enviar y recibir datos del agente de 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;
Toda la ruta anterior envía datos al agente de Dialogflow y recibe una respuesta a través de los siguientes pasos.
- Primero
Se autentica con la nube de Google y luego crea una sesión con Dialogflow utilizando el ID del proyecto de la nube de Google vinculado al agente de Dialogflow y también una ID aleatoria para identificar la sesión creada. En nuestra aplicación, estamos creando un identificador UUID en cada sesión creada usando el paquete UUID de JavaScript. Esto es muy útil al registrar o rastrear todas las conversaciones manejadas por un agente de Dialogflow. - Segundo
Creamos un objeto de solicitud de datos siguiendo el formato especificado en la documentación de Dialogflow. Este objeto de solicitud contiene la sesión creada y los datos del mensaje obtenidos del cuerpo de la solicitud que se pasarán al agente de Dialogflow. - Tercera
Con el métododetectIntent
de la sesión de Dialogflow, enviamos el objeto de solicitud de forma asíncrona y esperamos la respuesta del agente usando la sintaxis async/await de ES6 en un bloque try-catch. Si el métododetectIntent
devuelve una excepción, podemos detectar el error y devolverlo, en lugar de que bloquear toda la aplicación. En la documentación de Dialogflow se proporciona una muestra del objeto de respuesta devuelto por el agente y se puede inspeccionar para saber cómo extraer los datos del objeto.
Podemos hacer uso de Postman para probar la conexión de Dialogflow implementada anteriormente en la dialogflow-response
. Postman es una plataforma de colaboración para el desarrollo de API con funciones para probar las API integradas en las etapas de desarrollo o producción.
Nota: Si aún no está instalada, la aplicación de escritorio Postman no es necesaria para probar una API. A partir de septiembre de 2020, el cliente web de Postman pasó a un estado de disponibilidad general (GA) y se puede usar directamente desde un navegador.
Usando Postman Web Client, podemos crear un nuevo espacio de trabajo o usar uno existente para crear una solicitud POST
a nuestro punto final API en https://localhost:5000/api/agent/text-input
y agregar datos con una clave de message
y el valor de " Hola " en los parámetros de consulta.
Al hacer clic en el botón Enviar , se realizará una solicitud POST
al servidor Express en ejecución, con una respuesta similar a la que se muestra en la imagen a continuación:
Dentro de la imagen de arriba, podemos ver los datos de respuesta embellecidos del agente de Dialogflow a través del servidor Express. Los datos devueltos tienen el formato de acuerdo con la definición de respuesta de muestra proporcionada en la documentación de Webhook de Dialogflow.
Manejo de entradas de voz
De manera predeterminada, todos los agentes de Dialogflow están habilitados para procesar datos de texto y audio y también devolver una respuesta en formato de texto o audio. Sin embargo, trabajar con datos de entrada o salida de audio puede ser un poco más complejo que con datos de texto.
Para manejar y procesar las entradas de voz, comenzaremos con la implementación del endpoint /voice-input
que hemos creado previamente para recibir archivos de audio y enviarlos a Dialogflow a cambio de una respuesta del Agente:
// 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}`); } });
En una vista general alta, la ruta /voice-input
anterior recibe la entrada de voz de un usuario como un archivo que contiene el mensaje que se habla al asistente de chat y lo envía al agente de Dialogflow. Para comprender mejor este proceso, podemos dividirlo en los siguientes pasos más pequeños:
- Primero, agregamos y usamos connect-busboy como un middleware Express para analizar los datos del formulario que se envían en la solicitud desde la aplicación web. Después de lo cual nos autenticamos con Dialogflow usando la clave de servicio y creamos una sesión, de la misma manera que hicimos en la ruta anterior.
Luego, usando el método promisify del módulo de utilidad de Node.js incorporado, obtenemos y guardamos un equivalente de promesa del método de canalización Stream que se usará más tarde para canalizar múltiples flujos y también realizar una limpieza después de que se completen los flujos. - A continuación, creamos un objeto de solicitud que contiene la sesión de autenticación de Dialogflow y una configuración para el archivo de audio que se enviará a Dialogflow. El objeto de configuración de audio anidado permite que el agente de Dialogflow realice una conversión de voz a texto en el archivo de audio enviado.
- Luego, usando la sesión creada y el objeto de solicitud, detectamos la intención de un usuario del archivo de audio usando el método
detectStreamingIntent
que abre un nuevo flujo de datos desde el agente de Dialogflow a la aplicación de back-end. Los datos se enviarán de regreso en pequeños bits a través de este flujo y usando el " evento " de datos del flujo legible, almacenamos los datos en la variablestreamData
para su uso posterior. Una vez que se cierra la transmisión, se activa el evento " fin " y enviamos la respuesta del agente Dialogflow almacenado en la variablestreamData
a la aplicación web. - Por último, al usar el evento de flujo de archivos de connect-busboy, recibimos el flujo del archivo de audio enviado en el cuerpo de la solicitud y lo pasamos al equivalente prometido de Pipeline que creamos anteriormente. La función de esto es canalizar el flujo del archivo de audio que proviene de la solicitud al flujo de Dialogflow, canalizamos el flujo del archivo de audio al flujo abierto por el método
detectStreamingIntent
anterior.
Para probar y confirmar que los pasos anteriores funcionan según lo establecido, podemos realizar una solicitud de prueba que contenga un archivo de audio en el cuerpo de la solicitud al extremo /voice-input
mediante Postman.
El resultado anterior de Postman muestra la respuesta obtenida después de realizar una solicitud POST con los datos del formulario de un mensaje de nota de voz grabado que dice " Hola " incluido en el cuerpo de la solicitud.
En este punto, ahora tenemos una aplicación Express.js funcional que envía y recibe datos de Dialogflow, las dos partes de este artículo están listas. ¿Dónde queda ahora la integración de este Agente en una aplicación web al consumir las API creadas aquí desde una aplicación Reactjs?
Integración en una aplicación web
Para consumir nuestra API REST construida, expandiremos esta aplicación React.js existente que ya tiene una página de inicio que muestra una lista de vinos obtenidos de una API y soporte para decoradores que usan el complemento de decoradores de propuestas de babel. Lo refactorizaremos un poco al presentar Mobx para la administración de estado y también una nueva característica para recomendar un vino desde un componente de chat utilizando los puntos finales de API REST agregados desde la aplicación Express.js.
Para comenzar, comenzamos a administrar el estado de la aplicación usando MobX mientras creamos una tienda Mobx con algunos valores observables y algunos métodos como acciones.
// 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();
Arriba, creamos una tienda para la función del componente de chat dentro de la aplicación que tiene los siguientes valores:
-
isChatWindowOpen
El valor almacenado aquí controla la visibilidad del componente de chat donde se muestran los mensajes de Dialogflow. -
isLoadingChatMessages
Esto se usa para mostrar un indicador de carga cuando se realiza una solicitud para obtener una respuesta del agente de Dialogflow. -
agentMessages
Esta matriz almacena todas las respuestas provenientes de las solicitudes realizadas para obtener una respuesta del agente de Dialogflow. Los datos de la matriz se muestran más tarde en el componente. -
handleConversation
Este método decorado como una acción agrega datos a la matrizagentMessages
. Primero, agrega el mensaje del usuario pasado como argumento y luego realiza una solicitud mediante Axios a la aplicación de backend para obtener una respuesta de Dialogflow. Una vez resuelta la solicitud, agrega la respuesta de la solicitud a la matrizagentMessages
.
Nota: en ausencia del soporte de decoradores en una aplicación, MobX proporciona makeObservable que se puede usar en el constructor de la clase de tienda de destino. Vea un ejemplo aquí .
Con la configuración de la tienda, debemos envolver todo el árbol de la aplicación con el componente de orden superior del proveedor MobX a partir del componente raíz en el archivo index.js
.
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;
Arriba, envolvemos el componente raíz de la aplicación con el proveedor MobX y pasamos la tienda creada previamente como uno de los valores del proveedor. Ahora podemos proceder a leer desde la tienda dentro de los componentes conectados a la tienda.
Creación de una interfaz de chat
Para mostrar los mensajes enviados o recibidos de las solicitudes de API, necesitamos un nuevo componente con alguna interfaz de chat que muestre los mensajes enumerados. Para hacer esto, creamos un nuevo componente para mostrar primero algunos mensajes codificados y luego mostramos los mensajes en una lista ordenada.
// ./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
El componente anterior tiene el marcado HTML básico necesario para una aplicación de chat. Tiene un encabezado que muestra el nombre del agente y un icono para cerrar la ventana de chat, una burbuja de mensaje que contiene un texto codificado en una etiqueta de lista y, por último, un campo de entrada que tiene un controlador de eventos onChange
adjunto al campo de entrada para almacenar el texto escrito en el estado local del componente usando useState de React.
De la imagen de arriba, el componente de chat funciona como debería, mostrando una ventana de chat con estilo que tiene un solo mensaje de chat y el campo de entrada en la parte inferior. Sin embargo, queremos que el mensaje que se muestra sean respuestas reales obtenidas de la solicitud de API y no texto codificado.
Avanzamos para refactorizar el componente Chat, esta vez conectando y haciendo uso de valores en la tienda MobX dentro del componente.
// ./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));
De las partes resaltadas del código anterior, podemos ver que todo el componente de chat ahora se ha modificado para realizar las siguientes operaciones nuevas;
- Tiene acceso a los valores de la tienda MobX después de inyectar el valor de
ApplicationStore
. El componente también se ha convertido en un observador de estos valores almacenados, por lo que se vuelve a representar cuando cambia uno de los valores. - Comenzamos la conversación con el agente inmediatamente después de que se abre el componente de chat invocando el método
handleConversation
dentro de unuseEffect
para realizar una solicitud inmediatamente que se procesa el componente. - Ahora estamos haciendo uso del valor
isLoadingMessages
dentro del encabezado del componente Chat. Cuando una solicitud para obtener una respuesta del Agente está en curso, establecemos el valor deisLoadingMessages
entrue
y actualizamos el encabezado a Zara está escribiendo... -
agentMessages
la matriz agentMessages dentro de la tienda a un proxy después de que se establecen sus valores. Desde este componente, volvemos a convertir ese proxy en una matriz usando la utilidadtoJS
de MobX y almacenamos los valores en una variable dentro del componente. Esa matriz se itera aún más para llenar las burbujas de chat con los valores dentro de la matriz mediante una función de mapa.
Ahora, usando el componente de chat, podemos escribir una oración y esperar a que se muestre una respuesta en el agente.
Grabación de la entrada de voz del usuario
De forma predeterminada, todos los agentes de Dialogflow pueden aceptar entradas de voz o de texto en cualquier idioma específico de un usuario. Sin embargo, requiere algunos ajustes desde una aplicación web para obtener acceso al micrófono de un usuario y grabar una entrada de voz.
Para lograr esto, modificamos la tienda MobX para usar la API de grabación HTML MediaStream para grabar la voz de un usuario dentro de dos métodos nuevos en la tienda 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}`)); }; };
Al hacer clic en el ícono de grabación del componente de chat, se invoca el método startAudioConversation
en la tienda de MobX anterior para establecer el método de la propiedad observable isRecording
en true , para que el componente de chat brinde retroalimentación visual para mostrar que una grabación está en progreso.
Usando la interfaz del navegador del navegador, se accede al objeto Dispositivo multimedia para solicitar el micrófono del dispositivo del usuario. Una vez que se otorga el permiso a la solicitud getUserMedia
, resuelve su promesa con datos de MediaStream que luego pasamos al constructor de MediaRecorder para crear una grabadora usando las pistas de medios en la transmisión devuelta desde el micrófono del dispositivo del usuario. Luego almacenamos la instancia de la grabadora de medios en la propiedad de la recorder
de la tienda, ya que accederemos a ella desde otro método más adelante.
A continuación, llamamos al método de inicio en la instancia de la grabadora y, una vez finalizada la sesión de grabación, se activa la función ondataavailable
con un argumento de evento que contiene el flujo grabado en un blob que almacenamos en la propiedad de matriz de bits recordedBits
.
Al cerrar la sesión de los datos en el argumento del evento que se pasó al evento ondataavailable
en datos disponibles, podemos ver el blob y sus propiedades en la consola del navegador.
Ahora que podemos iniciar una transmisión de MediaRecorder, debemos poder detener la transmisión de MediaRecorder cuando un usuario termine de grabar su entrada de voz y enviar el archivo de audio generado a la aplicación Express.js.
El nuevo método agregado a la tienda a continuación detiene la transmisión y realiza una solicitud POST
que contiene la entrada de voz grabada.
//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!
Conclusión
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.
Referencias
- 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