Durable Agent で静的変数アクセスを消す - Microsoft Agent Framework (C#) その16
シリーズ記事
- その1: 「雑感」とハローワールド
- その2: ざっとリポジトリを見てみる
- その3: ワークフローを見てみよう
- その4: ワークフローの Executor を掘り下げる
- その5: ワークフローで条件分岐とループを扱う
- その6: Executor のステータス管理
- その7: チェックポイントの永続化
- その8: Human in the loop を試してみよう
- その9: Semantic Kernel の Plugin の移行
- その10: Durable Functions でワークフロー
- その11: エージェントを見てみよう
- その12: A2A対応のエージェントを作ってみよう
- その13: .NET 10 用の Agent プロジェクトテンプレート
- その14: Durable Agent を試してみよう
- その15: Durable Agent で長時間ツール呼び出し
- その16: Durable Agent で静的変数アクセスを消す
はじめに
前回、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 の引数をカスタマイズする方法になっちゃいましたが、こんなことも出来るよっていう紹介でした。
Discussion