Реализация последовательного потока общения
ОБЛАСТЬ ПРИМЕНЕНИЯ: ПАКЕТ SDK версии 4
Сбор данных путем размещения вопросов — это один из основных способов взаимодействия между ботом и пользователями. Библиотека диалогов предоставляет полезные встроенные функции, такие как классы запросов, которые позволяют легко задавать вопросы и проверять ответ, чтобы убедиться в том, что он соответствует определенному типу данных или удовлетворяет пользовательские правила проверки.
Вы можете управлять линейными и более сложными потоками бесед с помощью библиотеки диалогов. В линейном взаимодействии бот проходит по фиксированной последовательности шагов, после чего беседа завершается. Диалоговое окно полезно, если боту нужно собрать сведения от пользователя.
В этой статье показано, как реализовать поток линейного диалога, создавая подсказки и вызывая их из каскадного диалога. Примеры написания запросов без использования библиотеки диалогов см. в статье о создании собственных запросов для сбора вводимых пользовательских данных.
Примечание.
Пакеты SDK для JavaScript, C# и Python в Bot Framework будут поддерживаться и далее, однако пакет SDK для Java будет снят с эксплуатации, причём окончательная долгосрочная поддержка завершится в ноябре 2023 года.
Существующие боты, созданные с помощью пакета SDK для Java, будут продолжать функционировать.
Для создания нового бота рекомендуется использовать Microsoft Copilot Studio и прочитать о выборе подходящего решения copilot.
Дополнительные сведения см. в статье "Будущее создания бота".
Предварительные условия
- Понимание основных принципов работы ботов, управления состоянием и библиотеки диалогов.
- Копия примера запросов с несколькими поворотами на C#, JavaScript, Java или Python.
Об этом примере
В примере с несколькими запросами используется каскадное диалоговое окно, несколько запросов и диалоговое окно компонента для создания линейного взаимодействия, которое задает пользователю ряд вопросов. Код использует диалог для циклического выполнения следующих шагов:
Шаги | Тип запроса |
---|---|
Запрос к пользователю о режиме транспортировки | Запрос выбора |
Запрос имени пользователя | Запрос текста |
Запрос к пользователю, готов ли он указать свой возраст | Подтвердить запрос |
Если они ответили да, узнайте их возраст | Числовая строка с проверкой только для принятия возрастов, превышающих 0 и менее 150 |
Если пользователь не использует Microsoft Teams, запросите изображение профиля | Запрос на вложение с проверкой, чтобы разрешить отсутствующие вложения |
Спросите, является ли собранная информация "в порядке" | Повторный запрос подтверждения |
Наконец, если они ответили да, отобразите собранные сведения; в противном случае сообщите пользователю, что их сведения не будут храниться.
Создание главного диалога
Чтобы использовать диалоги, установите пакет NuGet Microsoft.Bot.Builder.Dialogs.
Бот взаимодействует с пользователем через UserProfileDialog
. При создании класса DialogBot
бота UserProfileDialog
устанавливается в качестве основного диалогового окна. Затем бот применяет вспомогательный метод 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
, не равное null. Если вы этого не сделали, возможно, диалоговое окно не работает так, как это было разработано. Ниже представлена реализация 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.Transport
, userProfile.Name
, userProfile.Age
и 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);
Запуск диалога
Bots\DialogBot.cs
Обработчик OnMessageActivityAsync
использует метод RunAsync
, чтобы начать или продолжить диалог.
OnTurnAsync
использует объекты управления состоянием бота для сохранения любых изменений состояния в хранилище. Метод ActivityHandler.OnTurnAsync
вызывает разные методы обработки действий, например OnMessageActivityAsync
. Таким образом, состояние сохраняется после завершения обработчика сообщений, но до завершения самого этапа.
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);
}
Регистрация служб для бота
Этот бот использует следующие службы:
- Основные службы бота: поставщик учетных данных, адаптер и реализация бота.
- Службы для управления состоянием: хранилище, состояние пользователя и состояние беседы.
- Диалог, который будет использовать бот.
Startup.cs
Регистрация служб для бота в Startup
. Эти службы доступны в других частях кода через механизм внедрения зависимостей.
{
// 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>();
Примечание.
Хранилище памяти используется только для тестирования и не предназначено для использования в рабочей среде. Для продакшн-ботов обязательно используйте хранилище постоянного типа.
Тестирование бота
- Если это еще не сделано, установите эмулятор Bot Framework.
- Выполните этот пример на локальном компьютере.
- Запустите эмулятор, подключитесь к боту и отправьте несколько сообщений, как показано ниже.
Дополнительная информация:
Сведения о диалоге и состоянии бота
В этом боте определены два метода доступа к свойству состояния:
- Один из них создается в состоянии беседы для свойства состояния диалога. Состояние диалогового окна отслеживает, где пользователь находится в диалоговых окнах набора диалогов и обновляется контекстом диалога, например при вызове методов запуска илипродолжения диалога .
- Один создается в рамках состояния пользователя для свойства профиля пользователя. Бот использует это для отслеживания информации о пользователе, и необходимо явно управлять этим состоянием в коде диалога.
Методы доступа к свойству состояния get и set получают и устанавливают значение этого свойства в кэше объекта управления состоянием. Кэш заполняется при первом запросе значения свойства состояния в течение обращения, но сохранять его нужно в явной форме. Чтобы сохранить изменения обоих этих свойств состояния, выполняется вызов метода сохранения изменений соответствующего объекта управления состоянием.
В этом примере обновляется состояние профиля пользователя из диалога. Эта практика может работать для некоторых ботов, но она не будет работать, если вы хотите повторно использовать диалоговое окно между ботами.
Есть несколько подходов, позволяющих отделить этапы диалога от состояний бота. Например, после сбора полной информации вы можете сделать следующее:
- Выполните метод end dialog, чтобы передать собранные данные в возвращаемом значении обратно в контекст родительского элемента. Это может быть обработчик поворота бота или более раннее активное диалоговое окно в стеке диалогов, и это способ разработки классов запросов.
- Создайте запрос к соответствующей службе. Это может быть хорошим вариантом, если бот выступает в роли интерфейса для более крупной службы.
Определение метода валидатора подсказок
UserProfileDialog.cs
Ниже представлен пример кода валидатора для определения метода AgePromptValidatorAsync
.
promptContext.Recognized.Value
содержит распарсенное значение, в данном случае это целое число для запроса числа.
promptContext.Recognized.Succeeded
указывает, удалось ли запросу разобрать ввод пользователя. Проверяющий элемент должен возвращать значение false, чтобы указать, что значение не было принято, и диалоговое окно запроса должно повторно проимышировать пользователя; в противном случае верните значение true, чтобы принять входные данные и вернуться из диалогового окна запроса. Значение в валидаторе можно изменить в соответствии с вашим сценарием.
}
// 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);
}