-
Notifications
You must be signed in to change notification settings - Fork 681
.NET: library-first agent registration via IServiceCollection + IEndpointRouteBuilder A2A mapping (#1454) #1456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would call it just
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And we should add tests similar to Line 11 in 731e3a7
|
||||
| { | ||||
| /// <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>(); | ||||
| } | ||||
| } | ||||
There was a problem hiding this comment.
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