2019 年 3 月に BotBuilder v4.3 がリリースされ、Web API Controller を使ったボットの開発がサポートされるようになりました。また ActivityHandler というより直感的にボットを実装する仕組みも追加されています。
参考: Conversational AI updates for March 2019
今回はこの機能追加について、Web API Controller でボットが起動する仕組みとActivityHandler の動作を見ていきます。
尚、これまでの方法も引き続きサポートされるようです。尚、この記事は ボットが起動する仕組みを理解する を読んだ前提で書いています。
サンプルコード
サンプルが new Test bot based on Controllers and ActivityHandler #1393 で追加されているので、こちらのコードをベースに見ていきます。
ソース Tree #1e250df2f9:Microsoft.Bot.Builder.TestBot
Startup
まずは Startup.cs から見ていきます。
IBotFrameworkHttpAdapter として BotFrameworkHttpAdapter を、IBot として MyBot を IoC コンテナに登録しています。それ以外は普通の Web API プロジェクトと同じです。
// 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 が Web API コントローラーです。このコードは非常にシンプルで api/bot に来た POST メソッドを受けて、BotFrameworkHttpAdapter アダプターの ProcessAsync に自動解決した MyBot インスタンスを渡して呼び出しているだけです。
// 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 は integraion 名前空間にあるため、ASP.NET Core 固有の処理です。
ProcessAsync メソッドでは前処理をした後、ProcessActivityAsync メソッドに Bot の OnTurnAsync メソッドを呼び出しています。
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 のコードを見ると OnTurnAsync メソッドの実装はなく、代わりに OnMessageActivityAsync メソッドが実装されています。またこれまでのバージョンのように IBot を継承せず、ActivityHandler を継承していることが分かります。
// 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 は BotBuilder 名前空間にあるため、ASP.NET Core 固有実装ではありません。
ここに OnTurnAsync メソッドがあります。
クライアントから来たメッセージがユーザー入力か、会話への参加といったシステム処理かをここで判定しています。
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* イベントに対する実装をボットのメインコードで出来るようになります。
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 から全ての設定を読むように変わったようです。
この変更で msbot CLI ツールが不要になるのかは、また別途確認します。
まとめ
多くの ASP.NET 開発者にとって ApiController や appsettings.json を使うことはごく自然なことであり、これまでの実装よりも直感的になったと思います。また On* イベントの処理に集中できることで、コードをよりシンプルにかつ統一的に記述できるようになりました。是非試してみてください。