Skip to content

Commit 7ea9164

Browse files
committed
Expose configuration metadata from configurable agent/chat
This may be helpful in troubleshooting and telemetry.
1 parent b2ff66d commit 7ea9164

File tree

4 files changed

+99
-14
lines changed

4 files changed

+99
-14
lines changed

src/Agents/ConfigurableAIAgent.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json;
1+
using System.Diagnostics;
2+
using System.Text.Json;
23
using Devlooped.Extensions.AI;
34
using Devlooped.Extensions.AI.Grok;
45
using Microsoft.Agents.AI;
@@ -23,8 +24,9 @@ public sealed partial class ConfigurableAIAgent : AIAgent, IDisposable
2324
readonly Action<string, ChatClientAgentOptions>? configure;
2425
IDisposable reloadToken;
2526
ChatClientAgent agent;
26-
IChatClient chat;
2727
ChatClientAgentOptions options;
28+
IChatClient chat;
29+
AIAgentMetadata metadata;
2830

2931
public ConfigurableAIAgent(IServiceProvider services, string section, string name, Action<string, ChatClientAgentOptions>? configure)
3032
{
@@ -38,7 +40,7 @@ public ConfigurableAIAgent(IServiceProvider services, string section, string nam
3840
this.name = Throw.IfNullOrEmpty(name);
3941
this.configure = configure;
4042

41-
(agent, options, chat) = Configure(configuration.GetRequiredSection(section));
43+
(agent, options, chat, metadata) = Configure(configuration.GetRequiredSection(section));
4244
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
4345
}
4446

@@ -50,6 +52,7 @@ public ConfigurableAIAgent(IServiceProvider services, string section, string nam
5052
{
5153
Type t when t == typeof(ChatClientAgentOptions) => options,
5254
Type t when t == typeof(IChatClient) => chat,
55+
Type t when typeof(AIAgentMetadata).IsAssignableFrom(t) => metadata,
5356
_ => agent.GetService(serviceType, serviceKey)
5457
};
5558

@@ -78,7 +81,7 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
7881
/// </summary>
7982
public ChatClientAgentOptions Options => options;
8083

81-
(ChatClientAgent, ChatClientAgentOptions, IChatClient) Configure(IConfigurationSection configSection)
84+
(ChatClientAgent, ChatClientAgentOptions, IChatClient, AIAgentMetadata) Configure(IConfigurationSection configSection)
8285
{
8386
var options = configSection.Get<AgentClientOptions>();
8487
options?.Name ??= name;
@@ -124,15 +127,18 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
124127

125128
LogConfigured(name);
126129

127-
return (new ChatClientAgent(client, options, services.GetRequiredService<ILoggerFactory>(), services), options, client);
130+
var agent = new ChatClientAgent(client, options, services.GetRequiredService<ILoggerFactory>(), services);
131+
var metadata = agent.GetService<AIAgentMetadata>() ?? new AIAgentMetadata(provider);
132+
133+
return (agent, options, client, new ConfigurableAIAgentMetadata(name, section, metadata.ProviderName));
128134
}
129135

130136
void OnReload(object? state)
131137
{
132138
var configSection = configuration.GetRequiredSection(section);
133139
reloadToken?.Dispose();
134140
chat?.Dispose();
135-
(agent, options, chat) = Configure(configSection);
141+
(agent, options, chat, metadata) = Configure(configSection);
136142
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
137143
}
138144

@@ -143,4 +149,15 @@ internal class AgentClientOptions : ChatClientAgentOptions
143149
{
144150
public string? Client { get; set; }
145151
}
152+
}
153+
154+
/// <summary>Metadata for a <see cref="ConfigurableAIAgent"/>.</summary>
155+
156+
[DebuggerDisplay("Name = {Name}, Section = {ConfigurationSection}, ProviderName = {ProviderName}")]
157+
public class ConfigurableAIAgentMetadata(string name, string configurationSection, string? providerName) : AIAgentMetadata(providerName)
158+
{
159+
/// <summary>Name of the agent.</summary>
160+
public string Name => name;
161+
/// <summary>Configuration section where the agent is defined.</summary>
162+
public string ConfigurationSection = configurationSection;
146163
}

src/Extensions/ConfigurableChatClient.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.ClientModel.Primitives;
1+
using System;
2+
using System.ClientModel.Primitives;
23
using System.ComponentModel;
34
using Azure;
45
using Azure.AI.Inference;
@@ -25,6 +26,7 @@ public sealed partial class ConfigurableChatClient : IChatClient, IDisposable
2526
readonly Action<string, IChatClient>? configure;
2627
IDisposable reloadToken;
2728
IChatClient innerClient;
29+
ChatClientMetadata metadata;
2830
object? options;
2931

3032
/// <summary>
@@ -46,28 +48,33 @@ public ConfigurableChatClient(IConfiguration configuration, ILogger logger, stri
4648
this.id = Throw.IfNullOrEmpty(id);
4749
this.configure = configure;
4850

49-
innerClient = Configure(configuration.GetRequiredSection(section));
51+
(innerClient, metadata) = Configure(configuration.GetRequiredSection(section));
5052
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
5153
}
5254

5355
/// <summary>Disposes the client and stops monitoring configuration changes.</summary>
5456
public void Dispose() => reloadToken?.Dispose();
5557

58+
/// <inheritdoc/>
59+
public object? GetService(Type serviceType, object? serviceKey = null) => serviceType switch
60+
{
61+
Type t when typeof(ChatClientMetadata).IsAssignableFrom(t) => metadata,
62+
Type t when t == typeof(IChatClient) => this,
63+
_ => innerClient.GetService(serviceType, serviceKey)
64+
};
65+
5666
/// <inheritdoc/>
5767
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
5868
=> innerClient.GetResponseAsync(messages, options, cancellationToken);
5969
/// <inheritdoc/>
6070
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
6171
=> innerClient.GetStreamingResponseAsync(messages, options, cancellationToken);
62-
/// <inheritdoc/>
63-
public object? GetService(Type serviceType, object? serviceKey = null)
64-
=> innerClient.GetService(serviceType, serviceKey);
6572

6673
/// <summary>Exposes the optional <see cref="ClientPipelineOptions"/> configured for the client.</summary>
6774
[EditorBrowsable(EditorBrowsableState.Never)]
6875
public object? Options => options;
6976

70-
IChatClient Configure(IConfigurationSection configSection)
77+
(IChatClient, ChatClientMetadata) Configure(IConfigurationSection configSection)
7178
{
7279
var options = SetOptions<ConfigurableClientOptions>(configSection);
7380
Throw.IfNullOrEmpty(options?.ModelId, $"{configSection}:modelid");
@@ -107,7 +114,9 @@ IChatClient Configure(IConfigurationSection configSection)
107114

108115
LogConfigured(id);
109116

110-
return client;
117+
var metadata = client.GetService<ChatClientMetadata>() ?? new ChatClientMetadata(null, null, null);
118+
119+
return (client, new ConfigurableChatClientMetadata(id, section, metadata.ProviderName, metadata.ProviderUri, metadata.DefaultModelId));
111120
}
112121

113122
TOptions? SetOptions<TOptions>(IConfigurationSection section) where TOptions : class
@@ -133,7 +142,7 @@ void OnReload(object? state)
133142
(innerClient as IDisposable)?.Dispose();
134143
reloadToken?.Dispose();
135144

136-
innerClient = Configure(configSection);
145+
(innerClient, metadata) = Configure(configSection);
137146

138147
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
139148
}
@@ -158,4 +167,14 @@ internal class ConfigurableAzureOptions : AzureOpenAIClientOptions
158167
public string? ApiKey { get; set; }
159168
public string? ModelId { get; set; }
160169
}
170+
}
171+
172+
/// <summary>Metadata for a <see cref="ConfigurableChatClient"/>.</summary>
173+
public class ConfigurableChatClientMetadata(string id, string configurationSection, string? providerName, Uri? providerUri, string? defaultModelId)
174+
: ChatClientMetadata(providerName, providerUri, defaultModelId)
175+
{
176+
/// <summary>The unique identifier of the configurable client.</summary>
177+
public string Id => id;
178+
/// <summary>The configuration section used to configure the client.</summary>
179+
public string ConfigurationSection => configurationSection;
161180
}

src/Tests/ConfigurableAgentTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,30 @@ public void CanGetFromAlternativeKey()
6161
Assert.Same(agent, app.Services.GetIAAgent("Bot"));
6262
}
6363

64+
[Fact]
65+
public void CanGetSectionAndIdFromMetadata()
66+
{
67+
var builder = new HostApplicationBuilder();
68+
69+
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
70+
{
71+
["ai:clients:chat:modelid"] = "gpt-4.1-nano",
72+
["ai:clients:chat:apikey"] = "sk-asdfasdf",
73+
["ai:agents:bot:client"] = "chat",
74+
});
75+
76+
builder.AddAIAgents();
77+
78+
var app = builder.Build();
79+
80+
var agent = app.Services.GetRequiredKeyedService<AIAgent>("bot");
81+
var metadata = agent.GetService<ConfigurableAIAgentMetadata>();
82+
83+
Assert.NotNull(metadata);
84+
Assert.Equal("bot", metadata.Name);
85+
Assert.Equal("ai:agents:bot", metadata.ConfigurationSection);
86+
}
87+
6488
[Fact]
6589
public void DedentsDescriptionAndInstructions()
6690
{

src/Tests/ConfigurableClientTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,31 @@ public void CanGetFromAlternativeKey()
5555
Assert.Same(grok, services.GetChatClient("grok"));
5656
}
5757

58+
[Fact]
59+
public void CanGetSectionAndIdFromMetadata()
60+
{
61+
var configuration = new ConfigurationBuilder()
62+
.AddInMemoryCollection(new Dictionary<string, string?>
63+
{
64+
["ai:clients:Grok:id"] = "groked",
65+
["ai:clients:Grok:modelid"] = "grok-4-fast",
66+
["ai:clients:Grok:ApiKey"] = "xai-asdfasdf",
67+
["ai:clients:Grok:endpoint"] = "https://api.x.ai",
68+
})
69+
.Build();
70+
71+
var services = new ServiceCollection()
72+
.AddSingleton<IConfiguration>(configuration)
73+
.AddChatClients(configuration)
74+
.BuildServiceProvider();
75+
76+
var grok = services.GetRequiredKeyedService<IChatClient>("groked");
77+
var metadata = grok.GetRequiredService<ConfigurableChatClientMetadata>();
78+
79+
Assert.Equal("groked", metadata.Id);
80+
Assert.Equal("ai:clients:Grok", metadata.ConfigurationSection);
81+
}
82+
5883
[Fact]
5984
public void CanOverrideClientId()
6085
{

0 commit comments

Comments
 (0)