Продолжая начатую тему хочется поделиться успешным опытом создания билингвистического Node.JS бота на Microsoft Bot Framework под Linux. От заказчика поступила задача разработать простой социальный бот в формате вопрос-ответ для большой торговой сети, однако сложность заключалась в другом — бот должен быть двуязычным: на английском и арабском. Хотя, как будет показано ниже, выбор инструментов для решения задачи сделал разработку лёгкой, приятной и интересной.
Как и ранее выбор фреймворка был сделан в пользу Microsoft Bot Framework, который имеет огромное количество функционала, сильно облегчающего построение и развёртывание бота: управление потоками диалогов, триггерные действия, сохранение состояния, красочные интерактивные сообщения, лёгкое подключение каналов, таких как Facebook Messenger, Skype, WebChat и много другого. Как оказалось, в нём также присутствует очень простой и удобный механизм локализации (о нём ниже).
Для распознавания смысла сообщений пользователя можно воспользоваться системой ИИ, такой как LUIS, IBM Watson, Google Dialogflow (Api.ai) и др. Естественнее и удобнее для BotBuilder использовать LUIS: есть встроенные в Bot Framework методы, классы и т.д. Однако в LUIS пока нет арабского языка — второй язык, на котором по требованию заказчика должен был работать бот. Поэтому выбор пал на IBM Watson, у которого, как оказалось, значительно более развитый функционал, стабильность и удобство работы. Заказчик изначально думал о возможности создания 2-х ботов, однако огромное разнообразие инструментов в IBM Watson и Bot Framework позволило легко объединить функционал в одном. Далее расскажем о том как это можно сделать.
Выбираем новую папку, в которой будет находиться проект и запускаем:
Устанавливаем необходимые пакеты для построения бота, подключения к Watson и асинхронных запросов:
Создаём файл
Это собственно база, от которой можно отталкиваться дальше. Здесь перед созданием бота, мы создаём объект Разговора (Conversation) с пользователем. Conversation используется для передачи ответов пользователя в Watson, который распознаёт в нём пары намерение-сущность (intent-entity). Переменные WATSON_URL и WORKSPACE_ID, как вы наверное уже поняли, хранятся в файле
Workspace (рабочее пространство) связано с экземпляром сервиса Разговора с пользователем, проще — с обученной моделью. Эта модель создаётся и обучается для одного языка. Для другого языка необходимо создать второй workspace. Получить список доступных нам workspaces и их идентификаторы можно, запустив простой скрипт:
Чтобы задействовать механизм локализации Microsoft Bot Framework, нам нужно для начала выяснить, на каком языке к нам обращается пользователь. И здесь нам на помощь опять приходит Watson, имеющий в арсенале огромное количество всевозможного API для перевода, распознавания, классификации, конвертации и т.п. Здесь также есть API для идентификации языка. Для его использования создаём небольшой модуль, который будет отвечать за запросы к этому API:
Подключим этот модуль в главном приложении:
Вначале основной функции бота вставим строки для определения текущего языка и установки соответствующей локали. SDK BotBuilder предоставляет метод
Список распознаваемых языков можно помотреть в Watson API Explorer, там же можно протестировать этот API.
Для каждого языка создаём 2 отдельных объекта Разговора (Conversation):
Эти объекты остаются неизменными, поэтому можно поместить их в начало app.js или вынести в отдельный файл. Затем после кода определения локали вставляем строку, инициализирующую нашу переменную conversation, изменяем и переменную workspace — теперь она также будет меняться динамически в зависимости от определённого языка:
По умолчанию система локализации Bot Builder SDK основана на файлах и позволяет боту поддерживать несколько языков, используя JSON-файлы, хранящиеся на диске. По умолчанию система локализации при вызове таких методов как

Структура этого JSON-файла — это простое отображение (соответствие) идентификатора сообщения к локализованной текстовой строке. Бот автоматически извлекает локализованную версию сообщения, если в метод
Ещё один способ получить локализованную текстовую строку по идентификатору сообщения — это вызов метода
Теперь мы легко можем реализовать ответ пользователю на любом языке. При реализации простого бота в формате вопрос-ответ, несомненным достоинством Watson для нас явилось то, что в независимости от языка workspace, распознанные пары intent-entity он может возвращать на любом языке (как обучишь), в нашем случае — на английском. Поэтому возможные ответы были удобно организованы в виде единственного JS-объекта для обоих языков, который работает как ассоциативный массив функций:
Теперь мы можем переписать коллбэк, который вызывается после запроса к Watson:
Здесь представлен простой вариант этой функции, в реальном проекте она, конечно, сложнее.
Вот и всё по теме! Мы получили билингвистический бот. После запуска можем насладиться результатом — автоматическими ответами бота:

Как и ранее выбор фреймворка был сделан в пользу Microsoft Bot Framework, который имеет огромное количество функционала, сильно облегчающего построение и развёртывание бота: управление потоками диалогов, триггерные действия, сохранение состояния, красочные интерактивные сообщения, лёгкое подключение каналов, таких как Facebook Messenger, Skype, WebChat и много другого. Как оказалось, в нём также присутствует очень простой и удобный механизм локализации (о нём ниже).
Для распознавания смысла сообщений пользователя можно воспользоваться системой ИИ, такой как LUIS, IBM Watson, Google Dialogflow (Api.ai) и др. Естественнее и удобнее для BotBuilder использовать LUIS: есть встроенные в Bot Framework методы, классы и т.д. Однако в LUIS пока нет арабского языка — второй язык, на котором по требованию заказчика должен был работать бот. Поэтому выбор пал на IBM Watson, у которого, как оказалось, значительно более развитый функционал, стабильность и удобство работы. Заказчик изначально думал о возможности создания 2-х ботов, однако огромное разнообразие инструментов в IBM Watson и Bot Framework позволило легко объединить функционал в одном. Далее расскажем о том как это можно сделать.
Выбираем новую папку, в которой будет находиться проект и запускаем:
npm init
Устанавливаем необходимые пакеты для построения бота, подключения к Watson и асинхронных запросов:
npm install dotenv npm install restify npm install botbuilder npm install watson-developer-cloud npm install request-promise
Создаём файл
app.js и копируем нижеследующий код:Код приложения:
var restify = require('restify'); var builder = require('botbuilder'); var Conversation = require('watson-developer-cloud/conversation/v1'); // watson sdk require('dotenv').config({ silent: true }); var contexts; var workspace = process.env.WORKSPACE_ID; // Setup Restify Server var server = restify.createServer(); server.listen(process.env.port || process.env.PORT || 3978, function() { console.log('%s listening to %s', server.name, server.url); }); // Create the service wrapper var conversation = new Conversation({ username: process.env.WATSON_USERNAME, password: process.env.WATSON_PASSWORD, url: process.env.WATSON_URL + process.env.WORKSPACE_ID + '/message?version=2017-05-26', version_date: Conversation.VERSION_DATE_2017_05_26 }); // Create chat connector for communicating with the Bot Framework Service var connector = new builder.ChatConnector({ appId: process.env.MICROSOFT_APP_ID, appPassword: process.env.MICROSOFT_APP_PASSWORD }); // Listen for messages from users server.post('/api/messages', connector.listen()); // Create your bot with a function to receive messages from the user var bot = new builder.UniversalBot(connector, function(session) { let payload = { workspace_id: workspace, context: [], input: { text: session.message.text } }; let conversationContext = { workspaceId: workspace, watsonContext: {} }; if (!conversationContext) { conversationContext = {}; } payload.context = conversationContext.watsonContext; conversation.message(payload, function(err, response) { if (err) { console.log(err); session.send(err); } else { console.log(JSON.stringify(response, null, 2)); session.send(response.output.text); conversationContext.watsonContext = response.context; } }); });
Это собственно база, от которой можно отталкиваться дальше. Здесь перед созданием бота, мы создаём объект Разговора (Conversation) с пользователем. Conversation используется для передачи ответов пользователя в Watson, который распознаёт в нём пары намерение-сущность (intent-entity). Переменные WATSON_URL и WORKSPACE_ID, как вы наверное уже поняли, хранятся в файле
.env:# Bot Framework Credentials MICROSOFT_APP_ID=... MICROSOFT_APP_PASSWORD=... #Watson Url WATSON_URL=https://gateway.watsonplatform.net/conversation/api/v1/workspaces/ WATSON_USERNAME=... WATSON_PASSWORD=... WORKSPACE_ID=<UUID>
Workspace (рабочее пространство) связано с экземпляром сервиса Разговора с пользователем, проще — с обученной моделью. Эта модель создаётся и обучается для одного языка. Для другого языка необходимо создать второй workspace. Получить список доступных нам workspaces и их идентификаторы можно, запустив простой скрипт:
workspaces.js
// This loads the environment variables from the .env file require('dotenv-extended').load(); var Conversation = require('watson-developer-cloud/conversation/v1'); // watson sdk var conversation = new Conversation({ username: process.env.WATSON_USERNAME, password: process.env.WATSON_PASSWORD, version_date: Conversation.VERSION_DATE_2017_05_26 }); conversation.listWorkspaces(function(err, response) { if (err) { console.error(err); } else { console.log(JSON.stringify(response, null, 2)); } });
node workspaces.js
Чтобы задействовать механизм локализации Microsoft Bot Framework, нам нужно для начала выяснить, на каком языке к нам обращается пользователь. И здесь нам на помощь опять приходит Watson, имеющий в арсенале огромное количество всевозможного API для перевода, распознавания, классификации, конвертации и т.п. Здесь также есть API для идентификации языка. Для его использования создаём небольшой модуль, который будет отвечать за запросы к этому API:
language.js
var request = require("request-promise"); module.exports.Detect = async function LanguageDetect(text) { let options = { baseUrl: "https://watson-api-explorer.mybluemix.net", uri: "/language-translator/api/v2/identify", method: "GET", qs: { // Query string like ?text=some text text: text }, json: true }; try { let result = await request(options); return result.languages[0].language; } catch (err) { console.error(err); } };
Подключим этот модуль в главном приложении:
var language = require('./language');
Вначале основной функции бота вставим строки для определения текущего языка и установки соответствующей локали. SDK BotBuilder предоставляет метод
session.preferredLocale() для сохранения или получения этого свойства для каждого пользователя:// Detect language en/ar first and set correspondent locale let locale = await language.Detect(session.message.text); session.preferredLocale(locale);
Список распознаваемых языков можно помотреть в Watson API Explorer, там же можно протестировать этот API.
Для каждого языка создаём 2 отдельных объекта Разговора (Conversation):
English and Arabic Conversation objects
// Get Watson service wrapper for English var conversation_en = new Conversation({ username: process.env.WATSON_USERNAME, password: process.env.WATSON_PASSWORD, url: process.env.WATSON_URL + process.env.WORKSPACE_ID_EN + '/message?version=2017-05-26', version_date: Conversation.VERSION_DATE_2017_05_26 }); // Get Watson service wrapper for Arabic var conversation_ar = new Conversation({ username: process.env.WATSON_USERNAME, password: process.env.WATSON_PASSWORD, url: process.env.WATSON_URL + process.env.WORKSPACE_ID_AR + '/message?version=2017-05-26', version_date: Conversation.VERSION_DATE_2017_05_26 });
Примечание. Обратите внимание: теперь в файле.envу нас находятся 2 переменныеWORKSPACE_ID_ENиWORKSPACE_ID_AR, вместо однойWORKSPACE_ID.
Эти объекты остаются неизменными, поэтому можно поместить их в начало app.js или вынести в отдельный файл. Затем после кода определения локали вставляем строку, инициализирующую нашу переменную conversation, изменяем и переменную workspace — теперь она также будет меняться динамически в зависимости от определённого языка:
Modifications to app.js
// Detect language en/ar first and set correspondent locale let locale = await language.Detect(session.message.text); session.preferredLocale(locale); let workspace = (locale == "ar") ? process.env.WORKSPACE_ID_AR : process.env.WORKSPACE_ID_EN; // Get Watson service wrapper according to the locale let conversation = (locale == "ar") ? conversation_ar : conversation_en; // Prepare Watson request let payload = { workspace_id: workspace, context: [], input: { text: session.message.text } }; let conversationContext = { workspaceId: workspace, watsonContext: {} }; ...
По умолчанию система локализации Bot Builder SDK основана на файлах и позволяет боту поддерживать несколько языков, используя JSON-файлы, хранящиеся на диске. По умолчанию система локализации при вызове таких методов как
builder.Prompts.choice() или session.send() ищет сообщения бота в файле ./locale/<тэг>/index.json, где языковой тэг IETF представляет выбранную локаль, для которой необходимо искать сообщения. На следующем скриншоте показана получившаяся структура директорий проекта для английского и арабского языков:
Структура этого JSON-файла — это простое отображение (соответствие) идентификатора сообщения к локализованной текстовой строке. Бот автоматически извлекает локализованную версию сообщения, если в метод
session.send() передаётся идентификатор сообщения вместо заранее локализованной текстовой строки:session.send("greeting_message");
Ещё один способ получить локализованную текстовую строку по идентификатору сообщения — это вызов метода
session.localizer.gettext(). Для удобства использования я написал расширение класса Session и написал обёртку по типу функции tr() из Qt (всё таки JavaScript временами очень удобная штука!). Здесь же можно реализовать подстановку токенов типа {name}, {id}, {phone} и т.п.:tr() extension function
const { Session } = require('botbuilder'); // Object extension function for strings localization (translation) Session.prototype.tr = function (text) { return this.localizer.gettext(this.preferredLocale(), text) .replace("{name}", this.userName()); }; // Object extension function to get user id Session.prototype.userId = function () { return this.message.address.user.id; }; // Object extension function to get user name Session.prototype.userName = function () { return this.message.address.user.name; };
Теперь мы легко можем реализовать ответ пользователю на любом языке. При реализации простого бота в формате вопрос-ответ, несомненным достоинством Watson для нас явилось то, что в независимости от языка workspace, распознанные пары intent-entity он может возвращать на любом языке (как обучишь), в нашем случае — на английском. Поэтому возможные ответы были удобно организованы в виде единственного JS-объекта для обоих языков, который работает как ассоциативный массив функций:
Responses object
var responses = { // Responses object "greeting": { "no_entities": async function (session) { session.send("greeting_message"); }, }, "purchase": { "sale-stop": async function (session) { session.send("3_sales_end_dates"); }, "product-sale": async function (session) { session.send("4_sale_still_running"); }, /** @param {Session} session */ "price-product": async function (session) { session.send(session.tr("6_product_prices")); }, "price": async function (session) { session.send(session.tr("6_product_prices")); }, }, "inquiry": { "job": async function (session) { session.send("5_job_opportunity"); }, ... }, ... }
Теперь мы можем переписать коллбэк, который вызывается после запроса к Watson:
Watson request callback function
// Send request to Watson conversation.message(payload, async function (err, response) { if (err) { console.log(err); session.send(err); } else { // Generate response to user according to Watson intent-entity pairs let processed = false; // Get intent let intent = (response.intents[0]) ? response.intents[0].intent : undefined; for(i = 0; i < response.entities.length; i++) { // Process single entity in response let entity = (response.entities[i]) ? response.entities[i].entity : undefined; // Process single entity in response if (responses[intent] && responses[intent][entity]) { await responses[intent][entity](session, intent, [response.entities[i]]); processed = true; break; } } // Message was not recognized if(!processed) { session.send(session.tr("get_started")); } conversationContext.watsonContext = response.context; } });
Здесь представлен простой вариант этой функции, в реальном проекте она, конечно, сложнее.
Вот и всё по теме! Мы получили билингвистический бот. После запуска можем насладиться результатом — автоматическими ответами бота:

