状態の管理
適用対象: SDK v4
ボット内の状態は、最新の Web アプリケーションと同じパラダイムに従います。また、Bot Framework SDK には、状態の管理を容易にするための抽象化概念がいくつか用意されています。
Web アプリと同様、ボットは本質的にはステートレスです。つまり、お使いのボットの別のインスタンスで、会話の任意のターンを処理できます。 一部のボットでは、このシンプルさが優先されます。ボットは追加情報なしで動作するか、必要な情報が受信メッセージ内にあることが保証されます。 他のユーザーの場合、ボットが便利な会話を行うためには、状態 (会話が中断された場所やユーザーに関して以前に受信したデータなど) が必要です。
状態が必要な理由
状態を維持すると、お使いのボットは、ユーザーまたは会話に関する特定の情報を記憶することで、より有意義な会話ができるようになります。 たとえば、前にユーザーと話したことがある場合は、ユーザーに関する以前の情報を保存できるため、その内容については再度尋ねる必要がありません。 また、状態によって現在のターンよりも長くデータが保持されるため、お使いのボットには、一連のマルチターンの会話にわたって情報が保持されます。
ボットに関連するため、状態を使用するには、ストレージ レイヤー、状態管理 (下図のボットの状態に含まれている)、および状態プロパティ アクセサーというレイヤーがいくつかあります。 この図は、これらのレイヤー間で行われる相互作用シーケンスの一部を示しています。実線の矢印はメソッドの呼び出しを、破線の矢印は応答 (戻り値の有無に関係なく) を表しています。
この図のフローについては、次のセクションでレイヤーごとに詳しく説明します。
ストレージ レイヤー
バックエンドから始めて、状態情報が実際に格納されるのが ストレージ レイヤーです。 これは、インメモリ、Azure、サード パーティのサーバーなどの物理ストレージと考えることができます。
Bot Framework SDK には、ストレージ レイヤー用の実装がいくつか用意されています。
- メモリ ストレージにより、テストを目的としたインメモリ ストレージが実装されます。 インメモリ データ ストレージは揮発性であり、一時的なものであるため、ローカル テストのみを対象としています。 ボットの再起動ごとにデータがクリアされます。
- Azure Blob Storage は、Azure Blob Storage オブジェクト データベースに接続されます。
- Azure Cosmos DB パーティション分割ストレージは、パーティション分割された Cosmos DB NoSQL データベースに接続されます。
重要
Cosmos DB ストレージ クラスは非推奨となりました。 最初に CosmosDbStorage で作成されたコンテナーにはパーティション キーが設定されておらず、"/_partitionKey" の既定のパーティション キーが与えられました。
Cosmos DB ストレージで作成されたコンテナーは、Cosmos DB パーティション分割ストレージで使用できます。 詳細については、Azure Cosmos DB でのパーティション分割に関するページを参照してください。
また、従来の Cosmos DB ストレージとは異なり、Cosmos DB パーティション分割ストレージは Cosmos DB アカウント内にデータベースを自動的に作成しません。 新しいデータベースを手動で作成する必要がありますが、CosmosDbPartitionedStorage によってコンテナーが作成されるため、コンテナーの手動作成はスキップしてください。
他のストレージ オプションに接続する方法については、ストレージへの直接書き込みに関するページをご覧ください。
状態管理
"状態管理" により、基盤となるストレージ レイヤーに対するボットの状態の読み取りと書き込みが自動化されます。 状態は、効率的なキーと値のペアである "状態プロパティ" として格納され、お使いのボットは、このプロパティを、基盤となる特定の実装を気にすることなく、状態管理オブジェクトを使用して読み書きできます。 これらの状態プロパティにより、その情報の格納方法が定義されます。 たとえば、特定のクラスまたはオブジェクトとして定義したプロパティを取得する場合、そのデータがどのように構造化されるかがわかります。
これらの状態プロパティはスコープ付きの "バケット" にまとめられます。これは、プロパティを整理しやすくするための単なるコレクションです。 SDK には、次の 3 つの "バケット" が含まれています。
- ユーザー状態
- 会話状態
- 個人的な会話状態
これらのバケットはすべて "ボット状態" クラスのサブクラスであり、これを基に他の種類のバケットをさまざまなスコープで定義できます。
これらの定義済みバケットは、バケットに応じて、使用できる範囲が限定されます。
- ユーザー状態は、会話に関係なく、ボットがそのチャンネルのそのユーザーと会話している任意のターンで使用できます
- 会話の状態は、グループの会話など、ユーザーに関係なく、特定の会話で任意のターンで使用できます
- 個人的な会話状態は、特定の会話とその特定のユーザーの両方に対象が限定されます
ヒント
ユーザー状態と会話状態の両方がチャンネルごとに範囲指定されます。 さまざまなチャンネルを使用してボットにアクセスしている 1 人のユーザーは、チャンネルごとにそのユーザーが存在し、ユーザー状態も異なるため、違うユーザーのように見えます。
これらの定義済みバケットそれぞれに使用されるキーは、ユーザー、会話、またはその両方に固有です。 state プロパティの値を設定する場合、キーは内部的に定義され、各ユーザーまたは会話が正しいバケットとプロパティに確実に配置されるように、ターン コンテキストに情報が含まれます。 具体的には、キーは次のように定義されます。
- ユーザー状態は、channel ID と from ID を使用してキーを生成します。 例: {Activity.ChannelId}/users/{Activity.From.Id}#YourPropertyName
- 会話状態は、channel ID と conversation ID を使用してキーを生成します。 例: {Activity.ChannelId}/conversations/{Activity.Conversation.Id}#YourPropertyName
- 個人的な会話状態は、channel ID、from ID、および conversation ID を使用してキーを生成します。 例: {Activity.ChannelId}/conversations/{Activity.Conversation.Id}/users/{Activity.From.Id}#YourPropertyName
それぞれの状態の種類を使用するタイミング
会話状態は、次のような会話のコンテキストの追跡に適しています。
- ボットがユーザーに質問したかどうかと、どのような質問を行ったか
- 会話の現在のトピック、または最後のトピック
ユーザー状態は、次のようなユーザーに関する情報の追跡に適しています。
- 重要度が低いユーザー情報。名前と基本設定、アラーム設定、アラートの基本設定など
- ユーザーがボットと交わした最後の会話に関する情報
- たとえば、product-support ボットでは、ユーザーがどの製品について質問したかが追跡されている場合があります。
個人的な会話状態は、グループの会話をサポートするチャンネルに適しています。ただし、その会話ではユーザーおよび会話固有の情報を追跡する必要があります。 たとえば、クラスルーム クリッカー ボットは、次を実行できます。
- 特定の質問に対する学生の応答を集計し、表示する。
- 各学生のパフォーマンスを集計し、集計した内容を、セッションの最後に非公開で学生に送り返す。
これらの定義済みバケットの使用の詳細については、状態に関するハウツー記事をご覧ください。
複数のデータベースへの接続
お使いのボットが複数のデータベースに接続する必要がある場合は、データベースごとにストレージ レイヤーを作成します。 セキュリティ、コンカレンシー、またはデータの場所のニーズが異なる情報をボットが収集する場合は、複数のデータベースを使用することを選択できます。
ストレージ レイヤーごとに、ご自身の状態プロパティをサポートするのに必要な状態管理オブジェクトを作成します。
状態プロパティ アクセサー
"状態プロパティ アクセサー" は、状態プロパティのいずれかを実際に読み書きするときに使用され、ターン内からご自身の状態プロパティにアクセスするための get、set、delete の各メソッドを提供します。 アクセサーを作成するには、プロパティ名を指定する必要があります。この操作は、通常、ご自身のボットを初期化しているときに行います。 その後、そのアクセサーを使用して、ボットの状態の該当するプロパティを取得して操作できます。
アクセサーは、基盤となるストレージから SDK が状態を取得できるようにして、ボットの "状態キャッシュ" を自動的に更新します。 状態キャッシュは、ご自身のボットによって維持されているローカル キャッシュで、状態オブジェクトが自動的に格納されます。これにより、基盤となるストレージにアクセスしなくても読み書き操作を行えるようになります。 状態がキャッシュにない場合は、アクセサーの get メソッドを呼び出すことで状態が取得され、キャッシュへの配置も行われます。 状態が取得されると、ローカル変数と同じように、状態プロパティを操作できるようになります。
アクセサーの delete メソッドでは、プロパティが、キャッシュだけでなく、基盤となるストレージからも削除されます。
重要
アクセサーの get メソッドを初めて呼び出すとき、オブジェクトがまだ存在しない場合は、ファクトリ メソッドを指定して作成する必要があります。 ファクトリ メソッドが指定されていない場合は、例外が発生します。 ファクトリ メソッドの使用方法の詳細については、状態に関するハウツー記事をご覧ください。
アクセサーから取得した状態プロパティに対する変更を保持するには、状態キャッシュのプロパティを更新する必要があります。 これを行うには、アクセサーの set メソッドを呼び出します。これにより、キャッシュでご自身のプロパティ値が設定され、後からそのターンで読み取りまたは更新の必要が出てきた場合に使用できます。 そのデータを基盤となるストレージに実際に保持するには (つまり、現在のターンの後に使用できるようにするには)、その状態を保存する必要があります。
状態プロパティのアクセサー メソッドのしくみ
アクセサー メソッドは、ご自身のボットで状態を操作する最も一般的な方法です。 それぞれのしくみと、基盤となるレイヤーの動作を次に示します。
- アクセサーの get メソッド:
- アクセサーが状態キャッシュにプロパティを要求します。
- プロパティがキャッシュにある場合は、それを返します。 それ以外の場合は、状態管理オブジェクトから取得します。 (まだ状態になっていない場合は、アクセサーで提供されているファクトリ メソッドを使用して呼び出しを 取得 します)。
- アクセサーの set メソッド:
- 新しいプロパティ値で状態キャッシュを更新します。
- 状態管理オブジェクトの save changes メソッド:
- 状態キャッシュのプロパティへの変更を確認します。
- そのプロパティをストレージに書き込みます。
ダイアログの状態
ダイアログ ライブラリでは、ボットの会話状態で定義されたダイアログ状態プロパティ アクセサーを使用して、会話内のダイアログの場所を保持します。 ダイアログ状態プロパティを使用すると、各ダイアログでターン間に一時的な情報を格納することもできます。
アダプティブ ダイアログには、より複雑なメモリ スコープ構造があるため、構成と認識の結果などに簡単にアクセスできます。 ダイアログ マネージャーは、ユーザーと会話の状態管理オブジェクトを使用して、これらのメモリ スコープを提供します。
ダイアログ ライブラリの詳細については、 ダイアログ ライブラリ に関する記事を参照してください。
- これらの種類のダイアログに固有の情報については、「 コンポーネントダイアログとウォーターフォールダイアログについて 」を参照してください。
- アダプティブ ダイアログに固有の情報については、アダプティブ ダイアログの概要とアダプティブ ダイアログの状態の管理に関する記事を参照してください。
状態の保存
アクセサーの set メソッドを呼び出して、更新された状態を記録するとき、その状態プロパティは、永続化されたストレージにはまだ保存されておらず、ご自身のボットの状態キャッシュに保存されているだけです。 状態キャッシュの変更をご自身の永続化された状態に保存するには、状態管理オブジェクトの save changes メソッドを呼び出す必要があります。このメソッドは、前述したボット状態クラス (ユーザー状態、会話状態など) の実装で使用できます。
状態管理オブジェクト (上記のバケットなど) に対して save changes メソッドを呼び出すと、そのバケットのそのポイントに設定したすべてのプロパティが状態キャッシュに保存されますが、ボットの状態にある可能性のある他のバケットには保存されません。
ヒント
ボットの状態では、"最後の書き込みが有効" という動作が実装されます。つまり、以前に書き込まれた状態は、最後の書き込みで上書きされます。 これは多くのアプリケーションで有効ですが、特に、ある程度のコンカレンシーまたは待ち時間が存在する可能性があるスケールアウト シナリオに、影響が及ぶことがあります。
ターン ハンドラーの完了後に状態を更新する可能性のある何らかのカスタム ミドルウェアがある場合は、ミドルウェアで状態を処理することを検討してください。