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
5 changes: 5 additions & 0 deletions .netconfig
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,8 @@
sha = 666a2a7c315f72199c418f11482a950fc69a8901
etag = 91ea15c07bfd784036c6ca931f5b2df7e9767b8367146d96c79caef09d63899f
weak
[file "src/Agents/Extensions/Throw.cs"]
url = https://github.com/devlooped/catbag/blob/main/System/Throw.cs
sha = 3012d56be7554c483e5c5d277144c063969cada9
etag = 43c81c6c6dcdf5baee40a9e3edc5e871e473e6c954c901b82bb87a3a48888ea0
weak
45 changes: 45 additions & 0 deletions src/Agents/AddAIAgentsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Devlooped.Extensions.AI;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Devlooped.Agents.AI;

public static class AddAIAgentsExtensions
{
public static IHostApplicationBuilder AddAIAgents(this IHostApplicationBuilder builder, Action<string, AIAgentBuilder>? configurePipeline = default, Action<string, ChatClientAgentOptions>? configureOptions = default, string prefix = "ai:agents")
{
builder.AddChatClients();

foreach (var entry in builder.Configuration.AsEnumerable().Where(x =>
x.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
x.Key.EndsWith("client", StringComparison.OrdinalIgnoreCase)))
{
var section = string.Join(':', entry.Key.Split(':')[..^1]);
// key == name (unlike chat clients, the AddAIAgent expects the key to be the name).
var name = builder.Configuration[$"{section}:name"] ?? section[(prefix.Length + 1)..];

var options = builder.Configuration.GetRequiredSection(section).Get<ChatClientAgentOptions>();
// We need logging set up for the configurable client to log changes
builder.Services.AddLogging();

builder.AddAIAgent(name, (sp, key) =>
{
var agent = new ConfigurableAIAgent(sp, section, key, configureOptions);

if (configurePipeline is not null)
{
var builder = agent.AsBuilder();
configurePipeline(key, builder);
return builder.Build(sp);
}

return agent;
});
}

return builder;
}
}
12 changes: 10 additions & 2 deletions src/Agents/Agents.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>Preview</LangVersion>
<PackageId>Devlooped.Agents.AI</PackageId>
<AssemblyName>Devlooped.Agents.AI</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<PackageId>$(AssemblyName)</PackageId>
<Description>Extensions for Microsoft.Agents.AI</Description>
<PackageLicenseExpression></PackageLicenseExpression>
<PackageLicenseFile>OSMFEULA.txt</PackageLicenseFile>
Expand All @@ -13,8 +15,10 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
<PackageReference Include="NuGetizer" Version="1.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251009.1" />
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.0.0-preview.251009.1" />
<PackageReference Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251009.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251009.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
Expand All @@ -25,4 +29,8 @@
<None Include="..\..\osmfeula.txt" Link="osmfeula.txt" PackagePath="OSMFEULA.txt" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Extensions\Extensions.csproj" />
</ItemGroup>

</Project>
99 changes: 99 additions & 0 deletions src/Agents/ConfigurableAIAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Devlooped.Agents.AI;

public sealed partial class ConfigurableAIAgent : AIAgent, IDisposable
{
readonly IServiceProvider services;
readonly IConfiguration configuration;
readonly string section;
readonly string name;
readonly ILogger logger;
readonly Action<string, ChatClientAgentOptions>? configure;
IDisposable reloadToken;
ChatClientAgent agent;
IChatClient chat;
ChatClientAgentOptions options;

public ConfigurableAIAgent(IServiceProvider services, string section, string name, Action<string, ChatClientAgentOptions>? configure)
{
if (section.Contains('.'))
throw new ArgumentException("Section separator must be ':', not '.'");

this.services = Throw.IfNull(services);
this.configuration = services.GetRequiredService<IConfiguration>();
this.logger = services.GetRequiredService<ILogger<ConfigurableAIAgent>>();
this.section = Throw.IfNullOrEmpty(section);
this.name = Throw.IfNullOrEmpty(name);
this.configure = configure;

(agent, options, chat) = Configure(configuration.GetRequiredSection(section));
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
}

public void Dispose() => reloadToken?.Dispose();

public override object? GetService(Type serviceType, object? serviceKey = null) => serviceType switch
{
Type t when t == typeof(ChatClientAgentOptions) => options,
Type t when t == typeof(IChatClient) => chat,
_ => agent.GetService(serviceType, serviceKey)
};

public override string Id => agent.Id;
public override string? Description => agent.Description;
public override string DisplayName => agent.DisplayName;
public override string? Name => agent.Name;
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> agent.DeserializeThread(serializedThread, jsonSerializerOptions);
public override AgentThread GetNewThread() => agent.GetNewThread();
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> agent.RunAsync(messages, thread, options, cancellationToken);
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> agent.RunStreamingAsync(messages, thread, options, cancellationToken);

(ChatClientAgent, ChatClientAgentOptions, IChatClient) Configure(IConfigurationSection configSection)
{
var options = configSection.Get<AgentClientOptions>();
options?.Name ??= name;

// If there was a custom id, we must validate it didn't change since that's not supported.
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<IChatClient>(options?.Client
?? throw new InvalidOperationException($"A client must be specified for agent '{name}' in configuration section '{section}'."));

var chat = configSection.GetSection("options").Get<ChatOptions>();
if (chat is not null)
options.ChatOptions = chat;

configure?.Invoke(name, options);

LogConfigured(name);

return (new ChatClientAgent(client, options, services.GetRequiredService<ILoggerFactory>(), services), options, client);
}

void OnReload(object? state)
{
var configSection = configuration.GetRequiredSection(section);
reloadToken?.Dispose();
chat?.Dispose();
(agent, options, chat) = Configure(configSection);
reloadToken = configuration.GetReloadToken().RegisterChangeCallback(OnReload, state: null);
}

[LoggerMessage(LogLevel.Information, "AIAgent '{Id}' configured.")]
private partial void LogConfigured(string id);

class AgentClientOptions : ChatClientAgentOptions
{
public required string Client { get; set; }
}
}
Loading