Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
<Project Path="src/Agents/Agents.csproj" Id="90827430-b415-47d6-aac9-2dbe4911b348" />
<Project Path="src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj" />
<Project Path="src/Extensions/Extensions.csproj" />
<Project Path="src/SampleChat/SampleChat.csproj" Id="63ca9077-db60-473a-813d-d3bb5befdf35" />
<Project Path="src/Tests/Tests.csproj" />
</Solution>
136 changes: 122 additions & 14 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,141 @@
![Icon](assets/img/icon-32.png) Devlooped AI Extensions
============

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

Extensions for Microsoft.Agents.AI and Microsoft.Extensions.AI.

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

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

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

To pay the Maintenance Fee, [become a Sponsor](https://github.com/sponsors/devlooped).
<!-- #agents-title -->
Extensions for Microsoft.Agents.AI, such as configuration-driven auto-reloading agents.
<!-- #agents-title -->

<!-- #agents -->
## Overview

Microsoft.Agents.AI (aka [Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
is a comprehensive API for building AI agents. Its programatic model (which follows closely
the [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)
approach) provides maximum flexibility with little prescriptive structure.

This package provides additional extensions to make developing agents easier and more
declarative.

## Configurable Agents

Tweaking agent options such as description, instructions, chat client to use and its
options, etc. is very common during development/testing. This package provides the ability to
drive those settings from configuration (with auto-reload support). This makes it far easier
to experiment with various combinations of agent instructions, chat client providers and
options, and model parameters without changing code, recompiling or even restarting the application:

> [!NOTE]
> This example shows integration with configurable chat clients feature from the
> Devlooped.Extensions.AI package, but any `IChatClient` registered in the DI container
> with a matching key can be used.

```json
{
"AI": {
"Agents": {
"MyAgent": {
"Description": "An AI agent that helps with customer support.",
"Instructions": "You are a helpful assistant for customer support.",
"Client": "Grok",
"Options": {
"ModelId": "grok-4",
"Temperature": 0.5,
}
}
},
"Clients": {
"Grok": {
"Endpoint": "https://api.grok.ai/v1",
"ModelId": "grok-4-fast-non-reasoning",
"ApiKey": "xai-asdf"
}
}
}
}
````

```csharp
var host = new HostApplicationBuilder(args);
host.Configuration.AddJsonFile("appsettings.json, optional: false, reloadOnChange: true);

// 👇 implicitly calls AddChatClients
host.AddAIAgents();

var app = host.Build();
var agent = app.Services.GetRequiredKeyedService<AIAgent>("MyAgent");
```

Agents are also properly registered in the corresponding Microsoft Agent Framework
[AgentCatalog](https://learn.microsoft.com/en-us/dotnet/api/microsoft.agents.ai.hosting.agentcatalog):

```csharp
var catalog = app.Services.GetRequiredService<AgentCatalog>();
await foreach (AIAgent agent in catalog.GetAgentsAsync())
{
var metadata = agent.GetService<AIAgentMetadata>();
Console.WriteLine($"Agent: {agent.Name} by {metadata.ProviderName}");
}
```

<!-- #agents -->

# Devlooped.Extensions.AI

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

<!-- #description -->
<!-- #extensions-title -->
Extensions for Microsoft.Extensions.AI
<!-- #description -->
<!-- #extensions-title -->

<!-- #extensions -->
## Configurable Chat Clients

Since tweaking chat options such as model identifier, reasoning effort, verbosity
and other model settings is very common, this package provides the ability to
drive those settings from configuration (with auto-reload support), both per-client
as well as per-request. This makes local development and testing much easier and
boosts the dev loop:

```json
{
"AI": {
"Clients": {
"Grok": {
"Endpoint": "https://api.grok.ai/v1",
"ModelId": "grok-4-fast-non-reasoning",
"ApiKey": "xai-asdf"
}
}
}
}
````

```csharp
var host = new HostApplicationBuilder(args);
host.Configuration.AddJsonFile("appsettings.json, optional: false, reloadOnChange: true);
host.AddChatClients();

var app = host.Build();
var grok = app.Services.GetRequiredKeyedService<IChatClient>("Grok");
```

Changing the `appsettings.json` file will automatically update the client
configuration without restarting the application.


<!-- #content -->
## Grok

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

<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
# Sponsors
Expand Down
10 changes: 7 additions & 3 deletions src/Agents/ConfigurableAIAgent.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json;
using Devlooped.Extensions.AI;
using Devlooped.Extensions.AI.Grok;
using Microsoft.Agents.AI;
Expand Down Expand Up @@ -61,7 +60,7 @@ public ConfigurableAIAgent(IServiceProvider services, string section, string nam
/// <inheritdoc/>
public override string DisplayName => agent.DisplayName;
/// <inheritdoc/>
public override string? Name => this.name;
public override string? Name => name;
/// <inheritdoc/>
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> agent.DeserializeThread(serializedThread, jsonSerializerOptions);
Expand All @@ -74,6 +73,11 @@ public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> message
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> agent.RunStreamingAsync(messages, thread, options, cancellationToken);

/// <summary>
/// Configured agent options.
/// </summary>
public ChatClientAgentOptions Options => options;

(ChatClientAgent, ChatClientAgentOptions, IChatClient) Configure(IConfigurationSection configSection)
{
var options = configSection.Get<AgentClientOptions>();
Expand Down
9 changes: 9 additions & 0 deletions src/Agents/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[![EULA](https://img.shields.io/badge/EULA-OSMF-blue?labelColor=black&color=C9FF30)](osmfeula.txt)
[![OSS](https://img.shields.io/github/license/devlooped/oss.svg?color=blue)](license.txt)
[![GitHub](https://img.shields.io/badge/-source-181717.svg?logo=GitHub)](https://github.com/devlooped/AI)

<!-- include ../../readme.md#agents-title -->
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->
<!-- include ../../readme.md#agents -->
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
<!-- exclude -->
3 changes: 2 additions & 1 deletion src/Extensions/AddChatClientsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public static class AddChatClientsExtensions
/// <param name="configureClient">Optional action to configure each client.</param>
/// <param name="prefix">The configuration prefix for clients. Defaults to "ai:clients".</param>
/// <returns>The host application builder.</returns>
public static IHostApplicationBuilder AddChatClients(this IHostApplicationBuilder builder, Action<string, ChatClientBuilder>? configurePipeline = default, Action<string, IChatClient>? configureClient = default, string prefix = "ai:clients")
public static TBuilder AddChatClients<TBuilder>(this TBuilder builder, Action<string, ChatClientBuilder>? configurePipeline = default, Action<string, IChatClient>? configureClient = default, string prefix = "ai:clients")
where TBuilder : IHostApplicationBuilder
{
AddChatClients(builder.Services, builder.Configuration, configurePipeline, configureClient, prefix);
return builder;
Expand Down
35 changes: 29 additions & 6 deletions src/Extensions/ConfigurableChatClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using Azure;
using System.ClientModel.Primitives;
using System.ComponentModel;
using Azure;
using Azure.AI.Inference;
using Azure.AI.OpenAI;
using Azure.Core;
using Devlooped.Extensions.AI.Grok;
using Devlooped.Extensions.AI.OpenAI;
using Microsoft.Extensions.AI;
Expand All @@ -14,7 +17,7 @@
/// A configuration-driven <see cref="IChatClient"/> which monitors configuration changes and
/// re-applies them to the inner client automatically.
/// </summary>
public sealed partial class ConfigurableChatClient : IDisposable, IChatClient
public sealed partial class ConfigurableChatClient : IChatClient, IDisposable
{
readonly IConfiguration configuration;
readonly string section;
Expand All @@ -23,7 +26,7 @@
readonly Action<string, IChatClient>? configure;
IDisposable reloadToken;
IChatClient innerClient;

object? options;

/// <summary>
/// Initializes a new instance of the <see cref="ConfigurableChatClient"/> class.
Expand Down Expand Up @@ -61,9 +64,13 @@
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
=> innerClient.GetStreamingResponseAsync(messages, options, cancellationToken);

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

IChatClient Configure(IConfigurationSection configSection)
{
var options = configSection.Get<ConfigurableClientOptions>();
var options = SetOptions<ConfigurableClientOptions>(configSection);
Throw.IfNullOrEmpty(options?.ModelId, $"{configSection}:modelid");

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

configure?.Invoke(id, client);
Expand All @@ -104,6 +111,22 @@
return client;
}

TOptions? SetOptions<TOptions>(IConfigurationSection section) where TOptions : class
{
var options = typeof(TOptions) switch
{
var t when t == typeof(ConfigurableClientOptions) => section.Get<ConfigurableClientOptions>() as TOptions,
var t when t == typeof(ConfigurableInferenceOptions) => section.Get<ConfigurableInferenceOptions>() as TOptions,
var t when t == typeof(ConfigurableAzureOptions) => section.Get<ConfigurableAzureOptions>() as TOptions,
#pragma warning disable SYSLIB1104 // The target type for a binder call could not be determined
_ => section.Get<TOptions>()

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)

Check warning on line 122 in src/Extensions/ConfigurableChatClient.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Binding logic was not generated for a binder call. Unsupported input patterns include generic calls, passing boxed objects, and passing types that are not 'public' or 'internal'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1104)
#pragma warning restore SYSLIB1104 // The target type for a binder call could not be determined
};

this.options = options;
return options;
}

void OnReload(object? state)
{
var configSection = configuration.GetRequiredSection(section);
Expand Down
2 changes: 1 addition & 1 deletion src/Extensions/Devlooped.Extensions.AI.targets
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<BuiltWithSdkPreview>true</BuiltWithSdkPreview>
<BuiltSdkPreviewVersion>10.0.100-preview.7.25380.108</BuiltSdkPreviewVersion>
<BuiltSdkPreviewVersion>10.0.100-rc.2.25502.107</BuiltSdkPreviewVersion>
</PropertyGroup>
<Target Name="EnsureSamePreviewSdkVersion" BeforeTargets="Build" Condition="'$(BuiltWithSdkPreview)' == 'true'">
<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." />
Expand Down
21 changes: 17 additions & 4 deletions src/Extensions/Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,22 @@
</ItemGroup>

<Target Name="UpdateSdkPreviewVersion" BeforeTargets="GetPackageContents">
<!-- Update packaging version targets -->
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltWithSdkPreview" Value="$(_NETCoreSdkIsPreview)" />
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltSdkPreviewVersion" Value="$(NETCoreSdkVersion)" />
<XmlPeek XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltWithSdkPreview">
<Output TaskParameter="Result" PropertyName="BuiltWithSdkPreview" />
</XmlPeek>
<XmlPeek XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets" Query="/Project/PropertyGroup/BuiltSdkPreviewVersion">
<Output TaskParameter="Result" PropertyName="BuiltSdkPreviewVersion" />
</XmlPeek>

<!-- Update packaging version targets if changed -->
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets"
Query="/Project/PropertyGroup/BuiltWithSdkPreview"
Value="$(_NETCoreSdkIsPreview)"
Condition="'$(_NETCoreSdkIsPreview)' != '$(BuiltWithSdkPreview)'"/>
<XmlPoke XmlInputPath="$(MSBuildProjectDirectory)\Devlooped.Extensions.AI.targets"
Query="/Project/PropertyGroup/BuiltSdkPreviewVersion"
Value="$(NETCoreSdkVersion)"
Condition="'$(NETCoreSdkVersion)' != '$(BuiltSdkPreviewVersion)'"/>
</Target>

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

To ensure the long-term sustainability of this project, use of `Devlooped.Extensions.AI` requires an
[Open Source Maintenance Fee](https://opensourcemaintenancefee.org). While the source
code is freely available under the terms of the
[MIT License](https://github.com/devlooped/Extensions.AI/blob/main/license.txt),
this package and other aspects of the project require
[adherence to the Maintenance Fee](https://github.com/devlooped/Extensions.AI/blob/main/osmfeula.txt).

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).

To pay the Maintenance Fee, [become a Sponsor](https://github.com/sponsors/devlooped).

<!-- include ../../readme.md#content -->
<!-- include ../../readme.md#extensions-title -->
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->
<!-- include ../../readme.md#extensions -->
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
<!-- exclude -->
23 changes: 23 additions & 0 deletions src/SampleChat/AppInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace SampleChat;

class AppInitializer
{
[ModuleInitializer]
public static void Init()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;

// Load environment variables from .env files in current dir and above.
DotNetEnv.Env.TraversePath().Load();

// Load environment variables from user profile directory.
var userEnv = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".env");
if (File.Exists(userEnv))
DotNetEnv.Env.Load(userEnv);
}
}
Loading