Bot Builder v4 でボット開発 : ボットが起動する仕組みを理解する - v4.3 で追加された方法編

2019 年 3 月に BotBuilder v4.3 がリリースされ、Web API Controller を使ったボットの開発がサポートされるようになりました。また ActivityHandler というより直感的にボットを実装する仕組みも追加されています。

[参考: Conversational AI updates for March 2019](https://azure.microsoft.com/en-us/blog/conversational-ai-updates-for-march-2019/) 

今回はこの機能追加について、Web API Controller でボットが起動する仕組みとActivityHandler の動作を見ていきます。

尚、これまでの方法も引き続きサポートされるようです。尚、この記事は [ボットが起動する仕組みを理解する](https://qiita.com/kenakamu/items/25667b7359bc1f38cf15) を読んだ前提で書いています。

# サンプルコード

サンプルが [new Test bot based on Controllers and ActivityHandler #1393](https://github.com/Microsoft/botbuilder-dotnet/pull/1393) で追加されているので、こちらのコードをベースに見ていきます。
ソース [Tree #1e250df2f9:Microsoft.Bot.Builder.TestBot](https://github.com/Microsoft/botbuilder-dotnet/tree/1e250df2f928ffdcba6487b1729ad31a6be29b2c/tests/Microsoft.Bot.Builder.TestBot)

# Startup

まずは [Startup.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/1e250df2f928ffdcba6487b1729ad31a6be29b2c/tests/Microsoft.Bot.Builder.TestBot/Startup.cs) から見ていきます。

IBotFrameworkHttpAdapter として BotFrameworkHttpAdapter を、IBot として MyBot を IoC コンテナに登録しています。それ以外は普通の Web API プロジェクトと同じです。

```csharp:Startup.cs
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.TestBot.Bots;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Bot.Builder.TestBot
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            // Load the credentials from configuration and create the credential provider.
            var appId = Configuration["BotFramework:AppId"];
            var password = Configuration["BotFramework:Password"];
            var credentialProvider = new SimpleCredentialProvider(appId, password);

            // Add the Adapter as a singleton and in this example the Bot as transient.
            services.AddSingleton<IBotFrameworkHttpAdapter>(sp => new BotFrameworkHttpAdapter(credentialProvider));
            services.AddTransient<IBot>(sp => new MyBot());
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseDefaultFiles();
            app.UseStaticFiles();

            //app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}
```

# Web API コントローラー

Web API コントローラー
[BotController.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/1e250df2f928ffdcba6487b1729ad31a6be29b2c/tests/Microsoft.Bot.Builder.TestBot/Controllers/BotController.cs) が Web API コントローラーです。このコードは非常にシンプルで api/bot に来た POST メソッドを受けて、BotFrameworkHttpAdapter アダプターの ProcessAsync に自動解決した MyBot インスタンスを渡して呼び出しているだけです。

```csharp:BotController.cs
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

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

namespace Microsoft.Bot.Builder.TestBot.Controllers
{
    [Route("bot")]
    [ApiController]
    public class BotController : ControllerBase
    {
        private IBotFrameworkHttpAdapter _adapter;
        private IBot _bot;

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

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

[BotFrameworkHttpAdapter.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/1e250df2f928ffdcba6487b1729ad31a6be29b2c/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/BotFrameworkHttpAdapter.cs) は integraion 名前空間にあるため、ASP.NET Core 固有の処理です。

ProcessAsync メソッドでは前処理をした後、ProcessActivityAsync メソッドに Bot の OnTurnAsync メソッドを呼び出しています。

```csharp
public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpResponse, IBot bot, CancellationToken cancellationToken = default(CancellationToken))
{
    if (httpRequest == null)
    {
        throw new ArgumentNullException(nameof(httpRequest));
    }

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

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

    // deserialize the incoming Activity
    var activity = HttpHelper.ReadRequest(httpRequest);

    // grab the auth header from the inbound http request
    var authHeader = httpRequest.Headers["Authorization"];

    // process the inbound activity with the bot
    var invokeResponse = await ProcessActivityAsync(authHeader, activity, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false);

    // write the response, potentially serializing the InvokeResponse
    HttpHelper.WriteResponse(httpResponse, invokeResponse);
}
```

# OnTurnAsync  メソッド

BotBuilder v4 が出たばかりの頃は、OnTurnAsync メソッドを実装するのは開発者の役目でした。ここがボットの入り口であり、その後はメインのダイアログを呼び出すなどベストプラクティスがあったものの、開発者が自由に実装できるため理想的な実装とならない場合がありましたが、今回は ActivityHandler によってその問題が改善されています。

## Bot の実装

[MyBot.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/1e250df2f928ffdcba6487b1729ad31a6be29b2c/tests/Microsoft.Bot.Builder.TestBot/Bots/MyBot.cs) のコードを見ると OnTurnAsync メソッドの実装はなく、代わりに OnMessageActivityAsync メソッドが実装されています。またこれまでのバージョンのように IBot を継承せず、ActivityHandler を継承していることが分かります。

```csharp
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

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

namespace Microsoft.Bot.Builder.TestBot.Bots
{
    public class MyBot : ActivityHandler
    {
        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            await turnContext.SendActivityAsync(MessageFactory.Text($"Echo: {turnContext.Activity.Text}"), cancellationToken);
        }
    }
}
```

## ActivityHandler.cs

[ActivityHandler.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/1e250df2f928ffdcba6487b1729ad31a6be29b2c/libraries/Microsoft.Bot.Builder/ActivityHandler.cs) は BotBuilder 名前空間にあるため、ASP.NET Core 固有実装ではありません。

ここに [OnTurnAsync](https://github.com/Microsoft/botbuilder-dotnet/blob/1e250df2f928ffdcba6487b1729ad31a6be29b2c/libraries/Microsoft.Bot.Builder/ActivityHandler.cs#L30) メソッドがあります。

クライアントから来たメッセージがユーザー入力か、会話への参加といったシステム処理かをここで判定しています。

```csharp
public virtual Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    if (turnContext == null)
    {
        throw new ArgumentNullException(nameof(turnContext));
    }

    if (turnContext.Activity == null)
    {
        throw new ArgumentException($"{nameof(turnContext)} must have non-null Activity.");
    }

    if (turnContext.Activity.Type == null)
    {
        throw new ArgumentException($"{nameof(turnContext)}.Activity must have non-null Type.");
    }

    switch (turnContext.Activity.Type)
    {
        case ActivityTypes.Message:
            return OnMessageActivityAsync(new DelegatingTurnContext<IMessageActivity>(turnContext), cancellationToken);

        case ActivityTypes.ConversationUpdate:
            return OnConversationUpdateActivityAsync(new DelegatingTurnContext<IConversationUpdateActivity>(turnContext), cancellationToken);

        case ActivityTypes.Event:
            return OnEventActivityAsync(new DelegatingTurnContext<IEventActivity>(turnContext), cancellationToken);

        default:
            return OnUnrecognizedActivityTypeAsync(turnContext, cancellationToken);
    }
}
```

例えば ActivityType が ConversationUpdate であった場合、さらに内容によってイベントを発行しています。これにより開発者は、On* イベントに対する実装をボットのメインコードで出来るようになります。

```csharp
protected virtual Task OnConversationUpdateActivityAsync(ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
    if (turnContext.Activity.MembersAdded != null)
    {
        if (turnContext.Activity.MembersAdded.Any(m => m.Id != turnContext.Activity.Recipient?.Id))
        {
            return OnMembersAddedAsync(turnContext.Activity.MembersAdded, turnContext, cancellationToken);
        }
    }
    else if (turnContext.Activity.MembersRemoved != null)
    {
        if (turnContext.Activity.MembersRemoved.Any(m => m.Id != turnContext.Activity.Recipient?.Id))
        {
            return OnMembersRemovedAsync(turnContext.Activity.MembersRemoved, turnContext, cancellationToken);
        }
    }

    return Task.CompletedTask;
}
```

# .bot ファイル

より新しいサンプルを見る限り、これまであった .bot ファイルから設定情報を読まず、appsettings.json から全ての設定を読むように変わったようです。

[appsettings](https://github.com/Microsoft/botbuilder-dotnet/blob/master/tests/Microsoft.Bot.Builder.TestBot/appsettings.json) 

この変更で msbot CLI ツールが不要になるのかは、また別途確認します。

# まとめ

多くの ASP.NET 開発者にとって ApiController や appsettings.json を使うことはごく自然なことであり、これまでの実装よりも直感的になったと思います。また On* イベントの処理に集中できることで、コードをよりシンプルにかつ統一的に記述できるようになりました。是非試してみてください。

[次の記事へ]()
[目次に戻る](https://qiita.com/kenakamu/items/6dc043cfc1f199032883)