Integracja agenta Dialogflow z aplikacją React
Opublikowany: 2022-03-10Dialogflow to platforma, która upraszcza proces tworzenia i projektowania konwersacyjnego asystenta czatu przetwarzającego język naturalny, który może przetwarzać wprowadzanie głosowe lub tekstowe, gdy jest używany z konsoli Dialogflow lub ze zintegrowanej aplikacji internetowej.
Chociaż zintegrowany agent Dialogflow został krótko omówiony w tym artykule, oczekuje się, że znasz Node.js i Dialogflow. Jeśli po raz pierwszy dowiadujesz się o Dialogflow, ten artykuł zawiera jasne wyjaśnienie, czym jest Dialogflow i jakie są jego koncepcje.
Ten artykuł jest przewodnikiem po tym, jak zbudować agenta Dialogflow z obsługą głosu i czatu, który można zintegrować z aplikacją internetową za pomocą aplikacji back-end Express.js jako łącza między aplikacją internetową React.js a agentem na samym Dialogflow. Pod koniec artykułu powinieneś być w stanie podłączyć własnego agenta Dialogflow do preferowanej aplikacji internetowej.
Aby ułatwić zapoznanie się z tym przewodnikiem, możesz przejść do części samouczka, która najbardziej Cię interesuje, lub postępować zgodnie z nimi w następującej kolejności:
- Konfigurowanie agenta Dialogflow
- Integracja agenta Dialogflow
- Konfigurowanie aplikacji Node Express
- Uwierzytelnianie za pomocą Dialogflow
- Czym są konta usług?
- Obsługa wejść głosowych
- Integracja z aplikacją internetową
- Tworzenie interfejsu czatu
- Nagrywanie głosu użytkownika
- Wniosek
- Bibliografia
1. Konfigurowanie agenta Dialogflow
Jak wyjaśniono w tym artykule, asystent czatu w Dialogflow nazywany jest agentem i składa się z mniejszych elementów, takich jak intencje, realizacja, baza wiedzy i wiele innych. Dialogflow udostępnia konsolę, w której użytkownicy mogą tworzyć, trenować i projektować przebieg konwersacji agenta. W naszym przypadku użycia przywrócimy agenta, który został wyeksportowany do folderu ZIP po przeszkoleniu, za pomocą funkcji eksportu i importu agenta.
Zanim wykonamy import, musimy utworzyć nowego agenta, który zostanie połączony z agentem, który ma zostać przywrócony. Aby utworzyć nowego agenta z konsoli, potrzebna jest unikalna nazwa, a także projekt w Google Cloud, z którym agent będzie połączony. Jeśli w Google Cloud nie ma istniejącego projektu, z którym można by się połączyć, tutaj można utworzyć nowy.
Wcześniej utworzono i przeszkolono agenta, który poleca produkty winiarskie użytkownikowi na podstawie jego budżetu. Ten agent został wyeksportowany do ZIP; możesz pobrać folder tutaj i przywrócić go do naszego nowo utworzonego agenta z zakładki Eksportuj i importuj znajdującej się na stronie Ustawienia agenta.
Importowany agent został wcześniej przeszkolony w zakresie polecania użytkownikowi produktu winiarskiego w oparciu o budżet użytkownika na zakup butelki wina.
Przeglądając zaimportowanego agenta, zobaczymy, że ma trzy utworzone intencje ze strony intencji. Jedna to intencja zastępcza, używana, gdy agent nie rozpoznaje danych wejściowych od użytkownika, druga to intencja powitalna używana podczas rozpoczynania rozmowy z agentem, a ostatnia intencja służy do polecania wina użytkownikowi na podstawie parametr kwoty w zdaniu. Niepokoi nas intencja get-wine-recommendation
Ta intencja ma pojedynczy kontekst wejściowy wine-recommendation
pochodzących z domyślnej intencji powitalnej, aby połączyć konwersację z tą intencją.
„Kontekst to system w agencie używany do kontrolowania przepływu konwersacji od jednej intencji do drugiej.”
Poniżej kontekstów znajdują się frazy szkoleniowe, które są zdaniami używanymi do szkolenia agenta, jakiego rodzaju wypowiedzi oczekiwać od użytkownika. Dzięki dużej różnorodności fraz szkoleniowych w intencji agent jest w stanie rozpoznać zdanie użytkownika i intencję, w której się znajduje.
Frazy szkoleniowe w ramach intencji get-wine-recommendation
” naszych agentów (jak pokazano poniżej) wskazują na wybór wina i kategorię cenową:
Patrząc na powyższy obrazek, widzimy wyszczególnione dostępne frazy szkoleniowe, a liczba waluty jest podświetlona na żółto dla każdej z nich. To wyróżnienie jest nazywane adnotacją w Dialogflow i jest automatycznie wykonywane w celu wyodrębnienia rozpoznanych typów danych, znanych jako encja, ze zdania użytkownika.
Po dopasowaniu tej intencji w rozmowie z agentem zostanie wysłane żądanie HTTP do usługi zewnętrznej w celu pobrania zalecanego wina na podstawie ceny wyodrębnionej jako parametr ze zdania użytkownika, poprzez użycie włączonego webhooka znajdującego się w sekcja Realizacja na dole tej strony intencji.
Agenta możemy przetestować za pomocą emulatora Dialogflow znajdującego się w prawej części konsoli Dialogflow. Aby przetestować, rozpoczynamy rozmowę od komunikatu „ Cześć ” i kontynuujemy z żądaną ilością wina. Webhook zostanie natychmiast wywołany, a agent wyświetli odpowiedź bogatą, podobną do poniższej.
Na powyższym obrazku widzimy adres URL webhooka wygenerowany za pomocą Ngrok i odpowiedź agenta po prawej stronie, pokazując wino w przedziale cenowym 20 USD wpisanym przez użytkownika.
W tym momencie agent Dialogflow został w pełni skonfigurowany. Możemy teraz rozpocząć integrację tego agenta z aplikacją internetową, aby umożliwić innym użytkownikom dostęp i interakcję z agentem bez dostępu do naszej konsoli Dialogflow.
Integracja agenta Dialogflow
Chociaż istnieją inne sposoby łączenia się z agentem Dialogflow, takie jak wysyłanie żądań HTTP do jego punktów końcowych REST, zalecanym sposobem łączenia się z Dialogflow jest użycie jego oficjalnej biblioteki klienckiej dostępnej w kilku językach programowania. W przypadku JavaScript pakiet @google-cloud/dialogflow jest dostępny do zainstalowania z NPM.
Wewnętrznie pakiet @google-cloud/dialogflow używa gRPC do połączeń sieciowych, co sprawia, że pakiet nie jest obsługiwany w środowisku przeglądarki, z wyjątkiem sytuacji, gdy jest on poprawiany przy użyciu pakietu internetowego, zalecanym sposobem korzystania z tego pakietu jest środowisko węzła. Możemy to zrobić, konfigurując aplikację zaplecza Express.js do korzystania z tego pakietu, a następnie dostarczać dane do aplikacji internetowej za pośrednictwem jej punktów końcowych API i to właśnie zrobimy dalej.
Konfigurowanie aplikacji Node Express
Aby skonfigurować aplikację ekspresową, tworzymy nowy katalog projektu, a następnie pobieramy potrzebne zależności za pomocą yarn
z otwartego terminala wiersza poleceń.
# 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
Po zainstalowaniu wymaganych zależności możemy przystąpić do konfiguracji bardzo szczupłego serwera Express.js, który obsługuje połączenia na określonym porcie z włączoną obsługą CORS dla aplikacji internetowej.
// 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}`));
Po wykonaniu kod w powyższym fragmencie uruchamia serwer HTTP, który nasłuchuje połączeń na określonym porcie Express.js. Ma również włączone współdzielenie zasobów między źródłami (CORS) dla wszystkich żądań przy użyciu pakietu cors jako oprogramowania pośredniczącego Express. Na razie ten serwer tylko nasłuchuje połączeń, nie może odpowiedzieć na żądanie, ponieważ nie ma utworzonej trasy, więc stwórzmy to.
Musimy teraz dodać dwie nowe trasy: jedną do wysyłania danych tekstowych, a drugą do wysyłania nagranych danych głosowych. Obaj zaakceptują żądanie POST
, a później wyślą dane zawarte w treści żądania do agenta 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
Powyżej utworzyliśmy oddzielną instancję routera dla dwóch utworzonych tras POST
, które na razie odpowiadają tylko kodem statusu 200
i zakodowaną fikcyjną odpowiedzią. Po zakończeniu uwierzytelniania w Dialogflow możemy wrócić, aby zaimplementować rzeczywiste połączenie z Dialogflow w tych punktach końcowych.
W ostatnim kroku konfiguracji naszej aplikacji zaplecza montujemy wcześniej utworzoną instancję routera utworzoną w aplikacji Express za pomocą app.use i ścieżki bazowej dla trasy.
// 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}`));
Powyżej dodaliśmy ścieżkę bazową do dwóch tras. Dwie możemy przetestować każdą z nich za pomocą POST
, używając cURL z wiersza poleceń, tak jak to zrobiono poniżej z pustą treścią żądania;
curl -X https://localhost:5000/api/agent/text-response
Po pomyślnym zakończeniu powyższego żądania możemy spodziewać się, że na konsoli zostanie wydrukowana odpowiedź zawierająca dane obiektu.
Teraz pozostaje nam nawiązanie rzeczywistego połączenia z Dialogflow, które obejmuje obsługę uwierzytelniania, wysyłania i odbierania danych od Agenta w Dialogflow za pomocą pakietu @google-cloud/dialogflow.
Uwierzytelnianie za pomocą Dialogflow
Każdy utworzony agent Dialogflow jest połączony z projektem w Google Cloud. Aby połączyć się zewnętrznie z agentem Dialogflow, uwierzytelniamy się w projekcie w chmurze Google i używamy Dialogflow jako jednego z zasobów projektu. Spośród sześciu dostępnych sposobów łączenia się z projektem w google-cloud, korzystanie z opcji Konta usług jest najwygodniejsze podczas łączenia się z konkretną usługą w chmurze google za pośrednictwem jej biblioteki klienta.
Uwaga : w przypadku aplikacji gotowych do użytku produkcyjnego zaleca się używanie krótkoterminowych kluczy interfejsu API zamiast kluczy konta usługi, aby zmniejszyć ryzyko dostania się klucza konta usługi w niepowołane ręce.
Czym są konta usług?
Konta usług to specjalny rodzaj kont w Google Cloud, stworzony do interakcji innych niż człowiek, głównie za pośrednictwem zewnętrznych interfejsów API. W naszej aplikacji dostęp do konta usługi będzie możliwy poprzez wygenerowany klucz przez bibliotekę klienta Dialogflow w celu uwierzytelnienia w Google Cloud.
Dokumentacja w chmurze dotycząca tworzenia kont usług i zarządzania nimi stanowi doskonały przewodnik po tworzeniu konta usługi. Podczas tworzenia konta usługi należy przypisać rolę administratora Dialogflow API do utworzonego konta usługi, jak pokazano w ostatnim kroku. Ta rola daje kontu usługi kontrolę administracyjną nad połączonym agentem Dialogflow.
Aby korzystać z konta usługi, musimy utworzyć klucz konta usługi. Poniższe kroki opisują, jak utworzyć taki plik w formacie JSON:
- Kliknij nowo utworzone konto usługi, aby przejść do strony konta usługi.
- Przewiń do sekcji Klucze i kliknij menu rozwijane Dodaj klucz , a następnie kliknij opcję Utwórz nowy klucz , która otwiera modalny.
- Wybierz format pliku JSON i kliknij przycisk Utwórz w prawym dolnym rogu modułu.
Uwaga: Zaleca się zachowanie prywatności klucza konta usługi i nie przydzielanie go do żadnego systemu kontroli wersji, ponieważ zawiera on bardzo wrażliwe dane dotyczące projektu w Google Cloud. Można to zrobić, dodając plik do pliku .gitignore
.
Mając utworzone konto usługi i klucz konta usługi dostępny w katalogu naszego projektu, możemy używać biblioteki klienta Dialogflow do wysyłania i odbierania danych od agenta 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;
Cała powyższa trasa wysyła dane do agenta Dialogflow i odbiera odpowiedź, wykonując poniższe czynności.
- Pierwszy
Uwierzytelnia się w chmurze Google, a następnie tworzy sesję z Dialogflow przy użyciu identyfikatora projektu Google Cloud powiązanego z agentem Dialogflow, a także losowego identyfikatora identyfikującego utworzoną sesję. W naszej aplikacji tworzymy identyfikator UUID na każdej sesji utworzonej za pomocą pakietu JavaScript UUID. Jest to bardzo przydatne podczas rejestrowania lub śledzenia wszystkich rozmów obsługiwanych przez agenta Dialogflow. - druga
Tworzymy dane obiektu żądania zgodnie z formatem określonym w dokumentacji Dialogflow. Ten obiekt żądania zawiera utworzoną sesję i dane komunikatu otrzymane z treści żądania, które mają zostać przekazane do agenta Dialogflow. - Trzeci
Korzystając z metodydetectIntent
z sesji Dialogflow wysyłamy obiekt żądania asynchronicznie i oczekujemy odpowiedzi Agenta przy użyciu składni ES6 async /detectIntent
w bloku try-catch. niż awaria całej aplikacji. Próbka obiektu odpowiedzi zwróconego przez agenta znajduje się w dokumentacji Dialogflow i można ją sprawdzić, aby dowiedzieć się, jak wyodrębnić dane z obiektu.
Możemy użyć Postmana do przetestowania połączenia Dialogflow zaimplementowanego powyżej w trasie dialogflow-response
. Postman to platforma współpracy do tworzenia interfejsów API z funkcjami do testowania interfejsów API zbudowanych na etapie rozwoju lub produkcji.
Uwaga: jeśli nie jest jeszcze zainstalowana, aplikacja komputerowa Postman nie jest potrzebna do testowania interfejsu API. Od września 2020 r. klient sieciowy Postmana przeszedł do stanu ogólnie dostępnego (GA) i można z niego korzystać bezpośrednio w przeglądarce.
Korzystając z klienta internetowego Postman, możemy utworzyć nowy obszar roboczy lub użyć istniejącego, aby utworzyć żądanie POST
do naszego punktu końcowego API pod adresem https://localhost:5000/api/agent/text-input
i dodać dane z kluczem message
i wartość „ Hi There ” w parametrach zapytania.
Po kliknięciu przycisku Wyślij do działającego serwera Express zostanie wysłane żądanie POST
— z odpowiedzią podobną do pokazanej na poniższym obrazku:
Na powyższym obrazku możemy zobaczyć szczegółowe dane odpowiedzi z agenta Dialogflow za pośrednictwem serwera Express. Zwracane dane są sformatowane zgodnie z przykładową definicją odpowiedzi podaną w dokumentacji Webhooka Dialogflow.
Obsługa wejść głosowych
Domyślnie wszystkie agenty Dialogflow mogą przetwarzać zarówno dane tekstowe, jak i dźwiękowe, a także zwracać odpowiedź w formacie tekstowym lub dźwiękowym. Jednak praca z danymi wejściowymi lub wyjściowymi audio może być nieco bardziej złożona niż dane tekstowe.
Aby obsłużyć i przetworzyć dane wejściowe głosowe, rozpoczniemy implementację dla punktu końcowego /voice-input
, który utworzyliśmy wcześniej w celu odbierania plików audio i wysyłania ich do Dialogflow w zamian za odpowiedź od Agenta:
// 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}`); } });
W ogólnym zarysie powyższa trasa /voice-input
odbiera dane głosowe użytkownika jako plik zawierający wiadomość wypowiadaną do asystenta czatu i wysyła ją do agenta Dialogflow. Aby lepiej zrozumieć ten proces, możemy podzielić go na następujące mniejsze kroki:
- Najpierw dodajemy i używamy connect-busboy jako oprogramowania pośredniczącego Express do analizowania danych formularzy wysyłanych w żądaniu z aplikacji internetowej. Następnie uwierzytelniamy się w Dialogflow za pomocą Service Key i tworzymy sesję, tak samo jak w poprzedniej trasie.
Następnie, korzystając z metody promisify z wbudowanego modułu Node.js util, otrzymujemy i zapisujemy obietnicę równoważną metodzie potoku Stream, która będzie używana później do przesyłania wielu strumieni, a także wykonywania czyszczenia po ich zakończeniu. - Następnie tworzymy obiekt żądania zawierający sesję uwierzytelniania Dialogflow i konfigurację dla pliku audio, który ma zostać wysłany do Dialogflow. Zagnieżdżony obiekt konfiguracji dźwięku umożliwia agentowi Dialogflow wykonanie konwersji mowy na tekst w przesłanym pliku audio.
- Następnie, korzystając z utworzonej sesji i obiektu żądania, wykrywamy intencję użytkownika z pliku audio za pomocą metody
detectStreamingIntent
, która otwiera nowy strumień danych z agenta Dialogflow do aplikacji backendowej. Dane zostaną odesłane w małych bitach przez ten strumień i korzystając z danych „ zdarzenie ” z czytelnego strumienia, przechowujemy dane w zmiennejstreamData
do późniejszego wykorzystania. Po zamknięciu strumienia wywoływane jest zdarzenie „ end ” i wysyłamy odpowiedź od agenta Dialogflow zapisanego w zmiennejstreamData
do aplikacji internetowej. - Na koniec, korzystając ze zdarzenia strumienia pliku z connect-busboy, otrzymujemy strumień pliku audio wysłanego w treści żądania i dalej przekazujemy go do obiecanego odpowiednika Pipeline, który stworzyliśmy wcześniej. Funkcją tego jest przesyłanie strumienia pliku audio przychodzącego z żądania do strumienia Dialogflow. Strumień pliku audio przesyłamy do strumienia otwartego przez powyższą metodę
detectStreamingIntent
.
Aby przetestować i potwierdzić, że powyższe kroki działają zgodnie z opisem, możemy wykonać żądanie testowe zawierające plik audio w treści żądania do punktu końcowego /voice-input
za pomocą Postmana.
Powyższy wynik Postman pokazuje odpowiedź otrzymaną po złożeniu żądania POST z danymi formularza nagranej wiadomości głosowej o treści „ Cześć ” zawartej w treści żądania.
W tym momencie mamy już funkcjonalną aplikację Express.js, która wysyła i odbiera dane z Dialogflow, dwie części tego artykułu są gotowe. Gdzie teraz pozostaje integracja tego Agenta z aplikacją internetową poprzez wykorzystanie interfejsów API utworzonych tutaj z aplikacji Reactjs.
Integracja z aplikacją internetową
Aby wykorzystać nasze wbudowane API REST, rozszerzymy tę istniejącą aplikację React.js, która ma już stronę główną pokazującą listę win pobranych z API i obsługę dekoratorów za pomocą wtyczki dekoratorów propozycji babel. Zmodyfikujemy to trochę, wprowadzając Mobx do zarządzania stanem, a także nową funkcję polecającą wine z komponentu czatu przy użyciu dodanych punktów końcowych API REST z aplikacji Express.js.
Aby rozpocząć, zaczynamy zarządzać stanem aplikacji za pomocą MobX, ponieważ tworzymy sklep Mobx z kilkoma obserwowalnymi wartościami i kilkoma metodami jako akcjami.
// 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();
Powyżej stworzyliśmy sklep dla funkcji komponentu czatu w aplikacji o następujących wartościach:
-
isChatWindowOpen
Zapisana tutaj wartość kontroluje widoczność komponentu czatu, w którym wyświetlane są wiadomości Dialogflow. -
isLoadingChatMessages
Służy do pokazywania wskaźnika ładowania, gdy wysyłane jest żądanie pobrania odpowiedzi od agenta Dialogflow. -
agentMessages
Ta tablica przechowuje wszystkie odpowiedzi pochodzące z żądań wysłanych w celu uzyskania odpowiedzi od agenta Dialogflow. Dane w tablicy są później wyświetlane w komponencie. -
handleConversation
Ta metoda oznaczona jako akcja dodaje dane do tablicyagentMessages
. Najpierw dodaje wiadomość użytkownika przekazaną jako argument, a następnie wysyła żądanie za pomocą Axios do aplikacji zaplecza, aby uzyskać odpowiedź z Dialogflow. Po rozwiązaniu żądania dodaje odpowiedź z żądania do tablicyagentMessages
.
Uwaga: W przypadku braku obsługi dekoratorów w aplikacji, MobX udostępnia makeObservable , który może być użyty w konstruktorze docelowej klasy sklepu. Zobacz przykład tutaj .
Przy konfiguracji sklepu musimy owinąć całe drzewo aplikacji komponentem wyższego rzędu dostawcy MobX, zaczynając od komponentu głównego w 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;
Powyżej owijamy główny komponent App z MobX Providerem i przekazujemy w utworzonym wcześniej sklepie jako jedną z wartości Providera. Teraz możemy przystąpić do odczytu ze sklepu w obrębie komponentów podłączonych do sklepu.
Tworzenie interfejsu czatu
Aby wyświetlić wiadomości wysłane lub odebrane z żądań API, potrzebujemy nowego komponentu z interfejsem czatu pokazującym wymienione wiadomości. Aby to zrobić, tworzymy nowy komponent, który wyświetla najpierw pewne zakodowane na stałe wiadomości, a później wyświetlamy je w uporządkowanej liście.
// ./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
Powyższy składnik zawiera podstawowe znaczniki HTML potrzebne do aplikacji czatu. Posiada nagłówek pokazujący nazwę Agenta i ikonę do zamykania okna czatu, dymek wiadomości zawierający zakodowany tekst w tagu listy, a na koniec pole wejściowe z obsługą zdarzenia onChange
dołączoną do pola wejściowego do przechowywania tekstu wpisanego do lokalny stan komponentu przy użyciu useState Reacta.
Na powyższym obrazku komponent czatu działa tak, jak powinien, pokazując stylizowane okno czatu z pojedynczą wiadomością czatu i polem wejściowym na dole. Chcemy jednak, aby wyświetlany komunikat był rzeczywistymi odpowiedziami otrzymanymi z żądania API, a nie zakodowanym tekstem.
Kontynuujemy refaktoryzację komponentu Chat, tym razem łącząc i wykorzystując wartości ze sklepu MobX w komponencie.
// ./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));
Z wyróżnionych części kodu powyżej widzimy, że cały komponent czatu został teraz zmodyfikowany, aby wykonać następujące nowe operacje;
- Ma dostęp do wartości sklepu MobX po wstrzyknięciu wartości
ApplicationStore
. Komponent został również obserwatorem tych wartości przechowywania, więc jest ponownie renderowany, gdy zmieni się jedna z wartości. - Rozpoczynamy konwersację z Agentem natychmiast po otwarciu komponentu czatu poprzez wywołanie metody
handleConversation
w haczykuuseEffect
w celu wykonania żądania natychmiast po wyrenderowaniu komponentu. - Korzystamy teraz z wartości
isLoadingMessages
w nagłówku komponentu Chat. Gdy prośba o otrzymanie odpowiedzi od Agenta jest w locie, ustawiamy wartośćisLoadingMessages
natrue
i aktualizujemy nagłówek tak, aby Zara wpisała… - Tablica
agentMessages
w sklepie jest aktualizowana do serwera proxy przez MobX po ustawieniu jej wartości. Z tego komponentu konwertujemy proxy z powrotem na tablicę za pomocą narzędziatoJS
z MobX i przechowujemy wartości w zmiennej w komponencie. Ta tablica jest dalej iterowana w celu wypełnienia dymków czatu wartościami z tablicy za pomocą funkcji mapy.
Teraz za pomocą komponentu chat możemy wpisać zdanie i poczekać na wyświetlenie odpowiedzi w agencie.
Nagrywanie głosu użytkownika
Domyślnie wszyscy agenci Dialogflow mogą akceptować głosowe lub tekstowe dane wejściowe w dowolnym określonym języku od użytkownika. Wymaga to jednak kilku zmian w aplikacji internetowej, aby uzyskać dostęp do mikrofonu użytkownika i nagrać głos.
Aby to osiągnąć, modyfikujemy sklep MobX, aby używał interfejsu API HTML MediaStream Recording do nagrywania głosu użytkownika za pomocą dwóch nowych metod w sklepie 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}`)); }; };
Po kliknięciu ikony nagrywania z komponentu czatu wywoływana jest metoda startAudioConversation
w sklepie MobX powyżej, aby ustawić metodę, której właściwość obserwowalny isRecording
ma wartość true , aby komponent czatu zapewniał wizualną informację zwrotną, aby pokazać, że nagrywanie jest w toku.
Korzystając z interfejsu nawigatora przeglądarki, obiekt Urządzenie multimedialne jest dostępny w celu zażądania mikrofonu urządzenia użytkownika. Po udzieleniu pozwolenia na żądanie getUserMedia
spełnia swoją obietnicę za pomocą danych MediaStream, które następnie przekazujemy do konstruktora MediaRecorder w celu utworzenia rejestratora przy użyciu ścieżek multimedialnych w strumieniu zwróconym z mikrofonu urządzenia użytkownika. Następnie przechowujemy instancję Media Recorder we właściwości recorder
sklepu, ponieważ później uzyskamy do niej dostęp za pomocą innej metody.
Następnie wywołujemy metodę start na instancji rejestratora, a po zakończeniu sesji nagrywania wywoływana jest funkcja ondataavailable
z argumentem zdarzenia zawierającym zarejestrowany strumień w obiekcie Blob, który przechowujemy we właściwości tablicy recordedBits
.
Wylogowując dane w argumencie zdarzenia przekazanym do zdarzenia fired ondataavailable
, możemy zobaczyć obiekt Blob i jego właściwości w konsoli przeglądarki.
Teraz, gdy możemy uruchomić strumień MediaRecorder, musimy być w stanie zatrzymać strumień MediaRecorder, gdy użytkownik zakończy nagrywanie swojego głosu i wyśle wygenerowany plik audio do aplikacji Express.js.
Nowa metoda dodana do sklepu poniżej zatrzymuje strumień i wysyła żądanie POST
zawierające nagrane dane głosowe.
//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!
Wniosek
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.
Bibliografia
- 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