Bot Framework と Microsoft Graph で DevOps その 16 : 会話のインターセプト処理
今回はボットアプリとユーザー間のやり取りを、インターセプトする方法を紹介します。
概要
ミドルウェアを会話の前後に差し込めると、会話のログ取得や、会話のフォワードなど、様々なことを既存のコードに影響を与えることなくできます。
インターセプトの実装
1. ボットアプリの Services フォルダに ActivityLogger.cs を追加し、コードを以下と差し替えます。
using Microsoft.Bot.Builder.History;
using Microsoft.Bot.Connector;
using System.Diagnostics;
using System.Threading.Tasks;
namespace O365Bot.Services
{
public class ActivityLogger : IActivityLogger
{
public async Task LogAsync(IActivity activity)
{
Debug.WriteLine($"From:{activity.From.Id} - To:{activity.Recipient.Id} - Message:{activity.AsMessageActivity()?.Text}");
}
}
}
2. Global.asax.cs に処理差し込み用のコードを追加します。以下のコードと差し替えます。自分用の IoC Container と登録先を間違えないようにしてください。
using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Internals.Fibers;
using O365Bot.Handlers;
using O365Bot.Services;
using System.Configuration;
using System.Web.Http;
namespace O365Bot
{
public class WebApiApplication : System.Web.HttpApplication
{
public static IContainer Container;
protected void Application_Start()
{
this.RegisterBotModules();
GlobalConfiguration.Configure(WebApiConfig.Register);
AuthBot.Models.AuthSettings.Mode = ConfigurationManager.AppSettings["ActiveDirectory.Mode"];
AuthBot.Models.AuthSettings.EndpointUrl = ConfigurationManager.AppSettings["ActiveDirectory.EndpointUrl"];
AuthBot.Models.AuthSettings.Tenant = ConfigurationManager.AppSettings["ActiveDirectory.Tenant"];
AuthBot.Models.AuthSettings.RedirectUrl = ConfigurationManager.AppSettings["ActiveDirectory.RedirectUrl"];
AuthBot.Models.AuthSettings.ClientId = ConfigurationManager.AppSettings["ActiveDirectory.ClientId"];
AuthBot.Models.AuthSettings.ClientSecret = ConfigurationManager.AppSettings["ActiveDirectory.ClientSecret"];
var builder = new ContainerBuilder();
builder.RegisterType<GraphService>().As<IEventService>();
builder.RegisterType<GraphService>().As<INotificationService>();
Container = builder.Build();
}
private void RegisterBotModules()
{
var builder = new ContainerBuilder();
// グローバルメッセージの処理登録
builder.RegisterModule(new ReflectionSurrogateModule());
builder.RegisterModule<GlobalMessageHandlers>();
// インターセプトの登録
builder.RegisterType<ActivityLogger>().AsImplementedInterfaces().InstancePerDependency();
builder.Update(Conversation.Container);
}
}
}
エミュレーターでの検証
1. 前回実装したプロアクティブ通知のコードがあると、エミュレーターからの検証が少し面倒なため、RootDialog.cs の DoWork メソッドを以下のコードに差し替えて、エミュレーターからの場合通知の登録をしないようにします。
private async Task DoWork(IDialogContext context, IMessageActivity message)
{
if (message.ChannelId != "emulator")
{
using (var scope = WebApiApplication.Container.BeginLifetimeScope())
{
var service = scope.Resolve<INotificationService>(new TypedParameter(typeof(IDialogContext), context));
// 変更を購読
var subscriptionId = context.UserData.GetValueOrDefault<string>("SubscriptionId", "");
if (string.IsNullOrEmpty(subscriptionId))
{
subscriptionId = await service.SubscribeEventChange();
context.UserData.SetValue("SubscriptionId", subscriptionId);
}
else
await service.RenewSubscribeEventChange(subscriptionId);
// 会話情報を購読 id をキーに格納
var conversationReference = message.ToConversationReference();
if (CacheService.caches.ContainsKey(subscriptionId))
CacheService.caches[subscriptionId] = conversationReference;
else
CacheService.caches.Add(subscriptionId, conversationReference);
// 会話情報はロケールを保存しないようなので、こちらも保存
if (!CacheService.caches.ContainsKey(message.From.Id))
CacheService.caches.Add(message.From.Id, Thread.CurrentThread.CurrentCulture.Name);
}
}
if (message.Text.Contains("get"))
// GetEventDialog を呼び出し、完了時に ResumeAfterDialog を実行
await context.Forward(new GetEventsDialog(), ResumeAfterDialog, message, CancellationToken.None);
else if (message.Text.Contains("add"))
// ユーザー入力を引き渡す必要はないので、CreateEventDialog を Call で呼び出す。
context.Call(new CreateEventDialog(), ResumeAfterDialog);
}
2. エミュレーターを起動して接続。
3. メッセージを送信。
4. Visual Studio の Output ビューに会話が記録されていることを確認。
テストの追加
今回は、ログを取得しかしていないため、ユーザー操作には影響がありません。よってファンクションテストは書きませんが、ユニットテストには、インターセプターを登録して、既存テストに影響ないか確認しておきます。
1. UnitTest1.cs の RegisterBotModules メソッドを以下のコードに差し替えます。
/// <summary>
/// グローバルメッセージおよびインターセプター登録
/// </summary>
private void RegisterBotModules(IContainer container)
{
var builder = new ContainerBuilder();
// グローバルメッセージの処理登録
builder.RegisterModule(new ReflectionSurrogateModule());
builder.RegisterModule<GlobalMessageHandlers>();
// インターセプトの登録
builder.RegisterType<ActivityLogger>().AsImplementedInterfaces().InstancePerDependency();
builder.Update(container);
}
2. すべてのユニットテストを実行します。
具体的な用途
今回はただのロギングでしたが、ログも実際は DocumentDB などに格納すると後で解析が楽にできます。その他の便利なシナリオは、人間へのハンドオフです。これはユーザーがボットと話している過程で、人間の介入が必要になった場合に切り替える処理のことです。サンプルが以下にあるので、是非ご覧ください。
https://github.com/tompaana/intermediator-bot-sample
皆さんならどんなシナリオで使うかも、是非教えてください。
まとめ
差し込みの入り口はとても簡単でしたが、できることが無限にあるので楽しいですね。影響があるものはユニットテスト、ファンクションテストともに書いてください。次回は多言語を理解する仕組みとして、LUIS (Language Understanding Intelligent Service) との統合を紹介します。