diff --git a/src/Agents/AIContextProviderFactory.cs b/src/Agents/AIContextProviderFactory.cs
new file mode 100644
index 0000000..c70de44
--- /dev/null
+++ b/src/Agents/AIContextProviderFactory.cs
@@ -0,0 +1,24 @@
+using Microsoft.Agents.AI;
+using static Microsoft.Agents.AI.ChatClientAgentOptions;
+
+namespace Devlooped.Agents.AI;
+
+///
+/// An implementation of an factory as a class that can provide
+/// the functionality to and integrates
+/// more easily into a service collection.
+///
+///
+/// The is a key extensibility point in Microsoft.Agents.AI, allowing
+/// augmentation of instructions, messages and tools before agent execution is performed.
+///
+public abstract class AIContextProviderFactory
+{
+ ///
+ /// Provides the implementation of ,
+ /// which is invoked whenever agent threads are created or rehydrated.
+ ///
+ /// The context to potentially hydrate state from.
+ /// The context provider that will enhance interactions with an agent.
+ public abstract AIContextProvider CreateProvider(AIContextProviderFactoryContext context);
+}
diff --git a/src/Agents/AddAIAgentsExtensions.cs b/src/Agents/AddAIAgentsExtensions.cs
index 89c2f95..74d2e48 100644
--- a/src/Agents/AddAIAgentsExtensions.cs
+++ b/src/Agents/AddAIAgentsExtensions.cs
@@ -1,4 +1,5 @@
-using Devlooped.Extensions.AI;
+using System.ComponentModel;
+using Devlooped.Extensions.AI;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Extensions.Configuration;
@@ -7,8 +8,20 @@
namespace Devlooped.Agents.AI;
+///
+/// Adds configuration-driven agents to an application host.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
public static class AddAIAgentsExtensions
{
+ ///
+ /// Adds AI agents to the host application builder based on configuration.
+ ///
+ /// The host application builder.
+ /// Optional action to configure the pipeline for each agent.
+ /// Optional action to configure options for each agent.
+ /// The configuration prefix for agents, defaults to "ai:agents".
+ /// The host application builder with AI agents added.
public static IHostApplicationBuilder AddAIAgents(this IHostApplicationBuilder builder, Action? configurePipeline = default, Action? configureOptions = default, string prefix = "ai:agents")
{
builder.AddChatClients();
diff --git a/src/Agents/ConfigurableAIAgent.cs b/src/Agents/ConfigurableAIAgent.cs
index 6b69c35..eaa9f34 100644
--- a/src/Agents/ConfigurableAIAgent.cs
+++ b/src/Agents/ConfigurableAIAgent.cs
@@ -7,6 +7,10 @@
namespace Devlooped.Agents.AI;
+///
+/// A configuration-driven which monitors configuration changes and
+/// re-applies them to the inner agent automatically.
+///
public sealed partial class ConfigurableAIAgent : AIAgent, IDisposable
{
readonly IServiceProvider services;
@@ -36,8 +40,10 @@ public ConfigurableAIAgent(IServiceProvider services, string section, string nam
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
}
+ /// Disposes the client and stops monitoring configuration changes.
public void Dispose() => reloadToken?.Dispose();
+ ///
public override object? GetService(Type serviceType, object? serviceKey = null) => serviceType switch
{
Type t when t == typeof(ChatClientAgentOptions) => options,
@@ -45,15 +51,23 @@ public ConfigurableAIAgent(IServiceProvider services, string section, string nam
_ => agent.GetService(serviceType, serviceKey)
};
+ ///
public override string Id => agent.Id;
+ ///
public override string? Description => agent.Description;
+ ///
public override string DisplayName => agent.DisplayName;
- public override string? Name => agent.Name;
+ ///
+ public override string? Name => this.name;
+ ///
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> agent.DeserializeThread(serializedThread, jsonSerializerOptions);
+ ///
public override AgentThread GetNewThread() => agent.GetNewThread();
+ ///
public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> agent.RunAsync(messages, thread, options, cancellationToken);
+ ///
public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> agent.RunStreamingAsync(messages, thread, options, cancellationToken);
@@ -75,6 +89,15 @@ public override IAsyncEnumerable RunStreamingAsync(IEnum
configure?.Invoke(name, options);
+ if (options.AIContextProviderFactory is null)
+ {
+ var contextFactory = services.GetKeyedService(name) ??
+ services.GetService();
+
+ if (contextFactory is not null)
+ options.AIContextProviderFactory = contextFactory.CreateProvider;
+ }
+
LogConfigured(name);
return (new ChatClientAgent(client, options, services.GetRequiredService(), services), options, client);
diff --git a/src/Extensions/AddChatClientsExtensions.cs b/src/Extensions/AddChatClientsExtensions.cs
index 58ffb77..dd06c4b 100644
--- a/src/Extensions/AddChatClientsExtensions.cs
+++ b/src/Extensions/AddChatClientsExtensions.cs
@@ -1,4 +1,5 @@
-using Devlooped.Extensions.AI.OpenAI;
+using System.ComponentModel;
+using Devlooped.Extensions.AI.OpenAI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -9,14 +10,35 @@
namespace Devlooped.Extensions.AI;
+///
+/// Adds configuration-driven chat clients to an application host or service collection.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
public static class AddChatClientsExtensions
{
+ ///
+ /// Adds configuration-driven chat clients to the host application builder.
+ ///
+ /// The host application builder.
+ /// Optional action to configure the pipeline for each client.
+ /// Optional action to configure each client.
+ /// The configuration prefix for clients. Defaults to "ai:clients".
+ /// The host application builder.
public static IHostApplicationBuilder AddChatClients(this IHostApplicationBuilder builder, Action? configurePipeline = default, Action? configureClient = default, string prefix = "ai:clients")
{
AddChatClients(builder.Services, builder.Configuration, configurePipeline, configureClient, prefix);
return builder;
}
+ ///
+ /// Adds configuration-driven chat clients to the service collection.
+ ///
+ /// The service collection.
+ /// The configuration.
+ /// Optional action to configure the pipeline for each client.
+ /// Optional action to configure each client.
+ /// The configuration prefix for clients. Defaults to "ai:clients".
+ /// The service collection.
public static IServiceCollection AddChatClients(this IServiceCollection services, IConfiguration configuration, Action? configurePipeline = default, Action? configureClient = default, string prefix = "ai:clients")
{
foreach (var entry in configuration.AsEnumerable().Where(x =>
diff --git a/src/Extensions/ConfigurableChatClient.cs b/src/Extensions/ConfigurableChatClient.cs
index 32b7994..c1d59d3 100644
--- a/src/Extensions/ConfigurableChatClient.cs
+++ b/src/Extensions/ConfigurableChatClient.cs
@@ -10,6 +10,10 @@
namespace Devlooped.Extensions.AI;
+///
+/// A configuration-driven which monitors configuration changes and
+/// re-applies them to the inner client automatically.
+///
public sealed partial class ConfigurableChatClient : IDisposable, IChatClient
{
readonly IConfiguration configuration;
@@ -20,6 +24,15 @@ public sealed partial class ConfigurableChatClient : IDisposable, IChatClient
IDisposable reloadToken;
IChatClient innerClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration to read settings from.
+ /// The logger to use for logging.
+ /// The configuration section to use.
+ /// The unique identifier for the client.
+ /// An optional action to configure the client after creation.
public ConfigurableChatClient(IConfiguration configuration, ILogger logger, string section, string id, Action? configure)
{
if (section.Contains('.'))
@@ -35,6 +48,7 @@ public ConfigurableChatClient(IConfiguration configuration, ILogger logger, stri
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
}
+ /// Disposes the client and stops monitoring configuration changes.
public void Dispose() => reloadToken?.Dispose();
///
diff --git a/src/Tests/ConfigurableAgentTests.cs b/src/Tests/ConfigurableAgentTests.cs
index 9bebe1e..66784c8 100644
--- a/src/Tests/ConfigurableAgentTests.cs
+++ b/src/Tests/ConfigurableAgentTests.cs
@@ -4,6 +4,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Moq;
namespace Devlooped.Agents.AI;
@@ -76,5 +77,62 @@ public void CanReloadConfiguration()
Assert.Equal("You are a very helpful chat agent.", agent.GetService()?.Instructions);
Assert.Equal("xai", agent.GetService()?.ProviderName);
}
+
+ [Fact]
+ public void AssignsContextProviderFromKeyedService()
+ {
+ var builder = new HostApplicationBuilder();
+ var context = Mock.Of();
+
+ builder.Services.AddKeyedSingleton("bot",
+ Mock.Of(x
+ => x.CreateProvider(It.IsAny()) == context));
+
+ builder.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
+ ["ai:clients:chat:apikey"] = "sk-asdfasdf",
+ ["ai:agents:bot:client"] = "chat",
+ ["ai:agents:bot:options:temperature"] = "0.5",
+ });
+
+ builder.AddAIAgents();
+
+ var app = builder.Build();
+ var agent = app.Services.GetRequiredKeyedService("bot");
+ var options = agent.GetService();
+
+ Assert.NotNull(options?.AIContextProviderFactory);
+ Assert.Same(context, options?.AIContextProviderFactory?.Invoke(new ChatClientAgentOptions.AIContextProviderFactoryContext()));
+ }
+
+ [Fact]
+ public void AssignsContextProviderFromService()
+ {
+ var builder = new HostApplicationBuilder();
+ var context = Mock.Of();
+
+ builder.Services.AddSingleton(
+ Mock.Of(x
+ => x.CreateProvider(It.IsAny()) == context));
+
+ builder.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
+ ["ai:clients:chat:apikey"] = "sk-asdfasdf",
+ ["ai:agents:bot:client"] = "chat",
+ ["ai:agents:bot:options:temperature"] = "0.5",
+ });
+
+ builder.AddAIAgents();
+
+ var app = builder.Build();
+ var agent = app.Services.GetRequiredKeyedService("bot");
+ var options = agent.GetService();
+
+ Assert.NotNull(options?.AIContextProviderFactory);
+ Assert.Same(context, options?.AIContextProviderFactory?.Invoke(new ChatClientAgentOptions.AIContextProviderFactoryContext()));
+ }
+
}
diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj
index 225c767..0ba67c8 100644
--- a/src/Tests/Tests.csproj
+++ b/src/Tests/Tests.csproj
@@ -9,6 +9,7 @@
+