Сбор данных путем размещения вопросов — это один из основных способов взаимодействия между ботом и пользователями. Библиотека диалогов предоставляет полезные встроенные функции, такие как классы запросов, которые позволяют легко задавать вопросы и проверять ответ, чтобы убедиться в том, что он соответствует определенному типу данных или удовлетворяет пользовательские правила проверки.
Вы можете управлять линейными и более сложными потоками бесед с помощью библиотеки диалогов. В линейном взаимодействии бот выполняется через фиксированную последовательность шагов, а беседа завершается. Диалоговое окно полезно, если боту нужно собрать сведения от пользователя.
В примере с несколькими запросами используется каскадное диалоговое окно, несколько запросов и диалоговое окно компонента для создания линейного взаимодействия, которое задает пользователю ряд вопросов. Код диалога циклически перебирает следующие действия:
Наконец, если они ответили да, отобразите собранные сведения; в противном случае сообщите пользователю, что их сведения не будут храниться.
Чтобы использовать диалоги, установите пакет NuGet Microsoft.Bot.Builder.Dialogs.
Бот взаимодействует с пользователем через UserProfileDialog
. При создании класса UserProfileDialog
бота DialogBot
устанавливается в качестве основного диалогового окна. Затем бот применяет вспомогательный метод Run
для доступа к этому диалогу.
Dialogs\UserProfileDialog.cs
Начните с создания, наследуемого UserProfileDialog
ComponentDialog
от класса, и имеет семь шагов.
В конструкторе UserProfileDialog
создайте каскадные шаги, запросы и каскадный диалог, затем добавьте их в набор диалогов. Запросы должны находиться в том же диалоговом окне, в котором они используются.
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);
}
Затем добавьте шаги, которые диалоговое окно использует для запроса ввода данных. Чтобы использовать запрос, вызовите его из любого шага диалога и получите результат на следующем шаге с помощью stepContext.Result
. Запросы данных, по сути, являются диалогами из двух этапов. Во-первых, запрос запрашивает входные данные. Затем он возвращает допустимое значение или начинается с начала с повторного выпуска, пока он не получит допустимые входные данные.
Из каскадного шага следует всегда возвращать ненулевое значение DialogTurnResult
. Если вы этого не сделали, возможно, диалоговое окно не работает так, как это было разработано. Ниже показана реализация NameStepAsync
в каскадном диалоговом окне.
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);
}
В AgeStepAsync
поле укажите запрос повторных попыток, когда входные данные пользователя не удается проверить, либо потому, что он находится в формате, который запрос не может проанализировать, либо входные данные завершались сбоем условий проверки. В этом случае, если запрос повторных попыток не был указан, запрос будет использовать начальный текст запроса для повторного воспроизведения пользователя для ввода.
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
Режим транспортировки, имя и возраст пользователя сохраняются в экземпляре класса 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
На последнем шаге проверьте stepContext.Result
возвращенное диалогом диалоговое окно, которое вызывается на предыдущем каскадном шаге. Если возвращаемое значение равно true, метод доступа к профилею пользователя получает и обновляет профиль пользователя. Чтобы получить профиль пользователя, вызовите GetAsync
и задайте значения userProfile.Age
userProfile.Transport
userProfile.Name
свойств и userProfile.Picture
свойств. Наконец, сводные сведения для пользователя перед вызовом EndDialogAsync
, который заканчивает диалоговое окно. Завершенный диалог удаляется из стека диалогов, а его результат (если есть) возвращается в родительский диалог. Родительским считается диалог или метод, в котором был запущен только что завершившийся диалог.
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);
Чтобы использовать диалоги, в проект следует установить пакет npm botbuilder-dialogs.
Бот взаимодействует с пользователем через UserProfileDialog
. При создании бота DialogBot
UserProfileDialog
он устанавливается в качестве основного диалогового окна. Затем бот применяет вспомогательный метод run
для доступа к этому диалогу.
dialogs/userProfileDialog.js
Начните с создания, наследуемого UserProfileDialog
ComponentDialog
от класса, и имеет семь шагов.
В конструкторе UserProfileDialog
создайте каскадные шаги, запросы и каскадный диалог, затем добавьте их в набор диалогов. Запросы должны находиться в том же диалоговом окне, в котором они используются.
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;
}
Затем добавьте шаги, которые диалоговое окно использует для запроса ввода данных. Чтобы использовать запрос, вызовите его из любого шага диалога и получите результат из контекста шага на следующем шаге. Здесь для этого используется step.result
. Запросы данных, по сути, являются диалогами из двух этапов. Во-первых, запрос запрашивает входные данные. Затем он возвращает допустимое значение или начинается с начала с повторного выпуска, пока он не получит допустимые входные данные.
Из каскадного шага следует всегда возвращать ненулевое значение DialogTurnResult
. Если вы этого не сделали, возможно, диалоговое окно не работает так, как это было разработано. Ниже показана реализация nameStep
в каскадном диалоговом окне.
async nameStep(step) {
step.values.transport = step.result.value;
return await step.prompt(NAME_PROMPT, 'Please enter your name.');
}
В ageStep
поле укажите запрос повторных попыток, когда входные данные пользователя не удается проверить, либо потому, что он находится в формате, который запрос не может проанализировать, либо входные данные не удается проверить критерии проверки, указанные в приведенном выше конструкторе. В этом случае, если запрос повторных попыток не был указан, запрос будет использовать начальный текст запроса для повторного воспроизведения пользователя для ввода.
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
Режим транспортировки, имя и возраст пользователя сохраняются в экземпляре класса UserProfile
.
class UserProfile {
constructor(transport, name, age, picture) {
this.transport = transport;
this.name = name;
this.age = age;
this.picture = picture;
}
}
dialogs/userProfileDialog.js
На последнем шаге проверьте step.result
возвращенное диалогом диалоговое окно, которое вызывается на предыдущем каскадном шаге. Если возвращаемое значение равно true, метод доступа к профилею пользователя получает и обновляет профиль пользователя. Чтобы получить профиль пользователя, вызвать get
и задать значения userProfile.age
userProfile.transport
userProfile.name
свойств и userProfile.picture
свойств. Наконец, сводные сведения для пользователя перед вызовом endDialog
, который заканчивает диалоговое окно. Завершенный диалог удаляется из стека диалогов, а его результат (если есть) возвращается в родительский диалог. Родительским считается диалог или метод, в котором был запущен только что завершившийся диалог.
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.');
}
Создание метода расширения для запуска каскадного диалога
Вспомогательный run
метод, определенный внутри userProfileDialog
, используется для создания и доступа к контексту диалога. Здесь accessor
является методом доступа к свойству состояния для диалога, а this
— это компонентный диалог для профиля пользователя. Так как диалоги компонентов определяют внутренний набор диалогов, внешний набор диалогов должен быть создан для кода обработчика сообщений и используется для создания контекста диалога.
Контекст диалога создается путем вызова метода createContext
и используется для взаимодействия с набором диалогов в обработчике шагов бота. Контекст диалога содержит сведения о текущем шаге, о родительском диалоге, а также состояние диалога для сохранения данных в ходе диалога.
Контекст диалогового окна позволяет запустить диалоговое окно с идентификатором строки или продолжить текущий диалог (например, каскадный диалог с несколькими шагами). Контекст диалога передается через все диалоги и каскадные действия бота.
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);
}
}
Бот взаимодействует с пользователем через UserProfileDialog
. При создании класса UserProfileDialog
бота DialogBot
устанавливается в качестве основного диалогового окна. Затем бот применяет вспомогательный метод Run
для доступа к этому диалогу.
UserProfileDialog.java
Начните с создания, наследуемого UserProfileDialog
ComponentDialog
от класса, и имеет семь шагов.
В конструкторе UserProfileDialog
создайте каскадные шаги, запросы и каскадный диалог, затем добавьте их в набор диалогов. Запросы должны находиться в том же диалоговом окне, в котором они используются.
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");
}
Затем добавьте шаги, которые диалоговое окно использует для запроса ввода данных. Чтобы использовать запрос, вызовите его из любого шага диалога и получите результат на следующем шаге с помощью stepContext.getResult()
. Запросы данных, по сути, являются диалогами из двух этапов. Во-первых, запрос запрашивает входные данные. Затем он возвращает допустимое значение или начинается с начала с повторного выпуска, пока он не получит допустимые входные данные.
Из каскадного шага следует всегда возвращать ненулевое значение DialogTurnResult
. Если вы этого не сделали, возможно, диалоговое окно не работает так, как это было разработано. Ниже показана реализация nameStep
в каскадном диалоговом окне.
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);
}
В ageStep
поле укажите запрос повторных попыток, когда входные данные пользователя не удается проверить, либо потому, что он находится в формате, который запрос не может проанализировать, либо входные данные завершались сбоем условий проверки. В этом случае, если запрос повторных попыток не был указан, запрос будет использовать начальный текст запроса для повторного воспроизведения пользователя для ввода.
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
Режим транспортировки, имя и возраст пользователя сохраняются в экземпляре класса 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
На последнем шаге проверьте stepContext.Result
возвращенное диалогом диалоговое окно, которое вызывается на предыдущем каскадном шаге. Если возвращаемое значение равно true, метод доступа к профилею пользователя получает и обновляет профиль пользователя. Чтобы получить профиль пользователя, вызовите get
и задайте значения userProfile.Age
userProfile.Transport
userProfile.Name
свойств и userProfile.Picture
свойств. Наконец, сводные сведения для пользователя перед вызовом endDialog
, который заканчивает диалоговое окно. Завершенный диалог удаляется из стека диалогов, а его результат (если есть) возвращается в родительский диалог. Родительским считается диалог или метод, в котором был запущен только что завершившийся диалог.
// 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);
}
}
}
Чтобы использовать диалоги, установите пакеты PyPI botbuilder-dialogs и botbuilder-ai, выполнив pip install botbuilder-dialogs
и pip install botbuilder-ai
в терминале.
Бот взаимодействует с пользователем через UserProfileDialog
. При создании UserProfileDialog
класса бота DialogBot
устанавливается в качестве основного диалогового окна. Затем бот применяет вспомогательный метод run_dialog
для доступа к этому диалогу.
dialogs\user_profile_dialog.py
Начните с создания, наследуемого UserProfileDialog
ComponentDialog
от класса, и имеет семь шагов.
В конструкторе UserProfileDialog
создайте каскадные шаги, запросы и каскадный диалог, затем добавьте их в набор диалогов. Запросы должны находиться в том же диалоговом окне, в котором они используются.
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__
Затем добавьте шаги, которые диалоговое окно использует для запроса ввода данных. Чтобы использовать запрос, вызовите его из любого шага диалога и получите результат на следующем шаге с помощью step_context.result
. Запросы данных, по сути, являются диалогами из двух этапов. Во-первых, запрос запрашивает входные данные. Затем он возвращает допустимое значение или начинается с начала с повторного выпуска, пока он не получит допустимые входные данные.
Из каскадного шага следует всегда возвращать ненулевое значение DialogTurnResult
. Если вы этого не сделали, возможно, диалоговое окно не работает так, как это было разработано. Здесь можно увидеть реализацию для каскадного name_step
диалога.
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.")),
)
В age_step
поле укажите запрос повторных попыток, когда входные данные пользователя не удается проверить, либо потому, что он находится в формате, который запрос не может проанализировать, либо входные данные не удается проверить критерии проверки, указанные в приведенном выше конструкторе. В этом случае, если запрос повторных попыток не указан, запрос будет использовать начальный текст запроса для повторного воспроизведения пользователя для ввода
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
Режим транспортировки, имя и возраст пользователя сохраняются в экземпляре класса 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
На последнем шаге проверьте step_context.result
возвращенное диалогом диалоговое окно, которое вызывается на предыдущем каскадном шаге. Если возвращаемое значение равно true, метод доступа к профилею пользователя получает и обновляет профиль пользователя. Чтобы получить профиль пользователя, вызвать get
и задать значения user_profile.transport
user_profile.name
свойств и user_profile.age
свойств. Наконец, сводные сведения для пользователя перед вызовом end_dialog
, который заканчивает диалоговое окно. Завершенный диалог удаляется из стека диалогов, а его результат (если есть) возвращается в родительский диалог. Родительским считается диалог или метод, в котором был запущен только что завершившийся диалог.
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__,
Создание метода расширения для запуска каскадного диалога
Вспомогательный run_dialog()
метод определен в вспомогательных элементах\dialog_helper.py , который используется для создания и доступа к контексту диалога. Здесь accessor
является методом доступа к свойству состояния для диалога, а dialog
— это компонентный диалог для профиля пользователя. Так как диалоги компонентов определяют внутренний набор диалогов, внешний набор диалогов должен быть создан для кода обработчика сообщений и использовать его для создания контекста диалога.
Создайте контекст диалогового окна путем вызова create_context
, который используется для взаимодействия с набором диалогов из обработчика поворота бота. Контекст диалога содержит сведения о текущем шаге, о родительском диалоге, а также состояние диалога для сохранения данных в ходе диалога.
Контекст диалога позволяет начать диалог по идентификатору строки или продолжить текущий диалог (например, выполнить очередной шаг каскадного диалога). Контекст диалога передается через все диалоги и каскадные действия бота.
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)
В этом примере обновляется состояние профиля пользователя из диалога. Эта практика может работать для некоторых ботов, но она не будет работать, если вы хотите повторно использовать диалоговое окно между ботами.
Есть несколько подходов, позволяющих отделить этапы диалога от состояний бота. Например, после сбора информации вы можете сделать следующее: