Skip to content
Open
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 @@ -22,13 +22,13 @@
// Configure the chat model and our agent.
builder.AddKeyedChatClient("chat-model");

builder.AddAIAgent(
builder.Services.AddAIAgent(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this (and other AddAIAgent) should not change here

"pirate",
instructions: "You are a pirate. Speak like a pirate",
description: "An agent that speaks like a pirate.",
chatClientServiceKey: "chat-model");

builder.AddAIAgent("knights-and-knaves", (sp, key) =>
builder.Services.AddAIAgent("knights-and-knaves", (sp, key) =>
{
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,47 @@

using A2A;
using A2A.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

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

/// <summary>
/// Provides extension methods for configuring A2A (Agent2Agent) communication in a host application builder.
/// Provides extension methods for configuring A2A (Agent2Agent) communication on an endpoint route builder.
/// </summary>
public static class WebApplicationExtensions
public static class EndpointRouteBuilderExtensions
{
/// <summary>
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified endpoint route builder.
/// </summary>
/// <param name="app">The web application used to configure the pipeline and routes.</param>
/// <param name="app">The endpoint route builder used to configure the pipeline and routes.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
public static void MapA2A(this WebApplication app, string agentName, string path)
public static void MapA2A(this IEndpointRouteBuilder app, string agentName, string path)
{
var agent = app.Services.GetRequiredKeyedService<AIAgent>(agentName);
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
var agent = app.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
var loggerFactory = app.ServiceProvider.GetRequiredService<ILoggerFactory>();

var taskManager = agent.MapA2A(loggerFactory: loggerFactory);
app.MapA2A(taskManager, path);
}

/// <summary>
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified endpoint route builder.
/// </summary>
/// <param name="app">The web application used to configure the pipeline and routes.</param>
/// <param name="app">The endpoint route builder used to configure the pipeline and routes.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <param name="agentCard">Agent card info to return on query.</param>
public static void MapA2A(
this WebApplication app,
this IEndpointRouteBuilder app,
string agentName,
string path,
AgentCard agentCard)
{
var agent = app.Services.GetRequiredKeyedService<AIAgent>(agentName);
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
var agent = app.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
var loggerFactory = app.ServiceProvider.GetRequiredService<ILoggerFactory>();

var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory);
app.MapA2A(taskManager, path);
Expand All @@ -52,10 +52,10 @@ public static void MapA2A(
/// Maps HTTP A2A communication endpoints to the specified path using the provided TaskManager.
/// TaskManager should be preconfigured before calling this method.
/// </summary>
/// <param name="app">The web application used to configure the pipeline and routes.</param>
/// <param name="app">The endpoint route builder used to configure the pipeline and routes.</param>
/// <param name="taskManager">Pre-configured A2A TaskManager to use for A2A endpoints handling.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
public static void MapA2A(this WebApplication app, TaskManager taskManager, string path)
public static void MapA2A(this IEndpointRouteBuilder app, TaskManager taskManager, string path)
{
// note: current SDK version registers multiple `.well-known/agent.json` handlers here.
// it makes app return HTTP 500, but will be fixed once new A2A SDK is released.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Linq;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI.Hosting;

/// <summary>
/// Provides extension methods for configuring AI agents in a service collection.
/// </summary>
public static class AgentHostingServiceCollectionExtensions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call it just ServiceCollectionExtensions because of the convention

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we should add tests similar to

{
/// <summary>
/// Adds an AI agent to the service collection using only a name and instructions, resolving the chat client from dependency injection.
/// </summary>
/// <param name="services">The service collection to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddAIAgent(this IServiceCollection services, string name, string? instructions)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
services.AddAIAgent(name, (sp, key) =>
{
var chatClient = sp.GetRequiredService<IChatClient>();
return new ChatClientAgent(chatClient, instructions, key);
});
return services;
}

/// <summary>
/// Adds an AI agent to the service collection with a provided chat client instance.
/// </summary>
/// <param name="services">The service collection to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="chatClient">The chat client which the agent will use for inference.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
services.AddAIAgent(name, (sp, key) => new ChatClientAgent(chatClient, instructions, key));
return services;
}

/// <summary>
/// Adds an AI agent to the service collection using a chat client resolved by an optional keyed service.
/// </summary>
/// <param name="services">The service collection to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If <see langword="null"/>, a non-keyed service will be resolved.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
services.AddAIAgent(name, (sp, key) =>
{
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
return new ChatClientAgent(chatClient, instructions, key);
});
return services;
}

/// <summary>
/// Adds an AI agent to the service collection using a chat client (optionally keyed) and a description.
/// </summary>
/// <param name="services">The service collection to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="description">A description of the agent.</param>
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If <see langword="null"/>, a non-keyed service will be resolved.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
services.AddAIAgent(name, (sp, key) =>
{
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
return new ChatClientAgent(chatClient, instructions: instructions, name: key, description: description);
});
return services;
}

/// <summary>
/// Adds an AI agent to the service collection using a custom factory delegate.
/// </summary>
/// <param name="services">The service collection to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="createAgentDelegate">A factory delegate that creates the AI agent instance. The delegate receives the service provider and agent key as parameters.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/>, <paramref name="name"/>, or <paramref name="createAgentDelegate"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when the agent factory delegate returns <see langword="null"/> or an agent whose <see cref="AIAgent.Name"/> does not match <paramref name="name"/>.</exception>
public static IServiceCollection AddAIAgent(this IServiceCollection services, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
{
Throw.IfNull(services);
Throw.IfNull(name);
Throw.IfNull(createAgentDelegate);
services.AddKeyedSingleton(name, (sp, key) =>
{
Throw.IfNull(key);
var keyString = key as string;
Throw.IfNullOrEmpty(keyString);
var agent = createAgentDelegate(sp, keyString) ?? throw new InvalidOperationException($"The agent factory did not return a valid {nameof(AIAgent)} instance for key '{keyString}'.");
if (!string.Equals(agent.Name, keyString, StringComparison.Ordinal))
{
throw new InvalidOperationException($"The agent factory returned an agent with name '{agent.Name}', but the expected name is '{keyString}'.");
}

return agent;
});

// Register the agent by name for discovery.
var agentHostBuilder = GetAgentRegistry(services);
agentHostBuilder.AgentNames.Add(name);
return services;
}

private static LocalAgentRegistry GetAgentRegistry(IServiceCollection services)
{
var descriptor = services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalAgentRegistry)));
if (descriptor?.ImplementationInstance is not LocalAgentRegistry instance)
{
instance = new LocalAgentRegistry();
ConfigureHostBuilder(services, instance);
}

return instance;
}

private static void ConfigureHostBuilder(IServiceCollection services, LocalAgentRegistry agentHostBuilderContext)
{
services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext));
services.AddSingleton<AgentCatalog, LocalAgentCatalog>();
}
}
Loading