Skip to content

Commit 757ca37

Browse files
committed
Add A2A Sample
1 parent 7cde607 commit 757ca37

7 files changed

Lines changed: 401 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
## Ignore Visual Studio temporary files, build results, and
2+
## files generated by popular Visual Studio add-ons.
3+
4+
# User-specific files
5+
*.rsuser
6+
*.suo
7+
*.user
8+
*.userosscache
9+
*.sln.docstates
10+
11+
# User-specific files (MonoDevelop/Xamarin Studio)
12+
*.userprefs
13+
14+
# Build results
15+
[Dd]ebug/
16+
[Dd]ebugPublic/
17+
[Rr]elease/
18+
[Rr]eleases/
19+
x64/
20+
x86/
21+
[Ww]in32/
22+
[Aa][Rr][Mm]/
23+
[Aa][Rr][Mm]64/
24+
bld/
25+
[Bb]in/
26+
[Oo]bj/
27+
[Ll]og/
28+
[Ll]ogs/
29+
30+
# Visual Studio 2015/2017/2019/2022 cache/options directory
31+
.vs/
32+
# Visual Studio 2017 auto generated files
33+
Generated\ Files/
34+
35+
# MSTest test Results
36+
[Tt]est[Rr]esult*/
37+
[Bb]uild[Ll]og.*
38+
39+
# NUNIT
40+
*.VisualState.xml
41+
TestResult.xml
42+
nunit-*.xml
43+
44+
# Build Results of an ATL Project
45+
[Dd]ebugPS/
46+
[Rr]eleasePS/
47+
dlldata.c
48+
49+
# BenchmarkDotNet
50+
BenchmarkDotNet.Artifacts/
51+
52+
# .NET Core
53+
project.lock.json
54+
project.fragment.lock.json
55+
artifacts/
56+
57+
# ASP.NET Scaffolding
58+
ScaffoldingReadMe.txt
59+
60+
# StyleCop
61+
StyleCopReport.xml
62+
63+
# FxCop
64+
FxCopReport.xml
65+
66+
# Service Fabric
67+
*.apk
68+
*.ap_
69+
70+
# Client-side Web assets
71+
node_modules/
72+
73+
# Azure / Local Settings
74+
appsettings.Development.json
75+
appsettings.local.json
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<RootNamespace>A2A_Agent_Framework</RootNamespace>
8+
<UserSecretsId>e29276df-556e-4ed9-a755-ffc549613596</UserSecretsId>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
13+
<PackageReference Include="Azure.Identity" Version="1.17.1" />
14+
<PackageReference Include="Microsoft.Agents.AI.Hosting.A2A.AspNetCore" Version="1.0.0-preview.251125.1" />
15+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
16+
<PackageReference Include="Microsoft.Extensions.AI" Version="10.0.1" />
17+
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
18+
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Nodes;
3+
4+
namespace A2A_Agent_Framework;
5+
6+
public class JsonRpcMiddleware
7+
{
8+
private readonly RequestDelegate _next;
9+
10+
public JsonRpcMiddleware(RequestDelegate next)
11+
{
12+
_next = next;
13+
}
14+
15+
public async Task InvokeAsync(HttpContext context)
16+
{
17+
// Check if it's a POST request and looks like it might be JSON-RPC
18+
if (context.Request.Method == HttpMethods.Post &&
19+
context.Request.ContentType?.Contains("application/json") == true)
20+
{
21+
context.Request.EnableBuffering();
22+
23+
// Read the body
24+
using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
25+
var body = await reader.ReadToEndAsync();
26+
context.Request.Body.Position = 0;
27+
28+
JsonNode? root = null;
29+
try
30+
{
31+
root = JsonNode.Parse(body);
32+
}
33+
catch
34+
{
35+
// Not valid JSON, ignore
36+
}
37+
38+
// Check for JSON-RPC signature
39+
if (root is JsonObject obj &&
40+
obj.ContainsKey("jsonrpc") &&
41+
obj["jsonrpc"]?.GetValue<string>() == "2.0" &&
42+
obj.ContainsKey("method") &&
43+
obj.ContainsKey("params"))
44+
{
45+
var id = obj["id"];
46+
var paramsNode = obj["params"];
47+
48+
// Replace request body with params
49+
var newBodyBytes = JsonSerializer.SerializeToUtf8Bytes(paramsNode);
50+
var newBodyStream = new MemoryStream(newBodyBytes);
51+
context.Request.Body = newBodyStream;
52+
context.Request.ContentLength = newBodyBytes.Length;
53+
54+
// Capture response
55+
var originalBodyStream = context.Response.Body;
56+
using var responseBodyStream = new MemoryStream();
57+
context.Response.Body = responseBodyStream;
58+
59+
try
60+
{
61+
await _next(context);
62+
63+
// Reset response body stream to read it
64+
responseBodyStream.Position = 0;
65+
var responseContent = await new StreamReader(responseBodyStream).ReadToEndAsync();
66+
67+
// Try to parse the response content as JSON
68+
JsonNode? resultNode = null;
69+
70+
// Check for SSE format (data: ...)
71+
if (responseContent.TrimStart().StartsWith("data:"))
72+
{
73+
using var stringReader = new StringReader(responseContent);
74+
string? line;
75+
while ((line = await stringReader.ReadLineAsync()) != null)
76+
{
77+
if (line.StartsWith("data:"))
78+
{
79+
var jsonPart = line.Substring(5).Trim();
80+
try
81+
{
82+
resultNode = JsonNode.Parse(jsonPart);
83+
if (resultNode != null) break; // Found valid JSON
84+
}
85+
catch { }
86+
}
87+
}
88+
}
89+
90+
if (resultNode == null)
91+
{
92+
try
93+
{
94+
if (!string.IsNullOrWhiteSpace(responseContent))
95+
{
96+
resultNode = JsonNode.Parse(responseContent);
97+
}
98+
}
99+
catch { }
100+
}
101+
102+
// Construct JSON-RPC response
103+
var rpcResponse = new JsonObject
104+
{
105+
["jsonrpc"] = "2.0",
106+
["id"] = id?.DeepClone(),
107+
};
108+
109+
if (context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)
110+
{
111+
rpcResponse["result"] = resultNode ?? responseContent;
112+
}
113+
else
114+
{
115+
rpcResponse["error"] = new JsonObject
116+
{
117+
["code"] = context.Response.StatusCode,
118+
["message"] = "Error processing request",
119+
["data"] = resultNode ?? responseContent
120+
};
121+
// Reset status code to 200 because JSON-RPC errors are usually 200 OK at HTTP level
122+
context.Response.StatusCode = 200;
123+
}
124+
125+
var rpcResponseBytes = JsonSerializer.SerializeToUtf8Bytes(rpcResponse);
126+
127+
// Write back to original stream
128+
context.Response.Body = originalBodyStream;
129+
context.Response.ContentLength = rpcResponseBytes.Length;
130+
context.Response.ContentType = "application/json";
131+
await context.Response.Body.WriteAsync(rpcResponseBytes);
132+
return;
133+
}
134+
catch (Exception ex)
135+
{
136+
// Handle exceptions
137+
context.Response.Body = originalBodyStream;
138+
var errorResponse = new JsonObject
139+
{
140+
["jsonrpc"] = "2.0",
141+
["id"] = id?.DeepClone(),
142+
["error"] = new JsonObject
143+
{
144+
["code"] = -32603,
145+
["message"] = "Internal error",
146+
["data"] = ex.Message
147+
}
148+
};
149+
context.Response.StatusCode = 200;
150+
context.Response.ContentType = "application/json";
151+
await context.Response.WriteAsJsonAsync(errorResponse);
152+
return;
153+
}
154+
}
155+
}
156+
157+
await _next(context);
158+
}
159+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using A2A.AspNetCore;
2+
using Azure;
3+
using Azure.AI.OpenAI;
4+
using Microsoft.Agents.AI.Hosting;
5+
using Microsoft.Extensions.AI;
6+
7+
var builder = WebApplication.CreateBuilder(args);
8+
9+
builder.Services.AddOpenApi();
10+
builder.Services.AddSwaggerGen();
11+
12+
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
13+
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
14+
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
15+
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
16+
string apiKey = builder.Configuration["AZURE_OPENAI_API_KEY"]
17+
?? throw new InvalidOperationException("AZURE_OPENAI_API_KEY is not set.");
18+
19+
// Register the chat client
20+
IChatClient chatClient = new AzureOpenAIClient(
21+
new Uri(endpoint),
22+
new AzureKeyCredential(apiKey))
23+
.GetChatClient(deploymentName)
24+
.AsIChatClient();
25+
builder.Services.AddSingleton(chatClient);
26+
27+
// Register an agent
28+
var botanicalAgent = builder.AddAIAgent("botanical", instructions: "You are a very knowledgeable botanical expert. In all your responses, you begin by introducing yourself as 'BotaniBot, your friendly botanical assistant.'");
29+
30+
var app = builder.Build();
31+
32+
app.UseMiddleware<A2A_Agent_Framework.JsonRpcMiddleware>();
33+
34+
app.MapOpenApi();
35+
app.UseSwagger();
36+
app.UseSwaggerUI();
37+
38+
// Expose the agent via A2A protocol. You can also customize the agentCard
39+
app.MapA2A(botanicalAgent, path: "/a2a/botanical", agentCard: new()
40+
{
41+
Name = "Botanical Agent",
42+
Description = "An agent that provides information about plants and botany.",
43+
Version = "1.0",
44+
Url = "https://<YOUR_URL>/a2a/botanical/v1/card",
45+
Capabilities = new A2A.AgentCapabilities
46+
{
47+
Streaming = true,
48+
PushNotifications = false,
49+
StateTransitionHistory = false,
50+
Extensions = new List<A2A.AgentExtension>()
51+
}
52+
});
53+
54+
app.Run();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# A2A Agent Framework Sample
2+
3+
This repository contains a sample implementation of an AI agent using the A2A Agent Framework. It demonstrates how to host a simple "botanical" agent.
4+
5+
## Prerequisites
6+
7+
- [.NET SDK 10.0](https://dotnet.microsoft.com/download/dotnet/10.0) or later.
8+
9+
## Configuration
10+
11+
Before running the application, you need to configure your Azure OpenAI settings. You can do this by setting the following environment variables or adding them to your `appsettings.json` (or `appsettings.Development.json`):
12+
13+
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint URL.
14+
- `AZURE_OPENAI_DEPLOYMENT_NAME`: The name of your deployment.
15+
- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key.
16+
17+
## How to Run
18+
19+
1. **Clone the repository:**
20+
```bash
21+
git clone <repository-url>
22+
cd A2A-Agent-Framework
23+
```
24+
25+
2. **Restore dependencies:**
26+
```bash
27+
dotnet restore
28+
```
29+
30+
3. **Build the project:**
31+
```bash
32+
dotnet build
33+
```
34+
35+
4. **Run the application:**
36+
```bash
37+
dotnet run
38+
```
39+
40+
The application will start and the agent will be available at the configured endpoint.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

0 commit comments

Comments
 (0)