Интеграция агента Dialogflow в приложение React

Опубликовано: 2022-03-10
Краткое резюме ↬ Когда дело доходит до создания помощника по диалоговому чату, который можно использовать на уровне малого или корпоративного уровня, Dialogflow, скорее всего, будет одним из первых вариантов, которые появятся в вашем списке поиска — и почему бы и нет? Он предлагает несколько функций, таких как возможность обработки аудио и текстовых входов, предоставление динамических ответов с использованием настраиваемых веб-перехватчиков, подключение к миллионам устройств с поддержкой Google с помощью помощника Google и многое другое. Но помимо его консоли, которая предназначена для проектирования и управления агентом, как мы можем создать чат-помощник, который также можно использовать в наших встроенных веб-приложениях?

Dialogflow — это платформа, упрощающая процесс создания и разработки помощника по диалоговому чату, обрабатывающего естественный язык, который может обрабатывать голосовой или текстовый ввод при использовании либо из консоли Dialogflow, либо из интегрированного веб-приложения.

Хотя в этой статье кратко объясняется интегрированный агент Dialogflow, ожидается, что вы разбираетесь в Node.js и Dialogflow. Если вы впервые узнаете о Dialogflow, эта статья дает четкое объяснение того, что такое Dialogflow и его концепции.

Эта статья представляет собой руководство по созданию агента Dialogflow с поддержкой голоса и чата, который можно интегрировать в веб-приложение с помощью серверного приложения Express.js в качестве связующего звена между веб-приложением React.js и агентом. на самом Dialogflow. К концу статьи вы сможете подключить свой собственный агент Dialogflow к предпочитаемому вами веб-приложению.

Чтобы упростить освоение этого руководства, вы можете пропустить ту часть руководства, которая вас больше всего интересует, или следовать им в следующем порядке по мере их появления:

  • Настройка агента Dialogflow
  • Интеграция агента Dialogflow
  • Настройка приложения Node Express
    • Аутентификация с помощью Dialogflow
    • Что такое сервисные аккаунты?
    • Обработка голосового ввода
  • Интеграция в веб-приложение
    • Создание интерфейса чата
    • Запись голосового ввода пользователя
  • Заключение
  • использованная литература

1. Настройка агента Dialogflow

Как объясняется в этой статье, чат-помощник в Dialogflow называется агентом и состоит из более мелких компонентов, таких как намерения, выполнение, база знаний и многое другое. Dialogflow предоставляет пользователям консоль для создания, обучения и проектирования потока диалога агента. В нашем случае мы восстановим агент, который был экспортирован в ZIP-папку после обучения, используя функцию экспорта и импорта агента.

Прежде чем мы выполним импорт, нам нужно создать новый агент, который будет объединен с агентом, который нужно восстановить. Чтобы создать нового агента из консоли, необходимо уникальное имя, а также проект в облаке Google, с которым будет связан агент. Если в Google Cloud нет существующего проекта для связи, здесь можно создать новый.

Ранее был создан и обучен агент, который будет рекомендовать винные продукты пользователю в зависимости от его бюджета. Этот агент был экспортирован в ZIP; Вы можете загрузить папку здесь и восстановить ее в нашем недавно созданном агенте на вкладке «Экспорт и импорт» на странице настроек агента.

Восстановление ранее экспортированного агента из папки ZIP
Восстановление ранее экспортированного агента из папки ZIP. (Большой превью)

Импортный агент был предварительно обучен рекомендовать пользователю винный продукт на основе бюджета пользователя на покупку бутылки вина.

Пройдя через импортированный агент, мы увидим, что у него есть три созданных намерения со страницы намерений. Одно из них — резервное намерение, используемое, когда агент не распознает ввод пользователя, другое — приветственное намерение, используемое при начале разговора с агентом, а последнее намерение используется для рекомендации вина пользователю на основе Параметр суммы в предложении. Нас беспокоит намерение get-wine-recommendation

Это намерение имеет один входной контекст wine-recommendation , исходящий из приветственного намерения по умолчанию, чтобы связать разговор с этим намерением.

«Контекст — это система внутри агента, используемая для управления потоком разговора от одного намерения к другому».

Под контекстами находятся обучающие фразы, которые представляют собой предложения, используемые для обучения агента тому, какого типа утверждения ожидать от пользователя. Благодаря большому количеству обучающих фраз в намерении агент может распознать предложение пользователя и намерение, в которое оно входит.

Обучающие фразы в намерении наших агентов get-wine-recommendation (как показано ниже) указывают на выбор вина и ценовую категорию:

Список доступных обучающих фраз с рекомендацией получить вино.
Страница намерения Get-wine-recommendation с доступными обучающими фразами. (Большой превью)

Глядя на изображение выше, мы видим перечисленные доступные обучающие фразы, и цифра валюты выделена желтым цветом для каждой из них. Это выделение известно как аннотация в Dialogflow, и оно автоматически выполняется для извлечения распознанных типов данных, известных как сущность, из предложения пользователя.

После того, как это намерение будет согласовано в диалоге с агентом, будет сделан HTTP-запрос к внешней службе, чтобы получить рекомендуемое вино на основе цены, извлеченной в качестве параметра из предложения пользователя, с помощью включенного веб-перехватчика, найденного в раздел Исполнение в нижней части этой страницы намерения.

Мы можем протестировать агент с помощью эмулятора Dialogflow, расположенного в правой части консоли Dialogflow. Чтобы проверить, мы начинаем разговор с сообщения « Привет » и продолжаем с желаемым количеством вина. Веб-перехватчик будет немедленно вызван, и агент покажет расширенный ответ, подобный приведенному ниже.

Тестирование импортированного веб-перехватчика агента агента.
Тестирование веб-перехватчика выполнения импортированного агента с помощью эмулятора агента в консоли. (Большой превью)

На изображении выше мы видим URL-адрес веб-перехватчика, сгенерированный с помощью Ngrok, и ответ агента справа, показывающий вино в ценовом диапазоне 20 долларов, введенное пользователем.

На данный момент агент Dialogflow полностью настроен. Теперь мы можем приступить к интеграции этого агента в веб-приложение, чтобы позволить другим пользователям получать доступ к агенту и взаимодействовать с ним без доступа к нашей консоли Dialogflow.

Еще после прыжка! Продолжить чтение ниже ↓

Интеграция агента Dialogflow

Хотя существуют и другие способы подключения к агенту Dialogflow, такие как выполнение HTTP-запросов к его конечным точкам REST, рекомендуемый способ подключения к Dialogflow — использование его официальной клиентской библиотеки, доступной на нескольких языках программирования. Для JavaScript пакет @google-cloud/dialogflow доступен для установки из NPM.

Внутри пакет @google-cloud/dialogflow использует gRPC для своих сетевых подключений, и это делает пакет неподдерживаемым в среде браузера, за исключением случаев, когда исправление выполняется с помощью веб-пакета. Рекомендуемый способ использования этого пакета — из среды Node. Мы можем сделать это, настроив серверное приложение Express.js для использования этого пакета, а затем отправляем данные в веб-приложение через его конечные точки API, и это то, что мы будем делать дальше.

Настройка приложения Node Express

Чтобы настроить экспресс-приложение, мы создаем новый каталог проекта, а затем получаем необходимые зависимости, используя yarn из открытого терминала командной строки.

 # 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

Установив необходимые зависимости, мы можем приступить к настройке очень компактного сервера Express.js, который обрабатывает соединения на указанном порту с включенной поддержкой CORS для веб-приложения.

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

При выполнении код в приведенном выше фрагменте запускает HTTP-сервер, который прослушивает соединения на указанном PORT Express.js. Он также поддерживает совместное использование ресурсов между источниками (CORS) для всех запросов с использованием пакета cors в качестве промежуточного программного обеспечения Express. На данный момент этот сервер только прослушивает подключения, он не может ответить на запрос, потому что у него нет созданного маршрута, поэтому давайте создадим его.

Теперь нам нужно добавить два новых маршрута: один для отправки текстовых данных, а другой для отправки записанного голосового ввода. Они оба примут запрос POST и позже отправят данные, содержащиеся в теле запроса, агенту 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

Выше мы создали отдельный экземпляр маршрутизатора для двух созданных маршрутов POST , которые на данный момент отвечают только кодом состояния 200 и жестко заданным фиктивным ответом. Когда мы завершим аутентификацию с помощью Dialogflow, мы можем вернуться к реализации фактического подключения к Dialogflow в этих конечных точках.

На последнем этапе настройки нашего серверного приложения мы монтируем ранее созданный экземпляр маршрутизатора, созданный в приложении Express, используя app.use и базовый путь для маршрута.

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

Выше мы добавили базовый путь к двум маршрутам. Два из них мы можем протестировать через POST -запрос, используя cURL из командной строки, как это делается ниже с пустым телом запроса;

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

После успешного выполнения вышеуказанного запроса мы можем ожидать, что ответ, содержащий данные объекта, будет выведен на консоль.

Теперь нам осталось установить фактическое соединение с Dialogflow, которое включает в себя обработку аутентификации, отправку и получение данных от агента в Dialogflow с использованием пакета @google-cloud/dialogflow.

Аутентификация с помощью Dialogflow

Каждый созданный агент Dialogflow связан с проектом в Google Cloud. Для внешнего подключения к агенту Dialogflow мы аутентифицируемся в проекте в облаке Google и используем Dialogflow в качестве одного из ресурсов проекта. Из шести доступных способов подключения к проекту в google-cloud использование варианта «Сервисные аккаунты» наиболее удобно при подключении к конкретному сервису в google cloud через его клиентскую библиотеку.

Примечание . Для готовых к работе приложений рекомендуется использовать ключи API с коротким сроком действия вместо ключей учетной записи службы, чтобы снизить риск попадания ключа учетной записи службы в чужие руки.

Что такое сервисные аккаунты?

Служебные учетные записи — это особый тип учетной записи в Google Cloud, созданный для нечеловеческого взаимодействия, в основном через внешние API. В нашем приложении доступ к сервисной учетной записи будет осуществляться через сгенерированный ключ клиентской библиотекой Dialogflow для аутентификации в Google Cloud.

Облачная документация по созданию учетных записей служб и управлению ими представляет собой отличное руководство по созданию учетной записи службы. При создании учетной записи службы роль администратора Dialogflow API должна быть назначена созданной учетной записи службы, как показано на последнем шаге. Эта роль дает учетной записи службы административный контроль над связанным агентом Dialogflow.

Чтобы использовать учетную запись службы, нам нужно создать ключ учетной записи службы. Следующие шаги ниже описывают, как создать его в формате JSON:

  1. Щелкните вновь созданную учетную запись службы, чтобы перейти на страницу учетной записи службы.
  2. Прокрутите до раздела «Ключи», щелкните раскрывающийся список « Добавить ключ » и выберите параметр « Создать новый ключ », который открывает модальное окно.
  3. Выберите формат файла JSON и нажмите кнопку «Создать» в правом нижнем углу модального окна.

Примечание. Рекомендуется хранить ключ сервисной учетной записи в секрете и не передавать его в какую-либо систему контроля версий, поскольку он содержит очень конфиденциальные данные о проекте в Google Cloud. Это можно сделать, добавив файл в файл .gitignore .

Создав учетную запись службы и ключ учетной записи службы, доступный в каталоге нашего проекта, мы можем использовать клиентскую библиотеку Dialogflow для отправки и получения данных от агента 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;

Весь описанный выше маршрут отправляет данные агенту Dialogflow и получает ответ, выполнив следующие действия.

  • Первый
    Он проходит аутентификацию в облаке Google, затем создает сеанс с Dialogflow, используя идентификатор проекта облачного проекта Google, связанный с агентом Dialogflow, а также случайный идентификатор для идентификации созданного сеанса. В нашем приложении мы создаем идентификатор UUID для каждого сеанса, созданного с использованием пакета JavaScript UUID. Это очень полезно при регистрации или отслеживании всех диалогов, обрабатываемых агентом Dialogflow.
  • Второй
    Мы создаем данные объекта запроса в соответствии с форматом, указанным в документации Dialogflow. Этот объект запроса содержит созданный сеанс и данные сообщения, полученные из тела запроса, которые должны быть переданы агенту Dialogflow.
  • В третьих
    Используя метод detectIntent из сеанса detectIntent , мы отправляем объект запроса асинхронно и ожидаем ответа Агента, используя синтаксис ES6 async/await в блоке try-catch. чем сбой всего приложения. Образец объекта ответа, возвращаемого агентом, предоставляется в документации Dialogflow, и его можно проверить, чтобы узнать, как извлечь данные из объекта.

Мы можем использовать Postman для тестирования соединения Dialogflow, реализованного выше в dialogflow-response . Postman — это платформа для совместной разработки API с функциями для тестирования API, созданных на этапах разработки или производства.

Примечание. Если настольное приложение Postman еще не установлено, для тестирования API не требуется. Начиная с сентября 2020 года веб-клиент Postman перешел в общедоступное состояние (GA), и его можно использовать непосредственно из браузера.

Используя веб-клиент Postman, мы можем либо создать новую рабочую область, либо использовать существующую, чтобы создать POST -запрос к нашей конечной точке API по адресу https://localhost:5000/api/agent/text-input и добавить данные с ключом message и значение « Hi There » в параметры запроса.

При нажатии кнопки « Отправить » на работающий сервер Express будет отправлен запрос POST с ответом, подобным показанному на изображении ниже:

Тестирование конечной точки API ввода текста с помощью Postman.
Тестирование конечной точки API ввода текста с помощью Postman. (Большой превью)

На изображении выше мы видим предварительно обработанные данные ответа от агента Dialogflow через сервер Express. Возвращаемые данные форматируются в соответствии с образцом определения ответа, приведенным в документации Dialogflow Webhook.

Обработка голосового ввода

По умолчанию все агенты Dialogflow могут обрабатывать как текстовые, так и аудиоданные, а также возвращать ответ в текстовом или аудиоформате. Однако работа с входными или выходными аудиоданными может быть немного сложнее, чем с текстовыми данными.

Для обработки и обработки голосового ввода мы начнем реализацию для конечной точки /voice-input , которую мы создали ранее, чтобы получать аудиофайлы и отправлять их в Dialogflow в обмен на ответ от Агента:

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

В общем, указанный выше маршрут /voice-input получает голосовой ввод пользователя в виде файла, содержащего сообщение, передаваемое помощнику чата, и отправляет его агенту Dialogflow. Чтобы лучше понять этот процесс, мы можем разбить его на следующие более мелкие шаги:

  • Во-первых, мы добавляем и используем connect-busboy в качестве промежуточного программного обеспечения Express для анализа данных формы, отправляемых в запросе из веб-приложения. После чего мы аутентифицируемся в Dialogflow с помощью служебного ключа и создаем сессию так же, как и в предыдущем маршруте.
    Затем, используя метод Promisify из встроенного служебного модуля Node.js, мы получаем и сохраняем обещание, эквивалентное методу конвейера Stream, которое будет использоваться позже для передачи нескольких потоков, а также выполнять очистку после завершения потоков.
  • Затем мы создаем объект запроса, содержащий сеанс аутентификации Dialogflow и конфигурацию аудиофайла, который будет отправлен в Dialogflow. Вложенный объект конфигурации аудио позволяет агенту Dialogflow выполнять преобразование речи в текст в отправленном аудиофайле.
  • Затем, используя созданный сеанс и объект запроса, мы определяем намерение пользователя из аудиофайла с помощью метода detectStreamingIntent , который открывает новый поток данных от агента Dialogflow для серверного приложения. Данные будут отправляться обратно небольшими битами через этот поток, и, используя « событие » данных из читаемого потока, мы сохраняем данные в переменной streamData для последующего использования. После закрытия потока запускается событие « end », и мы отправляем ответ от агента Dialogflow, хранящегося в переменной streamData , в веб-приложение.
  • Наконец, используя событие файлового потока от connect-busboy, мы получаем поток аудиофайла, отправленного в теле запроса, и далее передаем его в промис, эквивалентный конвейеру, который мы создали ранее. Функция этого состоит в том, чтобы направить поток аудиофайла, поступающий из запроса, в поток Dialogflow, мы направляем поток аудиофайла в поток, открытый с помощью метода detectStreamingIntent выше.

Чтобы проверить и убедиться, что описанные выше шаги работают, как указано, мы можем сделать тестовый запрос, содержащий аудиофайл в теле запроса, к конечной точке /voice-input с помощью Postman.

Тестирование конечной точки API голосового ввода с помощью Postman.
Тестирование конечной точки API голосового ввода с помощью почтальона с записанным голосовым файлом. (Большой превью)

Приведенный выше результат Postman показывает ответ, полученный после выполнения запроса POST с данными формы записанного сообщения голосовой заметки, говорящего « Привет », включенного в тело запроса.

На данный момент у нас есть функциональное приложение Express.js, которое отправляет и получает данные от Dialogflow, две части этой статьи выполнены. Что теперь осталось с интеграцией этого агента в веб-приложение, используя API-интерфейсы, созданные здесь из приложения Reactjs.

Интеграция в веб-приложение

Чтобы использовать наш встроенный REST API, мы расширим это существующее приложение React.js, у которого уже есть домашняя страница, показывающая список вин, полученных из API, и поддержка декораторов с помощью плагина декораторов предложения babel. Мы немного рефакторим его, представив Mobx для управления состоянием, а также новую функцию, позволяющую рекомендовать вино из компонента чата с использованием добавленных конечных точек REST API из приложения Express.js.

Для начала мы начинаем управлять состоянием приложения с помощью MobX, создавая хранилище Mobx с несколькими наблюдаемыми значениями и некоторыми методами в качестве действий.

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

Выше мы создали хранилище для функции компонента чата в приложении со следующими значениями:

  • isChatWindowOpen
    Сохраненное здесь значение управляет видимостью компонента чата, в котором отображаются сообщения Dialogflow.
  • isLoadingChatMessages
    Это используется для отображения индикатора загрузки, когда делается запрос на получение ответа от агента Dialogflow.
  • agentMessages
    В этом массиве хранятся все ответы, поступающие от запросов, сделанных для получения ответа от агента Dialogflow. Данные в массиве позже отображаются в компоненте.
  • handleConversation
    Этот метод, оформленный как действие, добавляет данные в массив agentMessages . Во-первых, он добавляет сообщение пользователя, переданное в качестве аргумента, а затем делает запрос, используя Axios, к серверному приложению, чтобы получить ответ от Dialogflow. После того, как запрос разрешен, он добавляет ответ из запроса в массив agentMessages .

Примечание. При отсутствии поддержки декораторов в приложении MobX предоставляет makeObservable , который можно использовать в конструкторе целевого класса хранилища. См. пример здесь .

При настройке хранилища нам необходимо обернуть все дерево приложений компонентом высшего порядка MobX Provider, начиная с корневого компонента в файле 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;

Выше мы оборачиваем корневой компонент приложения с помощью провайдера MobX и передаем ранее созданное хранилище в качестве одного из значений провайдера. Теперь мы можем перейти к чтению из хранилища в компонентах, подключенных к хранилищу.

Создание интерфейса чата

Чтобы отображать сообщения, отправленные или полученные из запросов API, нам нужен новый компонент с некоторым интерфейсом чата, показывающим перечисленные сообщения. Для этого мы создаем новый компонент, который сначала отображает некоторые жестко запрограммированные сообщения, а затем мы отображаем сообщения в упорядоченном списке.

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

Вышеупомянутый компонент имеет базовую HTML-разметку, необходимую для приложения чата. Он имеет заголовок, показывающий имя агента и значок для закрытия окна чата, всплывающее сообщение, содержащее жестко закодированный текст в теге списка, и, наконец, поле ввода с обработчиком события onChange , прикрепленным к полю ввода для хранения текста, введенного в локальное состояние компонента с помощью useState React.

Предварительный просмотр компонента чата с жестко закодированным сообщением от агента чата.
Предварительный просмотр компонента чата с жестко закодированным сообщением от агента чата. (Большой превью)

На изображении выше компонент чата работает так, как должен, показывая стилизованное окно чата с одним сообщением чата и полем ввода внизу. Однако мы хотим, чтобы отображаемое сообщение было фактическим ответом, полученным от запроса API, а не жестко закодированным текстом.

Мы переходим к рефакторингу компонента чата, на этот раз подключая и используя значения в хранилище MobX внутри компонента.

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

Из выделенных частей кода выше мы видим, что весь компонент чата теперь был изменен для выполнения следующих новых операций:

  • Он имеет доступ к значениям хранилища MobX после внедрения значения ApplicationStore . Компонент также стал наблюдателем этих хранимых значений, поэтому он перерисовывается при изменении одного из значений.
  • Мы начинаем беседу с агентом сразу после открытия компонента чата, вызывая метод handleConversation в хуке useEffect , чтобы сделать запрос сразу же, когда компонент отрисовывается.
  • Теперь мы используем значение isLoadingMessages в заголовке компонента Chat. Когда запрос на получение ответа от агента находится в обработке, мы устанавливаем значение isLoadingMessages в true и обновляем заголовок, чтобы Zara печатала…
  • Массив agentMessages в хранилище обновляется MobX до прокси-сервера после установки его значений. Из этого компонента мы преобразуем этот прокси обратно в массив с помощью утилиты toJS из MobX и сохраняем значения в переменной внутри компонента. Затем этот массив повторяется, чтобы заполнить пузырьки чата значениями из массива с помощью функции сопоставления.

Теперь, используя компонент чата, мы можем ввести предложение и дождаться отображения ответа в агенте.

Компонент чата, показывающий список данных, возвращенных из HTTP-запроса в экспресс-приложение.
Компонент чата, показывающий список данных, возвращенных из HTTP-запроса в экспресс-приложение. (Большой превью)

Запись голосового ввода пользователя

По умолчанию все агенты Dialogflow могут принимать от пользователя голосовой или текстовый ввод на любом указанном языке. Однако для получения доступа к микрофону пользователя и записи голосового ввода требуется несколько настроек веб-приложения.

Для этого мы модифицируем хранилище MobX, чтобы использовать API записи HTML MediaStream для записи голоса пользователя с помощью двух новых методов в хранилище 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}`)); }; };

При щелчке значка записи в компоненте чата вызывается метод startAudioConversation в хранилище MobX выше, чтобы установить для метода наблюдаемого свойства isRecording значение true , чтобы компонент чата предоставлял визуальную обратную связь, чтобы показать, что запись в процессе.

С помощью интерфейса навигатора браузера осуществляется доступ к объекту Media Device для запроса микрофона пользовательского устройства. После предоставления разрешения на запрос getUserMedia он разрешает свое обещание с помощью данных MediaStream, которые мы далее передаем конструктору MediaRecorder для создания устройства записи с использованием дорожек мультимедиа в потоке, возвращаемом с микрофона устройства пользователя. Затем мы сохраняем экземпляр медиа-рекордера в свойстве recorder хранилища, так как позже мы будем обращаться к нему из другого метода.

Затем мы вызываем метод start для экземпляра устройства записи, и после завершения сеанса записи функция ondataavailable запускается с аргументом события, содержащим записанный поток в Blob, который мы сохраняем в свойстве массива recordedBits .

Извлекая данные из аргумента события, переданного в событие fired ondataavailable , мы можем увидеть большой двоичный объект и его свойства в консоли браузера.

Консоль Browser Devtools, показывающая вышедший из системы большой двоичный объект, созданный средством записи мультимедиа после завершения записи.
Консоль Browser Devtools, показывающая вышедший из системы большой двоичный объект, созданный средством записи мультимедиа после завершения записи. (Большой превью)

Теперь, когда мы можем запустить поток MediaRecorder, нам нужно иметь возможность остановить поток MediaRecorder, когда пользователь закончит запись своего голосового ввода, и отправить сгенерированный аудиофайл в приложение Express.js.

Новый метод, добавленный в хранилище ниже, останавливает поток и делает запрос POST , содержащий записанный голосовой ввод.

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

Заключение

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.

использованная литература

  • 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