Bot Builder v4 でボット開発 : アダプター、TurnContext、Activity

前回の記事では、ボットが呼び出されるまでの仕組みを確認しました。その中でアダプター、TurnContext、Activity という概念が出てきましたが、これらは BotBuilder にとって非常に重要な概念であるため、ここで改めて説明します。

# アダプター (BotFrameworkAdapter/BotAdapter)

アダプターは **Microsoft.Bot.Builder** モジュールに含まれる機能で、 BotAdapter.cs をベースに BotFrameworkAdapter.cs に実装があります。

[ボット アダプター : 公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0#the-bot-adapter)

アダプターでは以下の処理を行います。

- Bot Connector との認証
- Bot Connector から受信したメッセージから TurnContext を作成しボットに渡す
- ボットからの返信を Bot Connector に送る
- ミドルウェアの管理
- 会話のメンバー管理

前回の記事では受信時の処理は確認したので、返信の処理だけ確認。

[GitHub より引用 : SendActivitiesAsync](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs#L247) ※ コメント追加

```csharp
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 メソッド](https://github.com/Microsoft/botbuilder-dotnet/blob/4bb6b8d5faa4b252379ac331d6f5140ea27c177b/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs#L184) で JwtTokenValidation.AuthenticateRequest を実行してヘッダーの値を検証します。また Connector Client で認証情報を利用して Bot Connector に対して返信します。

## ミドルウェア

ASP.NET Core ミドルウェアと同じ概念で、ボットの処理を行う前後に、カスタム処理をミドルウェアとして実装出来ます。後で実装してみます。

# TurnContext

ターン（Turn) とは、ボットに対して要求が送られてから、応答が返るまでの一連の流れのことを指し、TurnContext はターンで利用される情報が格納されたコンテキストオブジェクトです。TurnContext はボットにだけでなく、BotBuilder ミドルウェアにも渡されるので、その時点で編集されたり、カスタムのオブジェクトが追加されている可能性もあります。

TurnContext は BotFrameworkAdapter の [ProcessActivityAsync](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs#L201) メソッドで作成され、以下の情報を含みます。

- Adapter : 元のアダプター情報
- Activity : ユーザーからの要求
- TurnState : コンテキストに登録されたサービスを保持
- Responded : 応答済フラグ

また TurnContext には SendActivity/SendActivities というメソッドがあり、ここから直接返信ができる感じがしますが、実際のコードを見るとアダプターを経由していることが分かります。

[GitHub より引用 : SendActivitiesAsync](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder/TurnContext.cs#L218) ※ コメント追加

```csharp
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](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Schema/ActivityTypes.cs) に定義がある Activity の種類。ユーザーとのやり取りは message タイプ
- ServiceUrl/ChannelId : ユーザーが使っているサービスを特定する Url や ID
- ChannelData : チャネル固有の値
- From : 送信元
- Recepient : 送信先
- Conversation : 会話 ID や会話がグループかなど会話に関する情報
- Text/TextFormat : 受信したテキストの値とフォーマット
- Attachments/AttachmentLayout : 受信した受信ファイルのデータと情報

詳細は [Activity.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Schema/Activity.cs) を参照。

# ミドルウェアの実装

## 添付は受け取らないミドルウェア

ここでは添付ファイルを送ってきたときに、テキストを送るよう促すミドルウェアを作ります。

1\. MyMiddleware.cs ファイルを追加し、IMiddleware インターフェースを継承したコードを追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/05f2acdf-18f5-f226-ee15-aee0109478ed.png)

2\. 赤線にカーソルを合わせて「Ctrl + . (ドット)」を押下するか、バルブアイコンをクリックして、using を使い。この際、BotBuilder を選ぶ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/992fbf6f-6570-6cff-b5ad-0b614ba51ca1.png)

3\. もう一度同じ操作を行い、「implement interface」を指定。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f7cdd131-bbee-16c5-100c-59859cf130b8.png)

4\. OnTurnAsync メソッドに async を付けて以下のコードを追加。

```csharp
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 を追加。

```csharp
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
```

6\. Startup.cs の ConfigureServices を以下の様に変更。これで AddBot メソッド内でアダプターにミドルウェアが渡される。

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddBot<MyBot>(options => {
        options.Middleware.Add(new MyMiddleware());
    });
}
```
7\. F5 キーを押下してデバッグ開始。

8\. Bot Framework Emulator よりテキストを送信。問題ないことを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/0223586e-86cf-6c4e-dc4d-d873700d8e73.png)

9\. 添付ファイルを送信して、ミドルウェアで処理が止まることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/90a8db79-bc0f-41c0-1377-b4cc781ea2f7.png)

## ロギングミドルウェア

他によくある用途としては、受信したものを記録するミドルウェアがあります。こちらも実装してみましょう。

1\. MyLoggingMiddleware.cs を追加して、以下のコードと差し替え。

```csharp
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 が先に実行されるように設定。順番を間違えた場合、添付ファイルが来るとログが取得できない。

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddBot<MyBot>(options => {
        options.Middleware.Add(new MyLoggingMiddleware());
        options.Middleware.Add(new MyMiddleware());
    });
}
```

3\. デバッグ実行して動作を確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9a836b26-a92a-5a9e-f046-16149abad43f.png)

# まとめ

今回はアダプター、TurnContext、Activity の概念について確認していきました。次回は単純なダイアログとステート管理について見ていきます。

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

[この記事のサンプルコード](https://github.com/kenakamu/botbuilderv4completeguide/tree/article3)