Microsoft Bot Framework + IBM Watson =… би-лингвистический бот

в 22:22, , рубрики: Bot Framework, ibm watson, javascript, linux, localization, microsoft, node.js, Разработка под Linux

Продолжая начатую тему хочется поделиться успешным опытом создания билингвистического 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 позволило легко объединить функционал в одном. Далее расскажем о том как это можно сделать.

Выбираем новую папку, в которой будет находиться проект и запускаем:

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) {

    var payload = {
        workspace_id: workspace,
        context: [],
        input: {
            text: session.message.text
        }
    };

    var 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) {
    var 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 {
        var 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
var 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
var 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
var payload = {
    workspace_id: workspace,
    context: [],
    input: {
        text: session.message.text
    }
};

var conversationContext = {
    workspaceId: workspace,
    watsonContext: {}
};

...

По умолчанию система локализации Bot Builder SDK основана на файлах и позволяет боту поддерживать несколько языков, используя JSON-файлы, хранящиеся на диске. По умолчанию система локализации при вызове таких методов как builder.Prompts.choice() или session.send() ищет сообщения бота в файле ./locale/<тэг>/index.json, где языковой тэг IETF представляет выбранную локаль, для которой необходимо искать сообщения. На следующем скриншоте показана получившаяся структура директорий проекта для английского и арабского языков:

image

Структура этого 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;
    }
});

Здесь представлен простой вариант этой функции, в реальном проекте она, конечно, сложнее.

Вот и всё по теме! Мы получили билингвистический бот. После запуска можем насладиться результатом — автоматическими ответами бота:

Microsoft Bot Framework + IBM Watson=… би-лингвистический бот - 2

Автор: Алексей

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js