Integrando um agente do Dialogflow em um aplicativo React

Publicados: 2022-03-10
Resumo rápido ↬ Quando se trata de criar um assistente de bate-papo conversacional que possa ser usado em um nível pequeno ou empresarial, o Dialogflow provavelmente seria uma das primeiras opções que apareceriam em sua lista de pesquisa — e por que não? Ele oferece vários recursos, como a capacidade de processar entradas de áudio e texto, fornecer respostas dinâmicas usando webhooks personalizados, conectar-se a milhões de dispositivos habilitados para o Google usando o assistente do Google e muito mais. Mas, além de seu console que é fornecido para projetar e gerenciar um Agente, como podemos criar um assistente de bate-papo que também pode ser usado em nossos aplicativos da Web criados?

O Dialogflow é uma plataforma que simplifica o processo de criação e design de um assistente de bate-papo conversacional de processamento de linguagem natural que pode processar entrada de voz ou texto ao ser usado no console do Dialogflow ou em um aplicativo da Web integrado.

Embora o agente integrado do Dialogflow seja explicado brevemente neste artigo, espera-se que você entenda o Node.js e o Dialogflow. Se você está aprendendo sobre o Dialogflow pela primeira vez, este artigo fornece uma explicação clara sobre o que é o Dialogflow e seus conceitos.

Este artigo é um guia sobre como criar um agente do Dialogflow com suporte a voz e bate-papo que pode ser integrado a um aplicativo Web com a ajuda de um aplicativo de back-end Express.js como um link entre um aplicativo Web React.js e o Agente no próprio Dialogflow. No final do artigo, você poderá conectar seu próprio agente do Dialogflow ao seu aplicativo Web preferido.

Para tornar este guia fácil de seguir, você pode pular para a parte do tutorial que mais lhe interessa ou segui-la na seguinte ordem em que aparecem:

  • Como configurar um agente do Dialogflow
  • Integrando um agente do Dialogflow
  • Configurando um aplicativo Node Express
    • Autenticação com o Dialogflow
    • O que são contas de serviço?
    • Manipulando Entradas de Voz
  • Integrando em um aplicativo da Web
    • Criando uma interface de bate-papo
    • Gravando a entrada de voz do usuário
  • Conclusão
  • Referências

1. Configurando um agente do Dialogflow

Conforme explicado neste artigo, um assistente de chat no Dialogflow é chamado de Agente e é composto por componentes menores, como intents, fulfillment, base de conhecimento e muito mais. O Dialogflow fornece um console para que os usuários criem, treinem e projetem o fluxo de conversa de um agente. Em nosso caso de uso, restauraremos um agente que foi exportado para uma pasta ZIP após ser treinado, usando o recurso Exportar e Importar do agente.

Antes de realizarmos a importação, precisamos criar um novo agente que será mesclado com o agente prestes a ser restaurado. Para criar um novo agente a partir do console, é necessário um nome exclusivo e também um projeto no Google Cloud para vincular o agente. Se não houver um projeto existente no Google Cloud para vincular, um novo poderá ser criado aqui.

Um agente foi previamente criado e treinado para recomendar produtos vitivinícolas a um usuário com base em seu orçamento. Este agente foi exportado para um ZIP; você pode baixar a pasta aqui e restaurá-la em nosso agente recém-criado na guia Exportar e Importar encontrada na página Configurações do agente.

Restaurando um agente exportado anteriormente de uma pasta ZIP
Restaurando um agente exportado anteriormente de uma pasta ZIP. (Visualização grande)

O agente importado foi previamente treinado para recomendar um produto vitivinícola ao usuário com base no orçamento do usuário para a compra de uma garrafa de vinho.

Passando pelo agente importado, veremos que ele possui três intents criadas na página de intents. Um é um intent de fallback, usado quando o agente não reconhece a entrada de um usuário, o outro é um intent de boas-vindas usado quando uma conversa com o agente é iniciada e o último intent é usado para recomendar um vinho ao usuário com base no parâmetro de quantidade dentro da frase. O que nos preocupa é a intenção get-wine-recommendation

Essa intenção tem um único contexto de entrada de wine-recommendation proveniente da intenção de boas-vindas padrão para vincular a conversa a essa intenção.

“Um Contexto é um sistema dentro de um Agente usado para controlar o fluxo de uma conversa de uma intenção para outra.”

Abaixo dos contextos estão as frases de treinamento, que são frases usadas para treinar um agente sobre que tipo de instruções esperar de um usuário. Por meio de uma grande variedade de frases de treinamento em uma intenção, um agente é capaz de reconhecer a frase de um usuário e a intenção na qual ela se enquadra.

As frases de treinamento dentro da intenção get-wine-recommendation nossos agentes (conforme mostrado abaixo) indicam a escolha do vinho e a categoria de preço:

Lista de frases de treinamento disponíveis com intenção de recomendação de obter vinho.
Página de intenção de recomendação de obter vinho mostrando as frases de treinamento disponíveis. (Visualização grande)

Observando a imagem acima, podemos ver as frases de treinamento disponíveis listadas, e a figura da moeda é destacada na cor amarela para cada uma delas. Esse destaque é conhecido como uma anotação no Dialogflow e é feito automaticamente para extrair os tipos de dados reconhecidos conhecidos como entidade da frase de um usuário.

Depois que essa intenção for correspondida em uma conversa com o agente, uma solicitação HTTP será feita a um serviço externo para obter o vinho recomendado com base no preço extraído como parâmetro da frase de um usuário, por meio do uso do webhook habilitado encontrado em a seção Cumprimento na parte inferior desta página de intenção.

Podemos testar o agente usando o emulador do Dialogflow localizado na seção direita do console do Dialogflow. Para testar, iniciamos a conversa com uma mensagem de “ Oi ” e seguimos com a quantidade de vinho desejada. O webhook será imediatamente chamado e uma resposta rica semelhante à abaixo será mostrada pelo agente.

Testando o webhook do agente do agente importado.
Testando o webhook de atendimento do agente importado usando o emulador de agente no console. (Visualização grande)

Na imagem acima, podemos ver a URL do webhook gerada usando o Ngrok e a resposta do agente no lado direito mostrando um vinho dentro da faixa de preço de US$ 20 digitada pelo usuário.

Neste ponto, o agente do Dialogflow foi totalmente configurado. Agora podemos começar a integrar esse agente em um aplicativo da Web para permitir que outros usuários acessem e interajam com o agente sem acesso ao nosso console do Dialogflow.

Mais depois do salto! Continue lendo abaixo ↓

Integrando um agente do Dialogflow

Embora existam outros meios de se conectar a um agente do Dialogflow, como fazer solicitações HTTP para seus endpoints REST, a maneira recomendada de se conectar ao Dialogflow é por meio do uso de sua biblioteca cliente oficial disponível em várias linguagens de programação. Para JavaScript, o pacote @google-cloud/dialogflow está disponível para instalação no NPM.

Internamente, o pacote @google-cloud/dialogflow usa gRPC para suas conexões de rede e isso torna o pacote incompatível em um ambiente de navegador, exceto quando corrigido usando webpack, a maneira recomendada de usar este pacote é de um ambiente Node. Podemos fazer isso configurando um aplicativo de back-end Express.js para usar esse pacote e, em seguida, fornecer dados ao aplicativo da Web por meio de seus terminais de API e é isso que faremos a seguir.

Configurando um aplicativo Node Express

Para configurar um aplicativo expresso, criamos um novo diretório de projeto e, em seguida, pegamos as dependências necessárias usando o yarn de um terminal de linha de comando aberto.

 # 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

Com as dependências necessárias instaladas, podemos prosseguir com a configuração de um servidor Express.js muito enxuto que lida com conexões em uma porta especificada com suporte a CORS habilitado para o aplicativo da 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}`));

Quando executado, o código no snippet acima inicia um servidor HTTP que escuta conexões em um PORT Express.js especificado. Ele também tem o compartilhamento de recursos entre origens (CORS) habilitado em todas as solicitações usando o pacote cors como um middleware Express. Por enquanto, este servidor apenas escuta conexões, ele não pode responder a uma requisição porque não tem rota criada, então vamos criar isso.

Agora precisamos adicionar duas novas rotas: uma para enviar dados de texto e outra para enviar uma entrada de voz gravada. Ambos aceitarão uma solicitação POST e enviarão os dados contidos no corpo da solicitação para o agente do Dialogflow posteriormente.

 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

Acima, criamos uma instância de roteador separada para as duas rotas POST criadas que, por enquanto, respondem apenas com um código de status 200 e uma resposta fictícia codificada. Quando terminarmos de autenticar com o Dialogflow, podemos voltar para implementar uma conexão real com o Dialogflow nesses endpoints.

Para a última etapa da configuração do aplicativo de back-end, montamos a instância do roteador criada anteriormente no aplicativo Express usando app.use e um caminho base para a rota.

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

Acima, adicionamos um caminho base para as duas rotas, duas podemos testar qualquer uma delas por meio de uma solicitação POST usando cURL a partir de uma linha de comando, como é feito abaixo com um corpo de solicitação vazio;

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

Após a conclusão bem-sucedida da solicitação acima, podemos esperar uma resposta contendo dados do objeto sendo impressos no console.

Agora resta fazer uma conexão real com o Dialogflow, que inclui lidar com autenticação, enviar e receber dados do agente no Dialogflow usando o pacote @google-cloud/dialogflow.

Autenticação com o Dialogflow

Cada agente do Dialogflow criado está vinculado a um projeto no Google Cloud. Para se conectar externamente ao agente do Dialogflow, autenticamos com o projeto na nuvem do Google e usamos o Dialogflow como um dos recursos do projeto. Das seis maneiras disponíveis de se conectar a um projeto na nuvem do google, usar a opção Contas de serviço é a mais conveniente ao se conectar a um serviço específico na nuvem do google por meio de sua biblioteca de cliente.

Observação : para aplicativos prontos para produção, o uso de chaves de API de curta duração é recomendado em vez de chaves de conta de serviço para reduzir o risco de uma chave de conta de serviço cair em mãos erradas.

O que são contas de serviço?

As contas de serviço são um tipo especial de conta no Google Cloud, criadas para interação não humana, principalmente por meio de APIs externas. Em nosso aplicativo, a conta de serviço será acessada por meio de uma chave gerada pela biblioteca cliente do Dialogflow para autenticação no Google Cloud.

A documentação na nuvem sobre como criar e gerenciar contas de serviço fornece um excelente guia para criar uma conta de serviço. Ao criar a conta de serviço, a função de administrador da API do Dialogflow deve ser atribuída à conta de serviço criada, conforme mostrado na última etapa. Essa função dá à conta de serviço controle administrativo sobre o agente vinculado do Dialogflow.

Para usar a conta de serviço, precisamos criar uma chave de conta de serviço. As etapas a seguir descrevem como criar um no formato JSON:

  1. Clique na conta de serviço recém-criada para navegar até a página da conta de serviço.
  2. Role até a seção Chaves e clique no menu suspenso Adicionar chave e clique na opção Criar nova chave que abre um modal.
  3. Selecione um formato de arquivo JSON e clique no botão Criar no canto inferior direito do modal.

Observação: é recomendável manter uma chave de conta de serviço privada e não comprometê-la com nenhum sistema de controle de versão, pois ela contém dados altamente confidenciais sobre um projeto no Google Cloud. Isso pode ser feito adicionando o arquivo ao arquivo .gitignore .

Com uma conta de serviço criada e uma chave de conta de serviço disponível no diretório do nosso projeto, podemos usar a biblioteca de cliente do Dialogflow para enviar e receber dados do agente do 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;

A rota inteira acima envia dados para o agente do Dialogflow e recebe uma resposta por meio das etapas a seguir.

  • Primeiro
    Ele se autentica com a nuvem do Google e cria uma sessão com o Dialogflow usando o projectID do projeto de nuvem do Google vinculado ao agente do Dialogflow e também um ID aleatório para identificar a sessão criada. Em nosso aplicativo, estamos criando um identificador UUID em cada sessão criada usando o pacote JavaScript UUID. Isso é muito útil ao registrar ou rastrear todas as conversas tratadas por um agente do Dialogflow.
  • Segundo
    Criamos um objeto de solicitação de dados seguindo o formato especificado na documentação do Dialogflow. Esse objeto de solicitação contém a sessão criada e os dados da mensagem obtidos do corpo da solicitação que devem ser passados ​​para o agente do Dialogflow.
  • Terceiro
    Usando o método detectIntent da sessão do Dialogflow, enviamos o objeto de solicitação de forma assíncrona e aguardamos a resposta do Agente usando a sintaxe ES6 async/await em um bloco try-catch caso o método detectIntent retorne uma exceção, podemos capturar o erro e devolvê-lo, em vez do que travar o aplicativo inteiro. Uma amostra do objeto de resposta retornado do Agente é fornecida na documentação do Dialogflow e pode ser inspecionada para saber como extrair os dados do objeto.

Podemos usar o Postman para testar a conexão do Dialogflow implementada acima na rota dialogflow-response . Postman é uma plataforma de colaboração para desenvolvimento de APIs com recursos para testar APIs construídas em fases de desenvolvimento ou produção.

Observação: se ainda não estiver instalado, o aplicativo de desktop Postman não será necessário para testar uma API. A partir de setembro de 2020, o cliente da Web do Postman passou para um estado de disponibilidade geral (GA) e pode ser usado diretamente de um navegador.

Usando o Postman Web Client, podemos criar um novo espaço de trabalho ou usar um existente para criar uma solicitação POST para nosso endpoint de API em https://localhost:5000/api/agent/text-input e adicionar dados com uma chave de message e valor de “ Hi There ” nos parâmetros de consulta.

Ao clicar no botão Enviar , uma solicitação POST será feita para o servidor Express em execução — com uma resposta semelhante à mostrada na imagem abaixo:

Testando o endpoint da API de entrada de texto usando o Postman.
Testando o endpoint da API de entrada de texto usando o Postman. (Visualização grande)

Na imagem acima, podemos ver os dados de resposta embelezados do agente do Dialogflow por meio do servidor Express. Os dados retornados são formatados de acordo com a definição de resposta de amostra fornecida na documentação do Dialogflow Webhook.

Manipulando Entradas de Voz

Por padrão, todos os agentes do Dialogflow estão habilitados para processar dados de texto e áudio e também retornar uma resposta em formato de texto ou áudio. No entanto, trabalhar com dados de entrada ou saída de áudio pode ser um pouco mais complexo do que com dados de texto.

Para lidar e processar as entradas de voz, iniciaremos a implementação do endpoint /voice-input que criamos anteriormente para receber os arquivos de áudio e enviá-los ao Dialogflow em troca de uma resposta do 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}`); } });

Em uma visão geral alta, a rota /voice-input acima recebe a entrada de voz de um usuário como um arquivo contendo a mensagem que está sendo falada para o assistente de bate-papo e a envia para o agente do Dialogflow. Para entender melhor esse processo, podemos dividi-lo nas seguintes etapas menores:

  • Primeiro, adicionamos e usamos connect-busboy como um middleware Express para analisar os dados do formulário que estão sendo enviados na solicitação do aplicativo da web. Após isso, autenticamos com o Dialogflow usando a chave de serviço e criamos uma sessão, da mesma forma que fizemos na rota anterior.
    Em seguida, usando o método promisify do módulo util Node.js integrado, obtemos e salvamos uma promessa equivalente ao método de pipeline Stream para ser usado posteriormente para canalizar vários fluxos e também realizar uma limpeza após a conclusão dos fluxos.
  • Em seguida, criamos um objeto de solicitação contendo a sessão de autenticação do Dialogflow e uma configuração para o arquivo de áudio que será enviado ao Dialogflow. O objeto de configuração de áudio aninhado permite que o agente do Dialogflow execute uma conversão de fala em texto no arquivo de áudio enviado.
  • Em seguida, usando a sessão criada e o objeto de solicitação, detectamos a intenção de um usuário do arquivo de áudio usando o método detectStreamingIntent , que abre um novo fluxo de dados do agente do Dialogflow para o aplicativo de back-end. Os dados serão enviados de volta em pequenos bits por meio desse fluxo e, usando o “ evento ” de dados do fluxo legível, armazenamos os dados na variável streamData para uso posterior. Após o fechamento do fluxo, o evento “ end ” é acionado e enviamos a resposta do agente Dialogflow armazenado na variável streamData para o aplicativo Web.
  • Por fim, usando o evento file stream do connect-busboy, recebemos o stream do arquivo de áudio enviado no corpo da solicitação e o passamos para o equivalente de promessa do Pipeline que criamos anteriormente. A função disso é canalizar o fluxo do arquivo de áudio vindo da solicitação para o fluxo do Dialogflow, canalizamos o fluxo do arquivo de áudio para o fluxo aberto pelo método detectStreamingIntent acima.

Para testar e confirmar se as etapas acima estão funcionando conforme o estabelecido, podemos fazer uma solicitação de teste contendo um arquivo de áudio no corpo da solicitação para o endpoint /voice-input usando o Postman.

Testando o endpoint da API de entrada de voz usando o Postman.
Testando o endpoint da API de entrada de voz usando o carteiro com um arquivo de voz gravado. (Visualização grande)

O resultado do Postman acima mostra a resposta obtida após fazer uma solicitação POST com os dados do formulário de uma mensagem de nota de voz gravada dizendo “ Oi ” incluída no corpo da solicitação.

Neste ponto, agora temos um aplicativo Express.js funcional que envia e recebe dados do Dialogflow, as duas partes deste artigo estão concluídas. Onde resta agora integrar este Agente em uma aplicação web consumindo as APIs criadas aqui a partir de uma aplicação Reactjs.

Integrando em um aplicativo da Web

Para consumir nossa API REST construída, expandiremos este aplicativo React.js existente que já possui uma página inicial mostrando uma lista de vinhos obtidos de uma API e suporte para decoradores usando o plug-in de decoradores de proposta babel. Vamos refatorá-lo um pouco introduzindo o Mobx para gerenciamento de estado e também um novo recurso para recomendar um vinho de um componente de bate-papo usando os endpoints da API REST adicionados do aplicativo Express.js.

Para começar, começamos a gerenciar o estado do aplicativo usando MobX à medida que criamos um repositório Mobx com alguns valores observáveis ​​e alguns métodos como ações.

 // 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();

Acima criamos uma loja para o recurso de componente de chat dentro do aplicativo com os seguintes valores:

  • isChatWindowOpen
    O valor armazenado aqui controla a visibilidade do componente de chat onde são mostradas as mensagens do Dialogflow.
  • isLoadingChatMessages
    Isso é usado para mostrar um indicador de carregamento quando é feita uma solicitação para buscar uma resposta do agente do Dialogflow.
  • agentMessages
    Essa matriz armazena todas as respostas provenientes das solicitações feitas para obter uma resposta do agente do Dialogflow. Os dados na matriz são exibidos posteriormente no componente.
  • handleConversation
    Este método decorado como uma ação adiciona dados ao array agentMessages . Primeiro, ele adiciona a mensagem do usuário passada como um argumento e, em seguida, faz uma solicitação usando o Axios ao aplicativo de back-end para obter uma resposta do Dialogflow. Depois que a solicitação é resolvida, ele adiciona a resposta da solicitação à matriz agentMessages .

Nota: Na ausência do suporte de decoradores em um aplicativo, o MobX fornece makeObservable que pode ser usado no construtor da classe de armazenamento de destino. Veja um exemplo aqui .

Com a configuração da loja, precisamos envolver toda a árvore do aplicativo com o componente de ordem superior do MobX Provider, começando pelo componente raiz no arquivo 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;

Acima, envolvemos o componente App raiz com o Provedor MobX e passamos a loja criada anteriormente como um dos valores do Provedor. Agora podemos prosseguir com a leitura da loja dentro dos componentes conectados à loja.

Criando uma interface de bate-papo

Para exibir as mensagens enviadas ou recebidas das solicitações da API, precisamos de um novo componente com alguma interface de bate-papo mostrando as mensagens listadas. Para fazer isso, criamos um novo componente para exibir algumas mensagens codificadas primeiro e depois exibimos as mensagens em uma 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

O componente acima tem a marcação HTML básica necessária para um aplicativo de bate-papo. Ele tem um cabeçalho mostrando o nome do Agente e um ícone para fechar a janela de bate-papo, um balão de mensagem contendo um texto codificado em uma tag de lista e, por último, um campo de entrada com um manipulador de eventos onChange anexado ao campo de entrada para armazenar o texto digitado o estado local do componente usando o useState do React.

Uma visualização do componente de bate-papo com uma mensagem codificada do agente de bate-papo
Uma visualização do componente de bate-papo com uma mensagem codificada do agente de bate-papo. (Visualização grande)

Na imagem acima, o componente de bate-papo funciona como deveria, mostrando uma janela de bate-papo estilizada com uma única mensagem de bate-papo e o campo de entrada na parte inferior. No entanto, queremos que a mensagem seja mostrada como respostas reais obtidas da solicitação da API e não texto codificado.

Avançamos para refatorar o componente Chat, desta vez conectando e fazendo uso de valores na loja MobX dentro do 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));

Das partes destacadas do código acima, podemos ver que todo o componente de chat foi modificado para realizar as seguintes novas operações;

  • Ele tem acesso aos valores da loja MobX após injetar o valor ApplicationStore . O componente também se tornou um observador desses valores de armazenamento para que seja renderizado novamente quando um dos valores for alterado.
  • Iniciamos a conversa com o Agente imediatamente após a abertura do componente de chat, invocando o método handleConversation em um gancho useEffect para fazer uma solicitação imediatamente após a renderização do componente.
  • Agora estamos usando o valor isLoadingMessages no cabeçalho do componente Chat. Quando uma solicitação para obter uma resposta do Agente está em andamento, definimos o valor isLoadingMessages como true e atualizamos o cabeçalho para Zara está digitando…
  • A matriz agentMessages dentro da loja é atualizada para um proxy pelo MobX depois que seus valores são definidos. A partir desse componente, convertemos esse proxy de volta em um array usando o utilitário toJS do MobX e armazenamos os valores em uma variável dentro do componente. Essa matriz é iterada ainda mais para preencher as bolhas de bate-papo com os valores dentro da matriz usando uma função de mapa.

Agora, usando o componente de chat, podemos digitar uma frase e esperar que uma resposta seja exibida no agente.

Componente de bate-papo mostrando uma lista de dados retornada da solicitação HTTP para o aplicativo expresso.
Componente de bate-papo mostrando uma lista de dados retornada da solicitação HTTP para o aplicativo expresso. (Visualização grande)

Gravando a entrada de voz do usuário

Por padrão, todos os agentes do Dialogflow podem aceitar entrada baseada em voz ou texto em qualquer idioma especificado de um usuário. No entanto, são necessários alguns ajustes de um aplicativo da Web para obter acesso ao microfone de um usuário e gravar uma entrada de voz.

Para conseguir isso, modificamos a loja MobX para usar a API HTML MediaStream Recording para gravar a voz de um usuário em dois novos métodos na loja 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}`)); }; };

Ao clicar no ícone de registro do componente de bate-papo, o método startAudioConversation na loja MobX acima é invocado para definir o método que a propriedade observável isRecording é true , para que o componente de bate-papo forneça feedback visual para mostrar que uma gravação está em andamento.

Usando a interface do navegador do navegador, o objeto Media Device é acessado para solicitar o microfone do dispositivo do usuário. Depois que a permissão é concedida à solicitação getUserMedia , ela resolve sua promessa com dados MediaStream que passamos ao construtor MediaRecorder para criar um gravador usando as faixas de mídia no fluxo retornado do microfone do dispositivo do usuário. Em seguida, armazenamos a instância do gravador de mídia na propriedade do recorder da loja, pois iremos acessá-la de outro método posteriormente.

Em seguida, chamamos o método start na instância do gravador e, depois que a sessão de gravação é encerrada, a função ondataavailable é acionada com um argumento de evento contendo o fluxo gravado em um Blob que armazenamos na propriedade do array recordedBits .

Desconectando os dados no argumento event passado para o evento ondataavailable acionado, podemos ver o Blob e suas propriedades no console do navegador.

Console do Browser Devtools mostrando Blob desconectado criado pelo Media Recorder após o término de uma gravação.Pull Quotes
Console do Browser Devtools mostrando Blob desconectado criado pelo Media Recorder após o término de uma gravação. (Visualização grande)

Agora que podemos iniciar um fluxo do MediaRecorder, precisamos interromper o fluxo do MediaRecorder quando um usuário terminar de gravar sua entrada de voz e enviar o arquivo de áudio gerado para o aplicativo Express.js.

O novo método adicionado à loja abaixo interrompe o fluxo e faz uma solicitação POST contendo a entrada de voz gravada.

 //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!

Conclusão

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.

Referências

  • 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