かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Bot Framework SDK 4.4.3 時点でのボット作成の最小手順からダイアログまで

Bot Framework SDK (Bot Builder SDK?) v4 で v3 から大きく実装方法が変わったわけですが、結構 v4.1, 4.2, 4.3, 4.4... と進んでいくうちに、意外と v4 当初の作り方が非推奨になったりしてしょんぼりすることがあったので v4.4.3 時点での Bot Framework SDK の推奨っぽい土台作りを見てみようと思います。

ASP.NET Core は現段階での LTS の 2.1 を使ってみます。

ASP.NET Core Web アプリケーションから始めてみる

Bot Framework のプロジェクトテンプレートは最初から設定されてるので便利なのですが、何が設定されてるかは何処かで一度確認しないといけないので ASP.NET Core Web アプリケーションテンプレートから始めていこうと思います。ということで Empty から始めます。 あと HTTPS 今回はいらないのでオフっと。

f:id:okazuki:20190704223319p:plain

Microsoft.Bot.Builder.Integration.AspNet.Core パッケージを NuGet から追加します。

v4 初期の頃はエンドポイントは自動で Bot Framework SDK が追加してくれてたのですが、それがなくなったので自前で ASP.NET Core のコントローラーを定義するようになっています。それに対応するために MVC まわりの設定を Startup.csConfigure メソッドを変更します。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMvc();
}

この後 ConfigureServices メソッドでサービスを登録してまわるのですがボットがないと登録するボットが無いのでボット用のクラスを作成します。v4 当初は IBot インターフェースを実装するという形でしたが最近 ActivityHandler クラスを継承する形が推奨っぽいです。

using Microsoft.Bot.Builder;

namespace BotStepByStep
{
    public class MyBot : ActivityHandler
    {
    }
}

ActivityHandler クラスは IBot インターフェースを実装してるので同じっちゃぁ同じですけどね。

自分のボットクラスが出来たので Startup クラスの ConfigureServices メソッドに必要なものを追加していきます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
    services.AddSingleton<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>();
    services.AddTransient<IBot, MyBot>();
}

あとは api/messages で POST のリクエストを受け取るコントローラーを定義して、そこで IBotFrameworkAdapterIBot を使って Bot Framework に処理を流して完了です。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using System.Threading.Tasks;

namespace BotStepByStep.Controllers
{
    [Route("api/messages")]
    [ApiController]
    public class BotController : ControllerBase
    {
        private readonly IBotFrameworkHttpAdapter _adapter;
        private readonly IBot _bot;

        public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
        {
            _adapter = adapter;
            _bot = bot;
        }

        [HttpPost]
        public async Task PostAsync()
        {
            await _adapter.ProcessAsync(Request, Response, _bot);
        }
    }
}

v4 であった .bot ファイルが無くなった(非推奨になった)ので appsettings.json や Azure App Service の構成や Azure App Configuration で設定情報を管理できるようになるのでいいですね。

Echo ボットの実装

動かしたいところですが、このままだとボットに何も実装してないので以下のようにオウム返しする実装を追加します。

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;

namespace BotStepByStep
{
    public class MyBot : ActivityHandler
    {
        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            await turnContext.SendActivityAsync(turnContext.Activity.Text);
        }
    }
}

実行したら Bot Framework Emulator で繋いでみます。

繋いで適当に話しかけてみると…

f:id:okazuki:20190704223642p:plain

動いた!!やったね!!

ユーザーとの対話状態を覚えてほしい

そんなときはダイアログですね!!というわけで以下のダイアログのライブラリをインストールします。

  • Microsoft.Bot.Builder.Dialogs

Bot Framework には色々なダイアログの種類があるのですが…今回は WaterfallDialog を使ってみようと思います。 名前の通り順番に流れるようにステップをこなしていくダイアログです。

ダイアログ自体の詳細はこちらのドキュメントで。

docs.microsoft.com

まずはダイアログが裏で状態を覚えるために使うステート管理機能を ConfigureServices メソッドで登録します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
    services.AddSingleton<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>();
    services.AddTransient<IBot, MyBot>();

    // for state
    services.AddSingleton<IStorage, MemoryStorage>();
    services.AddSingleton<UserState>();
    services.AddSingleton<ConversationState>();
}

Bot にダイアログを追加します。ComponentDialog を継承した MainDialog クラスを作ります。

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

namespace BotStepByStep
{
    public class MainDialog : ComponentDialog
    {
        public MainDialog() : base("MainDialog")
        {
            AddDialog(new TextPrompt("TextPrompt"));
            AddDialog(new WaterfallDialog("Steps", new WaterfallStep[]
            {
                async (context, cancellationToken) =>
                {
                    await context.Context.SendActivityAsync("最初のステップです。");
                    return await context.PromptAsync("TextPrompt", new PromptOptions
                    {
                        Prompt = MessageFactory.Text("名前を入力してください"),
                    });
                },
                async (context, cancellationToken) =>
                {
                    var name = context.Result as string;
                    context.Values["name"] = name;
                    return await context.PromptAsync("TextPrompt", new PromptOptions
                    {
                        Prompt = MessageFactory.Text("何か気の利いたコメントを入力してください"),
                    });
                },
                async (context, cancellationToken) =>
                {
                    var comment = context.Result as string;
                    var name = context.Values["name"] as string;
                    await context.Context.SendActivityAsync($"{name}さんの好きな言葉は「{comment}」ですね。");
                    return await context.EndDialogAsync();
                },
            }));

            InitialDialogId = "Steps";
        }
    }
}

プロンプトが入力をいい感じにしてくれるやつです。複雑なダイアログのサンプルは以下のリポジトリーにあります。

github.com

そして ConfigureServices でダイアログを登録して

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
    services.AddSingleton<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>();
    services.AddTransient<IBot, MyBot>();

    // for state
    services.AddSingleton<IStorage, MemoryStorage>();
    services.AddSingleton<ConversationState>();

    // dialog
    services.AddSingleton<Dialog, MainDialog>();
}

Bot ではダイアログを起動して、ダイアログの処理が終わったらステートを保存するようにします。

using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using System.Threading;
using System.Threading.Tasks;

namespace BotStepByStep
{
    public class MyBot : ActivityHandler
    {
        public MyBot(ConversationState conversationState, Dialog dialog)
        {
            ConversationState = conversationState;
            Dialog = dialog;
        }

        public ConversationState ConversationState { get; }
        public Dialog Dialog { get; }

        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
            await ConversationState.SaveChangesAsync(turnContext, cancellationToken: cancellationToken);
        }
    }
}

動かしてみると…

f:id:okazuki:20190704234617p:plain

ちゃんと定義したダイアログに従って会話が今どこにいるのかという情報を保持しつつ処理が進んでいくのがわかると思います。

まとめ

そろそろ、Bot Framework 落ち着いたかな??