Compartir vía


Control de las interrupciones del usuario

SE APLICA A: SDK v4

El control de interrupciones es un aspecto importante de un bot sólido. Los usuarios no siempre seguirán el flujo de conversación que ha definido paso a paso. Es posible que intenten formular una pregunta en mitad del proceso o simplemente deseen cancelarlo en lugar de completarlo. Este artículo describe algunas maneras habituales de controlar las interrupciones de usuario en el bot.

Nota:

Los SDK de JavaScript, C# y Python de Bot Framework seguirán siendo compatibles, pero el SDK de Java se va a retirar con la compatibilidad final a largo plazo que finaliza en noviembre de 2023.

Los bots existentes creados con el SDK de Java seguirán funcionando.

Para la nueva compilación de bots, considere la posibilidad de usar Microsoft Copilot Studio y lea sobre cómo elegir la solución de copilot adecuada.

Para obtener más información, consulta El futuro de la creación de bots.

Requisitos previos

El ejemplo de bot principal usa Language Understanding (LUIS) para identificar las intenciones del usuario; sin embargo, la identificación de la intención del usuario no es el foco de este artículo. Para obtener información sobre la identificación de intenciones de usuario, consulte Comprensión del lenguaje natural y Adición de reconocimiento del lenguaje natural al bot.

Nota:

Reconocimiento del lenguaje (LUIS) se retirará el 1 de octubre de 2025. A partir del 1 de abril de 2023, no podrás crear nuevos recursos de LUIS. Hay disponible una versión más reciente de las funcionalidades de reconocimiento del lenguaje como parte del Lenguaje de Azure AI.

Reconocimiento del lenguaje conversacional (CLU), una característica del lenguaje de Azure AI, es la versión actualizada de LUIS. Para obtener más información sobre la compatibilidad con reconocimiento del lenguaje en el SDK de Bot Framework, consulte Reconocimiento del lenguaje natural.

Acerca de este ejemplo

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. En cualquier momento durante la conversación con el bot, el usuario puede emitir los comandos help o cancel para provocar una interrupción. Se controlan dos tipos de interrupciones:

  • 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ó. En el siguiente turno, continuar desde donde se dejó la conversación.
  • Nivel de diálogo: cancelar el procesamiento por completo para que el bot pueda volver a empezar.

Definición e implementación de la lógica de interrupción

En primer lugar, defina e implemente las interrupciones help y cancel.

Para usar diálogos, instale el paquete de NuGet Microsoft.Bot.Builder.Dialogs.

Dialogs\CancelAndHelpDialog.cs

Implemente la clase CancelAndHelpDialog para controlar las interrupciones de usuario. Los diálogos cancelables BookingDialog y DateResolverDialog derivan de esta clase.

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. 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.

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. 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ó.

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. 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.

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 turno

Una vez implementada la clase de control de interrupciones, compruebe lo que ocurre cuando este bot recibe un nuevo mensaje del usuario.

Dialogs\MainDialog.cs

Cuando llega la nueva actividad de mensaje, el bot ejecuta MainDialog. MainDialog pide al usuario en qué puede ayudarle. Y, a continuación, inicia BookingDialog en el método MainDialog.ActStepAsync, con una llamada a BeginDialogAsync tal como se muestra a continuación.

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.

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. Se utiliza para pedir a los usuarios los detalles de la reserva. Puede encontrar dicho código en Dialogs\BookingDialogs.cs.

Control de errores inesperados

El controlador de errores del adaptador controla las excepciones que no se detectaron en el bot.

AdapterWithErrorHandler.cs

En el ejemplo, el controlador OnTurnError del adaptador recibe las excepciones producidas por la lógica de turnos del bot. Si se produce una excepción, el controlador elimina el estado de la conversación actual para impedir que el bot se bloquee en un bucle de error causado por estar en mal estado.

    {
        // 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 servicios

Startup.cs

Por último, en Startup.cs, se crea el bot como transitorio y, en cada turno, se crea una nueva instancia del bot.


// Register the BookingDialog.

Como referencia, estas son las definiciones de clase que se usan en la llamada para crear el bot anterior.

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

Probar el bot

  1. Si aún no lo ha hecho, instale Bot Framework Emulator.
  2. Ejecute el ejemplo localmente en la máquina.
  3. Inicie el emulador, conéctese al bot y envíe mensajes como se muestra a continuación.

Información adicional

  • El ejemplo 24.bot-authentication-msgraph en C#, JavaScript, Python, o Java muestra cómo controlar una solicitud de cierre de sesión. Usa un patrón similar al que se muestra aquí para controlar las interrupciones.

  • Se debería enviar una respuesta predeterminada, en lugar de no hacer nada y dejar al usuario preguntándose qué es lo que pasa. 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.

  • 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. Antes de que finalice el turno, el bot debería enviar algún mensaje al usuario, aunque sea un simple reconocimiento de su entrada.