Создание собственных запросов на сбор данных, вводимых пользователем

ОБЛАСТЬ ПРИМЕНЕНИЯ: ПАКЕТ SDK версии 4

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

Совет

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

Примечание.

Пакеты SDK для JavaScript, C# и Python для Bot Framework по-прежнему будут поддерживаться, однако пакет SDK java отменяется с окончательной долгосрочной поддержкой, заканчивающейся в ноябре 2023 года.

Существующие боты, созданные с помощью пакета SDK для Java, будут продолжать функционировать.

Для создания нового бота рекомендуется использовать Power Virtual Agent и ознакомиться с выбором подходящего решения чат-бота.

Дополнительные сведения см. в статье "Будущее создания бота".

Необходимые компоненты

Сведения о примере кода

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

Схема классов для примера C#.

  • Класс UserProfile для хранения собранных ботом сведений о пользователе.
  • Класс ConversationFlow для управления состоянием беседы при сборе сведений о пользователе.
  • Внутреннее ConversationFlow.Question перечисление для отслеживания того, где вы находитесь в беседе.

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

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

Создание объектов беседы и пользователя

Создание объектов состояния пользователя и сеанса при запуске и их использование с помощью внедрения зависимостей в конструкторе бота.

Startup.cs

// Create the Bot Adapter with error handling enabled.
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

// Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
services.AddSingleton<IStorage, MemoryStorage>();

// Create the User state.
services.AddSingleton<UserState>();

// Create the Conversation state.
services.AddSingleton<ConversationState>();

Bots/CustomPromptBot.cs

private readonly BotState _userState;
private readonly BotState _conversationState;

public CustomPromptBot(ConversationState conversationState, UserState userState)
{
    _conversationState = conversationState;
    _userState = userState;
}

Создание методов доступа к свойствам

Создание методов доступа к свойствам профиля пользователя и потока диалога и вызов GetAsync для получения значения свойства из состояния.

Bots/CustomPromptBot.cs

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    var conversationStateAccessors = _conversationState.CreateProperty<ConversationFlow>(nameof(ConversationFlow));
    var flow = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationFlow(), cancellationToken);

    var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
    var profile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile(), cancellationToken);

Перед завершением шага вызовите SaveChangesAsync, чтобы записать изменения состояния в хранилище.

    await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}

Обработчик поворота сообщения

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

Bots/CustomPromptBot.cs

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    var conversationStateAccessors = _conversationState.CreateProperty<ConversationFlow>(nameof(ConversationFlow));
    var flow = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationFlow(), cancellationToken);

    var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
    var profile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile(), cancellationToken);

    await FillOutUserProfileAsync(flow, profile, turnContext, cancellationToken);

    // Save changes.
    await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}

Заполнение профиля пользователя

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

Каждый метод проверки характеризуется следующим образом:

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

Методы проверки описаны в следующем разделе.

Bots/CustomPromptBot.cs

{
    var input = turnContext.Activity.Text?.Trim();
    string message;

    switch (flow.LastQuestionAsked)
    {
        case ConversationFlow.Question.None:
            await turnContext.SendActivityAsync("Let's get started. What is your name?", null, null, cancellationToken);
            flow.LastQuestionAsked = ConversationFlow.Question.Name;
            break;
        case ConversationFlow.Question.Name:
            if (ValidateName(input, out var name, out message))
            {
                profile.Name = name;
                await turnContext.SendActivityAsync($"Hi {profile.Name}.", null, null, cancellationToken);
                await turnContext.SendActivityAsync("How old are you?", null, null, cancellationToken);
                flow.LastQuestionAsked = ConversationFlow.Question.Age;
                break;
            }
            else
            {
                await turnContext.SendActivityAsync(message ?? "I'm sorry, I didn't understand that.", null, null, cancellationToken);
                break;
            }

        case ConversationFlow.Question.Age:
            if (ValidateAge(input, out var age, out message))
            {
                profile.Age = age;
                await turnContext.SendActivityAsync($"I have your age as {profile.Age}.", null, null, cancellationToken);
                await turnContext.SendActivityAsync("When is your flight?", null, null, cancellationToken);
                flow.LastQuestionAsked = ConversationFlow.Question.Date;
                break;
            }
            else
            {
                await turnContext.SendActivityAsync(message ?? "I'm sorry, I didn't understand that.", null, null, cancellationToken);
                break;
            }

        case ConversationFlow.Question.Date:
            if (ValidateDate(input, out var date, out message))
            {
                profile.Date = date;
                await turnContext.SendActivityAsync($"Your cab ride to the airport is scheduled for {profile.Date}.");
                await turnContext.SendActivityAsync($"Thanks for completing the booking {profile.Name}.");
                await turnContext.SendActivityAsync($"Type anything to run the bot again.");
                flow.LastQuestionAsked = ConversationFlow.Question.None;
                profile = new UserProfile();
                break;
            }
            else
            {
                await turnContext.SendActivityAsync(message ?? "I'm sorry, I didn't understand that.", null, null, cancellationToken);
                break;
            }
    }
}

Синтаксический анализ и проверка входных данных

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

  • Name (имя) не может быть пустой строкой. Для нормализации выполняется усечение пробелов.
  • Значение age (возраст) должно находиться в диапазоне от 18 до 120. Для нормализации оно округляется до целого числа.
  • Date (дата) может быть любой датой или временем в будущем, если разница между указанным значением и текущим временем составляет не менее одного часа. Для нормализации возвращается только дата, содержащаяся во входных данных.

Примечание.

Для ввода времени и даты в примере используются библиотеки Microsoft/Recognizers-Text для выполнения начального анализа. Это просто один из способов анализа входных данных. Дополнительные сведения об этих библиотеках см. в readME проекта.

Bots/CustomPromptBot.cs

private static bool ValidateName(string input, out string name, out string message)
{
    name = null;
    message = null;

    if (string.IsNullOrWhiteSpace(input))
    {
        message = "Please enter a name that contains at least one character.";
    }
    else
    {
        name = input.Trim();
    }

    return message is null;
}

private static bool ValidateAge(string input, out int age, out string message)
{
    age = 0;
    message = null;

    // Try to recognize the input as a number. This works for responses such as "twelve" as well as "12".
    try
    {
        // Attempt to convert the Recognizer result to an integer. This works for "a dozen", "twelve", "12", and so on.
        // The recognizer returns a list of potential recognition results, if any.

        var results = NumberRecognizer.RecognizeNumber(input, Culture.English);

        foreach (var result in results)
        {
            // The result resolution is a dictionary, where the "value" entry contains the processed string.
            if (result.Resolution.TryGetValue("value", out var value))
            {
                age = Convert.ToInt32(value);
                if (age >= 18 && age <= 120)
                {
                    return true;
                }
            }
        }

        message = "Please enter an age between 18 and 120.";
    }
    catch
    {
        message = "I'm sorry, I could not interpret that as an age. Please enter an age between 18 and 120.";
    }

    return message is null;
}

private static bool ValidateDate(string input, out string date, out string message)
{
    date = null;
    message = null;

    // Try to recognize the input as a date-time. This works for responses such as "11/14/2018", "9pm", "tomorrow", "Sunday at 5pm", and so on.
    // The recognizer returns a list of potential recognition results, if any.
    try
    {
        var results = DateTimeRecognizer.RecognizeDateTime(input, Culture.English);

        // Check whether any of the recognized date-times are appropriate,
        // and if so, return the first appropriate date-time. We're checking for a value at least an hour in the future.
        var earliest = DateTime.Now.AddHours(1.0);

        foreach (var result in results)
        {
            // The result resolution is a dictionary, where the "values" entry contains the processed input.
            var resolutions = result.Resolution["values"] as List<Dictionary<string, string>>;

            foreach (var resolution in resolutions)
            {
                // The processed input contains a "value" entry if it is a date-time value, or "start" and
                // "end" entries if it is a date-time range.
                if (resolution.TryGetValue("value", out var dateString)
                    || resolution.TryGetValue("start", out dateString))
                {
                    if (DateTime.TryParse(dateString, out var candidate)
                        && earliest < candidate)
                    {
                        date = candidate.ToShortDateString();
                        return true;
                    }
                }
            }
        }

        message = "I'm sorry, please enter a date at least an hour out.";
    }
    catch
    {
        message = "I'm sorry, I could not interpret that as an appropriate date. Please enter a date at least an hour out.";
    }

    return false;
}

Локальная проверка бота

Скачайте и установите Bot Framework Emulator для локального тестирования бота.

  1. Выполните этот пример на локальном компьютере. Если вам нужны инструкции, ознакомьтесь с файлом README для примера C#, примера JS или примера Python.
  2. Выполните тестирование в Emulator.

Дополнительные ресурсы

Библиотека Dialogs предоставляет классы, которые автоматизируют многие аспекты управления беседами.

Следующий шаг