🙌

Microsoft Agent Framework (C#) を見てみよう その12 A2A対応のエージェントを作ってみよう

に公開

シリーズ記事

はじめに

前回は、Microsoft Agent Framework のエージェントの基本的な使い方を見てきました。今回は、エージェント間のやり取り (Agent to Agent: A2A) に対応したエージェントを作成してみようと思います。
Microsoft Agent Framework はエージェント同士が互いにコミュニケーションを取ることができる A2A 機能をサポートしています。これにより、複数のエージェントが協力してタスクを遂行したり、情報を共有したりすることが可能になります。

A2A のサポートは公式の A2A の C# SDK を使用して実装されています。因みに A2A の C# SDK は、A2A の公式リポジトリで開発されていますが、コミッターを見てみると Microsoft のメンバーが多く参加して開発しているみたいです。そのため、恐らくですが Agent Framework や Microsoft.Extensions.AI あたりを意識した作りになっているのではないかと想像できますね。

サーバーを作ってみよう

ということで、早速サーバーを作ってみましょう。ASP.NET Core の Web API プロジェクトを作成して、以下のパッケージをインストールします。

  • Microsoft.Agents.AI.Hosting.A2A.AspNetCore (1.0.0-preview.251016.1)

このパッケージの依存関係として A2A の SDK や Microsoft Agent Framework の基本的なパッケージも入っているので、これだけで A2A に対応したエージェントを作成するための準備が整います。今回は A2A の機能だけにフォーカスしたいので LLM の呼び出しをしない形で実装していこうと思います。

Agent Framework では AIAgent クラスを継承して任意のエージェントを実装できます。これを使うと LLM を呼び出したりしなくてもエージェントとして扱えるものを作成できます。今回は入力した値をそのまま返す EchoAgent を作成してみましょう。

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.Json;

namespace A2ATest;

// AIAgent を継承してカスタムエージェントを作れる
public class EchoAgent : AIAgent
{
    public override string? Name => nameof(EchoAgent);

    // シリアライズされたスレッドからスレッドを復元する
    public override AgentThread DeserializeThread(
        JsonElement serializedThread, 
        JsonSerializerOptions? jsonSerializerOptions = null) =>
        new EchoAgentThread(serializedThread, jsonSerializerOptions);

    // 新しいスレッドを作成する
    public override AgentThread GetNewThread() => new EchoAgentThread();

    // エージェントの処理を実装
    public override Task<AgentRunResponse> RunAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        // 最後のメッセージを取得して、その内容をエコーする
        var lastMessage = messages.Last();
        var result = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, $"Echo: {lastMessage.Text}"));
        return Task.FromResult(result);
    }

    // ストリーミングでのエージェントの処理を実装
    public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        // 最後のメッセージを取得して、その内容をエコーする
        var lastMessage = messages.Last();
        var message = $"Echo: {lastMessage.Text}";
        var enumerator = StringInfo.GetTextElementEnumerator(message);
        while(enumerator.MoveNext())
        {
            var chunk = enumerator.GetTextElement();
            yield return new AgentRunResponseUpdate(ChatRole.Assistant, chunk);
        }
    }

    // エージェントスレッドの実装
    internal class EchoAgentThread : InMemoryAgentThread
    {
        public EchoAgentThread() { }
        public EchoAgentThread(JsonElement state,
            JsonSerializerOptions? jsonSerializerOptions = null) :
            base(state, jsonSerializerOptions) { }
    }
}

この EchoAgent クラスは、AIAgent クラスを継承しており、RunAsync メソッドと RunStreamingAsync メソッドをオーバーライドしています。これらのメソッドでは、入力されたメッセージの最後の内容を取得し、それをエコーして返すように実装しています。 また、エージェントのスレッド管理のために EchoAgentThread クラスも定義しています。EchoAgentThread クラスは、InMemoryAgentThread クラスを継承しており、エージェントの状態をメモリ内で管理します。Name プロパティをオーバーライドしていますが、A2A で使うときには Name が必須なので、ここで固定の名前を返すようにしています。

次に、このエージェントを A2A サーバーとしてホストするようにしてみましょう。
まず、Agent が何をできるのかを示す AgentCard を作成します。今回は Echo するだけなのでシンプルな定義になります。今回は A2A 自体の説明はしないので各プロパティの設定は割愛します。(というか雰囲気でしかわからないです。)

static AgentCard CreateAgentCard()
{
    var capabilities = new AgentCapabilities
    {
        Streaming = false,
        PushNotifications = false,
    };

    var skill = new AgentSkill
    {
        Id = "echo-skill",
        Name = "Echo Skill",
        Description = "An echo skill that repeats back what you say.",
        Examples = ["Hello world"],
        Tags = ["echo", "test"],
    };

    return new AgentCard
    {
        Name = "Echo Agent",
        Description = "An agent that echoes back user input.",
        Capabilities = capabilities,
        Skills = [skill],
        DefaultInputModes = ["text"],
        DefaultOutputModes = ["text"],
    };
}

次に、ASP.NET Core の Program.cs ファイルで A2A サーバーをセットアップします。

using A2A;
using A2A.AspNetCore;
using A2ATest;
using Microsoft.Agents.AI.Hosting.A2A;
using Microsoft.Agents.AI.Hosting.A2A.AspNetCore;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseHttpsRedirection();

var agent = new EchoAgent();
var agentCard = CreateAgentCard();
var taskManager = agent.MapA2A(agentCard);
taskManager.OnAgentCardQuery += (context, query) =>
{
    agentCard.Url = context; // ここで自動的に実行時のURLを設定してあげる
    return Task.FromResult(agentCard);
};
app.MapA2A(taskManager, "/");
app.MapWellKnownAgentCard(taskManager, "/");
// 多分、次のバージョンで以下のように書けるようになる。
//app.MapA2A(
//    hostA2AAgent,
//    path: "/",
//    agentCard: hostA2AAgentCard,
//    taskManager => app.MapWellKnownAgentCard(taskManager, "/"));

app.Run();

MapA2A 拡張メソッドを使って、EchoAgent インスタンスを A2A エージェントとしてマッピングしています。これにより、指定したエンドポイントでエージェントがリクエストを受け付けるようになります。そして MapWellKnownAgentCard 拡張メソッドを使って、エージェントの AgentCard を公開しています。これにより /.well-known/agent-card.json エンドポイントでエージェントの情報が取得できるようになります。

実行して https://localhost:PORT/.well-known/agent-card.json にアクセスすると、以下のように AgentCard の内容が JSON 形式で取得できることが確認できます。

{
    "name": "Echo Agent",
    "description": "An agent that echoes back user input.",
    "url": "https://localhost:7046/",
    "iconUrl": null,
    "provider": null,
    "version": "",
    "protocolVersion": "0.3.0",
    "documentationUrl": null,
    "capabilities": {
        "streaming": false,
        "pushNotifications": false,
        "stateTransitionHistory": false,
        "extensions": []
    },
    "securitySchemes": null,
    "security": null,
    "defaultInputModes": [
        "text"
    ],
    "defaultOutputModes": [
        "text"
    ],
    "skills": [
        {
            "id": "echo-skill",
            "name": "Echo Skill",
            "description": "An echo skill that repeats back what you say.",
            "tags": [
                "echo",
                "test"
            ],
            "examples": [
                "Hello world"
            ],
            "inputModes": null,
            "outputModes": null,
            "security": null
        }
    ],
    "supportsAuthenticatedExtendedCard": false,
    "additionalInterfaces": [],
    "preferredTransport": null,
    "signatures": null
}

これで一旦サーバーが出来ました。

クライアントから呼び出してみよう

次にクライアントから呼び出してみましょう。クライアント用のコンソールアプリケーションを作成して、以下のパッケージをインストールします。

  • Microsoft.Agents.AI.A2A (v1.0.0-preview.251016.1)

このパッケージには A2A クライアントを作成するための機能が含まれています。以下のコードでサーバーに接続してメッセージを送信し、エコーされたレスポンスを受け取ります。

using A2A;
using Microsoft.Agents.AI;

var resolver = new A2ACardResolver(new("https://localhost:PORT")); // PORT はサーバーのポート番号に置き換えてください
var agentCard = await resolver.GetAgentCardAsync();

AIAgent agent = await agentCard.GetAIAgentAsync();
var response = await agent.RunAsync("こんにちは");
Console.WriteLine(response.Text);

A2ACardResolver クラスを使用して、サーバーから AgentCard を取得します。次に、GetAIAgentAsync メソッドを使ってエージェントインスタンスを作成し、RunAsync メソッドでメッセージを送信します。最後に、エコーされたレスポンスをコンソールに表示します。

実行すると、以下のようにエコーされたメッセージが表示されました。

Echo: こんにちは

ちゃんと A2A 経由でエージェントが動いていることが確認できました。

まとめ

ということで、今回は Microsoft Agent Framework を使って A2A に対応したエージェントを作成し、ASP.NET Core を使ってサーバーとしてホストし、クライアントから呼び出す方法を見てきました。A2A 機能を活用することで、リモートのエージェントを呼び出す事ができます。さらに、これは標準プロトコルなので将来的には様々なエージェントが A2A 経由で連携できるようになる可能性があります。今後の展開が楽しみですね。

Microsoft Agent Framework では、A2A のサーバーとクライアントの両方に対応しているので簡単に試すことができます。まだ A2A 自体が発展途上だったりライブラリ自体もプレビューなので破壊的変更にあう可能性はありますが、興味があるかたは是非試してみてください。

Microsoft (有志)

Discussion