Обработка прерываний диалога пользователем

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

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

Примечание.

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

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

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

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

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

В примере основного бота используется Распознавание речи (LUIS) для выявления намерений пользователей. Однако определение намерения пользователя не является фокусом этой статьи. Сведения о выявлении намерений пользователей см. в разделе " Распознавание естественного языка" и "Добавление распознавания естественного языка" в бот.

Примечание.

Распознавание речи (LUIS) будет прекращен 1 октября 2025 года. Начиная с 1 апреля 2023 года вы не сможете создавать новые ресурсы LUIS. Новая версия распознавания речи теперь доступна как часть языка ИИ Azure.

Распознавание речи (CLU) — это обновленная версия LUIS. Дополнительные сведения о поддержке распознавания речи в пакете SDK Bot Framework см. в разделе "Распознавание естественного языка".

Об этом примере

Пример в этой статье моделирует работу бота для бронирования авиабилетов, который использует диалоги для получения от пользователя информации о нужном рейсе. В любой момент общения с ботом пользователь может выдать команду help (помощь) или cancel (отмена), что должно вызвать прерывание. Здесь мы будем использовать два вида прерываний.

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

Определение и реализация логики прерывания

Сначала нам нужно определить и реализовать прерывания по командам help и cancel.

Чтобы использовать диалоги, установите пакет NuGet Microsoft.Bot.Builder.Dialogs.

Dialogs\CancelAndHelpDialog.cs

Реализуйте класс CancelAndHelpDialog для обработки прерываний со стороны пользователя. Диалоговые окна отмены BookingDialog и DateResolverDialog производные от этого класса.

public class CancelAndHelpDialog : ComponentDialog

CancelAndHelpDialog В классе OnContinueDialogAsync метод вызывает метод, чтобы проверка, если пользователь прервал InterruptAsync обычный поток. Если процесс прерывается, вызываются методы базового класса. В противном случае возвращается значение, полученное из InterruptAsync.

protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
    var result = await InterruptAsync(innerDc, cancellationToken);
    if (result != null)
    {
        return result;
    }

    return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}

Если пользователь вводит слово help, метод InterruptAsync отправляет сообщение и вызывает DialogTurnResult (DialogTurnStatus.Waiting), чтобы обозначить ожидание ответа от пользователя в диалоге верхнего уровня. В этом случае поток общения прерывается только на один шаг, а на следующем шаге продолжается с того места, где остановился диалог.

Если пользователь вводит "отмену", он вызывает CancelAllDialogsAsync его внутренний контекст диалогового окна, который очищает стек диалогов и приводит к выходу из него с отмененным состоянием и без значения результата. С точки зрения MainDialog (см. далее) все будет выглядеть так, как будто диалог бронирования завершился и вернул значение NULL. Этот равнозначно ситуации, когда пользователь отказался подтвердить бронирование.

private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
    if (innerDc.Context.Activity.Type == ActivityTypes.Message)
    {
        var text = innerDc.Context.Activity.Text.ToLowerInvariant();

        switch (text)
        {
            case "help":
            case "?":
                var helpMessage = MessageFactory.Text(HelpMsgText, HelpMsgText, InputHints.ExpectingInput);
                await innerDc.Context.SendActivityAsync(helpMessage, cancellationToken);
                return new DialogTurnResult(DialogTurnStatus.Waiting);

            case "cancel":
            case "quit":
                var cancelMessage = MessageFactory.Text(CancelMsgText, CancelMsgText, InputHints.IgnoringInput);
                await innerDc.Context.SendActivityAsync(cancelMessage, cancellationToken);
                return await innerDc.CancelAllDialogsAsync(cancellationToken);
        }
    }

    return null;
}

Проверка прерываний на каждом шаге

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

Dialogs\MainDialog.cs

При поступлении действия с новым сообщением бот выполняет MainDialog. В MainDialog у пользователя спрашивается, какая помощь тому требуется. Затем запускается BookingDialog в методе MainDialog.ActStepAsync с помощью вызова BeginDialogAsync, как показано ниже.

private async Task<DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if (!_luisRecognizer.IsConfigured)
    {
        // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance.
        return await stepContext.BeginDialogAsync(nameof(BookingDialog), new BookingDetails(), cancellationToken);
    }

    // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
    var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);
    switch (luisResult.TopIntent().intent)
    {
        case FlightBooking.Intent.BookFlight:
            await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken);

            // Initialize BookingDetails with any entities we may have found in the response.
            var bookingDetails = new BookingDetails()
            {
                // Get destination and origin from the composite entities arrays.
                Destination = luisResult.ToEntities.Airport,
                Origin = luisResult.FromEntities.Airport,
                TravelDate = luisResult.TravelDate,
            };

            // Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder.
            return await stepContext.BeginDialogAsync(nameof(BookingDialog), bookingDetails, cancellationToken);

        case FlightBooking.Intent.GetWeather:
            // We haven't implemented the GetWeatherDialog so we just display a TODO message.
            var getWeatherMessageText = "TODO: get weather flow here";
            var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
            break;

        default:
            // Catch all for unhandled intents
            var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
            var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
            break;
    }

    return await stepContext.NextAsync(null, cancellationToken);
}

Затем в методе FinalStepAsyncMainDialog класса диалоговое окно резервирования закончилось, а резервирование считается завершенным или отмененным.

private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // If the child dialog ("BookingDialog") was cancelled, the user failed to confirm or if the intent wasn't BookFlight
    // the Result here will be null.
    if (stepContext.Result is BookingDetails result)
    {
        // Now we have all the booking details call the booking service.

        // If the call to the booking service was successful tell the user.

        var timeProperty = new TimexProperty(result.TravelDate);
        var travelDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now);
        var messageText = $"I have you booked to {result.Destination} from {result.Origin} on {travelDateMsg}";
        var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
        await stepContext.Context.SendActivityAsync(message, cancellationToken);
    }

    // Restart the main dialog with a different message the second time around
    var promptMessage = "What else can I do for you?";
    return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage, cancellationToken);
}

BookingDialog Код не отображается здесь, так как он не связан напрямую с обработкой прерываний. Он используется для запроса пользователей для получения сведений о резервировании. Вы можете найти этот код в файле Dialogs\BookingDialogs.cs.

Обработка непредвиденных ошибок

Обработчик ошибок адаптера обрабатывает все исключения, которые не были пойманы в боте.

AdapterWithErrorHandler.cs

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

    {
        // Log any leaked exception from the application.
        // NOTE: In production environment, you should consider logging this to
        // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
        // to add telemetry capture to your bot.
        logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

        // Send a message to the user
        var errorMessageText = "The bot encountered an error or bug.";
        var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput);
        await turnContext.SendActivityAsync(errorMessage);

        errorMessageText = "To continue to run this bot, please fix the bot source code.";
        errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput);
        await turnContext.SendActivityAsync(errorMessage);

        if (conversationState != null)
        {
            try
            {
                // Delete the conversationState for the current conversation to prevent the
                // bot from getting stuck in a error-loop caused by being in a bad state.
                // ConversationState should be thought of as similar to "cookie-state" in a Web pages.
                await conversationState.DeleteAsync(turnContext);
            }
            catch (Exception e)
            {
                logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}");
            }
        }

        // Send a trace activity, which will be displayed in the Bot Framework Emulator
        await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
    };
}

Регистрация служб

Startup.cs

Наконец, в Startup.cs создается временный бот, то есть на каждом шаге создается новый экземпляр бота.


// Register the BookingDialog.

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

public class DialogAndWelcomeBot<T> : DialogBot<T>
public class DialogBot<T> : ActivityHandler
    where T : Dialog
public class MainDialog : ComponentDialog

Тестирование бота

  1. Если это еще не сделано, установите эмулятор Bot Framework.
  2. Выполните этот пример на локальном компьютере.
  3. Запустите эмулятор, подключитесь к боту и отправьте несколько сообщений, как показано ниже.

Дополнительная информация:

  • Пример 24.bot-authentication-msgraph в C#, JavaScript, Python или Java показывает, как обрабатывать запрос на выход. Для обработки прерываний в нем используется шаблон, аналогичный показанному здесь.

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

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