LoginSignup
4
0

More than 5 years have passed since last update.

Bot Builder v4 でボット開発 : 単純なダイアログとステート管理

Last updated at Posted at 2018-10-13

本記事では Bot Builder v4 のダイアログとステート管理について見ていきます。ダイアログについては、まずは単純なものを見ていき、今後の記事でより複雑なダイアログを見ていきます。

ダイアログ

ダイアログは Bot Builder でユーザーとボットの会話を管理する際に中心となる概念です。ダイアログはユーザーからの入力を処理して、次の出力を生成し、その過程を管理します。

例えばユーザープロファイルを作る場面で、ボットは名前、誕生日、仕事など複数の質問を行い、対応するユーザーの回答を記憶、最後にデータベースなどに情報を格納するという動作を行います。この際、以下のような内容を管理する必要があります。

  • ユーザーの入力が、どの質問に対応するものかを理解
  • ユーザー入力の検証と記憶
  • 次の質問を正しく選択して、ユーザーへ送信
  • 最後の質問に対する回答を得た場合、データベースの保存

Bot Builder ではダイアログクラスが以下の様に用意されています。

dialog

プロンプト

プロンプトは最小単位のダイアログであり、ユーザーからの入力に対して型チェックを行う機能を提供します。

  • テキスト型
  • 数値型
  • ブール型
  • リストからの選択肢
  • 添付ファイル
  • 日付型

またプロンプトは以下の性質があります。

  • 入力データの型を確認するだけで、ユーザーに送る文字列は別途指定
  • 上記理由からプロンプト単体は再利用が可能
  • ユーザーに対する送信と、ユーザーからの入力の2ステップで完結するため、ステート管理が必須

DialogSet

複数のダイアログをまとめて管理する機能として DialogSet があります。ダイアログの状態を管理するためには、DialogSet を、後述する DialogState を引数に作成して利用します。

以下の場合は、3 つのプロンプトダイアログを、それぞれ name, age, birthday として追加しています。

DialogSet dialogs = new DialogSet(accessors.ConversationDialogState);
dialogs.Add(new TextPrompt("name"));
dialogs.Add(new NumberPrompt("age"));
dialogs.Add(new DatePrompt("birthday"));

追加の順番と実行の順番には関係がなく、取り出す際には指定した名前 (id) を使います。

DialogContext とダイアログの実行

ダイアログの実行に必要な情報を持ったコンテキストで、DialogSet に TurnContext を渡して作成します。また通常のターンに該当するもので DialogTurn という概念があり、ダイアログ全体の処理を 1 ターンをして捉えます。

var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);

また DialogContext は以下のダイアログ実行メソッドをサポートします。

  • BeginDialogAsync
  • ContinueDialogAsync
  • ResumeDialogAsync
  • PromptAsync

ContinueDialogAsync

実装パターンとして、まず ContinueDialogAsync メソッドを実行します。既に開始しているダイアログがある場合は継続して実行され、ない場合は DialogTurnStatus.Empty が返ってきます。

var results = await dialogContext.ContinueDialogAsync(cancellationToken);

BeginDialogAsync

DialogTurnStatus.Empty が返ってきた場合は、新規にダイアログを開始するため、BeginDialogAsync を実行します。

await dialogContext.BeginDialogAsync("name",
    // ユーザーに対する表示
    new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください") }, 
    cancellationToken);

ResumeDialogAsync

ネストされたダイアログを利用する場合、子ダイアログが終わった際、親ダイアログで呼ばれます。今後の記事で詳細は紹介します。

PromptAsync

BeginDialogAsync のラッパー

GitHub より引用 : PromptAsync

public async Task<DialogTurnResult> PromptAsync(string dialogId, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken))
{
    if (string.IsNullOrEmpty(dialogId))
    {
        throw new ArgumentNullException(nameof(dialogId));
    }

    if (options == null)
    {
        throw new ArgumentNullException(nameof(options));
    }

    return await BeginDialogAsync(dialogId, options, cancellationToken).ConfigureAwait(false);
}

ステート管理

ダイアログが成立するためには、会話の状態を保存するためのステートを管理する必要があります。Bot Builder では以下 2 種類のステートを定義しています。

  • UserState : ユーザー単位のステート (複数の会話にまたがるステート)
  • ConversationState : 会話単位のステート (特定の会話に関係するステート)

ステートにはシリアライズできるオブジェクトを保存します。またデータの保存先として、SDK レベルで以下の 3 パターンをサポートします。

  • インメモリ (開発用)
  • Azure テーブル
  • Azure Cosmos DB

Azure のデータストアについては Microsoft.Bot.Builder.Azure モジュールで提供されます。

ダイアログステート

ダイアログのステートは ConversationState に DialogState 型として保存します。

アクセサー

ステートにデータを保存したり、保存済のデータを取得するには、IStatePropertyAccessor を使います。インターフェースでは Get/Set/Delete のメソッドがそれぞれ定義されていて、容易に情報を扱うことが可能です。BotBuilder ではアクセサー専用の UserState、ConversationState および IStatePropertyAccessor を持つクラスを定義して、IoC コンテナに登録して使うパターンがよく使われます。

以下の開発でも、このパターンを使った実装を行います。

プロンプトを使ったボットの開発

ここでは前回開発した「オウム返し」ボットを拡張して、名前を聞くボットを実装します。また現在の会話において、ユーザーとやり取りした回数を数える機能を追加します。

NuGet パッケージの追加

ダイアログ機能は Microsoft.Bot.Builder.Dialogs で提供されるため、VSCode の統合ターミナルより以下コマンドを実行。

dotnet add package Microsoft.Bot.Builder.Dialogs
dotnet restore

開発

1. MyStateAccessors.cs ファイルを追加し、クラス定義を追加。
image.png

2. コードを以下のものと差し替え。クラスに ConversationState プロパティを持ち、またダイアログを記録するプロパティを IStatePropertyAccessor として定義。

using System;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;

public class MyStateAccessors{
        public MyStateAccessors(
            ConversationState conversationState
            )
        {
            ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
        }

        public IStatePropertyAccessor<DialogState> ConversationDialogState { get; set; }
        public ConversationState ConversationState { get; }
}

3. Startup.cs の ConfigurationServices に記述した AddBot を以下の様に変更。

services.AddBot<MyBot>(options =>
{
    options.Middleware.Add(new MyLoggingMiddleware());
    options.Middleware.Add(new MyMiddleware());

    // ストレージとしてインメモリを利用
    IStorage dataStore = new MemoryStorage();
    // それぞれのステートを作成
    var conversationState = new ConversationState(dataStore);
    // オプションに追加
    options.State.Add(conversationState);
});

4. 同じく ConfigurationServices メソッド内で MyStateAccessors を IoC に登録。これで動的に MyStateAccessors を取得可能。

// MyStateAccessors を IoC に登録
services.AddSingleton(sp =>
{
    // AddBot で登録した options を取得。
    var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
    if (options == null)
    {
        throw new InvalidOperationException("BotFrameworkOptions を事前に構成してください。");
    }
    var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
    if (conversationState == null)
    {
        throw new InvalidOperationException("ConversationState を事前に定義してください。");
    }

    var accessors = new MyStateAccessors(conversationState)
    {
        // DialogState を ConversationState のプロパティとして設定
        ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
    };

    return accessors;
});

5. MyBot.cs に using を追加。

using Microsoft.Bot.Builder.Dialogs;

6. コンストラクターとクラスプロパティを以下の様に変更。引数として指定した MyStateAccessors は DI として自動解決される。

private MyStateAccessors accessors;
private DialogSet dialogs;

public MyBot(MyStateAccessors accessors)
{
    this.accessors = accessors;
    this.dialogs = new DialogSet(accessors.ConversationDialogState);
    // テキスト型のプロンプトとして id=name で作成
    dialogs.Add(new TextPrompt("name"));
}

7. OnTurnAsync メソッドを以下の様に変更。ユーザーに対するプロンプトの表示は、MessageFactory を使用。

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)
        {
            // id=name のプロンプトを送信
            await dialogContext.PromptAsync(
                "name",
                new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください") }, // ユーザーに対する表示
                cancellationToken);
        }
        // DialogTurnStatus が Complete の場合、ダイアログは完了したため結果を処理
        else if (results.Status == DialogTurnStatus.Complete)
        {
            if (results.Result != null)
            {
                await turnContext.SendActivityAsync(MessageFactory.Text($"ようこそ '{results.Result}' さん!"));
            }
        }
        // 最後に現在のダイアログステートを保存
        await accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    }
}

8. デバッグ実行して、Bot Framework Emulator で動作を確認。
image.png

まとめ

今回はまず単純なダイアログとステートの管理について見ていきました。一番シンプルに名前を聞くだけでも、ボットしてはステート管理する必要がありますが、BotBuilder を使うと簡単に実装出来ます。次回はより高度なダイアログを見ていきます。

次の記事へ
目次に戻る

この記事のサンプルコード

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0