Implementación de flujo de conversación secuencial

SE APLICA A: SDK v4

La recopilación de información mediante la publicación de preguntas es una de las principales formas de interacción de un bot con los usuarios. La biblioteca de diálogos proporciona características integradas útiles, como las clases prompt que facilitan la formulación de preguntas y validan la respuesta para asegurarse de que coincide con un tipo de datos específico o cumple con las reglas de validación personalizadas.

Mediante la biblioteca de diálogos, se pueden administrar flujos de conversación lineales y más complejos. En una interacción lineal, el bot ejecuta una secuencia fija de pasos y la conversación finaliza. Un diálogo es útil cuando el bot necesita recopilar información del usuario.

En este artículo se muestra cómo implementar un flujo de conversación lineal mediante la creación de solicitudes y su llamada desde un diálogo en cascada. Para obtener ejemplos de cómo escribir sus propias preguntas sin usar la biblioteca de diálogos, vea el artículo Creación de mensajes propios para recopilar datos de entrada del usuario.

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

Acerca de este ejemplo

El ejemplo de solicitudes de varios turnos usa un diálogo en cascada, algunas solicitudes y un diálogo de componente para crear una interacción lineal que formula al usuario una serie de preguntas. El código usa un diálogo para desplazarse por estos pasos:

Pasos Tipo de solicitud
Pedir al usuario su modo de transporte Solicitud de elección
Pedir al usuario su nombre Solicitud de texto
Pedir al usuario si desea proporcionar su edad Solicitud de confirmación
Si responde Sí, solicitar su edad Solicitud numérica con validación para que solo acepte edades mayores que 0 y menores de 150.
Si no usan Microsoft Teams, se les pide una imagen de perfil. Solicitud de datos adjuntos con validación para permitir la falta de datos adjuntos.
Preguntar si la información recopilada es correcta Reutilización de la solicitud de confirmación

Por último, si responde Sí, mostrar la información recopilada; de lo contrario, indicar al usuario que no se conservará su información.

Creación del diálogo principal

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

El bot interactúa con el usuario mediante UserProfileDialog. Cuando se crea la clase DialogBot del bot, se establece UserProfileDialog como su diálogo principal. El bot, a continuación, usa un método auxiliar Run para acceder al diálogo.

Diagrama de clases para el ejemplo de C#.

Dialogs\UserProfileDialog.cs

Empiece creando UserProfileDialog, que se deriva de la clase ComponentDialog y tiene siete pasos.

En el constructor UserProfileDialog, se crean los pasos de cascada, las solicitudes y el diálogo en cascada y se agregan al conjunto de diálogos. Las solicitudes deben estar en el mismo conjunto de diálogos en el que se utilizan.

public UserProfileDialog(UserState userState)
    : base(nameof(UserProfileDialog))
{
    _userProfileAccessor = userState.CreateProperty<UserProfile>("UserProfile");

    // This array defines how the Waterfall will execute.
    var waterfallSteps = new WaterfallStep[]
    {
        TransportStepAsync,
        NameStepAsync,
        NameConfirmStepAsync,
        AgeStepAsync,
        PictureStepAsync,
        SummaryStepAsync,
        ConfirmStepAsync,
    };

    // Add named dialogs to the DialogSet. These names are saved in the dialog state.
    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
    AddDialog(new TextPrompt(nameof(TextPrompt)));
    AddDialog(new NumberPrompt<int>(nameof(NumberPrompt<int>), AgePromptValidatorAsync));
    AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
    AddDialog(new AttachmentPrompt(nameof(AttachmentPrompt), PicturePromptValidatorAsync));

    // The initial child Dialog to run.
    InitialDialogId = nameof(WaterfallDialog);
}

A continuación, añada los pasos que usa el diálogo para solicitar la entrada. Para usar una solicitud, debe llamarla desde un paso del diálogo y recuperar el resultado de la solicitud en el paso siguiente con stepContext.Result. En un segundo plano, las preguntas consisten en un diálogo de dos pasos. En primer lugar, la solicitud pide una entrada. A continuación, devuelve el valor válido o comienza de nuevo desde el principio con una nueva solicitud hasta que recibe una entrada válida.

Siempre debe devolver un valor no NULL de DialogTurnResult desde un paso de cascada. Si no lo hace, el diálogo podría no funcionar según lo previsto. Aquí se muestra la implementación de NameStepAsync en el diálogo en cascada.

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["transport"] = ((FoundChoice)stepContext.Result).Value;

    return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") }, cancellationToken);
}

En AgeStepAsync, especifique una solicitud de reintento cuando se produce un error al validar la entrada del usuario, ya sea porque tiene un formato que la solicitud no puede analizar o porque se produce un error en un criterio de validación de la entrada. En este caso, si no se ha proporcionado una solicitud de reintento, la solicitud utilizará el texto de la solicitud inicial para volver a pedir la entrada al usuario.

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // User said "yes" so we will be prompting for the age.
        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        var promptOptions = new PromptOptions
        {
            Prompt = MessageFactory.Text("Please enter your age."),
            RetryPrompt = MessageFactory.Text("The value entered must be greater than 0 and less than 150."),
        };

        return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
    }
    else
    {
        // User said "no" so we will skip the next step. Give -1 as the age.
        return await stepContext.NextAsync(-1, cancellationToken);
    }
}

UserProfile.cs

El modo de transporte, el nombre y la edad del usuario se guardan en una instancia de la clase UserProfile.

public class UserProfile
{
    public string Transport { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

    public Attachment Picture { get; set; }
}

Dialogs\UserProfileDialog.cs

En el último paso, compruebe el valor de stepContext.Result devuelto por el diálogo que se llama en el paso de cascada anterior. Si el valor devuelto es true, el descriptor de acceso del perfil de usuario obtiene y actualiza el perfil de usuario. Para obtener el perfil de usuario, llame a GetAsync y, a continuación, establezca los valores de las propiedades userProfile.Transport, userProfile.Name, userProfile.Age y userProfile.Picture. Por último, resuma la información del usuario antes de llamar a EndDialogAsync, que finaliza el diálogo. La finalización del diálogo lo extrae de la pila de diálogos y devuelve un resultado opcional al elemento primario del diálogo. El elemento primario es el diálogo o método que inició el diálogo que acaba de terminar.

    else
    {
        msg += $" Your profile will not be kept.";
    }

    await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is the end.
    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["picture"] = ((IList<Attachment>)stepContext.Result)?.FirstOrDefault();

    // Get the current profile object from user state.
    var userProfile = await _userProfileAccessor.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

    userProfile.Transport = (string)stepContext.Values["transport"];
    userProfile.Name = (string)stepContext.Values["name"];
    userProfile.Age = (int)stepContext.Values["age"];
    userProfile.Picture = (Attachment)stepContext.Values["picture"];

    var msg = $"I have your mode of transport as {userProfile.Transport} and your name as {userProfile.Name}";

    if (userProfile.Age != -1)
    {
        msg += $" and your age as {userProfile.Age}";
    }

    msg += ".";

    await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

    if (userProfile.Picture != null)
    {
        try
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Attachment(userProfile.Picture, "This is your profile picture."), cancellationToken);
        }
        catch
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Text("A profile picture was saved but could not be displayed here."), cancellationToken);

Ejecución del diálogo

Bots\DialogBot.cs

El controlador OnMessageActivityAsync utiliza el método RunAsync para iniciar o continuar el diálogo. OnTurnAsync utiliza los objetos de administración de estado del bot para conservar los cambios de estado en el almacenamiento. El método ActivityHandler.OnTurnAsync llama a los diversos métodos del controlador de actividades, como OnMessageActivityAsync. De este modo, se guarda el estado después de que finalice el controlador de mensajes pero antes de la finalización del propio turno.

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
    await base.OnTurnAsync(turnContext, cancellationToken);

    // Save any state changes that might have occurred during the turn.
    await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    Logger.LogInformation("Running dialog with Message Activity.");

    // Run the Dialog with the new message Activity.
    await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}

Registro de los servicios del bot

Este bot usa los siguientes servicios:

  • Servicios básicos de un bot: un proveedor de credenciales, un adaptador y la implementación del bot.
  • Servicios para administrar el estado: almacenamiento, estado del usuario y estado de la conversación.
  • El diálogo que va a usar el bot.

Startup.cs

Registrar los servicios del bot en Startup. Estos servicios están disponibles para otras partes del código mediante la inserción de dependencias.

{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient().AddControllers().AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
        });

        // Create the Bot Framework Authentication to be used with the Bot Adapter.
        services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

        // 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. (Used in this bot's Dialog implementation.)
        services.AddSingleton<UserState>();

        // Create the Conversation state. (Used by the Dialog system itself.)
        services.AddSingleton<ConversationState>();

Nota:

El almacenamiento en memoria se usa solo con fines de prueba y no está pensado para su uso en producción. Asegúrese de usar un tipo de almacenamiento persistente para un bot de producción.

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

Una transcripción de ejemplo de una conversación con el bot de solicitud de varios turnos.

Información adicional

Acerca del estado del diálogo y el bot

En este bot, se definen dos descriptores de acceso de propiedad de estado:

  • Uno se crea en el estado de la conversación de la propiedad de estado del diálogo. El estado del diálogo realiza un seguimiento de dónde está el usuario dentro de los diálogos de un conjunto de diálogos y el contexto de diálogo lo actualiza, del mismo modo que cuando se llama a los métodos begin dialog o continue dialog.
  • Uno creado en el estado de usuario de la propiedad de perfil de usuario. El bot lo utiliza para realizar el seguimiento de la información que tiene sobre el usuario y este estado se administra explícitamente en el código del diálogo.

Los métodos get y set del descriptor de acceso de propiedad de estado obtienen y establecen el valor de la propiedad en la memoria caché del objeto de administración de estado. La memoria caché se rellena la primera vez que se solicita el valor de una propiedad de estado en un turno, pero se debe guardar explícitamente. Para conservar los cambios en ambas propiedades de estado, se llama al método save changes del objeto de administración de estado correspondiente.

En este ejemplo se actualiza el estado del perfil de usuario desde el diálogo. Esta práctica puede funcionar para algunos bots, pero no funcionará si desea reutilizar un diálogo en varios bots.

Hay varias opciones para mantener independientes los pasos del diálogo y el estado del bot. Por ejemplo, una vez que el diálogo recopila toda la información, puede:

  • Usar el método end dialog para proporcionar los datos recopilados como valor devuelto al contexto primario. Este puede ser el controlador de turnos del bot o un diálogo activo anterior en la pila de diálogos y es como se designan las clases de las solicitudes.
  • Generar una solicitud a un servicio adecuado. Esto podría funcionar bien si el bot actúa como un front-end de un servicio de mayor tamaño.

Definición de un método validador de solicitudes

UserProfileDialog.cs

A continuación, se muestra un ejemplo de código de validador para la definición del método AgePromptValidatorAsync. promptContext.Recognized.Value contiene el valor analizado, que es un entero aquí para la solicitud numérica. promptContext.Recognized.Succeeded indica si la solicitud pudo analizar la entrada del usuario o no. El validador debería devolver False para indicar que no se aceptó el valor y el diálogo de solicitud debe volver a preguntar al usuario; de lo contrario, devuelve True para aceptar la entrada y volver del diálogo de solicitud. Puede cambiar el valor en el validador según su escenario.

    }

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
    return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Is this ok?") }, cancellationToken);
}

Pasos siguientes