Глава 11: Создание пользовательской интеграции с SDK Dialogflow
В предыдущей главе мы описали веб-перехватчики; они выполняют намерение для отправки динамического кода после сопоставления намерения. Вы можете использовать библиотеки выполнения Dialogflow для этого или просто извлечь результаты из отправленного запроса Dialogflow. Он выполняет ваш собственный (бэкэнд) код. После того, как обнаружение намерения происходит с помощью интеграций Dialogflow, вам не нужно вызывать метод detectIntent вручную.
Dialogflow также предоставляет SDK, и это удобно, когда вы хотите создать свои собственные интеграции вместо использования готовых интеграций в Dialogflow. Примеры включают реализацию чат-бота в вашем собственном мобильном приложении, на вашем собственном оборудовании и интеграцию его в ваш собственный веб-сайт. Это означает, что вам нужно будет вручную вызывать/предоставлять следующие шаги (см. Рисунок 11-1):
- Реализация пользовательского интерфейса для интеграции (фронтенд) обычно включает поле textarea для отображения ответов и поле ввода для ввода высказываний.
- Реализация бэкэнда, которая интегрирует SDK Dialogflow для:
- Управления сеансами
- Аутентификации
- Обнаружения намерений (detectIntent)
- Возврата ответов
- Расширенные ответы в пользовательском интерфейсе реализации.
Рисунок 11-1. Архитектура пользовательской интеграции. От реализации пользовательского интерфейса к бэкэнд-приложению, которое взаимодействует с SDK Dialogflow
Ваш пользователь разговаривает с пользовательским интерфейсом вашей интеграции (например, фронтендом вашего веб-сайта).
Пользовательский интерфейс передает введенные сообщения чат-бота (или входящие аудиопотоки) в бэкэнд-интеграцию. Бэкэнд-интеграция разговаривает с SDK Dialogflow.
Dialogflow возвращает ответы бэкэнд-коду, и бэкэнд-код отправляет их обратно в пользовательский интерфейс, чтобы пользовательский интерфейс мог отображать ответы чат-бота на экране. Дополнительным преимуществом такого подхода является то, что он также не будет раскрывать ваш сервисный аккаунт (ключи аутентификации) фронтенду, чего вы не должны хотеть, так как хакеры могут легко прочитать и использовать ваши ключи.
Реализация пользовательского чат-бота на фронтенде вашего веб-сайта, Настройка
Для реализации пользовательской интеграции, например, на веб-сайте, вам нужно будет убедиться, что у вас включен API Dialogflow:
gcloud services enable dialogflow.googleapis.com
Также требуется загрузить сервисный аккаунт Dialogflow Integrations на ваш жесткий диск и назначить его переменной среды GOOGLE_APPLICATION_CREDENTIALS.
После этого вам нужно будет убедиться, что вы вошли в систему с помощью инструментов командной строки в правильный проект:
gcloud init
Если вы не выполните эти шаги, вы получите эту ошибку:
(node:95119) UnhandledPromiseRejectionWarning:
Error: 7 PERMISSION_DENIED: IAM permission 'dialogflow.sessions.detectIntent' on 'projects/project-id/agent' denied.
Просмотрите Главу 2 о том, как настроить ваш проект Dialogflow для получения дополнительной информации.
Реализация пользовательского интерфейса
Листинг 11-1 показывает пример HTML-страницы, которая включает пользовательский интерфейс чат-бота Dialogflow. В реальном мире вы, вероятно, захотите использовать клиентский фреймворк, такой как Angular, но чтобы сделать этот пример простым, давайте оставим его красивым и компактным.
Листинг 11-1. Пример пользовательского интерфейса чат-бота в HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Custom Web Chat</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
<!-- //1 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
<!-- //2 Найдите таблицу стилей в Git-репозитории этой книги-->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- //3-->
<form id="chatbox" autocomplete="off">
<h1 style="font-size: 18px;">Custom Web Chat</h1>
<div class="chatarea" id="ca">
<ul id="messages" class="history"></ul>
</div>
<div class="chatfooter">
<!-- //4-->
<div class="chatinput">
<input id="queryText" class="chatinput" placeholder="Reply to chatbot...">
</div>
<button type="submit" id="submit">Send</button>
</div>
</form>
<script>
//a) Загрузить socket io
const socketio = io();
//b) Как только socket.io установит соединение с серверным приложением,
// выполнить этот блок
const socket = socketio.on('connect', function() {
console.log('connected');
//c) Запустить этот блок, когда сервер ответит выполнением
socketio.on('returnResults', function (data) {
var objDiv = document.getElementById("ca");
console.log(data);
//d) Если есть queryResults, то динамически
// создать элементы списка для добавления к списку сообщений.
if(data[0].queryResult){
var agent = document.createElement("li");
agent.className = 'balloon agent';
agent.innerHTML = data[0].queryResult.fulfillmentText;
messages.appendChild(agent);
objDiv.scrollTop = objDiv.scrollHeight;
}
});
//welcome
socketio.emit('welcome');
});
//e) Создать несколько указателей на другие HTML-элементы
// const textarea = document.getElementById('textarea'); // Этот элемент не используется в предоставленном коде
const textInput = document.getElementById('queryText');
const submitBtn = document.getElementById('submit');
const messages = document.getElementById('messages');
//f) При отправке высказывания пользователя,
// создать элементы списка для добавления к списку сообщений.
submitBtn.onclick = function(e) {
e.preventDefault();
socketio.emit('message', textInput.value);
var user = document.createElement("li");
user.className = 'balloon user';
user.innerHTML = textInput.value;
messages.appendChild(user);
textInput.value = ""; // Очистить поле ввода
objDiv.scrollTop = objDiv.scrollHeight; // Прокрутить вниз
}
</script>
</body>
</html>
При просмотре этого кода следует обратить внимание на пару ключевых моментов:
- В заголовке я загружаю файл JavaScript из CDN: socket.io.js Socket.IO обеспечивает двунаправленную связь в реальном времени на основе событий. Он работает на любой платформе, в браузере или на устройстве, одинаково уделяя внимание надежности и скорости.
- Таблица стилей style.css загрузит несколько приятных стилей, чтобы текст пользователя и агента выглядел как текстовые шарики.
- В теле HTML-документа я создаю элемент формы. Этот элемент формы будет содержать список сообщений чат-бота. Это сообщения пользователя и сообщения агента Dialogflow.
- Форма также содержит поле ввода текста с кнопкой отправки; это позволяет пользователю вводить вопросы чат-боту.
- Теперь есть некоторый клиентский JavaScript для запуска:
- Сначала загрузите объект JavaScript Socket.IO.
- Как только Socket.IO установит соединение с серверным приложением, выполните этот блок кода.
- Запустите этот блок, когда сервер ответит выполнением.
- Если есть queryResults, то динамически создайте элементы списка для добавления к списку сообщений.
- Создайте несколько указателей на другие HTML-элементы.
- При отправке высказывания пользователя создайте элементы списка для добавления к списку сообщений.
Реализация бэкэнда
Теперь перейдем к бэкэнд-коду. Это приложение Node.js Express.
Для этого проекта мы будем использовать библиотеки npm из Листинга 11-2.
Листинг 11-2. Бэкэнд package.json, содержащий все библиотеки
{
"name": "dialogflow-custom-integration",
"version": "1.0.0",
"author": "Lee Boonstra",
"license": "Apache-2.0",
"description": "Custom Web integration with the Dialogflow SDK",
"engines": {
"node": ">=10.9.0 <13.0.0", // Уточненные версии Node.js
"yarn": ">=1.17.3 <=1.19.1" // Версии Yarn, если используется
},
"scripts": {
"start": "node app.js"
},
"private": true,
"dependencies": {
"cors": "^2.8.5",
"dialogflow": "^0.14.1", // Уточненная версия, может потребоваться обновление
"dotenv": "^8.2.0",
"express": "^4.17.1",
"socket.io": "^2.3.0", // Уточненная версия
"uuid": "^3.3.3" // Или более новая версия
}
}
Вот код, который будет запускать бэкэнд-интеграцию чат-бота.
Листинг 11-3. Код бэкэнд-интеграции, который взаимодействует с SDK Dialogflow
//1) Эти системные переменные можно установить из командной строки с помощью --PROJECT_ID, --PORT и --LANGUAGE
const projectId = process.env.npm_config_PROJECT_ID;
const port = ( process.env.npm_config_PORT || 3000 );
const languageCode = (process.env.npm_config_LANGUAGE || 'en-US');
//2) Загрузить все библиотеки, необходимые для этого приложения
const socketIo = require('socket.io');
const http = require('http');
const cors = require('cors');
const express = require('express');
const path = require('path');
// Это специфично для Dialogflow
const uuid = require('uuid');
const df = require('dialogflow').v2beta1; // Используем v2beta1
//3) Создать приложение express
const app = express();
//4) Настроить Express и загрузить статические файлы и HTML-страницу
app.use(cors());
app.use(express.static(path.join(__dirname, '/../ui/'))); // Убедитесь, что путь правильный
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, '/../ui/index.html')); // Убедитесь, что путь правильный
});
//5) Создать сервер и слушать переменную PORT
const server = http.createServer(app); // Передаем приложение Express
let io; // Объявляем io здесь
// Глобальные переменные для Dialogflow
let sessionClient;
let sessionPath;
let request = {}; // Инициализируем объект request
//6) Слушатель Socket.io, как только клиент подключится к сокету сервера
// затем выполнить этот блок.
io = socketIo(server); // Инициализируем Socket.IO с сервером
io.on('connect', (client) => {
console.log(`Client connected [id=${client.id}]`);
client.emit('server_setup', `Server connected [id=${client.id}]`);
//7) Когда клиент отправляет события 'message'
// затем выполнить этот блок
client.on('message', async function(msg) {
//console.log(msg);
//8) Обещание выполнить сопоставление намерений
const results = await detectIntent(msg);
console.log(results);
//9) Вернуть Dialogflow после сопоставления намерений в пользовательский интерфейс клиента.
client.emit('returnResults', results);
});
// Обработка события 'welcome' от клиента
client.on('welcome', async function() {
const welcomeResults = await detectIntentByEventName('WELCOME'); // Используем событие WELCOME по умолчанию
client.emit('returnResults', welcomeResults);
});
});
/**
* Настройка интеграции Dialogflow
*/
function setupDialogflow(){
//10) Dialogflow понадобится идентификатор сеанса
const sessionId = uuid.v4(); // Генерируем уникальный sessionId при настройке
//11) Dialogflow понадобится клиент сеанса DF
// Таким образом, каждый сеанс DF уникален
sessionClient = new df.SessionsClient(); // Инициализируем sessionClient
//12) Создать путь сеанса из клиента сеанса,
// который является комбинацией projectId и sessionId.
sessionPath = sessionClient.sessionPath(projectId, sessionId); // Используем глобальный projectId
//13) Эти объекты находятся в запросе Dialogflow
request = {
session: sessionPath,
queryInput: {}
};
}
/*
* Обнаружение намерения Dialogflow на основе текста
* @param text - строка
* @return обещание ответа
*/
async function detectIntent(text){
//14) Получить высказывание пользователя из пользовательского интерфейса
request.queryInput.text = {
languageCode: languageCode,
text: text
};
console.log(request);
//15) Метод SDK Dialogflow для обнаружения намерений.
// Он возвращает обещание, которое будет разрешено, как только
// данные выполнения поступят.
const responses = await sessionClient.detectIntent(request);
return responses;
}
/*
* Обнаружение намерения Dialogflow на основе события
* @param eventName - строка
* @return обещание ответа
*/
async function detectIntentByEventName(eventName){
request.queryInput.event = {
languageCode: languageCode,
name: eventName
};
console.log(request);
const responses = await sessionClient.detectIntent(request);
// Удалить событие, чтобы событие приветствия не вызывалось снова
delete request.queryInput.event;
return responses;
}
// Запустить этот код.
setupDialogflow(); // Вызываем функцию настройки Dialogflow
server.listen(port, () => { // Запускаем сервер Express
console.log('Running server on port %s', port);
});
- Эти системные переменные можно установить из командной строки с помощью
--PROJECT_ID
,--PORT
и--LANGUAGE
. - Загрузите все библиотеки, необходимые для этого приложения, причем последние специфичны для Dialogflow.
- Создайте приложение Express.
- Настройте Express и загрузите статические файлы и HTML-страницу.
- Создайте сервер и слушайте переменную PORT.
- Слушатель Socket.IO, как только клиент подключится к сокету сервера, выполните этот блок.
- Когда клиент отправляет события ‘message’, выполните этот блок.
- Это содержит функцию для выполнения сопоставления намерений.
- Как только результаты поступят, верните их в пользовательский интерфейс клиента.
- Dialogflow понадобится уникальный идентификатор сеанса:
sessionId = uuid.v4();
- Dialogflow понадобится клиент сеанса Dialogflow, чтобы сделать этот сеанс уникальным для своего пользователя:
sessionClient = new df.SessionsClient();
- Создайте путь сеанса из клиента сеанса, комбинацию projectId и sessionId. Теперь сеанс будет принадлежать только вашему агенту Dialogflow.
sessionPath = sessionClient.sessionPath(projectId, sessionId);
- Эти объекты находятся в запросе Dialogflow:
request = { session: sessionPath, queryInput: { text: { // Или event: {} languageCode: languageCode // text: text_input или name: event_name } } }
- Получите высказывание пользователя из пользовательского интерфейса.
- Это метод SDK Dialogflow для обнаружения намерений. Он возвращает обещание, которое будет разрешено, как только данные выполнения поступят:
await sessionClient.detectIntent(request);
Рисунок 11-2 показывает, как это будет выглядеть.
Рисунок 11-2. Рабочая пользовательская интеграция чата в веб-браузере
Чтобы запустить этот пример из репозитория GitHub, который принадлежит этой книге, вам нужно будет перейти в папку back-end.
Оттуда установите необходимые библиотеки:
npm install
Запустите приложение Node:
npm --PROJECT_ID=[ваш-id-проекта-google-cloud] run start
Перейдите в браузере по адресу http://localhost:3000.
Приветственное сообщение
Частый вопрос, который я постоянно получаю от людей, реализующих свои собственные интеграции: как я могу убедиться, что мой чат-бот приветствует пользователя при открытии чата? Это не так уж сложно; мы можем вызывать события в Dialogflow, и мы сделаем это, как только Socket.IO установит правильное соединение с клиентом (что происходит после загрузки страницы). Обратите внимание на Листинг 11-4.
Добавьте в index.html внутри слушателя connect:
socketio.emit('welcome');
Затем в вашем бэкэнд-файле app.js, внутри слушателя connect, мы будем слушать сообщения welcome, отправленные из пользовательского интерфейса.
Листинг 11-4. Прослушивание приветственного сообщения
client.on('welcome', async function() {
const welcomeResults = await detectIntentByEventName('welcome'); // Используем имя события 'welcome', можно изменить
client.emit('returnResults', welcomeResults);
});
Следующая новая функция в Листинге 11-5 работает довольно похоже на вызов detectIntent из Листинга 11-3, но вместо передачи текстового ввода запроса мы будем использовать имя события.
Листинг 11-5. Обнаружение намерения по имени события
async function detectIntentByEventName(eventName){
request.queryInput.event = {
languageCode: languageCode,
name: eventName
};
const responses = await sessionClient.detectIntent(request);
//удалить событие, чтобы событие приветствия не вызывалось снова
delete request.queryInput.event;
return responses;
}
Какое имя события? Событие приветствия, так как наше намерение приветствия имеет это событие, прикрепленное к нему, что вы можете видеть на Рисунке 11-3.
Рисунок 11-3. Событие Welcome в Dialogflow
Создание расширенных ответов в вашей интеграции чат-бота
Когда вы хотите использовать ответы с расширенными сообщениями в своей реализации, вам нужно будет создавать их вручную. Это имеет смысл, так как вы управляете пользовательским интерфейсом.
Давайте создадим несколько ответов с расширенными сообщениями в нашей интеграции.
Компонент гиперссылки, карта Google и компонент изображения
Сначала создайте новое намерение с обучающими фразами типа «У вас есть веб-адрес?»
Вам не нужно указывать текстовый ответ. Вместо этого вы создадите новую пользовательскую полезную нагрузку (custom payload), которая использует наш пользовательский JSON, как показано на Рисунке 11-4:
{
"web": {
"type": "hyperlink",
"text": "Check My Website",
"link": "http://www.leeboonstra.com"
}
}
Рисунок 11-4. Пользовательская полезная нагрузка для пользовательского расширенного сообщения с гиперссылкой
В вашей бэкэнд-интеграции, после запуска метода detectIntent, вам нужно будет пройтись по массиву queryResult.fulfillmentMessages. Он должен содержать объект с сообщением, установленным в полезную нагрузку.
fulfillmentMessages: Array(1)
0:
message: "payload"
payload:
fields:
platform: {stringValue: "custom-web", kind: "stringValue"}
web: {structValue: {...}, kind: "structValue"}
__proto__: Object
__proto__: Object
platform: "PLATFORM_UNSPECIFIED"
__proto__: Object
length: 1
__proto__: Array(0)
fulfillmentText: ""
Совет: Вы заметили, что с объектом полезной нагрузки что-то странное? Он не содержит того же объекта JSON, что и введенный вами в консоли Dialogflow.
Это потому, что Google gRPC API использует буферы протокола (google.protobuf.Struct). Буферы протокола — это языково-нейтральный, платформенно-нейтральный, расширяемый механизм Google для сериализации структурированных данных.
Вам нужно будет преобразовать protobuf в JSON. В моем GitHub для этого примера я включил простой скрипт-конвертер.
Ваш бэкэнд-код должен вернуть результаты в пользовательский интерфейс. Ваш клиентский JavaScript будет проходить по всем результатам и представлять результаты на экране с пользовательским стилем. Рисунок 11-5 показывает, как это будет выглядеть.
Рисунок 11-5. Пример пользовательской интеграции с поддержкой расширенных сообщений
Реализация
Давайте рассмотрим полную реализацию расширенных сообщений для гиперссылок, Google Maps и изображений. Мы уже видели пример пользовательской полезной нагрузки с гиперссылкой; Листинг 11-6 показывает некоторые примеры изображения и Google Map.
Листинг 11-6. Пользовательская полезная нагрузка в консоли Dialogflow
{
"web": {
"type": "image",
"alt": "Lee Boonstra",
"src": "https://www.leeboonstra.com/images/profile.jpg"
}
}
{
"web": {
"link": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2437.7995043017927!2d4.869772751476948!3d52.33778327968087!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47c60a05af168f5b%3A0x3e5bfe6e0b2ce441!2sGoogle+Amsterdam!5e0!3m2!1sen!2snl!4v1520965060384",
"type": "map"
}
}
Сразу после обещания detectIntent вы можете вызвать новый метод под названием getRichContent(), который передает в него разрешенные ответы обещания.
const responses = await sessionClient.detectIntent(request);
let data = getRichContent(responses);
return data;
Метод getRichContent выглядит так; см. Листинг 11-7.
Листинг 11-7. Метод getRichContent получает объект из protobuf
function getRichContent(responses){
const result = responses[0].queryResult;
let messages = [];
if(result.fulfillmentMessages.length > 0) {
for (let index = 0; index < result.fulfillmentMessages.length; index++) {
const msg = result.fulfillmentMessages[index];
if (msg.payload){
// Преобразование protobuf Struct в JSON
let data = structJson.structProtoToJson(msg.payload);
// Предполагаем, что полезная нагрузка имеет ключ 'web'
if (data && data.web) {
messages.push(data.web);
} else {
// Обработка случая, когда 'web' отсутствует или data null/undefined
console.log("Payload structure is not as expected or 'web' key is missing", data);
// Можно добавить текстовое сообщение по умолчанию или пропустить
if (msg.text && msg.text.text) {
messages.push(msg.text.text[0]); // Добавляем первый текстовый ответ, если он есть
}
}
} else {
// Если нет полезной нагрузки, добавляем текстовое сообщение
if (msg.text && msg.text.text) {
messages.push(msg.text.text[0]); // Добавляем первый текстовый ответ
}
}
}
return messages;
}
return []; // Возвращаем пустой массив, если нет сообщений
}
Сначала он проверяет, содержит ли fulfillmentMessages массив, имеющий хотя бы один элемент. Возможно иметь несколько сообщений выполнения, например, текстовое сообщение и два расширенных сообщения, поэтому нам нужно будет пройтись по ним в цикле. Если сообщение содержит пользовательскую полезную нагрузку, нам нужно будет получить данные и преобразовать protobuf в JSON. В противном случае мы можем добавить текстовую версию в новый массив сообщений. Этот массив будет отправлен в пользовательский интерфейс. Технически, вы также могли бы создать HTML-разметку на этом месте, но будет лучше сделать это на фронтенде, чтобы ваш код фронтенд-интерфейса был хорошо отделен от вашего бэкэнд-кода.
Я использую этот небольшой скрипт-конвертер для работы с буферами протокола. Я могу конвертировать из JSON в Struct или из Struct в JSON. Вы можете найти этот скрипт в репозитории GitHub из этой книги. Последнее, что нам нужно: structProtoToJson(data.web);
Не забудьте включить файл в верхнюю часть вашего бэкэнд-кода app.js:
const structJson = require('../back-end/structToJson');
Вот код, который мы будем использовать в пользовательском интерфейсе. Мой index.html будет содержать следующий блок, как только данные будут получены Socket.IO. Посмотрите на Листинг 11-8.
Листинг 11-8. Проход по результатам для поиска типа пользовательской полезной нагрузки и отображения его прямо на экране
socketio.on('returnResults', function (data) {
console.log(data);
var objDiv = document.getElementById("ca"); // Получаем контейнер чата
//d) Если есть результаты, то динамически
// создать элементы списка для добавления к списку сообщений.
if (data && data.length > 0) { // Проверяем, что data не пустой массив
for (let index = 0; index < data.length; index++) {
var e = data[index];
var balloon = document.createElement("li"); // Создаем элемент li для каждого сообщения
balloon.className = 'balloon agent'; // Присваиваем класс
// Проверяем тип сообщения и создаем соответствующий HTML
if (typeof e === 'object' && e !== null) { // Убедимся, что e - это объект
if (e.type == 'hyperlink'){
balloon.innerHTML = `<a href="${e.link}" target="_blank">${e.text}</a>`; // Добавляем target="_blank" для открытия в новой вкладке
} else if (e.type == 'map') {
balloon.innerHTML = `<iframe src="${e.link}" width="300" height="200" frameborder="0" style="border:0;" allowfullscreen="" aria-hidden="false" tabindex="0"></iframe>`; // Уменьшил размер для мобильных
} else if (e.type == 'image') {
balloon.innerHTML = `<img src="${e.src}" alt="${e.alt || 'Image'}" style="max-width: 100%; height: auto;" />`; // Добавил стили для адаптивности
} else {
// Если тип неизвестен, отображаем как текст (или JSON)
balloon.innerHTML = JSON.stringify(e);
}
} else {
// Если e - это просто строка (текстовое сообщение)
balloon.innerHTML = e;
}
messages.appendChild(balloon); // Добавляем элемент в список сообщений
}
objDiv.scrollTop = objDiv.scrollHeight; // Прокручиваем вниз
}
});
Он проверяет типы сообщений. Если тип — гиперссылка, то мы создадим тег anchor со ссылкой и текстом ссылки, предоставленными пользовательской полезной нагрузкой в консоли Dialogflow. Если тип — изображение, вы создадите тег image с src изображения и тегом alt, предоставленными пользовательской полезной нагрузкой в консоли Dialogflow. А когда это карта, то он будет использовать iframe с URL-адресом Google Maps, который был предоставлен пользовательской полезной нагрузкой в консоли Dialogflow.
Возможности безграничны. Если вы хотите создать пользовательские карточки, они, вероятно, будут содержать некоторые элементы div, но с пользовательским кодом таблицы стилей, чтобы они выглядели красиво.
Использование синтаксиса Markdown и условных шаблонов в ваших ответах Dialogflow
При работе с большими командами UX-дизайнер или копирайтер часто поддерживает разговор в консоли Dialogflow. Они хотят иметь контроль над стилем текста, не используя HTML-разметку в тексте, например, чтобы выделить определенные слова или отобразить гиперссылки.
При создании собственной интеграции интеграция поддержки markdown на самом деле очень проста.
Просто убедитесь, что ваш пользовательский интерфейс index.html включает библиотеку markdown, такую как Marked.
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
Ваши ответы на намерения теперь могут содержать синтаксис markdown, как показано на Рисунке 11-6:
**Ли** - *Адвокат разработчиков разговорного ИИ* в Google. В этой роли она фокусируется на *Dialogflow*, *Contact Center AI* и технологии *Speech*. Вот ее [веб-сайт](https://www.leeboonstra.com)
Рисунок 11-6. Использование текста markdown в текстовых полях ответа Dialogflow