.NET: Add sample showing how to create ai function per a2a skill (#1452)

* add sample showing how to create ai function per a2a skill

* Update dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/A2A/A2AAgent_AsFunctionTools/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/A2A/A2AAgent_AsFunctionTools/Program.cs

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* address pr review comments

* rename local function

* add readmes

* fix broken links

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2025-10-14 18:57:19 +01:00
committed by GitHub
Unverified
parent 9148392d00
commit 1f0faff623
11 changed files with 341 additions and 13 deletions
+2 -2
View File
@@ -499,7 +499,7 @@ We need to decide what AIContent types, each agent response type will be mapped
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.structured_output) |
| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/examples/getting-started/structured-output) at agent construction time |
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2aproject.github.io/A2A/v0.2.5/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
| Protocol Activity | Supports returning [Complex types](https://github.com/microsoft/Agents/blob/main/specs/activity/protocol-activity.md#complex-types) but no support for requesting a type |
### Response Reason Support
@@ -511,5 +511,5 @@ We need to decide what AIContent types, each agent response type will be mapped
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/api-reference/types/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) class with options that are tied closely to LLM operations. |
| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
| A2A | No equivalent present, response only contains a [message](https://a2aproject.github.io/A2A/v0.2.5/specification/#64-message-object) or [task](https://a2aproject.github.io/A2A/v0.2.5/specification/#61-task-object). |
| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |
| Protocol Activity | [No equivalent present.](https://github.com/microsoft/Agents/blob/main/specs/activity/protocol-activity.md) |
+4
View File
@@ -21,6 +21,10 @@
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
</Folder>
<Folder Name="/Samples/GettingStarted/A2A/">
<File Path="samples/GettingStarted/A2A/README.md" />
<Project Path="samples/GettingStarted/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj"/>
</Folder>
<Folder Name="/Samples/GettingStarted/AgentProviders/">
<File Path="samples/GettingStarted/AgentProviders/README.md" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.5.25277.114" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-preview.5.25277.114" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to represent an A2A agent as a set of function tools, where each function tool
// corresponds to a skill of the A2A agent, and register these function tools with another AI agent so
// it can leverage the A2A agent's skills.
using System.Text.RegularExpressions;
using A2A;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
// Initialize an A2ACardResolver to get an A2A agent card.
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
// Get the agent card
AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
AIAgent a2aAgent = await agentCard.GetAIAgentAsync();
// Create the main agent, and provide the a2a agent skills as a function tools.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(
instructions: "You are a helpful assistant that helps people with travel planning.",
tools: [.. CreateFunctionTools(a2aAgent, agentCard)]
);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls"));
static IEnumerable<AIFunction> CreateFunctionTools(AIAgent a2aAgent, AgentCard agentCard)
{
foreach (var skill in agentCard.Skills)
{
// A2A agent skills don't have schemas describing the expected shape of their inputs and outputs.
// Schemas can be beneficial for AI models to better understand the skill's contract, generate
// the skill's input accordingly and to know what to expect in the skill's output.
// However, the A2A specification defines properties such as name, description, tags, examples,
// inputModes, and outputModes to provide context about the skill's purpose, capabilities, usage,
// and supported MIME types. These properties are added to the function tool description to help
// the model determine the appropriate shape of the skill's input and output.
AIFunctionFactoryOptions options = new()
{
Name = FunctionNameSanitizer.Sanitize(skill.Name),
Description = $$"""
{
"description": "{{skill.Description}}",
"tags": "[{{string.Join(", ", skill.Tags ?? [])}}]",
"examples": "[{{string.Join(", ", skill.Examples ?? [])}}]",
"inputModes": "[{{string.Join(", ", skill.InputModes ?? [])}}]",
"outputModes": "[{{string.Join(", ", skill.OutputModes ?? [])}}]"
}
""",
};
yield return AIFunctionFactory.Create(RunAgentAsync, options);
}
async Task<string> RunAgentAsync(string input, CancellationToken cancellationToken)
{
var response = await a2aAgent.RunAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
return response.Text;
}
}
internal static partial class FunctionNameSanitizer
{
public static string Sanitize(string name)
{
return InvalidNameCharsRegex().Replace(name, "_");
}
[GeneratedRegex("[^0-9A-Za-z]+")]
private static partial Regex InvalidNameCharsRegex();
}
@@ -0,0 +1,22 @@
# A2A Agent as Function Tools
This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent,
and register these function tools with another AI agent so it can leverage the A2A agent's skills.
# Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Access to the A2A agent host service
**Note**: These samples need to be run against a valid A2A server. If no A2A server is available, they can be run against the echo-agent that can be
spun up locally by following the guidelines at: https://github.com/a2aproject/a2a-dotnet/blob/main/samples/AgentServer/README.md
Set the following environment variables:
```powershell
$env:A2A_AGENT_HOST="https://your-a2a-agent-host" # Replace with your A2A agent host endpoint
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
@@ -0,0 +1,50 @@
# Agent-to-Agent (A2A) Samples
These samples demonstrate how to work with Agent-to-Agent (A2A) specific features in the Agent Framework.
For other samples that demonstrate how to use AIAgent instances,
see the [Getting Started With Agents](../Agents/README.md) samples.
## Prerequisites
See the README.md for each sample for the prerequisites for that sample.
## Samples
|Sample|Description|
|---|---|
|[A2A Agent As Function Tools](./A2AAgent_AsFunctionTools/)|This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent, and register these function tools with another AI agent so it can leverage the A2A agent's skills.|
## Running the samples from the console
To run the samples, navigate to the desired sample directory, e.g.
```powershell
cd A2AAgent_AsFunctionTools
```
Set the required environment variables as documented in the sample readme.
If the variables are not set, you will be prompted for the values when running the samples.
Execute the following command to build the sample:
```powershell
dotnet build
```
Execute the following command to run the sample:
```powershell
dotnet run --no-build
```
Or just build and run in one step:
```powershell
dotnet run
```
## Running the samples from Visual Studio
Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`.
You will be prompted for any required environment variables if they are not already set.
+1
View File
@@ -9,6 +9,7 @@ of the agent framework.
|---|---|
|[Agents](./Agents/README.md)|Step by step instructions for getting started with agents|
|[Agent Providers](./AgentProviders/README.md)|Getting started with creating agents using various providers|
|[A2A](./A2A/README.md)|Getting started with A2A (Agent-to-Agent) specific features|
|[Agent Open Telemetry](./AgentOpenTelemetry/README.md)|Getting started with OpenTelemetry for agents|
|[Agent With OpenAI exchange types](./AgentWithOpenAI/README.md)|Using OpenAI exchange types with agents|
|[Workflow](./Workflows/README.md)|Getting started with Workflow|
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
namespace A2A;
/// <summary>
/// Provides extension methods for <see cref="AgentCard"/> to simplify the creation of A2A agents.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between A2A SDK client <see cref="AgentCard"/> and <see cref="AIAgent"/>.
/// </remarks>
public static class A2AAgentCardExtensions
{
/// <summary>
/// Retrieves an instance of <see cref="AIAgent"/> for an existing A2A agent.
/// </summary>
/// <remarks>
/// This method can be used to access A2A agents that support the
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
/// discovery mechanism.
/// </remarks>
/// <param name="card">The <see cref="AgentCard" /> to use for the agent creation.</param>
/// <param name="httpClient">The <see cref="HttpClient"/> to use for HTTP requests.</param>
/// <param name="loggerFactory">The logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
public static async Task<AIAgent> GetAIAgentAsync(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null)
{
// Create the A2A client using the agent URL from the card.
var a2aClient = new A2AClient(new Uri(card.Url), httpClient);
return a2aClient.GetAIAgent(name: card.Name, description: card.Description, loggerFactory: loggerFactory);
}
}
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -29,11 +28,9 @@ public static class A2ACardResolverExtensions
/// Retrieves an instance of <see cref="AIAgent"/> for an existing A2A agent.
/// </summary>
/// <remarks>
/// Creates an AI agent for an A2A agent whose host supports one of the A2A discovery mechanisms:
/// <list type="bullet">
/// <item><description><see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#1-well-known-uri">Well-Known URI</see></description></item>
/// <item><description><see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see></description></item>
/// </list>
/// This method can be used to access A2A agents that support the
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#1-well-known-uri">Well-Known URI</see>
/// discovery mechanism.
/// </remarks>
/// <param name="resolver">The <see cref="A2ACardResolver" /> to use for the agent creation.</param>
/// <param name="httpClient">The <see cref="HttpClient"/> to use for HTTP requests.</param>
@@ -45,9 +42,6 @@ public static class A2ACardResolverExtensions
// Obtain the agent card from the resolver.
var agentCard = await resolver.GetAgentCardAsync(cancellationToken).ConfigureAwait(false);
// Create the A2A client using the agent URL from the card.
var a2aClient = new A2AClient(new Uri(agentCard.Url), httpClient);
return a2aClient.GetAIAgent(name: agentCard.Name, description: agentCard.Description, loggerFactory: loggerFactory);
return await agentCard.GetAIAgentAsync(httpClient, loggerFactory).ConfigureAwait(false);
}
}
@@ -25,7 +25,7 @@ public static class A2AClientExtensions
/// Retrieves an instance of <see cref="AIAgent"/> for an existing A2A agent.
/// </summary>
/// <remarks>
/// This method can be used to create AI agents for A2A agents whose hosts support the
/// This method can be used to access A2A agents that support the
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#3-direct-configuration--private-discovery">Direct Configuration / Private Discovery</see>
/// discovery mechanism.
/// </remarks>
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using A2A;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AAgentCardExtensions"/> class.
/// </summary>
public sealed class A2AAgentCardExtensionsTests
{
private readonly AgentCard _agentCard;
public A2AAgentCardExtensionsTests()
{
this._agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for unit testing",
Url = "http://test-endpoint/agent"
};
}
[Fact]
public async Task GetAIAgentAsync_ReturnsAIAgentAsync()
{
// Act
var agent = await this._agentCard.GetAIAgentAsync();
// Assert
Assert.NotNull(agent);
Assert.IsType<A2AAgent>(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("A test agent for unit testing", agent.Description);
}
[Fact]
public async Task RunIAgentAsync_SendsRequestToTheUrlSpecifiedInAgentCardAsync()
{
// Arrange
using var handler = new HttpMessageHandlerStub();
using var httpClient = new HttpClient(handler, false);
handler.ResponsesToReturn.Enqueue(new AgentMessage
{
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }],
});
var agent = await this._agentCard.GetAIAgentAsync(httpClient);
// Act
await agent.RunAsync("Test input");
// Assert
Assert.Single(handler.CapturedUris);
Assert.Equal(new Uri("http://test-endpoint/agent"), handler.CapturedUris[0]);
}
internal sealed class HttpMessageHandlerStub : HttpMessageHandler
{
public Queue ResponsesToReturn { get; } = new();
public List<Uri> CapturedUris { get; } = [];
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
this.CapturedUris.Add(request.RequestUri!);
var response = this.ResponsesToReturn.Dequeue();
if (response is AgentCard agentCard)
{
var json = JsonSerializer.Serialize(agentCard);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
else if (response is AgentMessage message)
{
var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse<A2AEvent>("response-id", message);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse), Encoding.UTF8, "application/json")
};
}
// Return empty agent card if none specified
var emptyCard = new AgentCard();
var emptyJson = JsonSerializer.Serialize(emptyCard);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(emptyJson, Encoding.UTF8, "application/json")
};
}
}
}