From fc71d848e293b0a46b7f0fa273ed93c34bcb87e3 Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Sat, 4 Apr 2026 04:10:51 +0200 Subject: [PATCH] Add agent system with Core and Librarian agent implementations Define IAgent interface and implement CoreAgent (main conversational agent) and LibrarianAgent (knowledge retrieval agent). Includes DI service collection extensions for agent registration. --- Luna.Agents.Abstractions/IAgent.cs | 19 ++++++ .../Luna.Agents.Abstractions.csproj | 18 ++++++ Luna.Agents/Core/CoreAgent.cs | 55 +++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 32 ++++++++++ Luna.Agents/Librarian/LibrarianAgent.cs | 60 +++++++++++++++++++ Luna.Agents/Luna.Agents.csproj | 22 +++++++ 6 files changed, 206 insertions(+) create mode 100644 Luna.Agents.Abstractions/IAgent.cs create mode 100644 Luna.Agents.Abstractions/Luna.Agents.Abstractions.csproj create mode 100644 Luna.Agents/Core/CoreAgent.cs create mode 100644 Luna.Agents/Extensions/ServiceCollectionExtensions.cs create mode 100644 Luna.Agents/Librarian/LibrarianAgent.cs create mode 100644 Luna.Agents/Luna.Agents.csproj diff --git a/Luna.Agents.Abstractions/IAgent.cs b/Luna.Agents.Abstractions/IAgent.cs new file mode 100644 index 0000000..63e2a4d --- /dev/null +++ b/Luna.Agents.Abstractions/IAgent.cs @@ -0,0 +1,19 @@ +using Luna.Configuration; +using Microsoft.Extensions.AI; + +namespace Luna.Agents.Abstractions; + +public interface IAgent +{ + public static virtual string Name { get; } = null!; + + public string DisplayName { get; } + public string Description { get; } + public AgentOptions Options { get; init; } + + Task> ProcessAsync(IReadOnlyList messages, + CancellationToken ct = default); + + IAsyncEnumerable ProcessStreamingAsync(IReadOnlyList messages, + CancellationToken ct = default); +} diff --git a/Luna.Agents.Abstractions/Luna.Agents.Abstractions.csproj b/Luna.Agents.Abstractions/Luna.Agents.Abstractions.csproj new file mode 100644 index 0000000..99e7d90 --- /dev/null +++ b/Luna.Agents.Abstractions/Luna.Agents.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/Luna.Agents/Core/CoreAgent.cs b/Luna.Agents/Core/CoreAgent.cs new file mode 100644 index 0000000..5a58ea8 --- /dev/null +++ b/Luna.Agents/Core/CoreAgent.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using Luna.Agents.Abstractions; +using Luna.Configuration; +using Luna.Providers.Abstractions; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Luna.Agents.Core; + +public class CoreAgent( + [FromKeyedServices("Mistral")] IProvider provider, + IOptionsMonitor options, + ILogger logger) + : IAgent +{ + private readonly IChatClient chatClient = provider.GetChatClient(options.Get(Name).ModelId); + + public static string Name => "Core"; + + public string DisplayName => Options.DisplayName ?? Name; + public string Description => Options.Description ?? string.Empty; + public AgentOptions Options { get; init; } = options.Get(Name); + + public async Task> ProcessAsync( + IReadOnlyList messages, + CancellationToken ct = default) + { + var messagesJson = JsonSerializer.Serialize(messages); + logger.LogInformation("Processing messages: {MessagesJson}", messagesJson); + + var chatOptions = new ChatOptions + { + Instructions = Options.Instructions + }; + + var response = await chatClient.GetResponseAsync(messages, chatOptions, ct); + + logger.LogInformation("Response: {ResponseJson}", JsonSerializer.Serialize(response)); + return response.Messages; + } + + public IAsyncEnumerable ProcessStreamingAsync( + IReadOnlyList messages, + CancellationToken ct = default) + { + var chatOptions = new ChatOptions + { + Instructions = Options.Instructions + }; + + return chatClient.GetStreamingResponseAsync(messages, chatOptions, ct); + } +} \ No newline at end of file diff --git a/Luna.Agents/Extensions/ServiceCollectionExtensions.cs b/Luna.Agents/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..89905ea --- /dev/null +++ b/Luna.Agents/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +using Luna.Agents.Abstractions; +using Luna.Agents.Core; +using Luna.Agents.Librarian; +using Luna.Configuration.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Luna.Agents.Extensions; + +public static class ServiceCollectionExtensions +{ + extension(IServiceCollection services) + { + public IServiceCollection AddAgents() + { + services.AddAgent(); + services.AddAgent(); + + return services; + } + + public IServiceCollection AddAgent(string? name = null) + where TAgent : class, IAgent + { + name ??= TAgent.Name; + + services.AddAgentOptions(name); + services.AddKeyedTransient(name); + + return services; + } + } +} \ No newline at end of file diff --git a/Luna.Agents/Librarian/LibrarianAgent.cs b/Luna.Agents/Librarian/LibrarianAgent.cs new file mode 100644 index 0000000..902ba66 --- /dev/null +++ b/Luna.Agents/Librarian/LibrarianAgent.cs @@ -0,0 +1,60 @@ +using Luna.Agents.Abstractions; +using Luna.Configuration; +using Luna.Providers.Abstractions; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Luna.Agents.Librarian; + +public class LibrarianAgent( + [FromKeyedServices("Mistral")] IProvider provider, + IOptionsMonitor options) + : IAgent +{ + private readonly IChatClient chatClient = provider.GetChatClient(options.Get(Name).ModelId); + + public static string Name => "Librarian"; + + public string DisplayName => Options.DisplayName ?? Name; + public string Description => Options.Description ?? string.Empty; + public AgentOptions Options { get; init; } = options.Get(Name); + + public async Task> ProcessAsync( + IReadOnlyList messages, + CancellationToken ct) + { + var chatOptions = new ChatOptions + { + Instructions = Options.Instructions + }; + + var messagesWithoutSystemPrompt = messages + .Where(m => m.Role != ChatRole.System) + .ToList(); + + var response = await chatClient.GetResponseAsync(messagesWithoutSystemPrompt, chatOptions, ct); + return response.Messages + .Where(m => m.Role != ChatRole.System); + } + + public async IAsyncEnumerable ProcessStreamingAsync(IReadOnlyList messages, CancellationToken ct = default) + { + var chatOptions = new ChatOptions + { + Instructions = Options.Instructions + }; + + var messagesWithoutSystemPrompt = messages + .Where(m => m.Role != ChatRole.System) + .ToList(); + + await foreach (var update in chatClient.GetStreamingResponseAsync(messagesWithoutSystemPrompt, chatOptions, ct)) + { + if (update.Role != ChatRole.System) + { + yield return update; + } + } + } +} \ No newline at end of file diff --git a/Luna.Agents/Luna.Agents.csproj b/Luna.Agents/Luna.Agents.csproj new file mode 100644 index 0000000..4efe773 --- /dev/null +++ b/Luna.Agents/Luna.Agents.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + +