mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Refactor A2A and AIAgent hosting extensions (#1625)
* a2a reformat * and refactor extensions on serviceCollection * units * fix build * add remark for agentcard overloads
This commit is contained in:
committed by
GitHub
Unverified
parent
905e730dc2
commit
72c391bc08
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,7 +9,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A.AspNetCore" />
|
||||
<PackageReference Include="Azure.AI.Agents.Persistent" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
@@ -17,6 +16,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A\Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
|
||||
@@ -4,7 +4,6 @@ using A2A;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.A2A;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
|
||||
@@ -12,7 +11,7 @@ namespace A2AServer;
|
||||
|
||||
internal static class HostAgentFactory
|
||||
{
|
||||
internal static async Task<A2AHostAgent> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string assistantId, IList<AITool>? tools = null)
|
||||
internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string assistantId, IList<AITool>? tools = null)
|
||||
{
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
|
||||
PersistentAgent persistentAgent = await persistentAgentsClient.Administration.GetAgentAsync(assistantId);
|
||||
@@ -28,10 +27,10 @@ internal static class HostAgentFactory
|
||||
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
|
||||
};
|
||||
|
||||
return new A2AHostAgent(agent, agentCard);
|
||||
return new(agent, agentCard);
|
||||
}
|
||||
|
||||
internal static async Task<A2AHostAgent> CreateChatCompletionHostAgentAsync(string agentType, string model, string apiKey, string name, string instructions, IList<AITool>? tools = null)
|
||||
internal static async Task<(AIAgent, AgentCard)> CreateChatCompletionHostAgentAsync(string agentType, string model, string apiKey, string name, string instructions, IList<AITool>? tools = null)
|
||||
{
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetChatClient(model)
|
||||
@@ -45,7 +44,7 @@ internal static class HostAgentFactory
|
||||
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
|
||||
};
|
||||
|
||||
return new A2AHostAgent(agent, agentCard);
|
||||
return new(agent, agentCard);
|
||||
}
|
||||
|
||||
#region private
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using A2A;
|
||||
using A2A.AspNetCore;
|
||||
using A2AServer;
|
||||
using Microsoft.Agents.AI.A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -47,10 +47,12 @@ IList<AITool> tools =
|
||||
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByInvoiceId)
|
||||
];
|
||||
|
||||
A2AHostAgent? hostAgent = null;
|
||||
AIAgent hostA2AAgent;
|
||||
AgentCard hostA2AAgentCard;
|
||||
|
||||
if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentId))
|
||||
{
|
||||
hostAgent = agentType.ToUpperInvariant() switch
|
||||
(hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
|
||||
{
|
||||
"INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId, tools),
|
||||
"POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId),
|
||||
@@ -60,7 +62,7 @@ if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentId))
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
hostAgent = agentType.ToUpperInvariant() switch
|
||||
(hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
|
||||
{
|
||||
"INVOICE" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
|
||||
agentType, model, apiKey, "InvoiceAgent",
|
||||
@@ -102,7 +104,7 @@ else
|
||||
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
|
||||
}
|
||||
|
||||
app.MapA2A(hostAgent!.TaskManager!, "/");
|
||||
app.MapWellKnownAgentCard(hostAgent!.TaskManager!, "/");
|
||||
var a2aTaskManager = app.MapA2A(hostA2AAgent, path: "/", agentCard: hostA2AAgentCard);
|
||||
app.MapWellKnownAgentCard(a2aTaskManager, "/");
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -5,7 +5,6 @@ using AgentWebChat.AgentHost;
|
||||
using AgentWebChat.AgentHost.Utilities;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.AspNetCore;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Host which will attach an <see cref="AIAgent"/> to a <see cref="ITaskManager"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation only handles:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>TaskManager.OnMessageReceived</description></item>
|
||||
/// <item><description>TaskManager.OnAgentCardQuery</description></item>
|
||||
/// </list>
|
||||
/// Support for task management will be added later as part of the long-running task execution work.
|
||||
/// </remarks>
|
||||
public sealed class A2AHostAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="A2AHostAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agent">The <see cref="AIAgent"/> to host.</param>
|
||||
/// <param name="agentCard">The <see cref="AgentCard"/> for the hosted agent.</param>
|
||||
/// <param name="taskManager">The <see cref="ITaskManager"/> for handling agent tasks.</param>
|
||||
public A2AHostAgent(AIAgent agent, AgentCard agentCard, TaskManager? taskManager = null)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
Throw.IfNull(agentCard);
|
||||
|
||||
this.Agent = agent;
|
||||
this._agentCard = agentCard;
|
||||
|
||||
this.Attach(taskManager ?? new TaskManager());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
public AIAgent? Agent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated <see cref="ITaskManager"/> for handling agent tasks.
|
||||
/// </summary>
|
||||
public TaskManager? TaskManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the <see cref="A2AAgent"/> to the provided <see cref="ITaskManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="taskManager">The <see cref="ITaskManager"/> to attach to.</param>
|
||||
public void Attach(TaskManager taskManager)
|
||||
{
|
||||
Throw.IfNull(taskManager);
|
||||
|
||||
this.TaskManager = taskManager;
|
||||
taskManager.OnMessageReceived = this.OnMessageReceivedAsync;
|
||||
taskManager.OnAgentCardQuery = this.GetAgentCardAsync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a received message.
|
||||
/// </summary>
|
||||
/// <param name="messageSend">The <see cref="MessageSendParams"/> to handle.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public async Task<A2AResponse> OnMessageReceivedAsync(MessageSendParams messageSend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messageSend);
|
||||
Throw.IfNull(this.Agent);
|
||||
|
||||
if (this.TaskManager is null)
|
||||
{
|
||||
throw new InvalidOperationException("TaskManager must be attached before handling an agent message.");
|
||||
}
|
||||
|
||||
// Get message from the user
|
||||
var userMessage = messageSend.Message.ToChatMessage();
|
||||
|
||||
// Get the response from the agent
|
||||
var message = new AgentMessage();
|
||||
var agentResponse = await this.Agent.RunAsync(userMessage, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
foreach (var chatMessage in agentResponse.Messages)
|
||||
{
|
||||
var content = chatMessage.Text;
|
||||
message.Parts.Add(new TextPart() { Text = content! });
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AgentCard"/> associated with this hosted agent.
|
||||
/// </summary>
|
||||
/// <param name="agentUrl">Current URL for the agent.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Ensure the URL is in the correct format
|
||||
Uri uri = new(agentUrl);
|
||||
agentUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}/";
|
||||
|
||||
this._agentCard.Url = agentUrl;
|
||||
return Task.FromResult(this._agentCard);
|
||||
}
|
||||
|
||||
#region private
|
||||
private readonly AgentCard _agentCard;
|
||||
#endregion
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A;
|
||||
using A2A.AspNetCore;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.A2A;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring A2A (Agent2Agent) communication in a host application builder.
|
||||
/// </summary>
|
||||
public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</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>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
|
||||
{
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</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>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <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>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
|
||||
{
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, agentCard);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
|
||||
{
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var taskManager = agent.MapA2A(loggerFactory: loggerFactory);
|
||||
return endpoints.MapA2A(taskManager, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">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>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <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>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
|
||||
{
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory);
|
||||
return endpoints.MapA2A(taskManager, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps HTTP A2A communication endpoints to the specified path using the provided TaskManager.
|
||||
/// TaskManager should be preconfigured before calling this method.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</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>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, 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.
|
||||
// see https://github.com/microsoft/agent-framework/issues/476 for details
|
||||
A2ARouteBuilderExtensions.MapA2A(endpoints, taskManager, path);
|
||||
endpoints.MapHttpA2A(taskManager, path);
|
||||
|
||||
return taskManager;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A;
|
||||
using A2A.AspNetCore;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
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.
|
||||
/// </summary>
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="app">The web application 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)
|
||||
{
|
||||
var agent = app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
var loggerFactory = app.Services.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.
|
||||
/// </summary>
|
||||
/// <param name="app">The web application 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,
|
||||
string agentName,
|
||||
string path,
|
||||
AgentCard agentCard)
|
||||
{
|
||||
var agent = app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
|
||||
|
||||
var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory);
|
||||
app.MapA2A(taskManager, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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="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)
|
||||
{
|
||||
// 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.
|
||||
// see https://github.com/microsoft/agent-framework/issues/476 for details
|
||||
A2ARouteBuilderExtensions.MapA2A(app, taskManager, path);
|
||||
|
||||
app.MapHttpA2A(taskManager, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Hosting.Local;
|
||||
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
|
||||
{
|
||||
/// <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 IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return services.AddAIAgent(name, (sp, key) =>
|
||||
{
|
||||
var chatClient = sp.GetRequiredService<IChatClient>();
|
||||
return new ChatClientAgent(chatClient, instructions, key);
|
||||
});
|
||||
}
|
||||
|
||||
/// <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 IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return services.AddAIAgent(name, (sp, key) => new ChatClientAgent(chatClient, instructions, key));
|
||||
}
|
||||
|
||||
/// <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 IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return services.AddAIAgent(name, (sp, key) =>
|
||||
{
|
||||
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
|
||||
return new ChatClientAgent(chatClient, instructions, key);
|
||||
});
|
||||
}
|
||||
|
||||
/// <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 IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return 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);
|
||||
});
|
||||
}
|
||||
|
||||
/// <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 IHostedAgentBuilder 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 new HostedAgentBuilder(name, 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>();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Hosting.Local;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
@@ -26,8 +23,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return builder.AddAIAgent(name, instructions, chatClientServiceKey: null);
|
||||
return builder.Services.AddAIAgent(name, instructions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +39,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return builder.AddAIAgent(name, (sp, key) => new ChatClientAgent(chatClient, instructions, key));
|
||||
return builder.Services.AddAIAgent(name, instructions, chatClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,11 +56,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return builder.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 builder.Services.AddAIAgent(name, instructions, description, chatClientServiceKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,12 +71,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
return builder.AddAIAgent(name, (sp, key) =>
|
||||
{
|
||||
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
|
||||
return new ChatClientAgent(chatClient, instructions, key);
|
||||
});
|
||||
return builder.Services.AddAIAgent(name, instructions, chatClientServiceKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -99,44 +86,6 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(name);
|
||||
Throw.IfNull(createAgentDelegate);
|
||||
builder.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(builder);
|
||||
agentHostBuilder.AgentNames.Add(name);
|
||||
|
||||
return new HostedAgentBuilder(name, builder);
|
||||
}
|
||||
|
||||
private static LocalAgentRegistry GetAgentRegistry(IHostApplicationBuilder builder)
|
||||
{
|
||||
var descriptor = builder.Services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalAgentRegistry)));
|
||||
if (descriptor?.ImplementationInstance is not LocalAgentRegistry instance)
|
||||
{
|
||||
instance = new LocalAgentRegistry();
|
||||
ConfigureHostBuilder(builder, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static void ConfigureHostBuilder(IHostApplicationBuilder builder, LocalAgentRegistry agentHostBuilderContext)
|
||||
{
|
||||
builder.Services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext));
|
||||
builder.Services.AddSingleton<AgentCatalog, LocalAgentCatalog>();
|
||||
return builder.Services.AddAIAgent(name, createAgentDelegate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
@@ -7,11 +8,16 @@ namespace Microsoft.Agents.AI.Hosting;
|
||||
internal sealed class HostedAgentBuilder : IHostedAgentBuilder
|
||||
{
|
||||
public string Name { get; }
|
||||
public IHostApplicationBuilder HostApplicationBuilder { get; }
|
||||
public IServiceCollection ServiceCollection { get; }
|
||||
|
||||
public HostedAgentBuilder(string name, IHostApplicationBuilder hostApplicationBuilder)
|
||||
public HostedAgentBuilder(string name, IHostApplicationBuilder builder)
|
||||
: this(name, builder.Services)
|
||||
{
|
||||
}
|
||||
|
||||
public HostedAgentBuilder(string name, IServiceCollection serviceCollection)
|
||||
{
|
||||
this.Name = name;
|
||||
this.HostApplicationBuilder = hostApplicationBuilder;
|
||||
this.ServiceCollection = serviceCollection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
@@ -15,7 +15,7 @@ public interface IHostedAgentBuilder
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application host builder for configuring additional services.
|
||||
/// Gets the service collection for configuration.
|
||||
/// </summary>
|
||||
IHostApplicationBuilder HostApplicationBuilder { get; }
|
||||
IServiceCollection ServiceCollection { get; }
|
||||
}
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.UnitTests;
|
||||
|
||||
public class AgentHostingServiceCollectionExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that providing a null builder to AddAIAgent throws an ArgumentNullException.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_NullBuilder_ThrowsArgumentNullException() => Assert.Throws<ArgumentNullException>(
|
||||
() => AgentHostingServiceCollectionExtensions.AddAIAgent(null!, "agent", "instructions"));
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent without chat client key throws ArgumentNullException for null name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent(null!, "instructions"));
|
||||
Assert.Equal("name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent without chat client key allows null instructions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_NullInstructions_AllowsNull()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var result = services.AddAIAgent("agentName", (string)null!);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with chat client key throws ArgumentNullException for null name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgentWithKey_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent(null!, "instructions", "key"));
|
||||
Assert.Equal("name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with chat client key allows null instructions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgentWithKey_NullInstructions_AllowsNull()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var result = services.AddAIAgent("agentName", null!, "key");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null builder.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgentWithFactory_NullBuilder_ThrowsArgumentNullException() =>
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
AgentHostingServiceCollectionExtensions.AddAIAgent(null!, "agentName", (sp, key) => new Mock<AIAgent>().Object));
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgentWithFactory_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent(null!, (sp, key) => new Mock<AIAgent>().Object));
|
||||
Assert.Equal("name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null factory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgentWithFactory_NullFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent("agentName", (Func<IServiceProvider, string, AIAgent>)null!));
|
||||
Assert.Equal("createAgentDelegate", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with factory delegate returns the same builder instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgentWithFactory_ValidParameters_ReturnsBuilder()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
var result = services.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent registers the agent as a keyed singleton service.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_RegistersKeyedSingleton()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
const string AgentName = "testAgent";
|
||||
|
||||
services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object);
|
||||
|
||||
var descriptor = services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == AgentName &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent can be called multiple times with different agent names.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_MultipleCalls_RegistersMultipleAgents()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddAIAgent("agent1", "instructions1");
|
||||
services.AddAIAgent("agent2", "instructions2");
|
||||
services.AddAIAgent("agent3", "instructions3");
|
||||
|
||||
var agentDescriptors = services
|
||||
.Where(d => d.ServiceType == typeof(AIAgent) && d.ServiceKey is string)
|
||||
.ToList();
|
||||
|
||||
Assert.Equal(3, agentDescriptors.Count);
|
||||
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent1");
|
||||
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent2");
|
||||
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent handles empty strings for name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_EmptyName_ThrowsArgumentException()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
Assert.Throws<ArgumentException>(() => services.AddAIAgent("", "instructions"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent allows empty strings for instructions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_EmptyInstructions_Succeeds()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var result = services.AddAIAgent("agentName", "");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent without chat client key calls the overload with null key.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAIAgent_WithoutKey_CallsOverloadWithNullKey()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
var result = builder.AddAIAgent("agentName", "instructions");
|
||||
|
||||
// The agent should be registered (proving the method chain worked)
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => d.ServiceKey is "agentName" &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
Assert.NotNull(descriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with special characters in name works correctly for valid names.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("agent_name")] // underscore is allowed
|
||||
[InlineData("Agent123")] // alphanumeric is allowed
|
||||
[InlineData("_agent")] // can start with underscore
|
||||
[InlineData("agent-name")] // dash is allowed
|
||||
[InlineData("agent.name")] // period is allowed
|
||||
[InlineData("agent:type")] // colon is allowed
|
||||
[InlineData("my.agent_1:type-name")] // complex valid name
|
||||
public void AddAIAgent_ValidSpecialCharactersInName_Succeeds(string name)
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
var result = builder.AddAIAgent(name, "instructions");
|
||||
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == name &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
Assert.NotNull(descriptor);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user