LoginSignup
8
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-13

前回の記事では、ボットが呼び出されるまでの仕組みを確認しました。その中でアダプター、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 インターフェースを継承したコードを追加。
image.png

2. 赤線にカーソルを合わせて「Ctrl + . (ドット)」を押下するか、バルブアイコンをクリックして、using を使い。この際、BotBuilder を選ぶ。
image.png

3. もう一度同じ操作を行い、「implement interface」を指定。
image.png

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 よりテキストを送信。問題ないことを確認。
image.png

9. 添付ファイルを送信して、ミドルウェアで処理が止まることを確認。
image.png

ロギングミドルウェア

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

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());
    });
}

3. デバッグ実行して動作を確認。
image.png

まとめ

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

次の記事へ
目次へ戻る

この記事のサンプルコード

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2