Integrieren eines Dialogflow-Agenten in eine React-Anwendung
Veröffentlicht: 2022-03-10Dialogflow ist eine Plattform, die den Prozess der Erstellung und Gestaltung eines Konversations-Chat-Assistenten zur Verarbeitung natürlicher Sprache vereinfacht, der Sprach- oder Texteingaben verarbeiten kann, wenn er entweder über die Dialogflow-Konsole oder eine integrierte Webanwendung verwendet wird.
Obwohl der integrierte Dialogflow-Agent in diesem Artikel kurz erklärt wird, wird erwartet, dass Sie über Node.js- und Dialogflow-Kenntnisse verfügen. Wenn Sie zum ersten Mal etwas über Dialogflow lernen, enthält dieser Artikel eine klare Erklärung dessen, was Dialogflow ist und welche Konzepte es gibt.
Dieser Artikel ist eine Anleitung zum Erstellen eines Dialogflow-Agenten mit Sprach- und Chat-Unterstützung, der mithilfe einer Express.js-Back-End-Anwendung als Verbindung zwischen einer React.js-Webanwendung und dem Agenten in eine Webanwendung integriert werden kann auf Dialogflow selbst. Am Ende des Artikels sollten Sie in der Lage sein, Ihren eigenen Dialogflow-Agent mit Ihrer bevorzugten Webanwendung zu verbinden.
Um diese Anleitung leicht durchzuarbeiten, können Sie zu dem Teil des Tutorials springen, der Sie am meisten interessiert, oder sie in der folgenden Reihenfolge befolgen, wenn sie erscheinen:
- Einrichten eines Dialogflow-Agenten
- Integrieren eines Dialogflow-Agenten
- Einrichten einer Node Express-Anwendung
- Authentifizierung mit Dialogflow
- Was sind Dienstkonten?
- Umgang mit Spracheingaben
- Integration in eine Webanwendung
- Erstellen einer Chat-Schnittstelle
- Spracheingabe des Benutzers aufzeichnen
- Fazit
- Verweise
1. Einrichten eines Dialogflow-Agenten
Wie in diesem Artikel erläutert, wird ein Chat-Assistent auf Dialogflow als Agent bezeichnet und besteht aus kleineren Komponenten wie Absichten, Erfüllung, Wissensdatenbank und vielem mehr. Dialogflow bietet eine Konsole, mit der Benutzer den Gesprächsablauf eines Agenten erstellen, trainieren und gestalten können. In unserem Anwendungsfall stellen wir einen Agenten wieder her, der nach dem Training mit der Export- und Importfunktion des Agenten in einen ZIP-Ordner exportiert wurde.
Bevor wir den Import durchführen, müssen wir einen neuen Agenten erstellen, der mit dem wiederherzustellenden Agenten zusammengeführt wird. Um einen neuen Agenten über die Konsole zu erstellen, ist ein eindeutiger Name erforderlich sowie ein Projekt in der Google Cloud, mit dem der Agent verknüpft werden kann. Wenn kein vorhandenes Projekt in der Google Cloud zum Verknüpfen vorhanden ist, kann hier ein neues erstellt werden.
Ein Agent wurde zuvor erstellt und geschult, um einem Benutzer basierend auf seinem Budget Weinprodukte zu empfehlen. Dieser Agent wurde in eine ZIP-Datei exportiert; Sie können den Ordner hier herunterladen und ihn in unserem neu erstellten Agenten über die Registerkarte „Exportieren und Importieren“ auf der Seite „Agenteneinstellungen“ wiederherstellen.
Der importierte Agent wurde zuvor geschult, um dem Benutzer basierend auf dem Budget des Benutzers für den Kauf einer Flasche Wein ein Weinprodukt zu empfehlen.
Wenn wir den importierten Agenten durchgehen, sehen wir, dass er drei erstellte Absichten von der Absichtsseite hat. Eines ist ein Fallback-Intent, das verwendet wird, wenn der Agent Eingaben von einem Benutzer nicht erkennt, das andere ist ein Willkommens-Intent, das verwendet wird, wenn ein Gespräch mit dem Agenten gestartet wird, und das letzte Intent wird verwendet, um dem Benutzer einen Wein auf der Grundlage von zu empfehlen Betragsparameter innerhalb des Satzes. Besorgniserregend für uns ist die Absicht get-wine-recommendation
Diese Absicht hat einen einzelnen Eingabekontext der wine-recommendation
die von der Absicht „Default Welcome“ stammt, um die Konversation mit dieser Absicht zu verknüpfen.
„Ein Kontext ist ein System innerhalb eines Agenten, das verwendet wird, um den Fluss einer Konversation von einer Absicht zur anderen zu steuern.“
Unter den Kontexten befinden sich die Trainingsphrasen, bei denen es sich um Sätze handelt, die verwendet werden, um einen Agenten darin zu schulen, welche Art von Aussagen er von einem Benutzer erwarten kann. Durch eine Vielzahl von Trainingsphrasen innerhalb einer Absicht ist ein Agent in der Lage, den Satz eines Benutzers und die Absicht, in die er fällt, zu erkennen.
Die Trainingsphrasen in der Absicht unserer Agenten get-wine-recommendation
(wie unten gezeigt), geben die Weinauswahl und die Preiskategorie an:
Wenn wir uns das Bild oben ansehen, sehen wir die verfügbaren Trainingsphrasen, die aufgelistet sind, und die Währungszahl ist für jeden von ihnen in gelber Farbe hervorgehoben. Diese Hervorhebung wird in Dialogflow als Anmerkung bezeichnet und erfolgt automatisch, um die als Entität bekannten erkannten Datentypen aus dem Satz eines Benutzers zu extrahieren.
Nachdem diese Absicht in einem Gespräch mit dem Agenten abgeglichen wurde, wird eine HTTP-Anfrage an einen externen Dienst gestellt, um den empfohlenen Wein auf der Grundlage des als Parameter aus einem Benutzersatz extrahierten Preises abzurufen, indem der darin enthaltene aktivierte Webhook verwendet wird den Abschnitt Erfüllung unten auf dieser Absichtsseite.
Wir können den Agenten mit dem Dialogflow-Emulator testen, der sich im rechten Bereich der Dialogflow-Konsole befindet. Zum Testen beginnen wir das Gespräch mit einem „ Hallo “ und folgen mit der gewünschten Menge Wein. Der Webhook wird sofort aufgerufen und der Agent zeigt eine umfangreiche Antwort ähnlich der untenstehenden.
Aus dem obigen Bild können wir die mit Ngrok generierte Webhook-URL und die Antwort des Agenten auf der rechten Seite sehen, die einen vom Benutzer eingegebenen Wein innerhalb der Preisspanne von 20 $ zeigt.
Zu diesem Zeitpunkt ist der Dialogflow-Agent vollständig eingerichtet. Wir können jetzt damit beginnen, diesen Agenten in eine Webanwendung zu integrieren, damit andere Benutzer ohne Zugriff auf unsere Dialogflow-Konsole auf den Agenten zugreifen und mit ihm interagieren können.
Integrieren eines Dialogflow-Agenten
Während es andere Möglichkeiten gibt, eine Verbindung zu einem Dialogflow-Agenten herzustellen, z. B. HTTP-Anforderungen an seine REST-Endpunkte, wird empfohlen, eine Verbindung zu Dialogflow herzustellen, indem die offizielle Client-Bibliothek verwendet wird, die in mehreren Programmiersprachen verfügbar ist. Für JavaScript ist das Paket @google-cloud/dialogflow zur Installation von NPM verfügbar.
Intern verwendet das @google-cloud/dialogflow-Paket gRPC für seine Netzwerkverbindungen, wodurch das Paket in einer Browserumgebung nicht unterstützt wird, außer wenn es mit Webpack gepatcht wird. Die empfohlene Methode zur Verwendung dieses Pakets ist eine Node-Umgebung. Wir können dies tun, indem wir eine Express.js-Back-End-Anwendung einrichten, um dieses Paket zu verwenden, und dann Daten über ihre API-Endpunkte an die Webanwendung liefern, und das werden wir als Nächstes tun.
Einrichten einer Node Express-Anwendung
Um eine Express-Anwendung einzurichten, erstellen wir ein neues Projektverzeichnis und holen uns dann die erforderlichen Abhängigkeiten mit yarn
von einem geöffneten Befehlszeilenterminal.
# 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
Wenn die erforderlichen Abhängigkeiten installiert sind, können wir mit der Einrichtung eines sehr schlanken Express.js-Servers fortfahren, der Verbindungen an einem bestimmten Port mit aktivierter CORS-Unterstützung für die Web-App verarbeitet.
// 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}`));
Bei der Ausführung startet der Code im obigen Snippet einen HTTP-Server, der auf Verbindungen auf einem bestimmten PORT Express.js lauscht. Außerdem ist Cross-Origin Resource Sharing (CORS) für alle Anfragen aktiviert, die das cors-Paket als Express-Middleware verwenden. Im Moment lauscht dieser Server nur auf Verbindungen, er kann nicht auf eine Anfrage antworten, da er keine Route erstellt hat, also erstellen wir diese.
Wir müssen jetzt zwei neue Routen hinzufügen: eine zum Senden von Textdaten und die andere zum Senden einer aufgezeichneten Spracheingabe. Beide akzeptieren eine POST
-Anforderung und senden die im Anforderungstext enthaltenen Daten später an den Dialogflow-Agent.
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
Oben haben wir eine separate Router-Instanz für die beiden erstellten POST
-Routen erstellt, die vorerst nur mit einem 200
-Statuscode und einer hartcodierten Dummy-Antwort antworten. Wenn wir mit der Authentifizierung bei Dialogflow fertig sind, können wir zurückkommen, um eine tatsächliche Verbindung zu Dialogflow innerhalb dieser Endpunkte zu implementieren.
Als letzten Schritt bei der Einrichtung unserer Backend-Anwendung mounten wir die zuvor erstellte Router-Instanz, die mithilfe von app.use und einem Basispfad für die Route in der Express-Anwendung erstellt wurde.
// 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}`));
Oben haben wir den beiden Routen einen Basispfad hinzugefügt, zwei können wir über eine POST
-Anforderung mit cURL von einer Befehlszeile aus testen, wie es unten mit einem leeren Anforderungstext geschieht.
curl -X https://localhost:5000/api/agent/text-response
Nach erfolgreichem Abschluss der obigen Anfrage können wir erwarten, dass eine Antwort mit Objektdaten auf der Konsole ausgegeben wird.
Jetzt müssen wir nur noch eine tatsächliche Verbindung mit Dialogflow herstellen, die die Verarbeitung der Authentifizierung sowie das Senden und Empfangen von Daten vom Agent auf Dialogflow mithilfe des @google-cloud/dialogflow-Pakets umfasst.
Authentifizierung mit Dialogflow
Jeder erstellte Dialogflow-Agent ist mit einem Projekt in der Google Cloud verknüpft. Um eine externe Verbindung zum Dialogflow-Agent herzustellen, authentifizieren wir uns beim Projekt in der Google-Cloud und verwenden Dialogflow als eine der Ressourcen des Projekts. Von den sechs verfügbaren Möglichkeiten, eine Verbindung zu einem Projekt in der Google-Cloud herzustellen, ist die Verwendung der Option Dienstkonten die bequemste, wenn Sie sich über die Client-Bibliothek mit einem bestimmten Dienst in der Google-Cloud verbinden.
Hinweis : Für produktionsbereite Anwendungen wird die Verwendung von kurzlebigen API-Schlüsseln gegenüber Dienstkontoschlüsseln empfohlen, um das Risiko zu verringern, dass ein Dienstkontoschlüssel in die falschen Hände gerät.
Was sind Dienstkonten?
Dienstkonten sind eine spezielle Art von Konten in der Google Cloud, die für nichtmenschliche Interaktionen erstellt wurden, hauptsächlich über externe APIs. In unserer Anwendung wird auf das Dienstkonto über einen von der Dialogflow-Clientbibliothek generierten Schlüssel zugegriffen, um sich bei der Google Cloud zu authentifizieren.
Die Cloud-Dokumentation zum Erstellen und Verwalten von Dienstkonten bietet eine hervorragende Anleitung zum Erstellen eines Dienstkontos. Beim Erstellen des Dienstkontos sollte die Dialogflow-API-Administratorrolle dem erstellten Dienstkonto wie im letzten Schritt gezeigt zugewiesen werden. Diese Rolle gibt dem Dienstkonto die administrative Kontrolle über den verknüpften Dialogflow-Agent.
Um das Dienstkonto verwenden zu können, müssen wir einen Dienstkontoschlüssel erstellen. Die folgenden Schritte beschreiben, wie Sie eine im JSON-Format erstellen:
- Klicken Sie auf das neu erstellte Dienstkonto, um zur Seite Dienstkonto zu navigieren.
- Scrollen Sie zum Abschnitt Schlüssel und klicken Sie auf das Dropdown-Menü Schlüssel hinzufügen und klicken Sie auf die Option Neuen Schlüssel erstellen , wodurch ein Modal geöffnet wird.
- Wählen Sie ein JSON-Dateiformat aus und klicken Sie unten rechts im Modal auf die Schaltfläche Erstellen.
Hinweis: Es wird empfohlen, einen Dienstkontoschlüssel geheim zu halten und ihn nicht an ein Versionskontrollsystem zu übergeben, da er hochsensible Daten zu einem Projekt in der Google Cloud enthält. Dies kann durch Hinzufügen der Datei zur .gitignore
-Datei erfolgen.
Mit einem erstellten Dienstkonto und einem im Verzeichnis unseres Projekts verfügbaren Dienstkontoschlüssel können wir die Dialogflow-Clientbibliothek verwenden, um Daten vom Dialogflow-Agent zu senden und zu empfangen.
// 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;
Die gesamte obige Route sendet Daten an den Dialogflow-Agent und erhält in den folgenden Schritten eine Antwort.
- Zuerst
Es authentifiziert sich bei der Google-Cloud und erstellt dann eine Sitzung mit Dialogflow unter Verwendung der Projekt-ID des Google-Cloud-Projekts, das mit dem Dialogflow-Agenten verknüpft ist, sowie einer zufälligen ID zur Identifizierung der erstellten Sitzung. In unserer Anwendung erstellen wir eine UUID-Kennung für jede Sitzung, die mit dem JavaScript-UUID-Paket erstellt wurde. Dies ist sehr nützlich, wenn Sie alle Konversationen protokollieren oder nachverfolgen, die von einem Dialogflow-Agent abgewickelt werden. - Sekunde
Wir erstellen Anfrageobjektdaten gemäß dem angegebenen Format in der Dialogflow-Dokumentation. Dieses Anforderungsobjekt enthält die erstellte Sitzung und die aus dem Anforderungstext abgerufenen Nachrichtendaten, die an den Dialogflow-Agent übergeben werden sollen. - Dritter
Mit der MethodedetectIntent
aus der Dialogflow-Sitzung senden wir das Anforderungsobjekt asynchron und warten auf die Antwort des Agenten mit ES6 async / await-Syntax in einem Try-Catch-Block. Sollte die MethodedetectIntent
eine Ausnahme zurückgeben, können wir den Fehler abfangen und stattdessen zurückgeben als die gesamte Anwendung zum Absturz zu bringen. Ein Beispiel des vom Agent zurückgegebenen Antwortobjekts wird in der Dialogflow-Dokumentation bereitgestellt und kann überprüft werden, um zu erfahren, wie die Daten aus dem Objekt extrahiert werden.
Wir können Postman verwenden, um die oben implementierte Dialogflow-Verbindung in der dialogflow-response
Route zu testen. Postman ist eine Kollaborationsplattform für die API-Entwicklung mit Funktionen zum Testen von APIs, die entweder in der Entwicklungs- oder in der Produktionsphase erstellt wurden.
Hinweis: Wenn die Postman-Desktopanwendung noch nicht installiert ist, wird sie zum Testen einer API nicht benötigt. Ab September 2020 wurde der Webclient von Postman in den Status „Generally Available“ (GA) versetzt und kann direkt über einen Browser verwendet werden.
Mit dem Postman-Webclient können wir entweder einen neuen Arbeitsbereich erstellen oder einen vorhandenen verwenden, um eine POST
-Anforderung an unseren API-Endpunkt unter https://localhost:5000/api/agent/text-input
zu erstellen und Daten mit einem Schlüssel von hinzuzufügen message
und Wert von „ Hi There “ in die Abfrageparameter.
Beim Klicken auf die Schaltfläche „ Senden “ wird eine POST
-Anforderung an den laufenden Express-Server gesendet – mit einer Antwort ähnlich der im Bild unten gezeigten:
Im obigen Bild sehen wir die verschönerten Antwortdaten vom Dialogflow-Agent über den Express-Server. Die zurückgegebenen Daten werden gemäß der Beispielantwortdefinition in der Dialogflow-Webhook-Dokumentation formatiert.
Umgang mit Spracheingaben
Standardmäßig sind alle Dialogflow-Agents aktiviert, um sowohl Text- als auch Audiodaten zu verarbeiten und auch eine Antwort im Text- oder Audioformat zurückzugeben. Die Arbeit mit Audio-Eingabe- oder Ausgabedaten kann jedoch etwas komplexer sein als mit Textdaten.
Um Spracheingaben zu handhaben und zu verarbeiten, beginnen wir mit der Implementierung für den /voice-input
Endpunkt, den wir zuvor erstellt haben, um Audiodateien zu empfangen und sie im Austausch für eine Antwort vom Agenten an Dialogflow zu senden:
// 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}`); } });
Bei einer hohen Übersicht empfängt die obige /voice-input
Route die Spracheingabe eines Benutzers als Datei, die die Nachricht enthält, die an den Chat-Assistenten gesprochen wird, und sendet sie an den Dialogflow-Agent. Um diesen Prozess besser zu verstehen, können wir ihn in die folgenden kleineren Schritte unterteilen:
- Zuerst fügen wir connect-busboy hinzu und verwenden es als Express-Middleware zum Analysieren von Formulardaten, die in der Anfrage von der Webanwendung gesendet werden. Danach authentifizieren wir uns bei Dialogflow mit dem Dienstschlüssel und erstellen eine Sitzung, so wie wir es in der vorherigen Route getan haben.
Dann verwenden wir die Promisify-Methode aus dem integrierten Node.js-Utility-Modul, um ein Promise-Äquivalent der Stream-Pipeline-Methode zu erhalten und zu speichern, das später verwendet wird, um mehrere Streams zu leiten und auch eine Bereinigung durchzuführen, nachdem die Streams abgeschlossen sind. - Als Nächstes erstellen wir ein Anforderungsobjekt, das die Dialogflow-Authentifizierungssitzung und eine Konfiguration für die Audiodatei enthält, die an Dialogflow gesendet werden soll. Das verschachtelte Audiokonfigurationsobjekt ermöglicht dem Dialogflow-Agent, eine Speech-to-Text-Konvertierung der gesendeten Audiodatei durchzuführen.
- Als Nächstes erkennen wir mithilfe der erstellten Sitzung und des Anforderungsobjekts die Absicht eines Benutzers aus der Audiodatei mithilfe der Methode „
detectStreamingIntent
“, die einen neuen Datenstrom vom Dialogflow-Agent zur Back-End-Anwendung öffnet. Daten werden in kleinen Bits durch diesen Stream zurückgesendet und unter Verwendung des Daten-" Ereignisses " aus dem lesbaren Stream speichern wir die Daten in der VariablenstreamData
zur späteren Verwendung. Nachdem der Stream geschlossen wurde, wird das Ereignis „ end “ ausgelöst und wir senden die Antwort vom Dialogflow-Agent, die in der VariablenstreamData
gespeichert ist, an die Webanwendung. - Unter Verwendung des Datei-Stream-Ereignisses von connect-busboy erhalten wir schließlich den Stream der Audiodatei, die im Anforderungstext gesendet wurde, und leiten ihn weiter an das zuvor erstellte Promise-Äquivalent von Pipeline weiter. Die Funktion davon besteht darin, den von der Anfrage eingehenden Audiodatei-Stream an den Dialogflow-Stream weiterzuleiten. Wir leiten den Audiodatei-Stream an den Stream weiter, der von der Methode
detectStreamingIntent
oben geöffnet wurde.
Um zu testen und zu bestätigen, dass die obigen Schritte wie beschrieben funktionieren, können wir mit Postman eine Testanforderung mit einer Audiodatei im Anforderungstext an den /voice-input
Endpunkt senden.
Das obige Postman-Ergebnis zeigt die Antwort, die nach einer POST-Anforderung mit den Formulardaten einer aufgezeichneten Sprachnotiz mit dem Inhalt „ Hallo “ im Anforderungstext erhalten wurde.
An diesem Punkt haben wir jetzt eine funktionierende Express.js-Anwendung, die Daten von Dialogflow sendet und empfängt, die beiden Teile dieses Artikels sind fertig. Wo bleibt nun die Integration dieses Agenten in eine Webanwendung, indem die hier erstellten APIs aus einer Reactjs-Anwendung verwendet werden.
Integration in eine Webanwendung
Um unsere erstellte REST-API zu nutzen, werden wir diese vorhandene React.js-Anwendung erweitern, die bereits über eine Homepage verfügt, die eine Liste von Weinen zeigt, die von einer API abgerufen werden, und Unterstützung für Dekorateure, die das Plugin für Dekorateure für Vorschläge von babel verwenden. Wir werden es ein wenig umgestalten, indem wir Mobx für die Zustandsverwaltung und auch eine neue Funktion zum Empfehlen eines Weins aus einer Chat-Komponente unter Verwendung der hinzugefügten REST-API-Endpunkte aus der Express.js-Anwendung einführen.
Zu Beginn beginnen wir damit, den Zustand der Anwendung mit MobX zu verwalten, während wir einen Mobx-Speicher mit einigen beobachtbaren Werten und einigen Methoden als Aktionen erstellen.
// 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();
Oben haben wir einen Speicher für die Chat-Komponentenfunktion innerhalb der Anwendung mit den folgenden Werten erstellt:
-
isChatWindowOpen
Der hier gespeicherte Wert steuert die Sichtbarkeit der Chat-Komponente, in der die Nachrichten von Dialogflow angezeigt werden. -
isLoadingChatMessages
Dies wird verwendet, um einen Ladeindikator anzuzeigen, wenn eine Anforderung zum Abrufen einer Antwort vom Dialogflow-Agent gestellt wird. -
agentMessages
In diesem Array werden alle Antworten gespeichert, die von den Anfragen stammen, die gestellt wurden, um eine Antwort vom Dialogflow-Agent zu erhalten. Die Daten im Array werden später in der Komponente angezeigt. -
handleConversation
Diese als Aktion dekorierte Methode fügt Daten in dasagentMessages
Array ein. Zuerst fügt es die als Argument übergebene Nachricht des Benutzers hinzu und stellt dann mit Axios eine Anfrage an die Back-End-Anwendung, um eine Antwort von Dialogflow zu erhalten. Nachdem die Anfrage aufgelöst wurde, fügt es die Antwort von der Anfrage in dasagentMessages
Array ein.
Hinweis: In Ermangelung der Dekorationsunterstützung in einer Anwendung stellt MobX makeObservable bereit , das im Konstruktor der Zielspeicherklasse verwendet werden kann. Sehen Sie hier ein Beispiel.
Bei der Store-Einrichtung müssen wir den gesamten Anwendungsbaum mit der MobX Provider-Komponente höherer Ordnung umschließen, beginnend mit der Root-Komponente in der Datei 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;
Oben umschließen wir die Root-App-Komponente mit MobX Provider und übergeben den zuvor erstellten Store als einen der Werte des Providers. Jetzt können wir damit fortfahren, innerhalb der mit dem Geschäft verbundenen Komponenten aus dem Geschäft zu lesen.
Erstellen einer Chat-Schnittstelle
Um die von den API-Anfragen gesendeten oder empfangenen Nachrichten anzuzeigen, benötigen wir eine neue Komponente mit einer Chat-Oberfläche, die die aufgelisteten Nachrichten anzeigt. Dazu erstellen wir eine neue Komponente, um zuerst einige hartcodierte Nachrichten anzuzeigen, und später zeigen wir Nachrichten in einer geordneten Liste an.
// ./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
Die obige Komponente enthält das grundlegende HTML-Markup, das für eine Chat-Anwendung benötigt wird. Es hat eine Kopfzeile, die den Namen des Agenten und ein Symbol zum Schließen des Chatfensters anzeigt, eine Nachrichtenblase, die einen fest codierten Text in einem Listen-Tag enthält, und schließlich ein Eingabefeld mit einem onChange
-Ereignishandler, der an das Eingabefeld angehängt ist, um den eingegebenen Text zu speichern den lokalen Zustand der Komponente mit dem useState von React.
Im obigen Bild funktioniert die Chat-Komponente so, wie sie sollte, und zeigt ein gestyltes Chat-Fenster mit einer einzelnen Chat-Nachricht und dem Eingabefeld unten. Wir möchten jedoch, dass es sich bei der angezeigten Nachricht um tatsächliche Antworten handelt, die von der API-Anforderung erhalten wurden, und nicht um hartcodierten Text.
Wir fahren mit dem Refactoring der Chat-Komponente fort, wobei wir dieses Mal Werte im MobX-Speicher innerhalb der Komponente verbinden und verwenden.
// ./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));
Aus den hervorgehobenen Teilen des obigen Codes können wir ersehen, dass die gesamte Chat-Komponente jetzt modifiziert wurde, um die folgenden neuen Operationen auszuführen;
- Es hat Zugriff auf die MobX-Speicherwerte, nachdem es den
ApplicationStore
-Wert eingefügt hat. Die Komponente wurde auch zum Beobachter dieser Speicherwerte gemacht, sodass sie neu gerendert wird, wenn sich einer der Werte ändert. - Wir beginnen die Konversation mit dem Agenten unmittelbar nach dem Öffnen der Chat-Komponente, indem wir die
handleConversation
Methode innerhalb einesuseEffect
-Hooks aufrufen, um eine Anfrage zu stellen, sobald die Komponente gerendert wird. - Wir verwenden jetzt den Wert
isLoadingMessages
im Header der Chat-Komponente. Wenn eine Anfrage zum Erhalt einer Antwort vom Agenten ausgeführt wird, setzen wir den WertisLoadingMessages
auftrue
und aktualisieren den Header auf Zara is typing… - Das
agentMessages
Array innerhalb des Speichers wird von MobX auf einen Proxy aktualisiert, nachdem seine Werte festgelegt wurden. Von dieser Komponente konvertieren wir diesen Proxy mithilfe destoJS
Dienstprogramms von MobX zurück in ein Array und speichern die Werte in einer Variablen innerhalb der Komponente. Dieses Array wird weiter iteriert, um die Chatblasen mit den Werten innerhalb des Arrays unter Verwendung einer Zuordnungsfunktion zu füllen.
Jetzt können wir mit der Chat-Komponente einen Satz eingeben und warten, bis eine Antwort im Agenten angezeigt wird.
Spracheingabe des Benutzers aufzeichnen
Standardmäßig können alle Dialogflow-Agenten entweder sprach- oder textbasierte Eingaben in einer beliebigen angegebenen Sprache von einem Benutzer akzeptieren. Es erfordert jedoch einige Anpassungen von einer Webanwendung, um Zugriff auf das Mikrofon eines Benutzers zu erhalten und eine Spracheingabe aufzuzeichnen.
Um dies zu erreichen, ändern wir den MobX-Speicher so, dass er die HTML MediaStream-Aufzeichnungs-API verwendet, um die Stimme eines Benutzers mit zwei neuen Methoden im MobX-Speicher aufzuzeichnen.
// 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}`)); }; };
Beim Klicken auf das Aufnahmesymbol aus der Chat-Komponente wird die startAudioConversation
Methode im obigen MobX-Store aufgerufen, um die Methode der beobachtbaren isRecording
Eigenschaft auf true zu setzen, damit die Chat-Komponente ein visuelles Feedback liefert, um anzuzeigen, dass eine Aufnahme im Gange ist.
Unter Verwendung der Navigationsschnittstelle des Browsers wird auf das Mediengeräteobjekt zugegriffen, um das Gerätemikrofon des Benutzers anzufordern. Nachdem der getUserMedia
Anforderung die Erlaubnis erteilt wurde, löst sie ihr Versprechen mit MediaStream-Daten auf, die wir weiter an den MediaRecorder-Konstruktor übergeben, um einen Rekorder zu erstellen, der die Medienspuren in dem vom Mikrofon des Benutzergeräts zurückgegebenen Stream verwendet. Anschließend speichern wir die Media-Rekorder-Instanz in der recorder
-Eigenschaft des Geschäfts, da wir später über eine andere Methode darauf zugreifen werden.
Als nächstes rufen wir die start-Methode auf der Recorder-Instanz auf, und nachdem die Aufzeichnungssitzung beendet ist, wird die ondataavailable
Funktion mit einem Ereignisargument ausgelöst, das den recordedBits
Stream in einem Blob enthält, das wir in der Array-EigenschaftrecordedBits speichern.
Wenn wir die Daten im Ereignisargument abmelden, das an das ausgelöste ondataavailable
Ereignis übergeben wird, können wir das Blob und seine Eigenschaften in der Browserkonsole sehen.
Nachdem wir nun einen MediaRecorder-Stream starten können, müssen wir in der Lage sein, den MediaRecorder-Stream zu stoppen, wenn ein Benutzer mit der Aufzeichnung seiner Spracheingabe fertig ist, und die generierte Audiodatei an die Express.js-Anwendung senden.
Die neue Methode, die dem Speicher unten hinzugefügt wurde, stoppt den Stream und stellt eine POST
-Anfrage, die die aufgezeichnete Spracheingabe enthält.
//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!
Fazit
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.
Verweise
- 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