前回は単純なダイアログを紹介しましたが、今回はより複雑なダイアログを見ていきます。
ウォーターフォールダイアログ
複数のステップを順次処理する用のダイアログ。各ステップでは以下のような処理が行われる。
- プロンプトをユーザーに送信
- ユーザーからの入力を処理
- ウォーターフォール処理の完了
ウォーターフォールステップ
ウォーターフォールダイアログはウォーターフォールステップと呼ばれるメソッドを実行順に持ちます。ウォーターフォールステップは以下のシグネチャを持っています。
Task<DialogTurnResult> StepName(WaterfallStepContext stepContext, CancellationToken cancellationToken)
これらのステップを WaterfallStep 配列に設定して、ウォーターフォールダイアログを作成します。
var waterfallSteps = new WaterfallStep[]
{
Step1,
Step2,
Step3,
....
};
dialogs.Add(new WaterfallDialog("dialog_name", waterfallSteps));
WaterfallStepContext
DialogContext を継承しており、前処理の結果などウォーターフォールダイアログ実行に必要な情報を含んでいます。
コンポーネントダイアログ
再利用可能なダイアログを作成するための仕組みとして、コンポーネントダイアログが提供されています。DialogSet を独自に持てるため、ダイアログ名などの競合を避けることができる他、DialogContext も分離されます。
ウォーターフォールダイアログを使ったボットの開発
前回は名前を聞くプロンプトダイアログを実装しましたが、今回はウォーターフォールダイアログを使って、ユーザープロファイルを聞くボットを実装します。
モデルの追加とステート管理の更新
まずはユーザープロファイル用のクラス追加とアクセサーを更新します。
1. Models フォルダを追加して、UserProfile.cs を追加。以下のコードを追加。
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}
2. MyStateAccessors.cs ファイルを以下コードで差し替え。
using System;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
public class MyStateAccessors
{
public MyStateAccessors(
UserState userState,
ConversationState conversationState)
{
UserState = userState ?? throw new ArgumentNullException(nameof(userState));
ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
}
public IStatePropertyAccessor<UserProfile> UserProfile { get; set; }
public IStatePropertyAccessor<DialogState> ConversationDialogState { get; set; }
public UserState UserState { get; }
public ConversationState ConversationState { get; }
}
3. Startup.cs の ConfigureServices を以下の様に変更してアクセサーの変更を反映。
public void ConfigureServices(IServiceCollection services)
{
services.AddBot<MyBot>(options =>
{
options.Middleware.Add(new MyLoggingMiddleware());
options.Middleware.Add(new MyMiddleware());
// ストレージとしてインメモリを利用
IStorage dataStore = new MemoryStorage();
var userState = new UserState(dataStore);
var conversationState = new ConversationState(dataStore);
options.State.Add(userState);
options.State.Add(conversationState);
});
// MyStateAccessors を IoC に登録
services.AddSingleton(sp =>
{
// AddBot で登録した options を取得。
var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
if (options == null)
{
throw new InvalidOperationException("BotFrameworkOptions を事前に構成してください。");
}
var userState = options.State.OfType<UserState>().FirstOrDefault();
if (userState == null)
{
throw new InvalidOperationException("UserState を事前に定義してください。");
}
var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
if (conversationState == null)
{
throw new InvalidOperationException("ConversationState を事前に定義してください。");
}
var accessors = new MyStateAccessors(userState, conversationState)
{
// DialogState を作成
ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
// UserProfile を作成
UserProfile = userState.CreateProperty<UserProfile>("UserProfile")
};
return accessors;
});
}
ウォーターフォールダイアログの実装
次にウォーターフォールダイアログを使ったユーザープロファイル取得のコードを実装します。
1. MyBot.cs の MyBot クラスに以下メソッドを追加。それぞれがウォーターフォールダイアログのステップとなる。
private async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// "name" ダイアログ(プロンプト) を返信。
// ユーザーに対する表示は PromptOptions で指定。
return await stepContext.PromptAsync("name", new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください。") }, cancellationToken);
}
private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// UserProfile をステートより取得
var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
// Result より名前の更新
userProfile.Name = (string)stepContext.Result;
// 年齢を聞いてもいいか確認のため "confirm" ダイアログを送信。
return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text("年齢を聞いてもいいですか?") }, cancellationToken);
}
private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Result より結果の確認
if ((bool)stepContext.Result)
{
// 年齢を聞いてもいい場合は "age" ダイアログを送信
return await stepContext.PromptAsync("age", new PromptOptions { Prompt = MessageFactory.Text("年齢を入力してください。") }, cancellationToken);
}
else
{
// "いいえ" を選択した場合、次のステップに進む。"age" ダイアログの結果は "-1" を指定。
return await stepContext.NextAsync(-1, cancellationToken);
}
}
private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// 年齢の回答を取得
var age = (int)stepContext.Result;
// UserProfile をステートより取得
var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
// 年齢をスキップしなかった場合はユーザープロファイルに設定
if (age != -1)
userProfile.Age = age;
// 全て正しいか確認。"confirm" ダイアログを再利用。
var prompt = $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{userProfile.Name} 年齢:{userProfile.Age}";
return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text(prompt) }, cancellationToken);
}
private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if ((bool)stepContext.Result)
await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを保存します。"));
else
await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを破棄します。"));
// ダイアログを終了
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
2. コンストラクターを変更してウォーターフォールダイアログを定義。
public MyBot(MyStateAccessors accessors)
{
this.accessors = accessors;
this.dialogs = new DialogSet(accessors.ConversationDialogState);
// ウォーターフォールのステップを定義。処理順にメソッドを追加。
var waterfallSteps = new WaterfallStep[]
{
NameStepAsync,
NameConfirmStepAsync,
AgeStepAsync,
ConfirmStepAsync,
SummaryStepAsync,
};
// ウォーターフォールダイアログと各種プロンプトを追加
dialogs.Add(new WaterfallDialog("profile", waterfallSteps));
dialogs.Add(new TextPrompt("name"));
dialogs.Add(new NumberPrompt<int>("age"));
dialogs.Add(new ConfirmPrompt("confirm"));
}
3. OnTurnAsync で profile ダイアログを実行するように変更。
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext.Activity.Type == ActivityTypes.Message)
{
// DialogSet からコンテキストを作成
var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);
// まず ContinueDialogAsync を実行して既存のダイアログがあれば継続実行。
var results = await dialogContext.ContinueDialogAsync(cancellationToken);
// DialogTurnStatus が Empty の場合は既存のダイアログがないため、新規に実行
if (results.Status == DialogTurnStatus.Empty)
{
// ウォーターフォールダイアログを送信
await dialogContext.BeginDialogAsync("profile", null, cancellationToken);
}
// DialogTurnStatus が Complete の場合、ダイアログは完了したため結果を処理
else if (results.Status == DialogTurnStatus.Complete)
{
var userProfile = await accessors.UserProfile.GetAsync(turnContext);
await turnContext.SendActivityAsync(MessageFactory.Text($"ようこそ '{userProfile.Name}' さん!"));
}
// 最後に現在の UserProfile と DialogState を保存
await accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
await accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
}
4. デバッグを開始して BotFramework Emulator で動作を確認。
コンポーネントダイアログを使ったボットの開発
ここでは、上記で実装したウォーターフォールダイアログをコンポーネントダイアログとして再利用しやすい形に変えてみます。
1. ProfileDialog.cs を追加して、以下のコードと差し替え。内容は上記で実装したウォーターフォールダイアログとほぼ同じだが、ComponentDialog を継承しており、ダイアログ ID として nameof(ProfileDialog) を使用。また DialogSet は利用せず、AddDialog メソッドで各ダイアログを追加。
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
public class ProfileDialog : ComponentDialog
{
private MyStateAccessors accessors;
public ProfileDialog(MyStateAccessors accessors) : base(nameof(ProfileDialog))
{
this.accessors = accessors;
// ウォーターフォールのステップを定義。処理順にメソッドを追加。
var waterfallSteps = new WaterfallStep[]
{
NameStepAsync,
NameConfirmStepAsync,
AgeStepAsync,
ConfirmStepAsync,
SummaryStepAsync,
};
// ウォーターフォールダイアログと各種プロンプトを追加
AddDialog(new WaterfallDialog("profile", waterfallSteps));
AddDialog(new TextPrompt("name"));
AddDialog(new NumberPrompt<int>("age"));
AddDialog(new ConfirmPrompt("confirm"));
}
private async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// "name" ダイアログ(プロンプト) を返信。
// ユーザーに対する表示は PromptOptions で指定。
return await stepContext.PromptAsync("name", new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください。") }, cancellationToken);
}
private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// UserProfile をステートより取得
var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
// Result より名前の更新
userProfile.Name = (string)stepContext.Result;
// 年齢を聞いてもいいか確認のため "confirm" ダイアログを送信。
return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text("年齢を聞いてもいいですか?") }, cancellationToken);
}
private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Result より結果の確認
if ((bool)stepContext.Result)
{
// 年齢を聞いてもいい場合は "age" ダイアログを送信
return await stepContext.PromptAsync("age", new PromptOptions { Prompt = MessageFactory.Text("年齢を入力してください。") }, cancellationToken);
}
else
{
// "いいえ" を選択した場合、次のステップに進む。"age" ダイアログの結果は "-1" を指定。
return await stepContext.NextAsync(-1, cancellationToken);
}
}
private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// 年齢の回答を取得
var age = (int)stepContext.Result;
// UserProfile をステートより取得
var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
// 年齢をスキップしなかった場合はユーザープロファイルに設定
if (age != -1)
userProfile.Age = age;
// 全て正しいか確認。"confirm" ダイアログを再利用。
var prompt = $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{userProfile.Name} 年齢:{userProfile.Age}";
return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text(prompt) }, cancellationToken);
}
private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if ((bool)stepContext.Result)
await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを保存します。"));
else
await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを破棄します。"));
// ダイアログを終了
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
}
2. MyBot.cs をコンポーネントダイアログを呼び出すように変更。
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
public class MyBot : IBot
{
private MyStateAccessors accessors;
private DialogSet dialogs;
// DI で MyStateAccessors は自動解決
public MyBot(MyStateAccessors accessors)
{
this.accessors = accessors;
this.dialogs = new DialogSet(accessors.ConversationDialogState);
// コンポーネントダイアログを追加
dialogs.Add(new ProfileDialog(accessors));
}
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext.Activity.Type == ActivityTypes.Message)
{
// DialogSet からコンテキストを作成
var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);
// まず ContinueDialogAsync を実行して既存のダイアログがあれば継続実行。
var results = await dialogContext.ContinueDialogAsync(cancellationToken);
// DialogTurnStatus が Empty の場合は既存のダイアログがないため、新規に実行
if (results.Status == DialogTurnStatus.Empty)
{
// コンポーネントダイアログを送信
await dialogContext.BeginDialogAsync(nameof(ProfileDialog), null, cancellationToken);
}
// DialogTurnStatus が Complete の場合、ダイアログは完了したため結果を処理
else if (results.Status == DialogTurnStatus.Complete)
{
var userProfile = await accessors.UserProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);
await turnContext.SendActivityAsync(MessageFactory.Text($"ようこそ '{userProfile.Name}' さん!"));
}
// 最後に現在の UserProfile と DialogState を保存
await accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
await accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
}
}
3. BotFramework Emulator で動作を確認。
まとめ
今回はウォーターフォールダイアログとコンポーネントダイアログの使い方を見ていきました。またプロンプトの再利用シナリオもカバーしたため、より柔軟なダイアログを作れるようになりました。次回は複数ダイアログの管理について見ていきます。