Control de las interrupciones del usuarioHandle user interruptions

se aplica a: SDK V4APPLIES TO: SDK v4

El control de interrupciones es un aspecto importante de un bot sólido.Handling interruptions is an important aspect of a robust bot. Los usuarios no siempre seguirán el flujo de conversación que ha definido paso a paso.Users will not always follow your defined conversation flow, step by step. Es posible que intenten formular una pregunta en mitad del proceso o simplemente deseen cancelarlo en lugar de completarlo.They may try to ask a question in the middle of the process, or simply want to cancel it instead of completing it. En este tema se describen algunas maneras habituales de controlar las interrupciones de usuario en el bot.In this topic, this topic describes some common ways to handle user interruptions in your bot.

Requisitos previosPrerequisites

Acerca de este ejemploAbout this sample

El ejemplo utilizado en este artículo modela un bot de reservas de vuelos que usa diálogos para obtener la información del vuelo del usuario.The sample used in this article models a flight booking bot that uses dialogs to get flight information from the user. En cualquier momento durante la conversación con el bot, el usuario puede emitir los comandos help o cancel para provocar una interrupción.At any time during the conversation with the bot, the user can issue help or cancel commands to cause an interruption. Se controlan dos tipos de interrupciones:There are two types of interruptions handled:

  • Nivel de turno: omitir el procesamiento en el nivel de turno pero dejar el diálogo en la pila con la información que se proporcionó.Turn level: Bypass processing at the turn level but leave the dialog on the stack with the information that was provided. En el siguiente turno, continuar desde donde se dejó la conversación.In the next turn, continue from where the conversation left off.
  • Nivel de diálogo: cancelar el procesamiento por completo para que el bot pueda volver a empezar.Dialog level: Cancel the processing completely, so the bot can start all over again.

Definición e implementación de la lógica de interrupciónDefine and implement the interruption logic

En primer lugar, defina e implemente las interrupciones help y cancel.First, define and implement the help and cancel interruptions.

Para usar diálogos, instale el paquete de NuGet Microsoft.Bot.Builder.Dialogs.To use dialogs, install the Microsoft.Bot.Builder.Dialogs NuGet package.

Dialogs\CancelAndHelpDialog.csDialogs\CancelAndHelpDialog.cs

Implemente la clase CancelAndHelpDialog para controlar las interrupciones de usuario.Implement the CancelAndHelpDialog class to handle user interruptions. Los diálogos cancelables BookingDialog y DateResolverDialog derivan de esta clase.The cancellable dialogs, BookingDialog and DateResolverDialog derive from this class.

public class CancelAndHelpDialog : ComponentDialog

En la clase CancelAndHelpDialog, el método OnContinueDialogAsync llama al método InterruptAsync para comprobar si el usuario ha interrumpido el flujo normal.In the CancelAndHelpDialog class the OnContinueDialogAsync method calls the InterruptAsync method to check if the user has interrupted the normal flow. Si se interrumpe el flujo, se llama a los métodos de la clase base; en caso contrario, se devuelve el valor devuelto desde 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);
}

Si el usuario escribe "help", el método InterruptAsync envía un mensaje y, a continuación, llama a DialogTurnResult (DialogTurnStatus.Waiting) para indicar que el diálogo en la parte superior espera una respuesta del usuario.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. De este modo, se interrumpe el flujo de conversación solo para un turno y, en el siguiente turno, la conversación continúa donde se dejó.In this way, the conversation flow is interrupted for a turn only, and the next turn continues from where the conversation left off.

Si el usuario escribe "cancel", llama a CancelAllDialogsAsync en el contexto de diálogo interno, que borra la pila de diálogos y hace que se cierre con un estado cancelado y sin valor de resultado.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. Para MainDialog (que se muestra más adelante), parece que el diálogo de reservas finalizó y devolvió NULL, de modo similar a cuando el usuario decide no confirmar su reserva.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;
}

Comprobación de las interrupciones en cada turnoCheck for interruptions each turn

Una vez implementada la clase de control de interrupciones, compruebe lo que ocurre cuando este bot recibe un nuevo mensaje del usuario.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

Cuando llega la nueva actividad de mensaje, el bot ejecuta MainDialog.As the new message activity arrives, the bot runs the MainDialog. MainDialog pide al usuario en qué puede ayudarle.The MainDialog prompts the user for what it can help with. Y, a continuación, inicia BookingDialog en el método MainDialog.ActStepAsync, con una llamada a BeginDialogAsync tal como se muestra a continuación.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);
}

Seguidamente, en el método FinalStepAsync de la clase MainDialog, el diálogo de reservas finaliza y la reserva se considera completada o cancelada.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);
}

El código de BookingDialog no se muestra aquí, ya que no está directamente relacionado con el control de interrupciones.The code in BookingDialog is not shown here as it is not directly related to interruption handling. Se utiliza para pedir a los usuarios los detalles de la reserva.It is used to prompt users for booking details. Puede encontrar dicho código en Dialogs\BookingDialogs.cs.You can find that code in Dialogs\BookingDialogs.cs.

Control de errores inesperadosHandle unexpected errors

El controlador de errores del adaptador controla las excepciones que no se detectaron en el bot.The adapter's error handler handles any exceptions that were not caught in the bot.

AdapterWithErrorHandler.csAdapterWithErrorHandler.cs

En el ejemplo, el controlador del adaptador OnTurnError recibe las excepciones producidas por la lógica de turno del bot.In the sample, the adapter's OnTurnError handler receives any exceptions thrown by your bot's turn logic. Si se produce una excepción, el controlador elimina el estado de la conversación actual para evitar que el bot se atasca en un bucle de error causado por estar en un estado no válido.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");
};

Registro de los serviciosRegister services

Startup.csStartup.cs

Por último, en Startup.cs, se crea el bot como transitorio y, en cada turno, se crea una nueva instancia del bot.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>>();

Como referencia, estas son las definiciones de clase que se usan en la llamada para crear el bot anterior.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

Prueba del botTo test the bot

  1. Si aún no lo ha hecho, instale Bot Framework Emulator.If you have not done so already, install the Bot Framework Emulator.
  2. Ejecute el ejemplo localmente en la máquina.Run the sample locally on your machine.
  3. Inicie el emulador, conéctese al bot y envíe mensajes como se muestra a continuación.Start the Emulator, connect to your bot, and send messages as shown below.

Información adicionalAdditional information

  • El ejemplo 24.bot-authentication-msgraph en C#, JavaScripto Python muestra cómo controlar una solicitud de cierre de sesión.The 24.bot-authentication-msgraph sample in C#, JavaScript, or Python shows how to handle a logout request. Usa un patrón similar al que se muestra aquí para controlar las interrupciones.It uses a pattern similar to the one shown here for handling interruptions.

  • Se debería enviar una respuesta predeterminada, en lugar de no hacer nada y dejar al usuario preguntándose qué es lo que pasa.You should send a default response instead of doing nothing and leaving the user wondering what is going on. La respuesta predeterminada debe indicar al usuario cuáles son los comandos que entiende el bot, de modo que el usuario pueda retomar el proceso.The default response should tell the user what commands the bot understands so the user can get back on track.

  • En cualquier punto del turno, la propiedad responded del contexto del turno indica si el bot ha enviado un mensaje al usuario en el turno actual.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. Antes de que finalice el turno, el bot debería enviar algún mensaje al usuario, aunque sea un simple reconocimiento de su entrada.Before the turn ends, your bot should send some message to the user, even if it is a simple acknowledgement of their input.