Skip to content

Commit 8e814b3

Browse files
committed
Add docs and samples for both libraries
1 parent 0818af8 commit 8e814b3

14 files changed

+380
-43
lines changed

AI.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
<Project Path="src/Agents/Agents.csproj" Id="90827430-b415-47d6-aac9-2dbe4911b348" />
88
<Project Path="src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj" />
99
<Project Path="src/Extensions/Extensions.csproj" />
10+
<Project Path="src/SampleChat/SampleChat.csproj" Id="63ca9077-db60-473a-813d-d3bb5befdf35" />
1011
<Project Path="src/Tests/Tests.csproj" />
1112
</Solution>

readme.md

Lines changed: 122 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,141 @@
11
![Icon](assets/img/icon-32.png) Devlooped AI Extensions
22
============
33

4-
[![License](https://img.shields.io/github/license/devlooped/AI.svg?color=blue)](https://github.com//devlooped/AI/blob/main/license.txt)
5-
[![Build](https://github.com/devlooped/AI/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/devlooped/AI/actions/workflows/build.yml)
4+
[![EULA](https://img.shields.io/badge/EULA-OSMF-blue?labelColor=black&color=C9FF30)](osmfeula.txt)
5+
[![OSS](https://img.shields.io/github/license/devlooped/oss.svg?color=blue)](license.txt)
66

77
Extensions for Microsoft.Agents.AI and Microsoft.Extensions.AI.
88

9-
## Open Source Maintenance Fee
9+
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->
1010

11-
To ensure the long-term sustainability of this project, use of this project requires an
12-
[Open Source Maintenance Fee](https://opensourcemaintenancefee.org). While the source
13-
code is freely available under the terms of the [MIT License](./license.txt), all other aspects of the
14-
project --including opening or commenting on issues, participating in discussions and
15-
downloading releases-- require [adherence to the Maintenance Fee](./osmfeula.txt).
11+
# Devlooped.Agents.AI
1612

17-
In short, if you use this project to generate revenue, the [Maintenance Fee is required](./osmfeula.txt).
13+
[![Version](https://img.shields.io/nuget/vpre/Devlooped.Agents.AI.svg?color=royalblue)](https://www.nuget.org/packages/Devlooped.Agents.AI)
14+
[![Downloads](https://img.shields.io/nuget/dt/Devlooped.Agents.AI.svg?color=green)](https://www.nuget.org/packages/Devlooped.Agents.AI)
1815

19-
To pay the Maintenance Fee, [become a Sponsor](https://github.com/sponsors/devlooped).
16+
<!-- #agents-title -->
17+
Extensions for Microsoft.Agents.AI, such as configuration-driven auto-reloading agents.
18+
<!-- #agents-title -->
19+
20+
<!-- #agents -->
21+
## Overview
22+
23+
Microsoft.Agents.AI (aka [Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
24+
is a comprehensive API for building AI agents. Its programatic model (which follows closely
25+
the [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)
26+
approach) provides maximum flexibility with little prescriptive structure.
27+
28+
This package provides additional extensions to make developing agents easier and more
29+
declarative.
30+
31+
## Configurable Agents
32+
33+
Tweaking agent options such as description, instructions, chat client to use and its
34+
options, etc. is very common during development/testing. This package provides the ability to
35+
drive those settings from configuration (with auto-reload support). This makes it far easier
36+
to experiment with various combinations of agent instructions, chat client providers and
37+
options, and model parameters without changing code, recompiling or even restarting the application:
38+
39+
> [!NOTE]
40+
> This example shows integration with configurable chat clients feature from the
41+
> Devlooped.Extensions.AI package, but any `IChatClient` registered in the DI container
42+
> with a matching key can be used.
43+
44+
```json
45+
{
46+
"AI": {
47+
"Agents": {
48+
"MyAgent": {
49+
"Description": "An AI agent that helps with customer support.",
50+
"Instructions": "You are a helpful assistant for customer support.",
51+
"Client": "Grok",
52+
"Options": {
53+
"ModelId": "grok-4",
54+
"Temperature": 0.5,
55+
}
56+
}
57+
},
58+
"Clients": {
59+
"Grok": {
60+
"Endpoint": "https://api.grok.ai/v1",
61+
"ModelId": "grok-4-fast-non-reasoning",
62+
"ApiKey": "xai-asdf"
63+
}
64+
}
65+
}
66+
}
67+
````
68+
69+
```csharp
70+
var host = new HostApplicationBuilder(args);
71+
host.Configuration.AddJsonFile("appsettings.json, optional: false, reloadOnChange: true);
72+
73+
// 👇 implicitly calls AddChatClients
74+
host.AddAIAgents();
75+
76+
var app = host.Build();
77+
var agent = app.Services.GetRequiredKeyedService<AIAgent>("MyAgent");
78+
```
79+
80+
Agents are also properly registered in the corresponding Microsoft Agent Framework
81+
[AgentCatalog](https://learn.microsoft.com/en-us/dotnet/api/microsoft.agents.ai.hosting.agentcatalog):
82+
83+
```csharp
84+
var catalog = app.Services.GetRequiredService<AgentCatalog>();
85+
await foreach (AIAgent agent in catalog.GetAgentsAsync())
86+
{
87+
var metadata = agent.GetService<AIAgentMetadata>();
88+
Console.WriteLine($"Agent: {agent.Name} by {metadata.ProviderName}");
89+
}
90+
```
91+
92+
<!-- #agents -->
2093

2194
# Devlooped.Extensions.AI
2295

2396
[![Version](https://img.shields.io/nuget/vpre/Devlooped.Extensions.AI.svg?color=royalblue)](https://www.nuget.org/packages/Devlooped.Extensions.AI)
2497
[![Downloads](https://img.shields.io/nuget/dt/Devlooped.Extensions.AI.svg?color=green)](https://www.nuget.org/packages/Devlooped.Extensions.AI)
2598

26-
<!-- #description -->
99+
<!-- #extensions-title -->
27100
Extensions for Microsoft.Extensions.AI
28-
<!-- #description -->
101+
<!-- #extensions-title -->
102+
103+
<!-- #extensions -->
104+
## Configurable Chat Clients
105+
106+
Since tweaking chat options such as model identifier, reasoning effort, verbosity
107+
and other model settings is very common, this package provides the ability to
108+
drive those settings from configuration (with auto-reload support), both per-client
109+
as well as per-request. This makes local development and testing much easier and
110+
boosts the dev loop:
111+
112+
```json
113+
{
114+
"AI": {
115+
"Clients": {
116+
"Grok": {
117+
"Endpoint": "https://api.grok.ai/v1",
118+
"ModelId": "grok-4-fast-non-reasoning",
119+
"ApiKey": "xai-asdf"
120+
}
121+
}
122+
}
123+
}
124+
````
125+
126+
```csharp
127+
var host = new HostApplicationBuilder(args);
128+
host.Configuration.AddJsonFile("appsettings.json, optional: false, reloadOnChange: true);
129+
host.AddChatClients();
130+
131+
var app = host.Build();
132+
var grok = app.Services.GetRequiredKeyedService<IChatClient>("Grok");
133+
```
134+
135+
Changing the `appsettings.json` file will automatically update the client
136+
configuration without restarting the application.
137+
29138

30-
<!-- #content -->
31139
## Grok
32140

33141
Full support for Grok [Live Search](https://docs.x.ai/docs/guides/live-search)
@@ -332,7 +440,7 @@ IChatClient client = new GrokChatClient(Environment.GetEnvironmentVariable("XAI_
332440
})
333441
.Build();
334442
```
335-
<!-- #content -->
443+
<!-- #extensions -->
336444

337445
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
338446
# Sponsors

src/Agents/ConfigurableAIAgent.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.ComponentModel;
2-
using System.Text.Json;
1+
using System.Text.Json;
32
using Devlooped.Extensions.AI;
43
using Devlooped.Extensions.AI.Grok;
54
using Microsoft.Agents.AI;
@@ -61,7 +60,7 @@ public ConfigurableAIAgent(IServiceProvider services, string section, string nam
6160
/// <inheritdoc/>
6261
public override string DisplayName => agent.DisplayName;
6362
/// <inheritdoc/>
64-
public override string? Name => this.name;
63+
public override string? Name => name;
6564
/// <inheritdoc/>
6665
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
6766
=> agent.DeserializeThread(serializedThread, jsonSerializerOptions);
@@ -74,6 +73,11 @@ public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> message
7473
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
7574
=> agent.RunStreamingAsync(messages, thread, options, cancellationToken);
7675

76+
/// <summary>
77+
/// Configured agent options.
78+
/// </summary>
79+
public ChatClientAgentOptions Options => options;
80+
7781
(ChatClientAgent, ChatClientAgentOptions, IChatClient) Configure(IConfigurationSection configSection)
7882
{
7983
var options = configSection.Get<AgentClientOptions>();

src/Agents/readme.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[![EULA](https://img.shields.io/badge/EULA-OSMF-blue?labelColor=black&color=C9FF30)](osmfeula.txt)
2+
[![OSS](https://img.shields.io/github/license/devlooped/oss.svg?color=blue)](license.txt)
3+
[![GitHub](https://img.shields.io/badge/-source-181717.svg?logo=GitHub)](https://github.com/devlooped/AI)
4+
5+
<!-- include ../../readme.md#agents-title -->
6+
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->
7+
<!-- include ../../readme.md#agents -->
8+
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
9+
<!-- exclude -->

src/Extensions/AddChatClientsExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public static class AddChatClientsExtensions
2424
/// <param name="configureClient">Optional action to configure each client.</param>
2525
/// <param name="prefix">The configuration prefix for clients. Defaults to "ai:clients".</param>
2626
/// <returns>The host application builder.</returns>
27-
public static IHostApplicationBuilder AddChatClients(this IHostApplicationBuilder builder, Action<string, ChatClientBuilder>? configurePipeline = default, Action<string, IChatClient>? configureClient = default, string prefix = "ai:clients")
27+
public static TBuilder AddChatClients<TBuilder>(this TBuilder builder, Action<string, ChatClientBuilder>? configurePipeline = default, Action<string, IChatClient>? configureClient = default, string prefix = "ai:clients")
28+
where TBuilder : IHostApplicationBuilder
2829
{
2930
AddChatClients(builder.Services, builder.Configuration, configurePipeline, configureClient, prefix);
3031
return builder;

src/Extensions/ConfigurableChatClient.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
using Azure;
1+
using System.ClientModel.Primitives;
2+
using System.ComponentModel;
3+
using Azure;
24
using Azure.AI.Inference;
35
using Azure.AI.OpenAI;
6+
using Azure.Core;
47
using Devlooped.Extensions.AI.Grok;
58
using Devlooped.Extensions.AI.OpenAI;
69
using Microsoft.Extensions.AI;
@@ -14,7 +17,7 @@ namespace Devlooped.Extensions.AI;
1417
/// A configuration-driven <see cref="IChatClient"/> which monitors configuration changes and
1518
/// re-applies them to the inner client automatically.
1619
/// </summary>
17-
public sealed partial class ConfigurableChatClient : IDisposable, IChatClient
20+
public sealed partial class ConfigurableChatClient : IChatClient, IDisposable
1821
{
1922
readonly IConfiguration configuration;
2023
readonly string section;
@@ -23,7 +26,7 @@ public sealed partial class ConfigurableChatClient : IDisposable, IChatClient
2326
readonly Action<string, IChatClient>? configure;
2427
IDisposable reloadToken;
2528
IChatClient innerClient;
26-
29+
object? options;
2730

2831
/// <summary>
2932
/// Initializes a new instance of the <see cref="ConfigurableChatClient"/> class.
@@ -61,9 +64,13 @@ public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, Ch
6164
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
6265
=> innerClient.GetStreamingResponseAsync(messages, options, cancellationToken);
6366

67+
/// <summary>Exposes the optional <see cref="ClientPipelineOptions"/> configured for the client.</summary>
68+
[EditorBrowsable(EditorBrowsableState.Never)]
69+
public object? Options => options;
70+
6471
IChatClient Configure(IConfigurationSection configSection)
6572
{
66-
var options = configSection.Get<ConfigurableClientOptions>();
73+
var options = SetOptions<ConfigurableClientOptions>(configSection);
6774
Throw.IfNullOrEmpty(options?.ModelId, $"{configSection}:modelid");
6875

6976
// If there was a custom id, we must validate it didn't change since that's not supported.
@@ -92,9 +99,9 @@ IChatClient Configure(IConfigurationSection configSection)
9299
IChatClient client = options.Endpoint?.Host == "api.x.ai"
93100
? new GrokChatClient(apikey, options.ModelId, options)
94101
: options.Endpoint?.Host == "ai.azure.com"
95-
? new ChatCompletionsClient(options.Endpoint, new AzureKeyCredential(apikey), configSection.Get<ConfigurableInferenceOptions>()).AsIChatClient(options.ModelId)
102+
? new ChatCompletionsClient(options.Endpoint, new AzureKeyCredential(apikey), SetOptions<ConfigurableInferenceOptions>(configSection)).AsIChatClient(options.ModelId)
96103
: options.Endpoint?.Host.EndsWith("openai.azure.com") == true
97-
? new AzureOpenAIChatClient(options.Endpoint, new AzureKeyCredential(apikey), options.ModelId, configSection.Get<ConfigurableAzureOptions>())
104+
? new AzureOpenAIChatClient(options.Endpoint, new AzureKeyCredential(apikey), options.ModelId, SetOptions<ConfigurableAzureOptions>(configSection))
98105
: new OpenAIChatClient(apikey, options.ModelId, options);
99106

100107
configure?.Invoke(id, client);
@@ -104,6 +111,22 @@ IChatClient Configure(IConfigurationSection configSection)
104111
return client;
105112
}
106113

114+
TOptions? SetOptions<TOptions>(IConfigurationSection section) where TOptions : class
115+
{
116+
var options = typeof(TOptions) switch
117+
{
118+
var t when t == typeof(ConfigurableClientOptions) => section.Get<ConfigurableClientOptions>() as TOptions,
119+
var t when t == typeof(ConfigurableInferenceOptions) => section.Get<ConfigurableInferenceOptions>() as TOptions,
120+
var t when t == typeof(ConfigurableAzureOptions) => section.Get<ConfigurableAzureOptions>() as TOptions,
121+
#pragma warning disable SYSLIB1104 // The target type for a binder call could not be determined
122+
_ => section.Get<TOptions>()
123+
#pragma warning restore SYSLIB1104 // The target type for a binder call could not be determined
124+
};
125+
126+
this.options = options;
127+
return options;
128+
}
129+
107130
void OnReload(object? state)
108131
{
109132
var configSection = configuration.GetRequiredSection(section);

src/Extensions/Devlooped.Extensions.AI.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<BuiltWithSdkPreview>true</BuiltWithSdkPreview>
4-
<BuiltSdkPreviewVersion>10.0.100-preview.7.25380.108</BuiltSdkPreviewVersion>
4+
<BuiltSdkPreviewVersion>10.0.100-rc.2.25502.107</BuiltSdkPreviewVersion>
55
</PropertyGroup>
66
<Target Name="EnsureSamePreviewSdkVersion" BeforeTargets="Build" Condition="'$(BuiltWithSdkPreview)' == 'true'">
77
<Error Condition="'$(_NETCoreSdkIsPreview)' == 'false' or '$(NETCoreSdkVersion)' != '$(BuiltSdkPreviewVersion)'" Text="This version was built with a preview SDK and requires a matching one. Please install SDK version $(BuiltSdkPreviewVersion) or update to a newer package version." />

src/Extensions/Extensions.csproj

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,22 @@
4444
</ItemGroup>
4545

4646
<Target Name="UpdateSdkPreviewVersion" BeforeTargets="GetPackageContents">
47-
<!-- Update packaging version targets -->
48-
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltWithSdkPreview" Value="$(_NETCoreSdkIsPreview)" />
49-
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltSdkPreviewVersion" Value="$(NETCoreSdkVersion)" />
47+
<XmlPeek XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltWithSdkPreview">
48+
<Output TaskParameter="Result" PropertyName="BuiltWithSdkPreview" />
49+
</XmlPeek>
50+
<XmlPeek XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltSdkPreviewVersion">
51+
<Output TaskParameter="Result" PropertyName="BuiltSdkPreviewVersion" />
52+
</XmlPeek>
53+
54+
<!-- Update packaging version targets if changed -->
55+
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets"
56+
Query="/Project/PropertyGroup/BuiltWithSdkPreview"
57+
Value="$(_NETCoreSdkIsPreview)"
58+
Condition="'$(_NETCoreSdkIsPreview)' != '$(BuiltWithSdkPreview)'"/>
59+
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets"
60+
Query="/Project/PropertyGroup/BuiltSdkPreviewVersion"
61+
Value="$(NETCoreSdkVersion)"
62+
Condition="'$(NETCoreSdkVersion)' != '$(BuiltSdkPreviewVersion)'"/>
5063
</Target>
5164

52-
</Project>
65+
</Project>

src/Extensions/readme.md

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
<!-- include ../../readme.md#description -->
2-
## Open Source Maintenance Fee
1+
[![EULA](https://img.shields.io/badge/EULA-OSMF-blue?labelColor=black&color=C9FF30)](osmfeula.txt)
2+
[![OSS](https://img.shields.io/github/license/devlooped/oss.svg?color=blue)](license.txt)
3+
[![GitHub](https://img.shields.io/badge/-source-181717.svg?logo=GitHub)](https://github.com/devlooped/AI)
34

4-
To ensure the long-term sustainability of this project, use of `Devlooped.Extensions.AI` requires an
5-
[Open Source Maintenance Fee](https://opensourcemaintenancefee.org). While the source
6-
code is freely available under the terms of the
7-
[MIT License](https://github.com/devlooped/Extensions.AI/blob/main/license.txt),
8-
this package and other aspects of the project require
9-
[adherence to the Maintenance Fee](https://github.com/devlooped/Extensions.AI/blob/main/osmfeula.txt).
10-
11-
In short, if you use this project to generate revenue, the [Maintenance Fee is required](https://github.com/devlooped/Extensions.AI/blob/main/osmfeula.txt).
12-
13-
To pay the Maintenance Fee, [become a Sponsor](https://github.com/sponsors/devlooped).
14-
15-
<!-- include ../../readme.md#content -->
5+
<!-- include ../../readme.md#extensions-title -->
6+
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->
7+
<!-- include ../../readme.md#extensions -->
168
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
179
<!-- exclude -->

src/SampleChat/AppInitializer.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Runtime.CompilerServices;
2+
using System.Runtime.InteropServices;
3+
using System.Text;
4+
5+
namespace SampleChat;
6+
7+
class AppInitializer
8+
{
9+
[ModuleInitializer]
10+
public static void Init()
11+
{
12+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
13+
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;
14+
15+
// Load environment variables from .env files in current dir and above.
16+
DotNetEnv.Env.TraversePath().Load();
17+
18+
// Load environment variables from user profile directory.
19+
var userEnv = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".env");
20+
if (File.Exists(userEnv))
21+
DotNetEnv.Env.Load(userEnv);
22+
}
23+
}

0 commit comments

Comments
 (0)