From 5b20297bb0b904c9909d286e591d9d949915d1a0 Mon Sep 17 00:00:00 2001 From: enisn Date: Tue, 23 Sep 2025 14:25:21 +0300 Subject: [PATCH] Refactor system message handling and update docs Removed ChatClientWithSystemMessage and its extension, consolidating system message logic into user-extensible patterns. Renamed ChatClientNameAttribute to WorkspaceNameAttribute and restricted its usage to classes. Added ConfigureDefault to WorkspaceConfigurationDictionary for easier default workspace setup. Updated documentation to reflect these changes and clarify configuration patterns. --- .../infrastructure/artificial-intelligence.md | 110 +++++++++--------- .../Delegates/ChatClientWithSystemMessage.cs | 52 --------- .../Volo/Abp/AI/IKernelAccessor.cs | 9 +- ...Attribute.cs => WorkspaceNameAttribute.cs} | 2 +- .../Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs | 2 - .../Volo/Abp/AI/DefaultKernelAccessor.cs | 18 +++ .../ChatClientWithSystemMessageExtensions.cs | 12 -- .../AI/WorkspaceConfigurationDictionary.cs | 5 + 8 files changed, 84 insertions(+), 126 deletions(-) delete mode 100644 framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/Delegates/ChatClientWithSystemMessage.cs rename framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/{ChatClientNameAttribute.cs => WorkspaceNameAttribute.cs} (90%) create mode 100644 framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs delete mode 100644 framework/src/Volo.Abp.AI/Volo/Abp/AI/Extensions/ChatClientWithSystemMessageExtensions.cs diff --git a/docs/en/framework/infrastructure/artificial-intelligence.md b/docs/en/framework/infrastructure/artificial-intelligence.md index 6af5cf589c..652c7c8233 100644 --- a/docs/en/framework/infrastructure/artificial-intelligence.md +++ b/docs/en/framework/infrastructure/artificial-intelligence.md @@ -43,7 +43,7 @@ public class MyProjectModule : AbpModule #### Default configuration (quick start) -Configure the special workspace named `"Default"` to inject `IChatClient` by default. +Configure the default workspace to inject `IChatClient` directly. ```csharp using Microsoft.Extensions.AI; @@ -57,20 +57,13 @@ public class MyProjectModule : AbpModule { context.Services.PreConfigure(options => { - options.Workspaces.Configure(AbpAIModule.DefaultWorkspaceName, configuration => + options.Workspaces.ConfigureDefault(configuration => { configuration.ConfigureChatClient(chatClientConfiguration => { chatClientConfiguration.Builder = new ChatClientBuilder( sp => new OllamaApiClient("http://localhost:11434", "mistral") ); - - chatClientConfiguration.BuilderConfigurers.Add(builder => - { - builder.UseSystemMessage( - "You are a helpful assistant that greets users in a friendly manner with their names." - ); - }); }); // Chat client only in this quick start @@ -80,12 +73,6 @@ public class MyProjectModule : AbpModule } ``` -Notes: - -- Prefer `ConfigureChatClient(...)` / `ConfigureKernel(...)` methods for configuration. -- Set the `Builder` and then use `BuilderConfigurers.Add(...)` to apply incremental changes. -- If a workspace configures only the Kernel, a chat client may still be exposed for that workspace through the Kernel’s service provider (when available). - Once configured, inject the default chat client: ```csharp @@ -113,11 +100,6 @@ using Volo.Abp.AI; public class GreetingAssistant // ChatClient-only workspace { } - -[WorkspaceName("ContentPlanner")] -public class ContentPlanner // Kernel-only workspace -{ -} ``` Configure a ChatClient workspace: @@ -139,9 +121,8 @@ public class MyProjectModule : AbpModule chatClientConfiguration.BuilderConfigurers.Add(builder => { - builder.UseSystemMessage( - "You are a helpful assistant that greets users in a friendly manner with their names." - ); + // Anything you want to do with the builder: + // builder.UseFunctionInvocation().UseLogging(); // For example }); }); }); @@ -154,6 +135,7 @@ public class MyProjectModule : AbpModule #### Default configuration + ```csharp public class MyProjectModule : AbpModule { @@ -161,7 +143,7 @@ public class MyProjectModule : AbpModule { context.Services.PreConfigure(options => { - options.Workspaces.Configure(configuration => + options.Workspaces.ConfigureDefault(configuration => { configuration.ConfigureKernel(kernelConfiguration => { @@ -175,32 +157,52 @@ public class MyProjectModule : AbpModule } ``` -#### Workspace configuration +Once configured, inject the default kernel: ```csharp -using Microsoft.Extensions.AI; +using System.Threading.Tasks; using Volo.Abp.AI; -public class GreetingService +public class MyService { - private readonly IChatClient _chatClient; + private readonly IKernelAccessor _kernelAccessor; + public MyService(IKernelAccessor kernelAccessor) + { + _kernelAccessor = kernelAccessor; + } - public GreetingService(IChatClient chatClient) + public async Task DoSomethingAsync() { - _chatClient = chatClient; + var kernel = _kernelAccessor.Kernel; // Kernel might be null if no workspace is configured. + + var result = await kernel.InvokeAsync(/*... */); } +} +``` + +#### Workspace configuration - public async Task GreetAsync(string name) +```csharp +public class MyProjectModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) { - var response = await _chatClient.GetResponseAsync( - [new ChatMessage(ChatRole.User, $"Greet {name}")] - ); - return response?.Message?.Text ?? string.Empty; + context.Services.PreConfigure(options => + { + options.Workspaces.Configure(configuration => + { + configuration.ConfigureKernel(kernelConfiguration => + { + kernelConfiguration.Builder = Kernel.CreateBuilder() + .AddOpenAIChatCompletion("...", "..."); + }); + }); + }); } } ``` -#### Resolve +#### Workspace usage ```csharp using Microsoft.Extensions.AI; @@ -235,21 +237,12 @@ public class PlanningService ## Options -- `AbpAIOptions.Workspaces`: A `WorkspaceConfigurationDictionary` used to configure workspaces. - - `Configure(Action)` - - `Configure(string name, Action)` +`AbpAIOptions` configuration pattern offers `ConfigureChatClient(...)` and `ConfigureKernel(...)` methods for configuration. These methods are defined in the `WorkspaceConfiguration` class. They are used to configure the `ChatClient` and `Kernel` respectively. -- `WorkspaceConfiguration` per workspace: - - `ChatClient`: `ChatClientConfiguration` - - `Builder`: `ChatClientBuilder?` - - `ConfigureBuilder(Action)` - - `ConfigureBuilder(string name, Action)` (named actions executed in order) - - `Kernel`: `KernelConfiguration` - - `Builder`: `IKernelBuilder?` - - `ConfigureBuilder(Action)` - - `ConfigureBuilder(string name, Action)` +`Builder` is set once and is used to build the `ChatClient` or `Kernel` instance. `BuilderConfigurers` is a list of actions that are applied to the `Builder` instance for incremental changes. These actions are executed in the order they are added. + +If a workspace configures only the Kernel, a chat client may still be exposed for that workspace through the Kernel’s service provider (when available). -- `AbpAIWorkspaceOptions.ConfiguredWorkspaceNames`: Automatically set of workspace names configured during startup. Useful for diagnostics. ## Advanced Usage and Customizations @@ -262,9 +255,14 @@ Example sketch: ```csharp using Microsoft.Extensions.AI; -public class MyPolicyChatClient : DelegatingChatClient +public class SystemMessageChatClient : DelegatingChatClient { - public MyPolicyChatClient(IChatClient inner) : base(inner) { } + public SystemMessageChatClient(IChatClient inner, string systemMessage) : base(inner) + { + SystemMessage = systemMessage; + } + + public string SystemMessage { get; set; } public override Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { @@ -273,25 +271,23 @@ public class MyPolicyChatClient : DelegatingChatClient } } -public static class MyPolicyChatClientExtensions +public static class SystemMessageChatClientExtensions { - public static ChatClientBuilder UseMyPolicy(this ChatClientBuilder builder) + public static ChatClientBuilder UseSystemMessage(this ChatClientBuilder builder, string systemMessage) { - return builder.Use(client => new MyPolicyChatClient(client)); + return builder.Use(client => new SystemMessageChatClient(client, systemMessage)); } } ``` -It'll have similar usage with `.UseSystemMessage(...)` extension while configuring a chat client (see configuration examples above). ```cs chatClientConfiguration.BuilderConfigurers.Add(builder => { - builder.UseMyPolicy(); + builder.UseSystemMessage("You are a helpful assistant that greets users in a friendly manner with their names."); }); ``` - ## Technical Anatomy - `AbpAIModule`: Wires up configured workspaces, registers keyed services and default services for the `"Default"` workspace. diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/Delegates/ChatClientWithSystemMessage.cs b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/Delegates/ChatClientWithSystemMessage.cs deleted file mode 100644 index ccd6f1aa68..0000000000 --- a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/Delegates/ChatClientWithSystemMessage.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; - -namespace Volo.Abp.AI.Delegates; - -public class ChatClientWithSystemMessage : DelegatingChatClient -{ - public string SystemMessage { get; private set; } - - public ChatClientWithSystemMessage(IChatClient innerClient, string systemMessage) : base(innerClient) - { - SystemMessage = systemMessage; - } - - public override Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - return base.GetResponseAsync(PrepareMessages(messages), options, cancellationToken); - } - - public override IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - return base.GetStreamingResponseAsync(PrepareMessages(messages), options, cancellationToken); - } - - protected virtual List PrepareMessages(IEnumerable messages) - { - var messagesList = messages.ToList(); - - if(messagesList.Any(x => x.Role == ChatRole.System)) - { - // If there is a system message, skip it. It might be continued conversation. - // No need to add a new one to prevent duplication. - - // If developer provided system message, then it's overridden, still skipping. - - // Logger.LogWarning("System message is not supported in ChatClientWithSystemMessage. Skipping."); - - return messagesList; - } - - if(!SystemMessage.IsNullOrEmpty()) - { - messagesList.Insert(0, new ChatMessage(ChatRole.System, SystemMessage)); - } - - return messagesList; - } -} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IKernelAccessor.cs b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IKernelAccessor.cs index a63efe8186..7f55ad9e6f 100644 --- a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IKernelAccessor.cs +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IKernelAccessor.cs @@ -1,9 +1,14 @@ + using Microsoft.SemanticKernel; namespace Volo.Abp.AI; -public interface IKernelAccessor - where TWorkSpace : class +public interface IKernelAccessor { Kernel? Kernel { get; } +} + +public interface IKernelAccessor : IKernelAccessor + where TWorkSpace : class +{ } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/ChatClientNameAttribute.cs b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/WorkspaceNameAttribute.cs similarity index 90% rename from framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/ChatClientNameAttribute.cs rename to framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/WorkspaceNameAttribute.cs index 70e55fdf57..1cf34d0781 100644 --- a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/ChatClientNameAttribute.cs +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/WorkspaceNameAttribute.cs @@ -4,7 +4,7 @@ using System.Collections.Concurrent; namespace Volo.Abp.AI; -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct)] +[AttributeUsage(AttributeTargets.Class)] public class WorkspaceNameAttribute : Attribute { public string Name { get; } diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs index aa6f368fec..f13a6e6f02 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs @@ -92,7 +92,5 @@ public class AbpAIModule : AbpModule } context.Services.TryAddTransient(typeof(IKernelAccessor<>), typeof(TypedKernelAccessor<>)); - - } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs new file mode 100644 index 0000000000..5370a41dd1 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AI; + +[ExposeServices(typeof(IKernelAccessor))] +public class DefaultKernelAccessor : IKernelAccessor, ITransientDependency +{ + public Kernel? Kernel { get; } + + public DefaultKernelAccessor(IServiceProvider serviceProvider) + { + Kernel = serviceProvider.GetKeyedService( + AbpAIModule.DefaultWorkspaceName); + } +} diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/Extensions/ChatClientWithSystemMessageExtensions.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/Extensions/ChatClientWithSystemMessageExtensions.cs deleted file mode 100644 index 59eadfd2c6..0000000000 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/Extensions/ChatClientWithSystemMessageExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Extensions.AI; -using Volo.Abp.AI.Delegates; - -namespace Volo.Abp.AI.Extensions; - -public static class ChatClientWithSystemMessageExtensions -{ - public static ChatClientBuilder UseSystemMessage(this ChatClientBuilder builder, string systemMessage) - { - return builder.Use(client => new ChatClientWithSystemMessage(client, systemMessage)); - } -} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfigurationDictionary.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfigurationDictionary.cs index 14808f7bc6..6a5c77d7d0 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfigurationDictionary.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfigurationDictionary.cs @@ -21,4 +21,9 @@ public class WorkspaceConfigurationDictionary : Dictionary? configureAction = null) + { + Configure(AbpAIModule.DefaultWorkspaceName, configureAction); + } }