Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
app.UseExceptionHandler();

// attach a2a with simple message communication
app.MapA2A(agentName: "pirate", path: "/a2a/pirate");
app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", agentCard: new()
app.MapA2A(pirateAgentBuilder, path: "/a2a/pirate");
app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard: new()
{
Name = "Knights and Knaves",
Description = "An agent that helps you solve the knights and knaves puzzle.",
Expand Down
11 changes: 10 additions & 1 deletion dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,16 @@ public static ITaskManager MapA2A(
{
// A2A SDK assigns the url on its own
// we can help user if they did not set Url explicitly.
agentCard.Url ??= context;
if (string.IsNullOrEmpty(agentCard.Url))
{
var agentCardUrl = context.TrimEnd('/');
if (!context.EndsWith("/v1/card", StringComparison.Ordinal))
{
agentCardUrl += "/v1/card";
}

agentCard.Url = agentCardUrl;
}

return Task.FromResult(agentCard);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Text.Json;
using System.Threading.Tasks;
using A2A;
using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;

public sealed class A2AIntegrationTests
{
/// <summary>
/// Verifies that calling the A2A card endpoint with MapA2A returns an agent card with a URL populated.
/// </summary>
[Fact]
public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();

IChatClient mockChatClient = new DummyChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("test-agent", "Test instructions", chatClientServiceKey: "chat-client");
builder.Services.AddLogging();

using WebApplication app = builder.Build();

var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for A2A communication",
Version = "1.0"
};

// Map A2A with the agent card
app.MapA2A(agentBuilder, "/a2a/test-agent", agentCard);

await app.StartAsync();

try
{
// Get the test server client
TestServer testServer = app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
var httpClient = testServer.CreateClient();

// Act - Query the agent card endpoint
var requestUri = new Uri("/a2a/test-agent/v1/card", UriKind.Relative);
var response = await httpClient.GetAsync(requestUri);

// Assert
Assert.True(response.IsSuccessStatusCode, $"Expected successful response but got {response.StatusCode}");

var content = await response.Content.ReadAsStringAsync();
var jsonDoc = JsonDocument.Parse(content);
var root = jsonDoc.RootElement;

// Verify the card has expected properties
Assert.True(root.TryGetProperty("name", out var nameProperty));
Assert.Equal("Test Agent", nameProperty.GetString());

Assert.True(root.TryGetProperty("description", out var descProperty));
Assert.Equal("A test agent for A2A communication", descProperty.GetString());

// Verify the card has a URL property and it's not null/empty
Assert.True(root.TryGetProperty("url", out var urlProperty));
Assert.NotEqual(JsonValueKind.Null, urlProperty.ValueKind);

var url = urlProperty.GetString();
Assert.NotNull(url);
Assert.NotEmpty(url);
Assert.StartsWith("http", url, StringComparison.OrdinalIgnoreCase);
Assert.Equal($"{testServer.BaseAddress.ToString().TrimEnd('/')}/a2a/test-agent/v1/card", url);
}
finally
{
await app.StopAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using A2A;
using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -478,25 +476,4 @@ public void MapA2A_WithAgentBuilder_FullAgentCard_Succeeds()
var result = app.MapA2A(agentBuilder, "/a2a", agentCard);
Assert.NotNull(result);
}

private sealed class DummyChatClient : IChatClient
{
public void Dispose()
{
throw new NotImplementedException();
}

public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public object? GetService(Type serviceType, object? serviceKey = null) =>
serviceType.IsInstanceOfType(this) ? this : null;

public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;

namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;

internal sealed class DummyChatClient : IChatClient
{
public void Dispose()
{
throw new NotImplementedException();
}

public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public object? GetService(Type serviceType, object? serviceKey = null) =>
serviceType.IsInstanceOfType(this) ? this : null;

public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugCoreTargetFrameworks)</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" VersionOverride="8.0.21" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' != 'net8.0'" />

<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
</ItemGroup>
Expand Down
Loading