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.
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:
Por último, si responde Sí, mostrar la información recopilada; de lo contrario, indicar al usuario que no se conservará su información.
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.
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);
Para usar diálogos, el proyecto debe instalar el paquete de npm botbuilder-dialogs.
El bot interactúa con el usuario mediante UserProfileDialog
. Cuando se crea el DialogBot
del bot, el UserProfileDialog
se establece como el diálogo principal. El bot, a continuación, usa un método auxiliar run
para acceder al diálogo.
dialogs/userProfileDialog.js
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.
constructor(userState) {
super('userProfileDialog');
this.userProfile = userState.createProperty(USER_PROFILE);
this.addDialog(new TextPrompt(NAME_PROMPT));
this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
this.addDialog(new NumberPrompt(NUMBER_PROMPT, this.agePromptValidator));
this.addDialog(new AttachmentPrompt(ATTACHMENT_PROMPT, this.picturePromptValidator));
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.transportStep.bind(this),
this.nameStep.bind(this),
this.nameConfirmStep.bind(this),
this.ageStep.bind(this),
this.pictureStep.bind(this),
this.summaryStep.bind(this),
this.confirmStep.bind(this)
]));
this.initialDialogId = WATERFALL_DIALOG;
}
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 del contexto del paso, en este caso con step.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 nameStep
en el diálogo en cascada.
async nameStep(step) {
step.values.transport = step.result.value;
return await step.prompt(NAME_PROMPT, 'Please enter your name.');
}
En ageStep
, 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, como se especifica en el constructor anterior. 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.
async ageStep(step) {
if (step.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.
const promptOptions = { prompt: 'Please enter your age.', retryPrompt: 'The value entered must be greater than 0 and less than 150.' };
return await step.prompt(NUMBER_PROMPT, promptOptions);
} else {
// User said "no" so we will skip the next step. Give -1 as the age.
return await step.next(-1);
}
}
userProfile.js
El modo de transporte, el nombre y la edad del usuario se guardan en una instancia de la clase UserProfile
.
class UserProfile {
constructor(transport, name, age, picture) {
this.transport = transport;
this.name = name;
this.age = age;
this.picture = picture;
}
}
dialogs/userProfileDialog.js
En el último paso, compruebe el valor de step.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 get
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 endDialog
, 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.
await step.context.sendActivity(msg);
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
return await step.endDialog();
}
async summaryStep(step) {
step.values.picture = step.result && step.result[0];
// Get the current profile object from user state.
const userProfile = await this.userProfile.get(step.context, new UserProfile());
userProfile.transport = step.values.transport;
userProfile.name = step.values.name;
userProfile.age = step.values.age;
userProfile.picture = step.values.picture;
let 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 step.context.sendActivity(msg);
if (userProfile.picture) {
try {
await step.context.sendActivity(MessageFactory.attachment(userProfile.picture, 'This is your profile picture.'));
} catch {
await step.context.sendActivity('A profile picture was saved but could not be displayed here.');
}
Creación del método de extensión para ejecutar el diálogo en cascada
Se utiliza un método auxiliar run
, definido dentro de userProfileDialog
, para crear y acceder al contexto del diálogo. En este caso, accessor
es el descriptor de acceso de la propiedad de estado de la propiedad de estado del diálogo y this
es el diálogo de componente de perfil de usuario. Puesto que los diálogos de componente definen un conjunto de diálogos interno, se debe crear un conjunto de diálogos externo que sea visible para el código del controlador de mensajes y usarlo para crear un contexto de diálogo.
El contexto del diálogo se crea mediante una llamada al método createContext
y se usa para interactuar con el conjunto de diálogos desde dentro del controlador de turnos del bot. El contexto del diálogo incluye el contexto del turno actual, el diálogo primario y el estado del diálogo, que proporciona un método para conservar información en el diálogo.
El contexto del diálogo permite iniciar un diálogo con el identificador de cadena o continuar el diálogo actual (por ejemplo, un diálogo en cascada que tiene varios pasos). El contexto del diálogo se pasa a todos los diálogos y pasos de cascada del bot.
async run(turnContext, accessor) {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(turnContext);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
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.
UserProfileDialog.java
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 withUserState) {
super("UserProfileDialog");
userProfileAccessor = withUserState.createProperty("UserProfile");
WaterfallStep[] waterfallSteps = {
UserProfileDialog::transportStep,
UserProfileDialog::nameStep,
this::nameConfirmStep,
this::ageStep,
UserProfileDialog::pictureStep,
this::confirmStep,
this::summaryStep
};
// Add named dialogs to the DialogSet. These names are saved in the dialog state.
addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps)));
addDialog(new TextPrompt("TextPrompt"));
addDialog(new NumberPrompt<Integer>("NumberPrompt", UserProfileDialog::agePromptValidator, Integer.class));
addDialog(new ChoicePrompt("ChoicePrompt"));
addDialog(new ConfirmPrompt("ConfirmPrompt"));
addDialog(new AttachmentPrompt("AttachmentPrompt", UserProfileDialog::picturePromptValidator));
// The initial child Dialog to run.
setInitialDialogId("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.getResult()
. 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 nameStep
en el diálogo en cascada.
private static CompletableFuture<DialogTurnResult> nameStep(WaterfallStepContext stepContext) {
stepContext.getValues().put("transport", ((FoundChoice) stepContext.getResult()).getValue());
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Please enter your name."));
return stepContext.prompt("TextPrompt", promptOptions);
}
En ageStep
, 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 CompletableFuture<DialogTurnResult> ageStep(WaterfallStepContext stepContext) {
if ((Boolean)stepContext.getResult()) {
// 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.
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Please enter your age."));
promptOptions.setRetryPrompt(MessageFactory.text("The value entered must be greater than 0 and less than 150."));
return stepContext.prompt("NumberPrompt", promptOptions);
}
// User said "no" so we will skip the next step. Give -1 as the age.
return stepContext.next(-1);
}
UserProfile.java
El modo de transporte, el nombre y la edad del usuario se guardan en una instancia de la clase UserProfile
.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.microsoft.bot.sample.multiturnprompt;
import com.microsoft.bot.schema.Attachment;
/**
* This is our application state.
*/
public class UserProfile {
public String transport;
public String name;
public Integer age;
public Attachment picture;
}
UserProfileDialog.java
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 get
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 endDialog
, 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.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.microsoft.bot.sample.multiturnprompt;
import com.microsoft.bot.builder.MessageFactory;
import com.microsoft.bot.builder.StatePropertyAccessor;
import com.microsoft.bot.builder.UserState;
import com.microsoft.bot.connector.Channels;
import com.microsoft.bot.dialogs.ComponentDialog;
import com.microsoft.bot.dialogs.DialogTurnResult;
import com.microsoft.bot.dialogs.WaterfallDialog;
import com.microsoft.bot.dialogs.WaterfallStep;
import com.microsoft.bot.dialogs.WaterfallStepContext;
import com.microsoft.bot.dialogs.choices.ChoiceFactory;
import com.microsoft.bot.dialogs.choices.FoundChoice;
import com.microsoft.bot.dialogs.prompts.AttachmentPrompt;
import com.microsoft.bot.dialogs.prompts.ChoicePrompt;
import com.microsoft.bot.dialogs.prompts.ConfirmPrompt;
import com.microsoft.bot.dialogs.prompts.NumberPrompt;
import com.microsoft.bot.dialogs.prompts.PromptOptions;
import com.microsoft.bot.dialogs.prompts.PromptValidatorContext;
import com.microsoft.bot.dialogs.prompts.TextPrompt;
import com.microsoft.bot.schema.Attachment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.apache.commons.lang3.StringUtils;
public class UserProfileDialog extends ComponentDialog {
private final StatePropertyAccessor<UserProfile> userProfileAccessor;
public UserProfileDialog(UserState withUserState) {
super("UserProfileDialog");
userProfileAccessor = withUserState.createProperty("UserProfile");
WaterfallStep[] waterfallSteps = {
UserProfileDialog::transportStep,
UserProfileDialog::nameStep,
this::nameConfirmStep,
this::ageStep,
UserProfileDialog::pictureStep,
this::confirmStep,
this::summaryStep
};
// Add named dialogs to the DialogSet. These names are saved in the dialog state.
addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps)));
addDialog(new TextPrompt("TextPrompt"));
addDialog(new NumberPrompt<Integer>("NumberPrompt", UserProfileDialog::agePromptValidator, Integer.class));
addDialog(new ChoicePrompt("ChoicePrompt"));
addDialog(new ConfirmPrompt("ConfirmPrompt"));
addDialog(new AttachmentPrompt("AttachmentPrompt", UserProfileDialog::picturePromptValidator));
// The initial child Dialog to run.
setInitialDialogId("WaterfallDialog");
}
private static CompletableFuture<DialogTurnResult> transportStep(WaterfallStepContext stepContext) {
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
// Running a prompt here means the next WaterfallStep will be run when the user's response is received.
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Please enter your mode of transport."));
promptOptions.setChoices(ChoiceFactory.toChoices("Car", "Bus", "Bicycle"));
return stepContext.prompt("ChoicePrompt", promptOptions);
}
private static CompletableFuture<DialogTurnResult> nameStep(WaterfallStepContext stepContext) {
stepContext.getValues().put("transport", ((FoundChoice) stepContext.getResult()).getValue());
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Please enter your name."));
return stepContext.prompt("TextPrompt", promptOptions);
}
private CompletableFuture<DialogTurnResult> nameConfirmStep(WaterfallStepContext stepContext) {
stepContext.getValues().put("name", stepContext.getResult());
// We can send messages to the user at any point in the WaterfallStep.
return stepContext.getContext().sendActivity(MessageFactory.text(String.format("Thanks %s.", stepContext.getResult())))
.thenCompose(resourceResponse -> {
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Would you like to give your age?"));
return stepContext.prompt("ConfirmPrompt", promptOptions);
});
}
private CompletableFuture<DialogTurnResult> ageStep(WaterfallStepContext stepContext) {
if ((Boolean)stepContext.getResult()) {
// 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.
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Please enter your age."));
promptOptions.setRetryPrompt(MessageFactory.text("The value entered must be greater than 0 and less than 150."));
return stepContext.prompt("NumberPrompt", promptOptions);
}
// User said "no" so we will skip the next step. Give -1 as the age.
return stepContext.next(-1);
}
private static CompletableFuture<DialogTurnResult> pictureStep(WaterfallStepContext stepContext) {
stepContext.getValues().put("age", (Integer) stepContext.getResult());
String msg = (Integer)stepContext.getValues().get("age") == -1
? "No age given."
: String.format("I have your age as %d.", (Integer)stepContext.getValues().get("age"));
// We can send messages to the user at any point in the WaterfallStep.
return stepContext.getContext().sendActivity(MessageFactory.text(msg))
.thenCompose(resourceResponse -> {
if (StringUtils.equals(stepContext.getContext().getActivity().getChannelId(), Channels.MSTEAMS)) {
// This attachment prompt example is not designed to work for Teams attachments, so skip it in this case
return stepContext.getContext().sendActivity(MessageFactory.text("Skipping attachment prompt in Teams channel..."))
.thenCompose(resourceResponse1 -> stepContext.next(null));
}
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Please attach a profile picture (or type any message to skip)."));
promptOptions.setRetryPrompt(MessageFactory.text("The attachment must be a jpeg/png image file."));
return stepContext.prompt("AttachmentPrompt", promptOptions);
});
}
private CompletableFuture<DialogTurnResult> confirmStep(WaterfallStepContext stepContext) {
List<Attachment> attachments = (List<Attachment>)stepContext.getResult();
stepContext.getValues().put("picture", attachments == null ? null : attachments.get(0));
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
PromptOptions promptOptions = new PromptOptions();
promptOptions.setPrompt(MessageFactory.text("Is this ok?"));
return stepContext.prompt("ConfirmPrompt", promptOptions);
}
private CompletableFuture<DialogTurnResult> summaryStep(WaterfallStepContext stepContext) {
if ((Boolean)stepContext.getResult()) {
// Get the current profile object from user state.
return userProfileAccessor.get(stepContext.getContext(), () -> new UserProfile())
.thenCompose(userProfile -> {
userProfile.transport = (String) stepContext.getValues().get("transport");
userProfile.name = (String) stepContext.getValues().get("name");
userProfile.age = (Integer) stepContext.getValues().get("age");
userProfile.picture = (Attachment) stepContext.getValues().get("picture");
String msg = String.format(
"I have your mode of transport as %s and your name as %s",
userProfile.transport, userProfile.name
);
if (userProfile.age != -1) {
msg += String.format(" and your age as %s", userProfile.age);
}
msg += ".";
return stepContext.getContext().sendActivity(MessageFactory.text(msg))
.thenApply(resourceResponse -> userProfile);
})
.thenCompose(userProfile -> {
if (userProfile.picture != null) {
try {
return stepContext.getContext().sendActivity(
MessageFactory.attachment(userProfile.picture,
"This is your profile picture."
));
} catch(Exception ex) {
return stepContext.getContext().sendActivity(
MessageFactory.text(
"A profile picture was saved but could not be displayed here."
));
}
}
return stepContext.getContext().sendActivity(
MessageFactory.text("A profile picture wasn't attached.")
);
})
.thenCompose(resourceResponse -> stepContext.endDialog());
}
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is the end.
return stepContext.getContext().sendActivity(MessageFactory.text("Thanks. Your profile will not be kept."))
.thenCompose(resourceResponse -> stepContext.endDialog());
}
private static CompletableFuture<Boolean> agePromptValidator(
PromptValidatorContext<Integer> promptContext
) {
// This condition is our validation rule. You can also change the value at this point.
return CompletableFuture.completedFuture(
promptContext.getRecognized().getSucceeded()
&& promptContext.getRecognized().getValue() > 0
&& promptContext.getRecognized().getValue() < 150);
}
private static CompletableFuture<Boolean> picturePromptValidator(
PromptValidatorContext<List<Attachment>> promptContext
) {
if (promptContext.getRecognized().getSucceeded()) {
List<Attachment> attachments = promptContext.getRecognized().getValue();
List<Attachment> validImages = new ArrayList<>();
for (Attachment attachment : attachments) {
if (StringUtils.equals(
attachment.getContentType(), "image/jpeg") || StringUtils.equals(attachment.getContentType(), "image/png")
) {
validImages.add(attachment);
}
}
promptContext.getRecognized().setValue(validImages);
// If none of the attachments are valid images, the retry prompt should be sent.
return CompletableFuture.completedFuture(!validImages.isEmpty());
}
else {
// We can return true from a validator function even if Recognized.Succeeded is false.
return promptContext.getContext().sendActivity("No attachments received. Proceeding without a profile picture...")
.thenApply(resourceResponse -> true);
}
}
}
Para usar diálogos, instale los paquetes de PyPI botbuilder-dialogs y botbuilder-ai mediante la ejecución de pip install botbuilder-dialogs
y pip install botbuilder-ai
desde un terminal.
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_dialog
para acceder al diálogo.
dialogs\user_profile_dialog.py
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.
def __init__(self, user_state: UserState):
super(UserProfileDialog, self).__init__(UserProfileDialog.__name__)
self.user_profile_accessor = user_state.create_property("UserProfile")
self.add_dialog(
WaterfallDialog(
WaterfallDialog.__name__,
[
self.transport_step,
self.name_step,
self.name_confirm_step,
self.age_step,
self.picture_step,
self.summary_step,
self.confirm_step,
],
)
)
self.add_dialog(TextPrompt(TextPrompt.__name__))
self.add_dialog(
NumberPrompt(NumberPrompt.__name__, UserProfileDialog.age_prompt_validator)
)
self.add_dialog(ChoicePrompt(ChoicePrompt.__name__))
self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))
self.add_dialog(
AttachmentPrompt(
AttachmentPrompt.__name__, UserProfileDialog.picture_prompt_validator
)
)
self.initial_dialog_id = WaterfallDialog.__name__
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 step_context.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 name_step
en el diálogo en cascada.
async def name_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
step_context.values["transport"] = step_context.result.value
return await step_context.prompt(
TextPrompt.__name__,
PromptOptions(prompt=MessageFactory.text("Please enter your name.")),
)
En age_step
, 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, como se especifica en el constructor anterior. 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.
async def age_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
if step_context.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.
return await step_context.prompt(
NumberPrompt.__name__,
PromptOptions(
prompt=MessageFactory.text("Please enter your age."),
retry_prompt=MessageFactory.text(
"The value entered must be greater than 0 and less than 150."
),
),
)
# User said "no" so we will skip the next step. Give -1 as the age.
return await step_context.next(-1)
data_models\user_profile.py
El modo de transporte, el nombre y la edad del usuario se guardan en una instancia de la clase UserProfile
.
class UserProfile:
"""
This is our application state. Just a regular serializable Python class.
"""
def __init__(self, name: str = None, transport: str = None, age: int = 0, picture: Attachment = None):
self.name = name
self.transport = transport
self.age = age
self.picture = picture
dialogs\user_profile_dialog.py
En el último paso, compruebe el valor de step_context.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 get
y, a continuación, establezca los valores de las propiedades user_profile.transport
, user_profile.name
, y . Por último, resuma la información del usuario antes de llamar a end_dialog
, 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.
async def summary_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
step_context.values["picture"] = (
None if not step_context.result else step_context.result[0]
)
# Get the current profile object from user state. Changes to it
# will saved during Bot.on_turn.
user_profile = await self.user_profile_accessor.get(
step_context.context, UserProfile
)
user_profile.transport = step_context.values["transport"]
user_profile.name = step_context.values["name"]
user_profile.age = step_context.values["age"]
user_profile.picture = step_context.values["picture"]
msg = f"I have your mode of transport as {user_profile.transport} and your name as {user_profile.name}."
if user_profile.age != -1:
msg += f" And age as {user_profile.age}."
await step_context.context.send_activity(MessageFactory.text(msg))
if user_profile.picture:
await step_context.context.send_activity(
MessageFactory.attachment(
user_profile.picture, "This is your profile picture."
)
)
else:
await step_context.context.send_activity(
"A profile picture was saved but could not be displayed here."
)
# WaterfallStep always finishes with the end of the Waterfall or with another
# dialog, here it is the end.
return await step_context.prompt(
ConfirmPrompt.__name__,
Creación del método de extensión para ejecutar el diálogo en cascada
Se define un método auxiliar run_dialog()
dentro de helpers\dialog_helper.py que se usa para crear y acceder al contexto del diálogo. En este caso, accessor
es el descriptor de acceso de la propiedad de estado de la propiedad de estado del diálogo y dialog
es el diálogo de componente de perfil de usuario. Puesto que los diálogos de componente definen un conjunto de diálogos interno, se debe crear un conjunto de diálogos externo que sea visible para el código del controlador de mensajes y usarlo para crear un contexto de diálogo.
Cree el contexto del diálogo mediante una llamada a create_context
, que se usa para interactuar con el conjunto de diálogos desde dentro del controlador de turnos del bot. El contexto del diálogo incluye el contexto del turno actual, el diálogo primario y el estado del diálogo, que proporciona un método para conservar información en el diálogo.
El contexto del diálogo permite iniciar un diálogo con el identificador de cadena o continuar el diálogo actual (por ejemplo, un diálogo en cascada que tiene varios pasos). El contexto del diálogo se pasa a todos los diálogos y pasos de cascada del bot.
class DialogHelper:
@staticmethod
async def run_dialog(
dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor
):
dialog_set = DialogSet(accessor)
dialog_set.add(dialog)
dialog_context = await dialog_set.create_context(turn_context)
results = await dialog_context.continue_dialog()
if results.status == DialogTurnStatus.Empty:
await dialog_context.begin_dialog(dialog.id)
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: