From fdc7110003bbb3575708a0e7da0a1ce45f492fd5 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Tue, 14 Oct 2025 16:59:40 -0300 Subject: [PATCH] Add support for setting reasoning effort and verbosity via config We already support those properties via extensions in our Extensions.AI project, but agent was not setting them properly. This is otherwise quite cumbersome to set (see https://github.com/microsoft/agent-framework/issues/1455). We also turn on configuration binder source generation and review the emitted warnings for properties that wouldn't be deserialized (since they can't be easily mapped) and ignore the warnings/errors accordingly. We might want to offer custom converters for them in the future. --- src/Agents/AddAIAgentsExtensions.cs | 3 ++- src/Agents/Agents.csproj | 4 ++++ src/Agents/ConfigurableAIAgent.cs | 19 ++++++++++++------- src/Extensions/AddChatClientsExtensions.cs | 2 +- src/Extensions/ChatExtensions.cs | 20 ++++++++++++++++++++ src/Extensions/ConfigurableChatClient.cs | 6 +++--- src/Extensions/Extensions.csproj | 7 ++++++- src/Tests/ConfigurableAgentTests.cs | 22 ++++++++++++++++++++++ 8 files changed, 70 insertions(+), 13 deletions(-) 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); + } }