Skip to content

Commit faf6d74

Browse files
committed
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 microsoft/agent-framework#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.
1 parent ca96fe3 commit faf6d74

File tree

8 files changed

+70
-13
lines changed

8 files changed

+70
-13
lines changed

src/Agents/AddAIAgentsExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public static class AddAIAgentsExtensions
2222
/// <param name="configureOptions">Optional action to configure options for each agent.</param>
2323
/// <param name="prefix">The configuration prefix for agents, defaults to "ai:agents".</param>
2424
/// <returns>The host application builder with AI agents added.</returns>
25-
public static IHostApplicationBuilder AddAIAgents(this IHostApplicationBuilder builder, Action<string, AIAgentBuilder>? configurePipeline = default, Action<string, ChatClientAgentOptions>? configureOptions = default, string prefix = "ai:agents")
25+
public static TBuilder AddAIAgents<TBuilder>(this TBuilder builder, Action<string, AIAgentBuilder>? configurePipeline = default, Action<string, ChatClientAgentOptions>? configureOptions = default, string prefix = "ai:agents")
26+
where TBuilder : IHostApplicationBuilder
2627
{
2728
builder.AddChatClients();
2829

src/Agents/Agents.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
<PackageLicenseFile>OSMFEULA.txt</PackageLicenseFile>
1212
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
1313
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
14+
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
15+
<!-- CS0436: IVT Extensions > Agents -->
16+
<!-- SYSLIB1100/SYSLIB1101: not all ChatOptions are supported for binder gen: ResponseFormat, ToolMode, Tools -->
17+
<NoWarn>$(NoWarn);CS0436;SYSLIB1100;SYSLIB1101</NoWarn>
1418
</PropertyGroup>
1519

1620
<ItemGroup>

src/Agents/ConfigurableAIAgent.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Text.Json;
1+
using System.ComponentModel;
2+
using System.Text.Json;
3+
using Devlooped.Extensions.AI;
24
using Microsoft.Agents.AI;
35
using Microsoft.Extensions.AI;
46
using Microsoft.Extensions.Configuration;
@@ -80,10 +82,13 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
8082
if (configuration[$"{section}:name"] is { } newname && newname != name)
8183
throw new InvalidOperationException($"The name of a configured agent cannot be changed at runtime. Expected '{name}' but was '{newname}'.");
8284

83-
var client = services.GetRequiredKeyedService<IChatClient>(options?.Client
84-
?? throw new InvalidOperationException($"A client must be specified for agent '{name}' in configuration section '{section}'."));
85+
var client = services.GetKeyedService<IChatClient>(options?.Client
86+
?? throw new InvalidOperationException($"A client must be specified for agent '{name}' in configuration section '{section}'."))
87+
?? throw new InvalidOperationException($"Specified chat client '{options?.Client}' for agent '{name}' is not registered.");
8588

86-
var chat = configSection.GetSection("options").Get<ChatOptions>();
89+
#pragma warning disable SYSLIB1100
90+
var chat = configSection.GetSection("options").Get<ExtendedChatOptions>();
91+
#pragma warning restore SYSLIB1100
8792
if (chat is not null)
8893
options.ChatOptions = chat;
8994

@@ -124,8 +129,8 @@ void OnReload(object? state)
124129
[LoggerMessage(LogLevel.Information, "AIAgent '{Id}' configured.")]
125130
private partial void LogConfigured(string id);
126131

127-
class AgentClientOptions : ChatClientAgentOptions
132+
internal class AgentClientOptions : ChatClientAgentOptions
128133
{
129-
public required string Client { get; set; }
134+
public string? Client { get; set; }
130135
}
131-
}
136+
}

src/Extensions/AddChatClientsExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static IServiceCollection AddChatClients(this IServiceCollection services
7373
return services;
7474
}
7575

76-
class ChatClientOptions : OpenAIClientOptions
76+
internal class ChatClientOptions : OpenAIClientOptions
7777
{
7878
public string? ApiKey { get; set; }
7979
public string? ModelId { get; set; }

src/Extensions/ChatExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,24 @@ public Verbosity? Verbosity
6464
}
6565
}
6666
}
67+
}
68+
69+
// Workaround to get the config binder to set these extension properties.
70+
/// <summary>
71+
/// Defines extended <see cref="ChatOptions"/> we provide via extension properties.
72+
/// </summary>
73+
/// <devdoc>This should ideally even be auto-generated from the available extensions so it's always in sync.</devdoc>
74+
[EditorBrowsable(EditorBrowsableState.Never)]
75+
public class ExtendedChatOptions : ChatOptions
76+
{
77+
public ReasoningEffort? ReasoningEffort
78+
{
79+
get => ((ChatOptions)this).ReasoningEffort;
80+
set => ((ChatOptions)this).ReasoningEffort = value;
81+
}
82+
public Verbosity? Verbosity
83+
{
84+
get => ((ChatOptions)this).Verbosity;
85+
set => ((ChatOptions)this).Verbosity = value;
86+
}
6787
}

src/Extensions/ConfigurableChatClient.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,19 +119,19 @@ void OnReload(object? state)
119119
[LoggerMessage(LogLevel.Information, "ChatClient '{Id}' configured.")]
120120
private partial void LogConfigured(string id);
121121

122-
class ConfigurableClientOptions : OpenAIClientOptions
122+
internal class ConfigurableClientOptions : OpenAIClientOptions
123123
{
124124
public string? ApiKey { get; set; }
125125
public string? ModelId { get; set; }
126126
}
127127

128-
class ConfigurableInferenceOptions : AzureAIInferenceClientOptions
128+
internal class ConfigurableInferenceOptions : AzureAIInferenceClientOptions
129129
{
130130
public string? ApiKey { get; set; }
131131
public string? ModelId { get; set; }
132132
}
133133

134-
class ConfigurableAzureOptions : AzureOpenAIClientOptions
134+
internal class ConfigurableAzureOptions : AzureOpenAIClientOptions
135135
{
136136
public string? ApiKey { get; set; }
137137
public string? ModelId { get; set; }

src/Extensions/Extensions.csproj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
<PropertyGroup>
44
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
55
<LangVersion>Preview</LangVersion>
6-
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
76
<AssemblyName>Devlooped.Extensions.AI</AssemblyName>
87
<RootNamespace>$(AssemblyName)</RootNamespace>
98
<PackageId>$(AssemblyName)</PackageId>
@@ -12,6 +11,11 @@
1211
<PackageLicenseFile>OSMFEULA.txt</PackageLicenseFile>
1312
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
1413
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
14+
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
15+
<!-- OPENAI001: responses API -->
16+
<!-- AOAI001: Azure.AI.OpenAI.AzureOpenAIClientOptions.DefaultHeaders -->
17+
<!-- SYSLIB1100/SYSLIB1101: not all ChatOptions are supported for binder gen: ResponseFormat, ToolMode, Tools -->
18+
<NoWarn>$(NoWarn);OPENAI001;AOAI001;SYSLIB1100;SYSLIB1101</NoWarn>
1519
</PropertyGroup>
1620

1721
<ItemGroup>
@@ -36,6 +40,7 @@
3640
<None Update="Devlooped.Extensions.AI.targets" PackFolder="build" />
3741
<None Update="Devlooped.Extensions.AI.props" PackFolder="build" />
3842
<None Include="..\..\osmfeula.txt" Link="osmfeula.txt" PackagePath="OSMFEULA.txt" />
43+
<InternalsVisibleTo Include="Devlooped.Agents.AI" />
3944
</ItemGroup>
4045

4146
<Target Name="UpdateSdkPreviewVersion" BeforeTargets="GetPackageContents">

src/Tests/ConfigurableAgentTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,27 @@ public void AssignsMessageStoreFactoryFromService()
189189
Assert.NotNull(options?.ChatMessageStoreFactory);
190190
Assert.Same(context, options?.ChatMessageStoreFactory?.Invoke(new()));
191191
}
192+
193+
[Fact]
194+
public void CanSetOpenAIReasoningAndVerbosity()
195+
{
196+
var builder = new HostApplicationBuilder();
197+
198+
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
199+
{
200+
["ai:clients:openai:modelid"] = "gpt-4.1",
201+
["ai:clients:openai:apikey"] = "sk-asdfasdf",
202+
["ai:agents:bot:client"] = "openai",
203+
["ai:agents:bot:options:reasoningeffort"] = "minimal",
204+
["ai:agents:bot:options:verbosity"] = "low",
205+
});
206+
207+
var app = builder.AddAIAgents().Build();
208+
var agent = app.Services.GetRequiredKeyedService<AIAgent>("bot");
209+
var options = agent.GetService<ChatClientAgentOptions>();
210+
211+
Assert.Equal(Verbosity.Low, options?.ChatOptions?.Verbosity);
212+
Assert.Equal(ReasoningEffort.Minimal, options?.ChatOptions?.ReasoningEffort);
213+
}
192214
}
193215

0 commit comments

Comments
 (0)