🙌

Durable Agent で静的変数アクセスを消す - Microsoft Agent Framework (C#) その16

に公開

シリーズ記事

はじめに

前回、Durable Agent で長時間実行されるツールを呼び出す方法を見てきました。この時に Durable Agent のツール呼び出しの関数内で DurableAgentContext.Current を使ってオーケストレーター関数を操作します。例えばオーケストレーター関数を起動する部分のコードを前の記事から抜粋します。

[Description("天気レポート生成ワークフローを開始し、追跡用のインスタンスIDを返します。")]
public string StartWeatherReportWorkflow([Description("天気を取得する場所")] string location)
{
    logger.LogInformation("場所 '{Location}' の天気レポートワークフローを開始します", location);

    // オーケストレーションをスケジュールし、ツール呼び出しの完了後に実行を開始します
    string instanceId = DurableAgentContext.Current.ScheduleNewOrchestration(
        name: $"{nameof(WeatherOrchestratorFunctions)}_{nameof(WeatherOrchestratorFunctions.RunOrchestrator)}",
        input: location);

    logger.LogInformation(
        "場所 '{Location}' の天気レポートワークフローがスケジュールされました。インスタンスID: {InstanceId}",
        location,
        instanceId);

    return $"ワークフローが開始されました。インスタンスID: {instanceId}";
}

このような static プロパティを使うとテストしづらいので、なるべくパラメーターで渡す形にしたいです。例えば上記のコードを以下のように書きたいです。

[Description("天気レポート生成ワークフローを開始し、追跡用のインスタンスIDを返します。")]
public string StartWeatherReportWorkflow(
    DurableAgentContext context,
    [Description("天気を取得する場所")] string location)
{
    logger.LogInformation("場所 '{Location}' の天気レポートワークフローを開始します", location);

    // オーケストレーションをスケジュールし、ツール呼び出しの完了後に実行を開始します
    string instanceId = context.ScheduleNewOrchestration(
        name: $"{nameof(WeatherOrchestratorFunctions)}_{nameof(WeatherOrchestratorFunctions.RunOrchestrator)}",
        input: location);

    logger.LogInformation(
        "場所 '{Location}' の天気レポートワークフローがスケジュールされました。インスタンスID: {InstanceId}",
        location,
        instanceId);

    return $"ワークフローが開始されました。インスタンスID: {instanceId}";
}

ここら辺は Microsoft.Extensions.AI の AIFunction まわりの仕組みを使って実現できます。AIFunction のファクトリー メソッドに AIFunctionFactoryOptions を渡すことが出来て、ここの ConfigureParameterBinding プロパティでパラメーターの値のバインディング方法をカスタマイズできます。 ParameterInfo を受け取って AIFunctionFactoryOptions.ParameterBindingOptions を返すデリゲートを指定します。BindParameter で実際のパラメーター生成ロジックと ExcludeFromSchema で AI 用のスキーマに含めるかどうかを設定出来ます。デフォルトを返せば通常のプロセスに出来ます。

ということで、Program.cs

public AIAgent CreateCatAgent()
{
    var tools = serviceProvider.GetRequiredService<OrchestratorManageTools>();
    // 関数のファクトリのオプション
    AIFunctionFactoryOptions options = new()
    {
        // DurableAgentContext パラメーターのバインディングをカスタマイズ
        ConfigureParameterBinding = parameterInfo =>
        {
            if (parameterInfo.ParameterType != typeof(DurableAgentContext))
            {
                // DurableAgentContext 型でなければデフォルトのバインディングを使う
                return default;
            }

            // DurableAgentContext 型の場合は現在のコンテキストを返す
            return new AIFunctionFactoryOptions.ParameterBindingOptions
            {
                BindParameter = (parameterInfo, arguments) => DurableAgentContext.Current,
                ExcludeFromSchema = true,
            };
        },
    };

    return serviceProvider.GetRequiredService<IChatClient>()
        .CreateAIAgent(
            name: "cat-agent",
            instructions: """
            あなたは猫型アシスタントです。猫らしく振舞うように語尾は「にゃん」をつけてください。
            あなたは天気レポート生成ワークフローを管理できます。
            ワークフローの開始、監視、イベントの送信を行うツールにアクセスできます。
            """,
            tools: [
                // ツール呼び出し時に DurableAgentContext を注入するようにオプションを渡す
                AIFunctionFactory.Create(tools.StartWeatherReportWorkflow, options),
                AIFunctionFactory.Create(tools.GetWorkflowStatusAsync, options),
                AIFunctionFactory.Create(tools.SubmitNextLocationAsync, options),
            ]);
}

後は、各々のメソッドの引数に DurableAgentContext context を追加して DurableAgentContext.Current の部分を context パラメーターを使用するようにすれば完了です。

using System.ComponentModel;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;

namespace FunctionApp19;

/// <summary>
/// 天気オーケストレーターワークフローを管理するツール
/// </summary>
internal sealed class OrchestratorManageTools(ILogger<OrchestratorManageTools> logger)
{
    [Description("天気レポート生成ワークフローを開始し、追跡用のインスタンスIDを返します。")]
    public string StartWeatherReportWorkflow(
        // 追加!
        DurableAgentContext context,
        [Description("天気を取得する場所")] string location)
    {
        // 省略
    }

    [Description("ワークフローオーケストレーションのステータスを取得します。")]
    public async Task<object> GetWorkflowStatusAsync(
        // 追加!
        DurableAgentContext context,
        [Description("確認するワークフローのインスタンスID")] string instanceId,
        [Description("詳細情報を含めるかどうか")] bool includeDetails = true)
    {
        // 省略
    }

    [Description("天気レポートワークフローに次の場所を指定するイベントを送信します。")]
    public async Task SubmitNextLocationAsync(
        // 追加!
        DurableAgentContext context,
        [Description("イベントを送信するワークフローのインスタンスID")] string instanceId,
        [Description("次に天気を取得する場所。nullまたは空の場合はワークフローを終了します。")] string? location)
    {
        // 省略
    }
}

これで実行すると、static プロパティを使わずに DurableAgentContext を利用できるようになります。実際に動かしてみるとちゃんと動きました!やったね。

とまぁ、ここまでやって気づいたのは DurableAgentContext クラスのメソッド自体が abstract メソッドになっていないのでモックは作れませんでした。悲しい。

ということで、ここら辺のメソッドを単体テストしたかったら DurableAgentContext をラップした感じのものを作らないといけないっぽいです。まぁ、そんなにいらない…?いや、いるような気がする。どうだろう。

まとめ

ということで、今回は Agent Framework というよりは Microsoft.Extensions.AI の AIFunction の引数をカスタマイズする方法になっちゃいましたが、こんなことも出来るよっていう紹介でした。

Microsoft (有志)

Discussion