Создание чат-ботов используя Bot Builder SDK 4 — часть 2

Привет. Это второй урок из курса «Создание чат-ботов используя Bot Builder SDK 4» Поговорим мы с вами об использовании диалогов в Bot Builder SDK 4.

Аналогом диалогов в чат-ботах являются функции в программах. С помощью диалогов мы можем функционал, предназначенный для выполнения определённых задач определять в отдельные блоки. Диалоги можно как объединять, чтобы обрабатывать сложные сценарии, так и использовать по отдельности. Вы можете добавлять к ним подсказки и валидацию вводимых данных, управляя потоком беседы так, как вам необходимо.

Давайте посмотрим на практике, как это всё работает.

Создадим чат-бота со следующими возможностями: При подключении нового пользователя происходит сбор некоторых данных, после сбора данных пользователю даётся набор некоторых инструментов, которыми он может пользоваться при работе с чат-ботом.

Я создал новый чат-бот из шаблона в Visual Studio, удалил класс EchoState и переименовал класс EchoBot в AllInOneBot. Для хранения состояния будем использовать словарь, в качестве ключа которого будет строка, а в качестве значения объект. Для работы с диалогами необходимо подключить библиотеку Microsoft.Bot.Builder.Dialogs Так как в момент записи данного видео нет стабильной версии данной библиотеки, нужно не забыть установить параметр «Include prerelease»

Include prerelease в Nuget

Для работы с диалогами используется класс DialogSet, который принимает классы, реализующие интерфейс IDialog. Добавим поле _dialogs, типа DialogSet. Также добавим конструктор, в котором будет происходить инициализация данного поля и добавление нужных диалогов. При добавлении диалогов они должны иметь уникальное текстовое имя.

private DialogSet _dialogs;

public AllInOneBot()
{
    _dialogs = new DialogSet();
}

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

_dialogs.Add("textPrompt", new TextPrompt()); 

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

_dialogs.Add("addUserInfo", new WaterfallStep[]
{
    async (dc, args, next) =>
    {
        dc.ActiveDialog.State = new Dictionary<string, object>();
        await dc.Prompt("textPrompt", "Введи имя.");

    },
    async (dc, args, next) =>
    {
        dc.ActiveDialog.State["name"] = args["Value"];
        await dc.Prompt("textPrompt", "Введи фамилию?");

    },
    async (dc, args, next) =>
    {
        dc.ActiveDialog.State["lastName"] = args["Value"];
        await dc.Context.SendActivity($"Спасибо, получены ваши данные\n\rИмя {dc.ActiveDialog.State["name"]}, фамилия {dc.ActiveDialog.State["lastName"]}");
        await dc.End();
    }
});

После этого давайте перейдём в фукцию OnTurn, добавим контекст для работы с диалогами dialogCtx. А после этого добавим вызов диалога adduserInfo после получения события типа ConversationUpdate

public async Task OnTurn(ITurnContext context)
{
    var state = context.GetConversationState<Dictionary<string, object>>();

    var dialogCtx = _dialogs.CreateContext(context, state);
    if (context.Activity.Type == ActivityTypes.ConversationUpdate)
    {
        var newUserName = context.Activity.MembersAdded.FirstOrDefault()?.Name;
        if (!string.Equals("Bot", newUserName))
        {
            await dialogCtx.Begin("addUserInfo");
        }
    }
}

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

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

Запуск чат-бота в эмуляторе

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

Для простоты добавим такие команды как преобразование в Base64 строку и обратно.

Добавим каталог Commands, в нём создадим класс ToBase64Command, наследуем данный класс от интерфейса IDialog и реализуем метод DialogBegin В самом методе будем получать текст, переданный пользователем в чат бот, затем приводить его к base64 и возвращать результат пользователю.

В AllInOneBot нужно добавить ключ данного диалога к имеющимся диалогам, ключ назовём «tobase64» Теперь изменим логику обработки полученного от пользователя сообщения. После того как сообщение было получено, будем определять, что это может быть за команда, командой будем считать первое слово до пробела, если, конечно, такое имеется. Если команда tobase64, тогда будем вызывать запуск диалога ToBase64Command, выполняя метод Begin, для контекста диалогов со значением ключа tobase64.

Проверим работу чат-бота. Сперва мы заполняем данные имени и фамилии, затем, если вместе с командой tobase64 мы передаём требуемый текст, то всё выполняется корректно, base64 строка возвращается пользователю.

Выполнение команды tobase64

Но если мы просто выполним команду tobase64, то чат-бот возвратит ошибку, так как не было передано текста для обработки.

Выполнение команды tobase64 с ошибкой

Чтобы решить данную проблему, давайте перейдём в диалог и добавим проверку на наличие текста для преобразования. Если текста нет, будем выводить для пользователя сообщение с просьбой ввода текста для обработки. Если же текст был передан, тогда будем возвращать результат и завершать работу с данным диалогом, выполнив метод End контекста диалогов. Также, чтобы вы продолжали работать с данным диалогом до его завершения либо намеренного перехода в другой диалог, необходимо выполнить небольшие правки, сделаем наследование данного диалога от интерфейса IDialogContinue, и реализуем метод DialogContinue, требуемый в данном интерфейсе.

public class ToBase64Command: IDialogContinue
{
    public async Task DialogBegin(DialogContext dc, IDictionary<string, object> dialogArgs = null)
    {
        var message = dc.Context.Activity.Text;

        if (!string.IsNullOrEmpty(message))
        {
            var textBytes = System.Text.Encoding.UTF8.GetBytes(message);
            var base64text = System.Convert.ToBase64String(textBytes);

            await dc.Context.SendActivity(base64text);
            await dc.End();
        }
        else
        {
            await dc.Context.SendActivity("Введите текст для преобразования в base64:");
        }
    }

    public async Task DialogContinue(DialogContext dc)
    {
        await DialogBegin(dc);
    }
}

Проверим работу чат-бота. Запустим эмулятор, заполнив требуемые данные, выполним команду tobase64, чат-бот попросит нас ввести текст для обработки, после ввода текста мы получим результат base64 строки.

Корректное выполнение команды tobase64

Таким образом мы можем передавать на обработку большие тексты и всё будет корректно обрабатываться. Теперь давайте добавим диалог для преобразования base64 строк в обычный текст. Добавим диалог FromBase64Command, наследуем его от интерфейса IDialogContinue, реализуем 2 требуемых метода. В методе DialogBegin сделаем преобразование base64 в обычный текст, логика работы будет та же что и для предыдущего диалога.

public class FromBase64Command: IDialogContinue
{
    public async Task DialogBegin(DialogContext dc, IDictionary<string, object> dialogArgs = null)
    {
        var message = dc.Context.Activity.Text;
        if (!string.IsNullOrEmpty(message))
        {
            var base64EncodedBytes = System.Convert.FromBase64String(message);
            var normalText = System.Text.Encoding.UTF8.GetString(base64EncodedBytes);

            await dc.Context.SendActivity(normalText);
            await dc.End();
        }
        else
        {
            await dc.Context.SendActivity("Введите base64 текст");
        }
    }

    public async Task DialogContinue(DialogContext dc)
    {
        await DialogBegin(dc);
    }
}

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

public async Task OnTurn(ITurnContext context)
{
    var state = context.GetConversationState<Dictionary<string, object>>();

    var dialogCtx = _dialogs.CreateContext(context, state);
    if (context.Activity.Type == ActivityTypes.ConversationUpdate)
    {
        var newUserName = context.Activity.MembersAdded.FirstOrDefault()?.Name;
        if (!string.Equals("Bot", newUserName))
        {
            await dialogCtx.Begin("addUserInfo");
        }
    }

    if (context.Activity.Type == ActivityTypes.Message)
    {
        await dialogCtx.Continue();

        var message = context.Activity.Text.Trim();
        var indexOfSpace = message.IndexOf(" ", StringComparison.Ordinal);
        var command = indexOfSpace != -1 ? message.Substring(0, indexOfSpace).ToLower() : message.ToLower();

        switch (command)
        {
            case "tobase64":

                context.Activity.Text = indexOfSpace >= 0
                    ? context.Activity.Text.Substring(indexOfSpace, message.Length - indexOfSpace)
                    : String.Empty;
                await dialogCtx.Begin("tobase64");
                break;
            case "frombase64":

                context.Activity.Text = indexOfSpace >= 0
                    ? context.Activity.Text.Substring(indexOfSpace, message.Length - indexOfSpace)
                    : String.Empty;
                await dialogCtx.Begin("frombase64");
                break;
        }
    }
}

Проверим что получилось. Выполняя команду frombase64 мы успешно можем преобразовывать ранее полученные base64 строки, чат-бот функционирует успешно.

Корректное выполнение команды tobase64

Рассмотрим еще один важный момент. Думаю, вы обратили внимание, что для получения пользовательских данных в WaterfallStep всю логику мы писали непосредственно в классе AllInOneBot. Сейчас я покажу как можно вынести данную логику отдельно, что будет гораздо удобнее в использовании и грамотнее с точки зрения архитектуры. Добавим новый класс AddUserInfoDialog, наследуем его от класса DialogContainer и реализуем конструктор AddUserInfoDialog. Добавим константу, в которой будет храниться имя по умолчанию для данного диалога, сделаем её такой же, как и имя класса. Скопируем текущий функционал WaterfallStep и textPrompt. В конструкторе AllInOneBot удалим старое добавление WaterfallStep и подключим созданный диалог. Так же в добавленном диалоге не забываем использовать имя диалога по умолчанию, при добавлении диалога.

public class AddUserInfoDialog: DialogContainer
{
    private const string DefaultName = nameof(AddUserInfoDialog);
    public AddUserInfoDialog(string dialogId = DefaultName, DialogSet dialogs = null) : base(dialogId, dialogs)
    {
        Dialogs.Add(dialogId, new WaterfallStep[]
        {
            async (dc, args, next) =>
            {
                dc.ActiveDialog.State = new Dictionary<string, object>();
                await dc.Prompt("textPrompt", "Введи имя.");

            },
            async (dc, args, next) =>
            {
                dc.ActiveDialog.State["name"] = args["Value"];
                await dc.Prompt("textPrompt", "Введи фамилию?");

            },
            async (dc, args, next) =>
            {
                dc.ActiveDialog.State["lastName"] = args["Value"];
                await dc.Context.SendActivity($"Спасибо, получены ваши данные\n\rИмя {dc.ActiveDialog.State["name"]}, фамилия {dc.ActiveDialog.State["lastName"]}");
                await dc.End();
            }
        });

        Dialogs.Add("textPrompt", new TextPrompt());
    }
}

После этого можно открыть эмулятор и проверять, всё ли корректно работает. Как можно видеть, функционал работает, как и раньше, но при этом используемый код разнесён по проекту.

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

На следующем уроке мы поговорим об использовании карточек в чат-ботах, узнаем, как функционал карточек работает в четвёртой версии SDK и добавим работу с карточками в проект чат-бота.

А на этом всё, с Вами был Амельченя Андрей, приятного программирования.

Добавить комментарий