Bot Framework と Microsoft Graph で DevOps その 20 : ステートサービスで状態の管理

今回は状態管理について。既にアプリのコードで多用してますけど、あらためて説明を。

概要

ボットに欠かせない重要な機能が状態管理。例えば、前回ユーザーが何を言ったかという単純な事から、ユーザーや会話のコンテキストを理解するため等に使います。Bot Framework はステートサービスと呼ばれる状態管理の仕組みを提供しており、ダイアログやフォームフローは、裏でこの仕組みを使ってユーザーとの会話を管理しています。

スコープ

Bot Framework は以下 3 つのスコープで状態管理が行えます。

  • ユーザー単位 (UserData) : 会話に関わらずユーザー個人としてのステート。
  • 会話単位 (ConversationData) : 現在の会話に関するステート。会話は対個人も対グループもある。
  • 会話の中の個人単位 (PrivateConversationData) : 現在の会話かつ、特定個人に関するステート。

どのスコープにデータを保存するかは、データの性質によって変えます。

ステートサービスのデータ保存場所

既定でマイクロソフトが管理するサーバーに保存しますが、独自に管理したい場合、任意の Azure Table Storage か DocumentDB に変更が可能です。サンプルが GitHub に公開されているので参考にしてください。データの保存地域やスペックも独自に決められるため、パフォーマンス向上にもなります。

尚、以下主な注意点。

  • ステートはチャネル単位。実質同一ユーザーでもチャネルが異なるとステートは別と扱われる。
  • 保存するデータはシリアライズ可能なもの。
  • 保存できるデータサイズは 64 KB。情報元はこちら。マスターデータ的なものは別途他の DB から取得し、ボットアプリに関連する情報のみステートサービスに保存。
  • ステートサービスはボットアプリを公開するサーバーとは異なる場所にデータを保存するため、サービスを再公開してもデータは消えない。一方エミュレータを使っている場合、ステート情報はエミュレータ内に保持。よってエミュレータを再起動するとデータが消える。

ステートクライアント

ステートサービスに接続するためのクライアントは、以下の方法で作成、取得できます。

新規に作成

 StateClient stateClient = new StateClient(new MicrosoftAppCredentials(microsoftAppId, microsoftAppPassword));

Activity から作成

 StateClient stateClient = activity.GetStateClient();

ダイアログの場合は、IDialogContext に既にステートが含まれます。

ステートクライアントの使い方詳細はこちら

同時実行制御

同時に複数のスレッドからステートを保存し競合が発生すると、HTTP ステータス 412 エラーが出ます。以下のようにハンドルしてください。

 try
 {
 // ユーザーデータの取得
 BotData userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);

// データの変更
 userData.SetProperty<bool>("SentGreeting", true);

// データを保存
 await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
 }
 catch (HttpOperationException err)
 {
 // HTTP status 412 Precondition Failed エラーをハンドル
 }

ボットアプリでの活用場所

ダイアログ

ダイアログおよびフォームフローは、ダイアログ全体をシリアライズして、ステートを管理しています。明示的な保存や取得は必要ありません。デバッグ中に PrivateConversationData の DialogState が実体です。

image

Office 365 Webhook の購読 ID プロアクティブ通知の回で実装した Office 365 Webhook の情報を、ユーザー単位のステートに格納しています。

 // 変更を購読
 var subscriptionId = context.UserData.GetValueOrDefault<string>("SubscriptionId", "");
 if (string.IsNullOrEmpty(subscriptionId))
 {
 subscriptionId = await service.SubscribeEventChange();
 context.UserData.SetValue("SubscriptionId", subscriptionId);
 }

AuthBot

AuthBot も認証情報をステートサービスに格納しています。GitHub のコードを確認してください。

ユニットテスト

既に利用しているユニットテストの仕組みでは、ステートサービスもモックされています。ただしチャネルなどを個別に明記した方法でデータの管理をしている場合は考慮が必要です。

まとめ

個人的には、会話の進捗状況管理が一番面倒だと思います。BotBuilder のダイアログはそこを自動でカバーしているのが最高です。他に保存すべきステートがあればステートサービスを活用してください。次回は音声対応について紹介します。