사용자 중단 처리Handle user interruptions

적용 대상: SDK v4APPLIES TO: SDK v4

중단을 처리하는 작업은 강력한 봇의 중요한 측면입니다.Handling interruptions is an important aspect of a robust bot. 사용자가 항상 정의된 대화 흐름을 단계별로 따르는 것은 아닙니다.Users will not always follow your defined conversation flow, step by step. 사용자가 프로세스 중간에 질문을 하거나 프로세스를 완료하지 않고 취소하려는 경우가 있을 수 있습니다.They may try to ask a question in the middle of the process, or simply want to cancel it instead of completing it. 이 항목에서는 봇에서 사용자 중단을 처리하는 몇 가지 방법을 설명합니다.In this topic, this topic describes some common ways to handle user interruptions in your bot.

필수 구성 요소Prerequisites

이 샘플 정보About this sample

이 문서에 사용된 샘플은 대화를 통해 사용자의 항공편 정보를 확인하는 항공편 예약 봇을 모델링합니다.The sample used in this article models a flight booking bot that uses dialogs to get flight information from the user. 사용자는 봇과 대화 중에 언제든지 help 또는 cancel 명령을 실행하여 대화를 중단할 수 있습니다.At any time during the conversation with the bot, the user can issue help or cancel commands to cause an interruption. 여기서 처리하는 중단 유형은 두 가지가 있습니다.There are two types of interruptions handled:

  • 순서 수준: 순서 수준에서 처리를 무시하되, 스택의 대화에 제공된 정보를 남겨놓습니다.Turn level: Bypass processing at the turn level but leave the dialog on the stack with the information that was provided. 다음 순서에 중단된 부분부터 계속 대화합니다.In the next turn, continue from where the conversation left off.
  • 대화 수준: 봇이 완전히 다시 시작될 수 있도록 처리를 완전히 취소합니다.Dialog level: Cancel the processing completely, so the bot can start all over again.

중단 논리 정의 및 구현Define and implement the interruption logic

먼저 helpcancel 중단을 정의하고 구현합니다.First, define and implement the help and cancel interruptions.

대화를 사용하려면 Microsoft.Bot.Builder.Dialogs NuGet 패키지를 설치합니다.To use dialogs, install the Microsoft.Bot.Builder.Dialogs NuGet package.

Dialogs\CancelAndHelpDialog.csDialogs\CancelAndHelpDialog.cs

사용자 중단을 처리하는 CancelAndHelpDialog 클래스를 구현합니다.Implement the CancelAndHelpDialog class to handle user interruptions. 취소 가능한 대화 상자 BookingDialogDateResolverDialog는 이 클래스에서 파생됩니다.The cancellable dialogs, BookingDialog and DateResolverDialog derive from this class.

public class CancelAndHelpDialog : ComponentDialog

CancelAndHelpDialog 클래스에서 OnContinueDialogAsync 메서드는 InterruptAsync 메서드를 호출하여 사용자가 정상적인 흐름을 중단했는지 여부를 확인합니다.In the CancelAndHelpDialog class the OnContinueDialogAsync method calls the InterruptAsync method to check if the user has interrupted the normal flow. 흐름이 중단된 경우 기본 클래스 메서드가 호출되며, 그렇지 않은 경우에는 InterruptAsync의 반환 값이 반환됩니다.If the flow is interrupted, base class methods are called; otherwise, the return value from the InterruptAsync is returned.

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)를 호출하여 맨 위에 있는 대화가 사용자의 응답을 기다리고 있음을 나타냅니다.If the user types "help", the InterruptAsync method sends a message and then calls DialogTurnResult (DialogTurnStatus.Waiting) to indicate that the dialog on top is waiting for a response from the user. 이러한 방식으로 대화 흐름이 한 순서 동안만 중단되고, 다음 순서에는 대화가 중단된 부분부터 계속됩니다.In this way, the conversation flow is interrupted for a turn only, and the next turn continues from where the conversation left off.

사용자가 "cancel"을 입력하면 내부 대화 컨텍스트의 CancelAllDialogsAsync가 호출되어 대화 스택이 삭제되며 취소된 상태로 아무런 결과 값 없이 종료됩니다.If the user types "cancel", it calls CancelAllDialogsAsync on its inner dialog context, which clears its dialog stack and causes it to exit with a cancelled status and no result value. 뒷부분에 표시되는 MainDialog에는 사용자가 예약을 확인하지 않도록 선택한 경우와 마찬가지로 예약 대화가 종료되고 null이 반환된 것으로 나타납니다.To the MainDialog (shown later on), it will appear that the booking dialog ended and returned null, similar to when the user chooses not to confirm their booking.

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;
}

각 순서의 중단 확인Check for interruptions each turn

인터럽트 처리 클래스가 구현되었으면 이 봇이 사용자로부터 새 메시지를 받을 때 어떤 일이 발생하는지 살펴보세요.Once the interrupt handling class is implemented, review what happens when this bot receives a new message from the user.

Dialogs\MainDialog.csDialogs\MainDialog.cs

새 메시지 작업이 도착하면 봇이 MainDialog를 실행합니다.As the new message activity arrives, the bot runs the MainDialog. MainDialog가 사용자에게 어떤 도움이 필요한지 묻는 메시지를 표시합니다.The MainDialog prompts the user for what it can help with. 그런 다음, 아래 표시된 것처럼 BeginDialogAsync를 호출하여 MainDialog.ActStepAsync 메서드에서 BookingDialog를 시작합니다.And then it starts the BookingDialog in the MainDialog.ActStepAsync method, with a call to BeginDialogAsync as shown below.

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);
}

다음으로, MainDialog 클래스의 FinalStepAsync 메서드에서 예약 대화가 종료되고, 예약이 완료되거나 취소된 것으로 간주됩니다.Next, in the FinalStepAsync method of the MainDialog class, the booking dialog ended and the booking is considered to be complete or cancelled.

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의 코드는 중단 처리와 직접적인 관련이 없으므로 여기에 표시하지 않았습니다.The code in BookingDialog is not shown here as it is not directly related to interruption handling. 이는 사용자에게 예약 세부 정보를 묻는 데 사용됩니다.It is used to prompt users for booking details. 이 코드는 Dialogs\BookingDialogs.cs 에서 확인할 수 있습니다.You can find that code in Dialogs\BookingDialogs.cs.

예기치 않은 오류 처리Handle unexpected errors

어댑터의 오류 처리기는 봇에서 catch되지 않은 예외를 처리합니다.The adapter's error handler handles any exceptions that were not caught in the bot.

AdapterWithErrorHandler.csAdapterWithErrorHandler.cs

샘플에서 어댑터의 OnTurnError 처리기는 봇의 턴 논리에 의해 throw된 예외를 수신합니다.In the sample, the adapter's OnTurnError handler receives any exceptions thrown by your bot's turn logic. 예외가 throw되면 처리기는 현재 대화에 대한 대화 상태를 삭제하여 봇이 잘못된 상태에 있기 때문에 오류 루프에 빠를 수 없도록 합니다.If there is an exception thrown, the handler deletes the conversation state for the current conversation to prevent the bot from getting stuck in an error-loop caused by being in a bad state.

OnTurnError = async (turnContext, exception) =>
{
    // 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");
};

서비스 등록Register services

Startup.csStartup.cs

마지막으로, Startup.cs에서 봇이 일시적으로 생성되고, 매 순서에 봇의 새 인스턴스가 생성됩니다.Finally, in Startup.cs, the bot is created as a transient, and on every turn, a new instance of the bot is created.

// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddTransient<IBot, DialogAndWelcomeBot<MainDialog>>();

참고로, 위의 봇을 만드는 호출에서 사용되는 클래스 정의는 다음과 같습니다.For reference, here are the class definitions that are used in the call to create the bot above.

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

봇 테스트To test the bot

  1. 아직 설치하지 않은 경우 Bot Framework Emulator를 설치합니다.If you have not done so already, install the Bot Framework Emulator.
  2. 샘플을 머신에서 로컬로 실행합니다.Run the sample locally on your machine.
  3. 에뮬레이터를 시작하고 봇에 연결한 다음, 아래와 같은 메시지를 보냅니다.Start the Emulator, connect to your bot, and send messages as shown below.

추가 정보Additional information

  • C#, JavaScript또는 Python의 24.bot-authentication-msgraph 샘플은 로그아웃 요청을 처리하는 방법을 보여줍니다.The 24.bot-authentication-msgraph sample in C#, JavaScript, or Python shows how to handle a logout request. 중단 처리를 위해 여기에 표시된 것과 비슷한 패턴을 사용합니다.It uses a pattern similar to the one shown here for handling interruptions.

  • 아무 작업도 수행하지 않고 사용자가 무엇을 해야 할지 고민하는 상황을 만들지 말고, 기본 응답을 전송해야 합니다.You should send a default response instead of doing nothing and leaving the user wondering what is going on. 기본 응답은 사용자가 다시 정상적인 프로세스를 진행할 수 있도록 봇이 이해하는 명령을 사용자에게 알려주어야 합니다.The default response should tell the user what commands the bot understands so the user can get back on track.

  • 순서 진행 중에 순서 컨텍스트의 responded 속성은 봇이 이번 순서에 사용자에게 메시지를 보냈는지 여부를 나타냅니다.At any point in the turn, the turn context's responded property indicates whether the bot has sent a message to the user this turn. 봇은 순서가 종료되기 전에 사용자에게 메시지를 보내야 합니다(사용자 입력에 대한 간단한 수신 확인이라도 보내야 함).Before the turn ends, your bot should send some message to the user, even if it is a simple acknowledgement of their input.