diff --git a/src/Agents/AddAIAgentsExtensions.cs b/src/Agents/AddAIAgentsExtensions.cs index 74d2e48..6b5e6b6 100644 --- a/src/Agents/AddAIAgentsExtensions.cs +++ b/src/Agents/AddAIAgentsExtensions.cs @@ -22,7 +22,8 @@ public static class AddAIAgentsExtensions /// 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") + public static TBuilder AddAIAgents(this TBuilder builder, Action? configurePipeline = default, Action? configureOptions = default, string prefix = "ai:agents") + where TBuilder : IHostApplicationBuilder { builder.AddChatClients(); diff --git a/src/Agents/Agents.csproj b/src/Agents/Agents.csproj index ae66455..559793e 100644 --- a/src/Agents/Agents.csproj +++ b/src/Agents/Agents.csproj @@ -11,6 +11,10 @@ OSMFEULA.txt true true + true + + + $(NoWarn);CS0436;SYSLIB1100;SYSLIB1101 diff --git a/src/Agents/ConfigurableAIAgent.cs b/src/Agents/ConfigurableAIAgent.cs index b0fe1c5..ea08ee6 100644 --- a/src/Agents/ConfigurableAIAgent.cs +++ b/src/Agents/ConfigurableAIAgent.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +using System.ComponentModel; +using System.Text.Json; +using Devlooped.Extensions.AI; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; @@ -80,10 +82,13 @@ public override IAsyncEnumerable RunStreamingAsync(IEnum if (configuration[$"{section}:name"] is { } newname && newname != name) throw new InvalidOperationException($"The name of a configured agent cannot be changed at runtime. Expected '{name}' but was '{newname}'."); - var client = services.GetRequiredKeyedService(options?.Client - ?? throw new InvalidOperationException($"A client must be specified for agent '{name}' in configuration section '{section}'.")); + var client = services.GetKeyedService(options?.Client + ?? throw new InvalidOperationException($"A client must be specified for agent '{name}' in configuration section '{section}'.")) + ?? throw new InvalidOperationException($"Specified chat client '{options?.Client}' for agent '{name}' is not registered."); - var chat = configSection.GetSection("options").Get(); +#pragma warning disable SYSLIB1100 + var chat = configSection.GetSection("options").Get(); +#pragma warning restore SYSLIB1100 if (chat is not null) options.ChatOptions = chat; @@ -124,8 +129,8 @@ void OnReload(object? state) [LoggerMessage(LogLevel.Information, "AIAgent '{Id}' configured.")] private partial void LogConfigured(string id); - class AgentClientOptions : ChatClientAgentOptions + internal class AgentClientOptions : ChatClientAgentOptions { - public required string Client { get; set; } + public string? Client { get; set; } } -} +} \ No newline at end of file diff --git a/src/Extensions/AddChatClientsExtensions.cs b/src/Extensions/AddChatClientsExtensions.cs index dd06c4b..b25705f 100644 --- a/src/Extensions/AddChatClientsExtensions.cs +++ b/src/Extensions/AddChatClientsExtensions.cs @@ -73,7 +73,7 @@ public static IServiceCollection AddChatClients(this IServiceCollection services return services; } - class ChatClientOptions : OpenAIClientOptions + internal class ChatClientOptions : OpenAIClientOptions { public string? ApiKey { get; set; } public string? ModelId { get; set; } diff --git a/src/Extensions/ChatExtensions.cs b/src/Extensions/ChatExtensions.cs index ae2f987..97b58d0 100644 --- a/src/Extensions/ChatExtensions.cs +++ b/src/Extensions/ChatExtensions.cs @@ -64,4 +64,24 @@ public Verbosity? Verbosity } } } +} + +// Workaround to get the config binder to set these extension properties. +/// +/// Defines extended we provide via extension properties. +/// +/// This should ideally even be auto-generated from the available extensions so it's always in sync. +[EditorBrowsable(EditorBrowsableState.Never)] +public class ExtendedChatOptions : ChatOptions +{ + public ReasoningEffort? ReasoningEffort + { + get => ((ChatOptions)this).ReasoningEffort; + set => ((ChatOptions)this).ReasoningEffort = value; + } + public Verbosity? Verbosity + { + get => ((ChatOptions)this).Verbosity; + set => ((ChatOptions)this).Verbosity = value; + } } \ No newline at end of file diff --git a/src/Extensions/ConfigurableChatClient.cs b/src/Extensions/ConfigurableChatClient.cs index c1d59d3..a483190 100644 --- a/src/Extensions/ConfigurableChatClient.cs +++ b/src/Extensions/ConfigurableChatClient.cs @@ -119,19 +119,19 @@ void OnReload(object? state) [LoggerMessage(LogLevel.Information, "ChatClient '{Id}' configured.")] private partial void LogConfigured(string id); - class ConfigurableClientOptions : OpenAIClientOptions + internal class ConfigurableClientOptions : OpenAIClientOptions { public string? ApiKey { get; set; } public string? ModelId { get; set; } } - class ConfigurableInferenceOptions : AzureAIInferenceClientOptions + internal class ConfigurableInferenceOptions : AzureAIInferenceClientOptions { public string? ApiKey { get; set; } public string? ModelId { get; set; } } - class ConfigurableAzureOptions : AzureOpenAIClientOptions + internal class ConfigurableAzureOptions : AzureOpenAIClientOptions { public string? ApiKey { get; set; } public string? ModelId { get; set; } diff --git a/src/Extensions/Extensions.csproj b/src/Extensions/Extensions.csproj index 89edffc..7be9277 100644 --- a/src/Extensions/Extensions.csproj +++ b/src/Extensions/Extensions.csproj @@ -3,7 +3,6 @@ net8.0;net9.0;net10.0 Preview - $(NoWarn);OPENAI001 Devlooped.Extensions.AI $(AssemblyName) $(AssemblyName) @@ -12,6 +11,11 @@ OSMFEULA.txt true true + true + + + + $(NoWarn);OPENAI001;AOAI001;SYSLIB1100;SYSLIB1101 @@ -36,6 +40,7 @@ + diff --git a/src/Tests/ConfigurableAgentTests.cs b/src/Tests/ConfigurableAgentTests.cs index 392a259..3f5f4ee 100644 --- a/src/Tests/ConfigurableAgentTests.cs +++ b/src/Tests/ConfigurableAgentTests.cs @@ -189,5 +189,27 @@ public void AssignsMessageStoreFactoryFromService() Assert.NotNull(options?.ChatMessageStoreFactory); Assert.Same(context, options?.ChatMessageStoreFactory?.Invoke(new())); } + + [Fact] + public void CanSetOpenAIReasoningAndVerbosity() + { + var builder = new HostApplicationBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["ai:clients:openai:modelid"] = "gpt-4.1", + ["ai:clients:openai:apikey"] = "sk-asdfasdf", + ["ai:agents:bot:client"] = "openai", + ["ai:agents:bot:options:reasoningeffort"] = "minimal", + ["ai:agents:bot:options:verbosity"] = "low", + }); + + var app = builder.AddAIAgents().Build(); + var agent = app.Services.GetRequiredKeyedService("bot"); + var options = agent.GetService(); + + Assert.Equal(Verbosity.Low, options?.ChatOptions?.Verbosity); + Assert.Equal(ReasoningEffort.Minimal, options?.ChatOptions?.ReasoningEffort); + } }