Integrazione di un agente Dialogflow in un'applicazione React

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Quando si tratta di creare un assistente di chat conversazionale che potrebbe essere utilizzato a livello di piccola o impresa, Dialogflow sarebbe molto probabilmente una delle prime opzioni che verrebbero visualizzate nell'elenco di ricerca, e perché no? Offre diverse funzionalità come la possibilità di elaborare input audio e di testo, fornire risposte dinamiche utilizzando webhook personalizzati, connettersi a milioni di dispositivi abilitati a Google utilizzando l'Assistente Google e molto altro ancora. Ma a parte la sua console che viene fornita per progettare e gestire un agente, come possiamo creare un assistente di chat che può essere utilizzato anche all'interno delle nostre applicazioni web costruite?

Dialogflow è una piattaforma che semplifica il processo di creazione e progettazione di un assistente di chat conversazionale per l'elaborazione del linguaggio naturale in grado di elaborare l'input vocale o di testo quando viene utilizzato dalla console Dialogflow o da un'applicazione Web integrata.

Sebbene in questo articolo venga spiegato brevemente l'agente integrato di Dialogflow, è prevedibile che tu abbia una conoscenza di Node.js e Dialogflow. Se stai imparando Dialogflow per la prima volta, questo articolo fornisce una chiara spiegazione di cos'è Dialogflow e dei suoi concetti.

Questo articolo è una guida su come creare un agente Dialogflow con supporto vocale e chat che può essere integrato in un'applicazione Web con l'aiuto di un'applicazione back-end Express.js come collegamento tra un'applicazione Web React.js e l'Agente su Dialogflow stesso. Entro la fine dell'articolo, dovresti essere in grado di connettere il tuo agente Dialogflow alla tua applicazione web preferita.

Per rendere questa guida facile da seguire, puoi saltare alla parte del tutorial che ti interessa di più o seguirli nel seguente ordine man mano che appaiono:

  • Configurazione di un agente Dialogflow
  • Integrazione di un agente Dialogflow
  • Configurazione di un'applicazione Node Express
    • Autenticazione con Dialogflow
    • Cosa sono gli account di servizio?
    • Gestione degli ingressi vocali
  • Integrazione in un'applicazione Web
    • Creazione di un'interfaccia di chat
    • Registrazione dell'input vocale dell'utente
  • Conclusione
  • Riferimenti

1. Configurazione di un agente Dialogflow

Come spiegato in questo articolo, un assistente di chat su Dialogflow è chiamato Agent e comprende componenti più piccoli come intenti, realizzazione, knowledge base e molto altro. Dialogflow fornisce agli utenti una console per creare, addestrare e progettare il flusso di conversazione di un agente. Nel nostro caso d'uso, ripristineremo un agente che è stato esportato in una cartella ZIP dopo essere stato addestrato, utilizzando la funzione di esportazione e importazione dell'agente.

Prima di eseguire l'importazione, è necessario creare un nuovo agente che verrà unito all'agente che sta per essere ripristinato. Per creare un nuovo agente dalla console, è necessario un nome univoco e anche un progetto su Google Cloud con cui collegare l'agente. Se non esiste alcun progetto esistente su Google Cloud a cui collegarsi, è possibile crearne uno nuovo qui.

Un agente è stato precedentemente creato e formato per consigliare i prodotti del vino a un utente in base al suo budget. Questo agente è stato esportato in uno ZIP; puoi scaricare la cartella qui e ripristinarla nel nostro agente appena creato dalla scheda Esporta e Importa che si trova nella pagina Impostazioni agente.

Ripristino di un agente precedentemente esportato da una cartella ZIP
Ripristino di un agente precedentemente esportato da una cartella ZIP. (Grande anteprima)

L'agente importato è stato precedentemente addestrato a consigliare un prodotto di vino all'utente in base al budget dell'utente per l'acquisto di una bottiglia di vino.

Passando attraverso l'agente importato, vedremo che ha tre intenti creati dalla pagina degli intenti. Uno è un intento di fallback, utilizzato quando l'agente non riconosce l'input di un utente, l'altro è un intento di benvenuto utilizzato quando viene avviata una conversazione con l'agente e l'ultimo intento viene utilizzato per consigliare un vino all'utente in base al parametro importo all'interno della frase. Ci interessa l'intento get-wine-recommendation

Questo intento ha un unico contesto di input di wine-recommendation proveniente dall'intento di benvenuto predefinito per collegare la conversazione a questo intento.

"Un Contesto è un sistema all'interno di un Agent utilizzato per controllare il flusso di una conversazione da un intento all'altro."

Sotto i contesti ci sono le frasi di addestramento, che sono frasi usate per addestrare un agente sul tipo di affermazioni aspettarsi da un utente. Attraverso un'ampia varietà di frasi di addestramento all'interno di un intento, un agente è in grado di riconoscere la frase di un utente e l'intento in cui cade.

Le frasi di formazione all'interno dei nostri agenti get-wine-recommendation intent (come mostrato di seguito) indicano la scelta del vino e la categoria di prezzo:

Elenco delle frasi di formazione disponibili con l'intento di ottenere-wine-recommendation.
Pagina di intenti per la raccomandazione del vino che mostra le frasi di formazione disponibili. (Grande anteprima)

Osservando l'immagine sopra, possiamo vedere le frasi di allenamento disponibili elencate e la cifra della valuta è evidenziata in giallo per ciascuna di esse. Questa evidenziazione è nota come annotazione su Dialogflow e viene eseguita automaticamente per estrarre i tipi di dati riconosciuti noti come entità dalla frase di un utente.

Dopo che questo intento è stato abbinato in una conversazione con l'agente, verrà inviata una richiesta HTTP a un servizio esterno per ottenere il vino consigliato in base al prezzo estratto come parametro da una frase dell'utente, tramite l'uso del webhook abilitato che si trova all'interno la sezione Adempimento in fondo a questa pagina di intenti.

Possiamo testare l'agente utilizzando l'emulatore Dialogflow situato nella sezione destra della console Dialogflow. Per testare, iniziamo la conversazione con un messaggio " Ciao " e seguiamo con la quantità di vino desiderata. Il webhook verrà immediatamente chiamato e l'agente mostrerà una risposta ricca simile a quella di seguito.

Verifica del webhook agente agente importato.
Testare il webhook di evasione ordini dell'agente importato utilizzando l'emulatore agente nella console. (Grande anteprima)

Dall'immagine sopra possiamo vedere l'URL del webhook generato utilizzando Ngrok e la risposta dell'agente sul lato destro che mostra un vino all'interno della fascia di prezzo di $ 20 digitata dall'utente.

A questo punto, l'agente Dialogflow è stato completamente configurato. Ora possiamo iniziare a integrare questo agente in un'applicazione Web per consentire ad altri utenti di accedere e interagire con l'agente senza accedere alla nostra console Dialogflow.

Altro dopo il salto! Continua a leggere sotto ↓

Integrazione di un agente Dialogflow

Sebbene esistano altri mezzi per connettersi a un agente Dialogflow, come effettuare richieste HTTP ai suoi endpoint REST, il modo consigliato per connettersi a Dialogflow è attraverso l'uso della sua libreria client ufficiale disponibile in diversi linguaggi di programmazione. Per JavaScript, il pacchetto @google-cloud/dialogflow è disponibile per l'installazione da NPM.

Internamente il pacchetto @google-cloud/dialogflow utilizza gRPC per le sue connessioni di rete e questo rende il pacchetto non supportato all'interno di un ambiente browser tranne quando è stato applicato patch utilizzando webpack, il modo consigliato per utilizzare questo pacchetto è da un ambiente Node. Possiamo farlo configurando un'applicazione back-end Express.js per utilizzare questo pacchetto, quindi servire i dati all'applicazione Web tramite i suoi endpoint API e questo è ciò che faremo dopo.

Configurazione di un'applicazione Node Express

Per configurare un'applicazione rapida creiamo una nuova directory di progetto, quindi prendiamo le dipendenze necessarie usando il yarn da un terminale a riga di comando aperto.

 # 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 le dipendenze necessarie installate, possiamo procedere alla configurazione di un server Express.js molto snello che gestisce le connessioni su una porta specifica con il supporto CORS abilitato per l'app 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 viene eseguito, il codice nello snippet sopra avvia un server HTTP che ascolta le connessioni su un PORT Express.js specificato. Dispone inoltre di CORS (Cross-Origin Resource Sharing) abilitata su tutte le richieste che utilizzano il pacchetto cors come middleware Express. Per ora, questo server ascolta solo le connessioni, non può rispondere a una richiesta perché non ha un percorso creato, quindi creiamolo.

Ora dobbiamo aggiungere due nuovi percorsi: uno per l'invio di dati di testo mentre l'altro per l'invio di un input vocale registrato. Entrambi accetteranno una richiesta POST e invieranno successivamente i dati contenuti nel corpo della richiesta all'agente 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

Sopra abbiamo creato un'istanza router separata per i due percorsi POST creati che per ora rispondono solo con un codice di stato 200 e una risposta fittizia hardcoded. Al termine dell'autenticazione con Dialogflow, possiamo tornare per implementare una connessione effettiva a Dialogflow all'interno di questi endpoint.

Per l'ultimo passaggio della configurazione dell'applicazione back-end, montiamo l'istanza del router creata in precedenza e creata nell'applicazione Express utilizzando app.use e un percorso di base per il percorso.

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

Sopra, abbiamo aggiunto un percorso di base ai due percorsi, due possiamo testarne uno tramite una richiesta POST usando cURL da una riga di comando come viene fatto di seguito con un corpo della richiesta vuoto;

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

Dopo il completamento con successo della richiesta di cui sopra, possiamo aspettarci di vedere una risposta contenente i dati dell'oggetto stampati sulla console.

Ora non resta che stabilire una connessione effettiva con Dialogflow che include la gestione dell'autenticazione, l'invio e la ricezione di dati dall'agente su Dialogflow utilizzando il pacchetto @google-cloud/dialogflow.

Autenticazione con Dialogflow

Ogni agente Dialogflow creato è collegato a un progetto su Google Cloud. Per connetterci esternamente all'agente Dialogflow, ci autentichiamo con il progetto sul cloud di Google e utilizziamo Dialogflow come una delle risorse del progetto. Dei sei modi disponibili per connettersi a un progetto su google-cloud, l'utilizzo dell'opzione Account di servizio è il più conveniente quando ci si connette a un particolare servizio su google cloud tramite la sua libreria client.

Nota : per le applicazioni pronte per la produzione, si consiglia l'uso di chiavi API di breve durata rispetto alle chiavi dell'account di servizio per ridurre il rischio che una chiave dell'account di servizio finisca nelle mani sbagliate.

Cosa sono gli account di servizio?

Gli account di servizio sono un tipo speciale di account su Google Cloud, creato per l'interazione non umana, principalmente tramite API esterne. Nella nostra applicazione, sarà possibile accedere all'account di servizio tramite una chiave generata dalla libreria client Dialogflow per l'autenticazione con Google Cloud.

La documentazione cloud sulla creazione e la gestione degli account di servizio fornisce un'eccellente guida per creare un account di servizio. Quando si crea l'account di servizio, il ruolo di amministratore dell'API Dialogflow deve essere assegnato all'account di servizio creato, come mostrato nell'ultimo passaggio. Questo ruolo fornisce all'account di servizio il controllo amministrativo sull'agente Dialogflow collegato.

Per utilizzare l'account di servizio, è necessario creare una chiave dell'account di servizio. I seguenti passaggi illustrano come crearne uno in formato JSON:

  1. Fare clic sull'account di servizio appena creato per accedere alla pagina dell'account di servizio.
  2. Scorri fino alla sezione Chiavi e fai clic sul menu a discesa Aggiungi chiave e fai clic sull'opzione Crea nuova chiave che apre un modale.
  3. Seleziona un formato di file JSON e fai clic sul pulsante Crea in basso a destra del modale.

Nota: si consiglia di mantenere privata una chiave dell'account di servizio e di non impegnarla in alcun sistema di controllo della versione poiché contiene dati altamente sensibili su un progetto su Google Cloud. Questo può essere fatto aggiungendo il file al file .gitignore .

Con un account di servizio creato e una chiave di account di servizio disponibile all'interno della directory del nostro progetto, possiamo utilizzare la libreria client Dialogflow per inviare e ricevere dati dall'agente 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;

L'intero percorso sopra invia i dati all'agente Dialogflow e riceve una risposta attraverso i passaggi seguenti.

  • Primo
    Si autentica con il cloud di Google, quindi crea una sessione con Dialogflow utilizzando il projectID del progetto cloud di Google collegato all'agente Dialogflow e anche un ID casuale per identificare la sessione creata. Nella nostra applicazione, stiamo creando un identificatore UUID su ogni sessione creata utilizzando il pacchetto UUID JavaScript. Ciò è molto utile durante la registrazione o la traccia di tutte le conversazioni gestite da un agente Dialogflow.
  • Secondo
    Creiamo una richiesta di dati dell'oggetto seguendo il formato specificato nella documentazione di Dialogflow. Questo oggetto di richiesta contiene la sessione creata ei dati del messaggio ottenuti dal corpo della richiesta che devono essere passati all'agente Dialogflow.
  • Terzo
    Usando il metodo detectIntent dalla sessione Dialogflow, inviamo l'oggetto richiesta in modo asincrono e attendiamo la risposta dell'agente usando ES6 async / await sintassi in un blocco try-catch se il metodo detectIntent restituisce un'eccezione, possiamo catturare l'errore e restituirlo, piuttosto rispetto all'arresto anomalo dell'intera applicazione. Un campione dell'oggetto di risposta restituito dall'agente è fornito nella documentazione di Dialogflow e può essere ispezionato per sapere come estrarre i dati dall'oggetto.

Possiamo utilizzare Postman per testare la connessione Dialogflow implementata sopra nel percorso dialogflow-response . Postman è una piattaforma di collaborazione per lo sviluppo di API con funzionalità per testare le API integrate nelle fasi di sviluppo o produzione.

Nota: se non è già installata, l'applicazione desktop Postman non è necessaria per testare un'API. A partire da settembre 2020, il client Web di Postman è passato allo stato General Available (GA) e può essere utilizzato direttamente da un browser.

Utilizzando il client Web Postman, possiamo creare un nuovo spazio di lavoro o utilizzarne uno esistente per creare una richiesta POST al nostro endpoint API su https://localhost:5000/api/agent/text-input e aggiungere dati con una chiave di message e valore di " Hi There " nei parametri della query.

Al clic del pulsante Invia , verrà inviata una richiesta POST al server Express in esecuzione, con una risposta simile a quella mostrata nell'immagine seguente:

Test dell'endpoint API di input di testo utilizzando Postman.
Test dell'endpoint API di input di testo utilizzando Postman. (Grande anteprima)

Nell'immagine sopra, possiamo vedere i dati di risposta abbelliti dall'agente Dialogflow attraverso il server Express. I dati restituiti vengono formattati in base alla definizione di risposta di esempio fornita nella documentazione di Dialogflow Webhook.

Gestione degli ingressi vocali

Per impostazione predefinita, tutti gli agenti Dialogflow sono abilitati all'elaborazione di dati sia di testo che audio e restituiscono anche una risposta in formato testo o audio. Tuttavia, lavorare con i dati di input o output audio può essere un po' più complesso dei dati di testo.

Per gestire ed elaborare gli input vocali, inizieremo l'implementazione per l'endpoint /voice-input che abbiamo precedentemente creato per ricevere file audio e inviarli a Dialogflow in cambio di una risposta dall'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}`); } });

In una panoramica elevata, il percorso /voice-input sopra riceve l'input vocale di un utente come file contenente il messaggio pronunciato all'assistente chat e lo invia all'agente Dialogflow. Per comprendere meglio questo processo, possiamo scomporlo nei seguenti passaggi più piccoli:

  • Innanzitutto, aggiungiamo e utilizziamo connect-busboy come middleware Express per analizzare i dati dei moduli inviati nella richiesta dall'applicazione Web. Dopodiché ci autentichiamo con Dialogflow usando la Service Key e creiamo una sessione, come abbiamo fatto nel percorso precedente.
    Quindi, utilizzando il metodo promisify dal modulo util Node.js integrato, otteniamo e salviamo una promessa equivalente del metodo Stream pipeline da utilizzare in seguito per reindirizzare più flussi ed eseguire anche una pulizia dopo che i flussi sono stati completati.
  • Successivamente, creiamo un oggetto di richiesta contenente la sessione di autenticazione di Dialogflow e una configurazione per il file audio che sta per essere inviato a Dialogflow. L'oggetto di configurazione dell'audio nidificato consente all'agente Dialogflow di eseguire una conversione da riconoscimento vocale a testo sul file audio inviato.
  • Successivamente, utilizzando la sessione creata e l'oggetto request, rileviamo l'intento di un utente dal file audio utilizzando il metodo detectStreamingIntent che apre un nuovo flusso di dati dall'agente Dialogflow all'applicazione back-end. I dati verranno restituiti in piccoli bit attraverso questo flusso e utilizzando i dati " evento " dal flusso leggibile memorizziamo i dati nella variabile streamData per un uso successivo. Dopo che lo stream è stato chiuso, viene generato l'evento " end " e inviamo la risposta dall'agente Dialogflow memorizzato nella variabile streamData all'applicazione Web.
  • Infine, utilizzando l'evento di flusso di file di connect-busboy, riceviamo il flusso del file audio inviato nel corpo della richiesta e lo passiamo ulteriormente nell'equivalente della promessa di Pipeline che abbiamo creato in precedenza. La funzione di questo è di reindirizzare il flusso di file audio proveniente dalla richiesta al flusso di Dialogflow, convogliamo il flusso di file audio al flusso aperto dal metodo detectStreamingIntent sopra.

Per verificare e confermare che i passaggi precedenti funzionino come indicato, possiamo effettuare una richiesta di test contenente un file audio nel corpo della richiesta all'endpoint /voice-input utilizzando Postman.

Testare l'endpoint dell'API di input vocale utilizzando Postman.
Test dell'endpoint API di input vocale utilizzando postino con un file vocale registrato. (Grande anteprima)

Il risultato Postman sopra mostra la risposta ottenuta dopo aver effettuato una richiesta POST con i dati del modulo di un messaggio di nota vocale registrato che dice " Ciao " incluso nel corpo della richiesta.

A questo punto, ora abbiamo un'applicazione Express.js funzionale che invia e riceve dati da Dialogflow, le due parti di questo articolo sono terminate. Dove ora è rimasto con l'integrazione di questo agente in un'applicazione Web consumando le API create qui da un'applicazione Reactjs.

Integrazione in un'applicazione Web

Per utilizzare la nostra API REST costruita, espanderemo questa applicazione React.js esistente che ha già una home page che mostra un elenco di vini recuperati da un'API e supporto per decoratori che utilizzano il plug-in decoratori di proposte babel. Lo ridimensioneremo un po' introducendo Mobx per la gestione dello stato e anche una nuova funzionalità per consigliare un vino da un componente di chat utilizzando gli endpoint API REST aggiunti dall'applicazione Express.js.

Per iniziare, iniziamo a gestire lo stato dell'applicazione utilizzando MobX mentre creiamo un negozio Mobx con alcuni valori osservabili e alcuni metodi come azioni.

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

Sopra abbiamo creato un negozio per la funzione del componente chat all'interno dell'applicazione con i seguenti valori:

  • isChatWindowOpen
    Il valore qui memorizzato controlla la visibilità del componente chat in cui vengono visualizzati i messaggi di Dialogflow.
  • isLoadingChatMessages
    Viene utilizzato per mostrare un indicatore di caricamento quando viene effettuata una richiesta per recuperare una risposta dall'agente Dialogflow.
  • agentMessages
    Questo array memorizza tutte le risposte provenienti dalle richieste effettuate per ottenere una risposta dall'agente Dialogflow. I dati nell'array vengono successivamente visualizzati nel componente.
  • handleConversation
    Questo metodo decorato come un'azione aggiunge dati all'array agentMessages . Innanzitutto, aggiunge il messaggio dell'utente passato come argomento, quindi effettua una richiesta utilizzando Axios all'applicazione back-end per ottenere una risposta da Dialogflow. Dopo che la richiesta è stata risolta, aggiunge la risposta dalla richiesta all'array agentMessages .

Nota: in assenza del supporto dei decoratori in un'applicazione, MobX fornisce makeObservable che può essere utilizzato nel costruttore della classe store di destinazione. Vedi un esempio qui .

Con la configurazione del negozio, è necessario eseguire il wrapping dell'intero albero dell'applicazione con il componente di ordine superiore del provider MobX a partire dal componente radice nel file 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;

Sopra eseguiamo il wrapping del componente root dell'app con il provider MobX e passiamo allo store creato in precedenza come uno dei valori del provider. Ora possiamo procedere alla lettura dal negozio all'interno dei componenti collegati al negozio.

Creazione di un'interfaccia di chat

Per visualizzare i messaggi inviati o ricevuti dalle richieste API, abbiamo bisogno di un nuovo componente con qualche interfaccia di chat che mostri i messaggi elencati. Per fare ciò, creiamo un nuovo componente per visualizzare prima alcuni messaggi codificati e poi visualizzare i messaggi in un elenco ordinato.

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

Il componente sopra ha il markup HTML di base necessario per un'applicazione di chat. Ha un'intestazione che mostra il nome dell'agente e un'icona per chiudere la finestra della chat, un fumetto contenente un testo codificato in un tag elenco e infine un campo di input con un gestore di eventi onChange allegato al campo di input per memorizzare il testo digitato lo stato locale del componente utilizzando useState di React.

Un'anteprima del componente chat con un messaggio codificato dall'agente chat
Un'anteprima del componente chat con un messaggio codificato dall'agente chat. (Grande anteprima)

Dall'immagine sopra, il componente chat funziona come dovrebbe, mostrando una finestra di chat in stile con un singolo messaggio di chat e il campo di input in basso. Tuttavia, desideriamo che il messaggio visualizzato sia le risposte effettive ottenute dalla richiesta API e non il testo codificato.

Procediamo con il refactoring del componente Chat, questa volta collegando e utilizzando i valori nel negozio MobX all'interno 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));

Dalle parti evidenziate del codice sopra, possiamo vedere che l'intero componente della chat è stato ora modificato per eseguire le seguenti nuove operazioni;

  • Ha accesso ai valori dell'archivio ApplicationStore dopo aver inserito il valore dell'ApplicationStore. Il componente è stato anche reso un osservatore di questi valori di archivio, quindi esegue nuovamente il rendering quando uno dei valori cambia.
  • Iniziamo la conversazione con l'Agente subito dopo l'apertura del componente chat invocando il metodo handleConversation all'interno di un hook useEffect per effettuare una richiesta immediatamente il componente viene renderizzato.
  • Stiamo ora utilizzando il valore isLoadingMessages all'interno dell'intestazione del componente Chat. Quando una richiesta per ottenere una risposta dall'agente è in volo, impostiamo il valore isLoadingMessages su true e aggiorniamo l'intestazione su Zara sta digitando...
  • L'array agentMessages all'interno del negozio viene aggiornato a un proxy da MobX dopo che i suoi valori sono stati impostati. Da questo componente, riconvertiamo quel proxy in un array utilizzando l'utilità toJS di MobX e memorizziamo i valori in una variabile all'interno del componente. Tale array viene ulteriormente ripetuto per popolare le bolle di chat con i valori all'interno dell'array utilizzando una funzione di mappa.

Ora utilizzando il componente chat possiamo digitare una frase e attendere che venga visualizzata una risposta nell'agente.

Componente di chat che mostra un elenco di dati restituiti dalla richiesta HTTP all'applicazione Express.
Componente di chat che mostra un elenco di dati restituiti dalla richiesta HTTP all'applicazione Express. (Grande anteprima)

Registrazione dell'input vocale dell'utente

Per impostazione predefinita, tutti gli agenti Dialogflow possono accettare input vocali o testuali in qualsiasi lingua specificata da un utente. Tuttavia, sono necessarie alcune modifiche da un'applicazione Web per accedere al microfono di un utente e registrare un input vocale.

Per ottenere ciò, modifichiamo il negozio MobX in modo che utilizzi l'API di registrazione HTML MediaStream per registrare la voce di un utente con due nuovi metodi nel negozio 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 clic dell'icona del record dal componente chat, viene richiamato il metodo startAudioConversation nell'archivio MobX sopra per impostare il metodo che la proprietà osservabile isRecording è su true , affinché il componente chat fornisca un feedback visivo per mostrare che una registrazione è in corso.

Tramite l'interfaccia del navigatore del browser, si accede all'oggetto Dispositivo multimediale per richiedere il microfono del dispositivo dell'utente. Dopo che l'autorizzazione è stata concessa alla richiesta getUserMedia , risolve la sua promessa con un dato MediaStream che passiamo ulteriormente al costruttore MediaRecorder per creare un registratore utilizzando le tracce multimediali nel flusso restituito dal microfono del dispositivo dell'utente. Quindi memorizziamo l'istanza del registratore multimediale nella proprietà del recorder del negozio poiché in seguito vi accederemo da un altro metodo.

Successivamente, chiamiamo il metodo start sull'istanza del registratore e, al termine della sessione di registrazione, la funzione ondataavailable viene attivata con un argomento evento contenente il flusso recordedBits in un BLOB che memorizziamo nella proprietà dell'array registerBits.

Disconnettendo i dati nell'argomento evento passato all'evento ondataavailable , possiamo vedere il BLOB e le sue proprietà nella console del browser.

Browser Devtools console che mostra il BLOB disconnesso creato dal Media Recorder al termine di una registrazione.Pull Quotes
Browser Devtools console che mostra il BLOB disconnesso creato da Media Recorder al termine di una registrazione. (Grande anteprima)

Ora che possiamo avviare un flusso MediaRecorder, dobbiamo essere in grado di interrompere il flusso MediaRecorder quando un utente ha finito di registrare il proprio input vocale e inviare il file audio generato all'applicazione Express.js.

Il nuovo metodo aggiunto allo store sottostante interrompe il flusso ed effettua una richiesta POST contenente l'input vocale registrato.

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

Conclusione

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.

Riferimenti

  • 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