LoginSignup
12
5

More than 3 years have passed since last update.

Bot Builder v4 でボット開発 : ボットが起動する仕組みを理解する

Last updated at Posted at 2018-10-13

前回の記事では、最小構成で「オウム返し」ボットを開発しました。本記事では、そのコードを使って、ボットが呼び出されるまでの仕組みを理解していきます。

公式ページにある Bot が動作する仕組み

以下の図は公式ページにあるシーケンス図です。
bot sequence

  • Bot Framework Service なるものから HTTP POST が来る
  • Web Server Integration で受けて ProcessActivity を呼ぶ
  • Adapter、TurnContext と Middleware でそれらを受けて、OnTurn を呼ぶ
  • 自分のボットが呼び出される
  • 自分のボットから SendActivity で返信を返す (HTTP 応答ではなく、新規の POST)
  • それぞれの HTTP 要求に対して 200 を返す

今回はこの動作のうち、OnTurn が呼ばれるところまでを見ていきます。

NuGet パッケージ

前回のボットでは、BotBuilder 関連の 以下 NuGet パッケージを使いました。

Microsoft.Bot.Builder.Integration.AspNet.Core
Microsoft.Bot.Builder
Microsoft.Bot.Connector
Microsoft.Bot.Schema

全てオープンソースで公開されています。GitHub: botbuilder-dotnet

Microsoft.Bot.Builder.Integration.AspNet.Core
BotBuilder と ASP.NET Core を繋ぐモジュール

Microsoft.Bot.Builder
Bot Builder v4 のメインモジュール

Microsoft.Bot.Connector
ボットと Bot Framework Connector を繋ぐモジュール

Microsoft.Bot.Schema
Bot Builder のスキーマを定義したモジュール

この中で Microsoft.Bot.Builder.Integration.AspNet.Core パッケージは BotBuilder と ASP.NET Core を繋ぐ、ASP.NET Core ミドルウェアとして動作します。

ASP.NET Core ミドルウェア

ミドルウェアは、ユーザーから送られてきた HTTP 要求を処理するコンポーネントです。既定のミドルウェアに追加して、開発者が独自に開発することが出来ます。ミドルウェアは要求と応答に対して操作を行え、next() で次のミドルウェアに処理を渡します。

  • HTTP 要求に対する処理
  • next(); で次のミドルウェアを呼び出す
  • HTTP 応答に対する処理

middleware

開発したミドルウェアは Startup.cs の Configure メソッド内で登録します。ミドルウェは登録した順番で呼び出されます。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseDefaultFiles() // 既定のファイルを使うミドルウェア
       .UseStaticFiles() // 静的ファイルを使うミドルウェア
       .UseMvc(); // MVC を使うミドルウェア
}

詳細は ASP.NET Core のミドルウェア参照。

BotBuilder を追加する際も、以下の様に Configure にミドルウェアとして追加しました。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseBotFramework();
}

UseBotFramework メソッドは Microsoft.Bot.Builder.Integration.AspNet.Core が提供する機能で、中身は以下の様になっています。

GitHub より引用 : UseBotFramework メソッド ※コメント追加

public static IApplicationBuilder UseBotFramework(this IApplicationBuilder applicationBuilder)
{
    if (applicationBuilder == null)
    {
        throw new ArgumentNullException(nameof(applicationBuilder));
    }

    // IoC に登録済の構成を取得。IoC については後で説明。
    var applicationServices = applicationBuilder.ApplicationServices;    
    var configuration = applicationServices.GetService<IConfiguration>();

    if (configuration != null)
    {
        // 構成情報より各種エンドポイント情報を取得
        var openIdEndpoint = configuration.GetSection(AuthenticationConstants.BotOpenIdMetadataKey)?.Value;
        if (!string.IsNullOrEmpty(openIdEndpoint))
        {
            ChannelValidation.OpenIdMetadataUrl = openIdEndpoint;
        }
        var oauthApiEndpoint = configuration.GetSection(AuthenticationConstants.OAuthUrlKey)?.Value;
        if (!string.IsNullOrEmpty(oauthApiEndpoint))
        {
            OAuthClient.OAuthEndpoint = oauthApiEndpoint;
        }
        var emulateOAuthCards = configuration.GetSection(AuthenticationConstants.EmulateOAuthCardsKey)?.Value;
        if (!string.IsNullOrEmpty(emulateOAuthCards) && bool.TryParse(emulateOAuthCards, out bool emualteOAuthCardsValue))
        {
            OAuthClient.EmulateOAuthCards = emualteOAuthCardsValue;
        }
    }

    // BotFrameworkOptions オプションを取得してパスを確認。
    var options = applicationServices.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
    var paths = options.Paths;
    // パス (/api/messages) に BotMessageHandler の HandleAsync メソッドをマップ
    applicationBuilder.Map(
        paths.BasePath + paths.MessagesPath,
        botActivitiesAppBuilder => botActivitiesAppBuilder.Run(new BotMessageHandler().HandleAsync));

    return applicationBuilder;
}

構成情報などの取り扱いはありますが、大事な点は /api/messages パスに要求が来たら、BotMessageHandler の HandleAsync が呼ばれるという点です。尚、パス情報は以下のコードにハードコードされています。

GitHub より引用 : BotFrameworkPaths

public BotFrameworkPaths()
{
    BasePath = "/api";
    MessagesPath = "/messages";
}

では BotMessageHandler の HandleAsync メソッドを見ていきましょう。

GitHub より引用 : HandleAsync ※コメント追加および一部不要コード削除

public async Task HandleAsync(HttpContext httpContext)
{
    var request = httpContext.Request;
    var response = httpContext.Response;
    // POST メソッドだけ反応。またヘッダーや中身があるかも確認。
    if (request.Method != HttpMethods.Post)
    {
        response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
        return;
    }
    if (request.ContentLength == 0)
    {
        response.StatusCode = (int)HttpStatusCode.BadRequest;
        return;
    }
    if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaTypeHeaderValue) || mediaTypeHeaderValue.MediaType != "application/json")
    {
        response.StatusCode = (int)HttpStatusCode.NotAcceptable;
        return;
    }

    // IoC で登録済の BotFrameworkAdapter および、 IBot を継承したボットを取得。
    // どこで登録しているかは後で説明。
    var requestServices = httpContext.RequestServices;
    var botFrameworkAdapter = requestServices.GetRequiredService<BotFrameworkAdapter>();
    var bot = requestServices.GetRequiredService<IBot>();

    try
    {
        // 実際の処理を実行。
        var invokeResponse = await ProcessMessageRequestAsync(
            request,
            botFrameworkAdapter,
            bot.OnTurnAsync, // Bot の OnTurnAsync 
            default(CancellationToken));

        // 結果を確認
        if (invokeResponse == null)
        {
            response.StatusCode = (int)HttpStatusCode.OK;
        }
        else
        {
            response.ContentType = "application/json";
            response.StatusCode = invokeResponse.Status;

            using (var writer = new StreamWriter(response.Body))
            {
                using (var jsonWriter = new JsonTextWriter(writer))
                {
                    BotMessageSerializer.Serialize(jsonWriter, invokeResponse.Body);
                }
            }
        }
    }
    catch (UnauthorizedAccessException)
    {
        response.StatusCode = (int)HttpStatusCode.Forbidden;
    }
}

ここでは HTTP 要求の中身を確認後、IoC に登録済の BotFrameworkAdapter と IBot を取得して ProcessMessageRequestAsync メソッドを実行しています。また IBot については OnTurnAsync メソッドを引数として渡しています。IoC については後で説明しますので、先に ProcessMessageRequestAsync を確認します。

GitHub より引用 : ProcessMessageRequestAsync ※コメント追加

protected override async Task<InvokeResponse> ProcessMessageRequestAsync(HttpRequest request, BotFrameworkAdapter botFrameworkAdapter, BotCallbackHandler botCallbackHandler, CancellationToken cancellationToken)
{
    // Activity を作成。中身を HTTP 要求から取得。
    var activity = default(Activity);
    using (var bodyReader = new JsonTextReader(new StreamReader(request.Body, Encoding.UTF8)))
    {
        activity = BotMessageHandlerBase.BotMessageSerializer.Deserialize<Activity>(bodyReader);
    }

#pragma warning disable UseConfigureAwait // Use ConfigureAwait
    // アダプターの ProcessActivityAsync メソッドに Activity およびコールバック (OnTurnAsync) を渡す。
    var invokeResponse = await botFrameworkAdapter.ProcessActivityAsync(
            request.Headers["Authorization"],
            activity,
            botCallbackHandler,
            cancellationToken);
#pragma warning restore UseConfigureAwait // Use ConfigureAwait

    return invokeResponse;
}

ここでは HTTP 要求より Activity を作って ProcessActivityAsync を実行しています。尚、ここまでは Microsoft.Bot.Builder.Integration.AspNet.Core でしたが、BotFrameworkAdapter は Bot Builder 本体側のパッケージのコードになるため、ASP.NET Core との繋ぎはこの辺りで終わりです。

GitHub から引用 : ProcessActivityAsync ※コメント追加


public async Task<InvokeResponse> ProcessActivityAsync(string authHeader, Activity activity, BotCallbackHandler callback, CancellationToken cancellationToken)
{
    BotAssert.ActivityNotNull(activity);
    // Jwt トークンの検証および ClaimsIdentity の作成。
    var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _channelProvider, _httpClient).ConfigureAwait(false);
    return await ProcessActivityAsync(claimsIdentity, activity, callback, cancellationToken).ConfigureAwait(false);
}

public async Task<InvokeResponse> ProcessActivityAsync(ClaimsIdentity identity, Activity activity, BotCallbackHandler callback, CancellationToken cancellationToken)
{
    BotAssert.ActivityNotNull(activity);

    // TurnContext を 自分自身と Activity を引数に作成。TurnContext の詳細はまた別の記事で。
    using (var context = new TurnContext(this, activity))
    {
        // TurnContext に認証情報を設定
        context.TurnState.Add<IIdentity>(BotIdentityKey, identity);
        // Connector クライアントを追加
        var connectorClient = await CreateConnectorClientAsync(activity.ServiceUrl, identity, cancellationToken).ConfigureAwait(false);
        context.TurnState.Add(connectorClient);

        // 処理実行
        await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);

        // Invoke シナリオ。詳細は別記事で。
        if (activity.Type == ActivityTypes.Invoke)
        {
            Activity invokeResponse = context.TurnState.Get<Activity>(InvokeReponseKey);
            if (invokeResponse == null)
            {
                // ToDo: Trace Here
                throw new InvalidOperationException("Bot failed to return a valid 'invokeResponse' activity.");
            }
            else
            {
                return (InvokeResponse)invokeResponse.Value;
            }
        }

        // Invoke 以外は何もしない。
        return null;
    }
}

ここでは TurnContext を作成しています。TurnContext に認証情報の設定と、Microsoft.Bot.Builder.Connector で提供される ConnectorClient の設定を行い、実際の処理に移るべく RunPipelineAsync が呼ばれています。

TurnContext は非常に重要であるため、あらためて別の記事で紹介します。

GitHub より引用 : RunPipelineAsync ※ コメント追加

protected async Task RunPipelineAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken)
{
    BotAssert.ContextNotNull(turnContext);

    // Bot Builder に登録されたミドルウェアをまず実行。これは ASP.NET Core ミドルウェアとはまた別のため、別の記事で説明。
    if (turnContext.Activity != null)
    {
        try
        {
            await MiddlewareSet.ReceiveActivityWithStatusAsync(turnContext, callback, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e)
        {
            if (OnTurnError != null)
            {
                await OnTurnError.Invoke(turnContext, e).ConfigureAwait(false);
            }
            else
            {
                throw;
            }
        }
    }
    else
    {
        // コールバック (OnTurnAsync) を実行。
        if (callback != null)
        {
            await callback(turnContext, cancellationToken).ConfigureAwait(false);
        }
    }
}

ここでようやく開発した「オウム返しボット」の OnTurnAsync が呼ばれました。

ASP.NET Core 依存関係の挿入 (Dependency Injection : DI)

DI とは、Inversion of Control (IoC) コンテナという場所に登録した情報を元に、必要なオブジェクトを動的に取り出す仕組みのことで、ASP.NET Core では標準でこの機能が提供されます。

詳細は ASP.NET Core での依存関係の挿入 参照

登録は Startup.cs の ConfigureServices メソッドで行います。前回記事の「オウム返し」ボットでも以下の様に登録をしました。

public void ConfigureServices(IServiceCollection services)
{
    services.AddBot<MyBot>();
}

こちらのコードでは、MyBot クラスを登録し、必要な時に取り出せるようにしています。では AddBot が何をしているか見ていきましょう。

GitHub より引用 : AddBot ※ コメント追加

public static IServiceCollection AddBot<TBot>(this IServiceCollection services, Action<BotFrameworkOptions> configureAction = null)
    where TBot : class, IBot
{
    // 引数のチェック
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }
    if (configureAction != null)
    {
        services.Configure(configureAction);
    }

    // IBot を要求した際、TBot (今回の場合 MyBot) を返すように登録。
    services.AddTransient<IBot, TBot>();
    // BotFrameworkAdapter を要求した際、常に同じインスタンスを返すように登録。
    services.AddSingleton(sp =>
    {
        // 登録済の BotFrameworkOptions を取得
        var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
        // BotFrameworkAdapter を新規に作成し、OnTurnError に指定されたハンドルを設定。
        var botFrameworkAdapter = new BotFrameworkAdapter(options.CredentialProvider, options.ChannelProvider, options.ConnectorClientRetryPolicy, options.HttpClient)
        {
            OnTurnError = options.OnTurnError,
        };
        // Bot Builder のミドルウェアを順次登録
        foreach (var middleware in options.Middleware)
        {
            botFrameworkAdapter.Use(middleware);
        }

        return botFrameworkAdapter;
    });

    return services;
}

ここでは IBot が要求した際に TBot (今回の場合 MyBot) を、BotFrameworkAdapter を要求した場合、同じインスタンスを返すように設定しています。この設定があるため、上記で紹介した HandleAsync メソッド内で動的に IBot と BotFrameworkAdapter を取得できています。

「オウム返し」ボットでは BotFrameworkOptions やミドルウェアは何も登録していませんが、登録した場合はこのコードで設定がされることになります。

IoC/DI は今後も利用するため重要な概念です。

まとめ

今回は Startup.cs に設定したコードが実際にどのように動作して、結果ボットが動作するかを見ていきました。次回はアダプター、TurnContext と Activity を見ていきます。

次の記事へ
目次に戻る

12
5
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
12
5