前回の記事では、ボットが呼び出されるまでの仕組みを確認しました。その中でアダプター、TurnContext、Activity という概念が出てきましたが、これらは BotBuilder にとって非常に重要な概念であるため、ここで改めて説明します。
アダプター (BotFrameworkAdapter/BotAdapter)
アダプターは Microsoft.Bot.Builder モジュールに含まれる機能で、 BotAdapter.cs をベースに BotFrameworkAdapter.cs に実装があります。
アダプターでは以下の処理を行います。
- Bot Connector との認証
- Bot Connector から受信したメッセージから TurnContext を作成しボットに渡す
- ボットからの返信を Bot Connector に送る
- ミドルウェアの管理
- 会話のメンバー管理
前回の記事では受信時の処理は確認したので、返信の処理だけ確認。
GitHub より引用 : SendActivitiesAsync ※ コメント追加
public override async Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext turnContext, Activity[] activities, CancellationToken cancellationToken)
{
// 必要なオブジェクトがあるか確認
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (activities == null)
{
throw new ArgumentNullException(nameof(activities));
}
if (activities.Length == 0)
{
throw new ArgumentException("Expecting one or more activities, but the array was empty.", nameof(activities));
}
// 応答作成
var responses = new ResourceResponse[activities.Length];
for (var index = 0; index < activities.Length; index++)
{
var activity = activities[index];
var response = default(ResourceResponse);
// 状況に合わせて処理を変更。
if (activity.Type == ActivityTypesEx.Delay)
{
// 既定で Delay タイプはないためシミュレートされたもの。指定された時間だけ待ってその後応答する。
int delayMs = (int)activity.Value;
await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
}
else if (activity.Type == ActivityTypesEx.InvokeResponse)
{
// Invoke の場合、応答は後ほど作成しここでは情報のみ追加。
turnContext.TurnState.Add(InvokeReponseKey, activity);
}
else if (activity.Type == ActivityTypes.Trace && activity.ChannelId != "emulator")
{
// Trace の場合は Emulator のみ対応
}
else if (!string.IsNullOrWhiteSpace(activity.ReplyToId))
{
// Activity の ReplyToId がある場合
// IoC より IConnectorClient を取得し、そこから ReplyToActivityAsync で返信を行う。
var connectorClient = turnContext.TurnState.Get<IConnectorClient>();
response = await connectorClient.Conversations.ReplyToActivityAsync(activity, cancellationToken).ConfigureAwait(false);
}
else
{
// IoC より IConnectorClient を取得し、そこから SendToConversationAsync で返信を行う。
var connectorClient = turnContext.TurnState.Get<IConnectorClient>();
response = await connectorClient.Conversations.SendToConversationAsync(activity, cancellationToken).ConfigureAwait(false);
}
// この時点で応答がない場合はシンプルな応答を返す。
if (response == null)
{
response = new ResourceResponse(activity.Id ?? string.Empty);
}
responses[index] = response;
}
return responses;
}
認証
ProcessActivityAsync メソッド で JwtTokenValidation.AuthenticateRequest を実行してヘッダーの値を検証します。また Connector Client で認証情報を利用して Bot Connector に対して返信します。
ミドルウェア
ASP.NET Core ミドルウェアと同じ概念で、ボットの処理を行う前後に、カスタム処理をミドルウェアとして実装出来ます。後で実装してみます。
TurnContext
ターン(Turn) とは、ボットに対して要求が送られてから、応答が返るまでの一連の流れのことを指し、TurnContext はターンで利用される情報が格納されたコンテキストオブジェクトです。TurnContext はボットにだけでなく、BotBuilder ミドルウェアにも渡されるので、その時点で編集されたり、カスタムのオブジェクトが追加されている可能性もあります。
TurnContext は BotFrameworkAdapter の ProcessActivityAsync メソッドで作成され、以下の情報を含みます。
- Adapter : 元のアダプター情報
- Activity : ユーザーからの要求
- TurnState : コンテキストに登録されたサービスを保持
- Responded : 応答済フラグ
また TurnContext には SendActivity/SendActivities というメソッドがあり、ここから直接返信ができる感じがしますが、実際のコードを見るとアダプターを経由していることが分かります。
GitHub より引用 : SendActivitiesAsync ※ コメント追加
public Task<ResourceResponse[]> SendActivitiesAsync(IActivity[] activities, CancellationToken cancellationToken = default(CancellationToken))
{
// 必要なオブジェクトがあるか確認
if (activities == null)
{
throw new ArgumentNullException(nameof(activities));
}
if (activities.Length == 0)
{
throw new ArgumentException("Expecting one or more activities, but the array was empty.", nameof(activities));
}
// 会話の情報を取得
var conversationReference = this.Activity.GetConversationReference();
var bufferedActivities = new List<Activity>(activities.Length);
for (var index = 0; index < activities.Length; index++)
{
// 返信に会話の情報を付与
bufferedActivities.Add(activities[index].ApplyConversationReference(
conversationReference));
}
// 返信時にコールバックがない場合は、アダプターから直接返信
if (_onSendActivities.Count == 0)
{
return SendActivitiesThroughAdapter();
}
// コールバックがある場合は都度処理
return SendActivitiesThroughCallbackPipeline();
// コールバックがある場合の処理
Task<ResourceResponse[]> SendActivitiesThroughCallbackPipeline(int nextCallbackIndex = 0)
{
// 最後のコールバックだった場合はアダプタで送信
if (nextCallbackIndex == _onSendActivities.Count)
{
return SendActivitiesThroughAdapter();
}
// コールバック処理
return _onSendActivities[nextCallbackIndex].Invoke(this, bufferedActivities, () => SendActivitiesThroughCallbackPipeline(nextCallbackIndex + 1));
}
// アダプターから返信
async Task<ResourceResponse[]> SendActivitiesThroughAdapter()
{
// SendActivitiesAsync で返信
var responses = await Adapter.SendActivitiesAsync(this, bufferedActivities.ToArray(), cancellationToken).ConfigureAwait(false);
var sentNonTraceActivity = false;
for (var index = 0; index < responses.Length; index++)
{
var activity = bufferedActivities[index];
activity.Id = responses[index].Id;
sentNonTraceActivity |= activity.Type != ActivityTypes.Trace;
}
if (sentNonTraceActivity)
{
Responded = true;
}
return responses;
}
}
上記のようにコールバック処理だけ行い、実際の返信は BotFrameworkAdapter に処理を渡しています。
Activity
Activity はユーザーとボットのやり取りに必要な情報を含んでおり、BotBuilder v3 と互換性があります。BotAdapter でデシリアライズされ、TurnContext に渡されます。非常に多くのプロパティがあるため、ここでは重要なものをいくつか紹介します。
- Type : ActivityTypes.cs に定義がある Activity の種類。ユーザーとのやり取りは message タイプ
- ServiceUrl/ChannelId : ユーザーが使っているサービスを特定する Url や ID
- ChannelData : チャネル固有の値
- From : 送信元
- Recepient : 送信先
- Conversation : 会話 ID や会話がグループかなど会話に関する情報
- Text/TextFormat : 受信したテキストの値とフォーマット
- Attachments/AttachmentLayout : 受信した受信ファイルのデータと情報
詳細は Activity.cs を参照。
ミドルウェアの実装
添付は受け取らないミドルウェア
ここでは添付ファイルを送ってきたときに、テキストを送るよう促すミドルウェアを作ります。
1. MyMiddleware.cs ファイルを追加し、IMiddleware インターフェースを継承したコードを追加。
2. 赤線にカーソルを合わせて「Ctrl + . (ドット)」を押下するか、バルブアイコンをクリックして、using を使い。この際、BotBuilder を選ぶ。
3. もう一度同じ操作を行い、「implement interface」を指定。
4. OnTurnAsync メソッドに async を付けて以下のコードを追加。
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
{
var activity = turnContext.Activity;
// 添付ファイルがある場合は処理を止める
if (activity.Type == ActivityTypes.Message
&& activity.Attachments != null
&& activity.Attachments.Count != 0)
{
await turnContext.SendActivityAsync("テキストを送ってください");
}
// それ以外の場合は次の処理を実行
else
await next.Invoke(cancellationToken);
}
5. 必要な using を追加。
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
6. Startup.cs の ConfigureServices を以下の様に変更。これで AddBot メソッド内でアダプターにミドルウェアが渡される。
public void ConfigureServices(IServiceCollection services)
{
services.AddBot<MyBot>(options => {
options.Middleware.Add(new MyMiddleware());
});
}
7. F5 キーを押下してデバッグ開始。
8. Bot Framework Emulator よりテキストを送信。問題ないことを確認。
9. 添付ファイルを送信して、ミドルウェアで処理が止まることを確認。
ロギングミドルウェア
他によくある用途としては、受信したものを記録するミドルウェアがあります。こちらも実装してみましょう。
1. MyLoggingMiddleware.cs を追加して、以下のコードと差し替え。
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using Microsoft.Bot.Builder;
public class MyLoggingMiddleware : IMiddleware
{
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
{
Debug.WriteLine($"{turnContext.Activity.From}:{turnContext.Activity.Type}");
Debug.WriteLineIf(
!string.IsNullOrEmpty(turnContext.Activity.Text),
turnContext.Activity.Text);
await next.Invoke(cancellationToken);
}
}
2. Startup.cs の ConfigureServices を変更。MyLoggingMiddleware が先に実行されるように設定。順番を間違えた場合、添付ファイルが来るとログが取得できない。
public void ConfigureServices(IServiceCollection services)
{
services.AddBot<MyBot>(options => {
options.Middleware.Add(new MyLoggingMiddleware());
options.Middleware.Add(new MyMiddleware());
});
}
まとめ
今回はアダプター、TurnContext、Activity の概念について確認していきました。次回は単純なダイアログとステート管理について見ていきます。