From d824ecc2e9fd3f8e06cf0c0db92d1d841e0aee72 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 15 Jan 2026 08:52:38 +0800 Subject: [PATCH 01/25] feat(ai): Add localized error codes --- .../LINGYUN/Abp/AI/AbpAICoreModule.cs | 16 +++++++++++++++- .../LINGYUN/Abp/AI/AbpAIErrorCodes.cs | 9 +++++++++ .../Abp/AI/Localization/Resources/en.json | 6 ++++++ .../Abp/AI/Localization/Resources/zh-Hans.json | 6 ++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs index 8718805c2..8bdcf3754 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using Volo.Abp.AI; using Volo.Abp.Localization; +using Volo.Abp.Localization.ExceptionHandling; using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.AI; @@ -21,15 +23,27 @@ public class AbpAICoreModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + Configure(options => { - options.Resources.Add(); + options.Resources + .Add() + .AddVirtualJson("/LINGYUN/Abp/AI/Localization/Resources"); }); Configure(options => { options.ChatClientProviders.Add(); }); + + Configure(options => + { + options.MapCodeNamespace(AbpAIErrorCodes.Namespace, typeof(AbpAIResource)); + }); } private static void AutoAddDefinitionProviders(IServiceCollection services) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs new file mode 100644 index 000000000..bb261dea1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs @@ -0,0 +1,9 @@ +namespace LINGYUN.Abp.AI; +public static class AbpAIErrorCodes +{ + public const string Namespace = "Abp.AI"; + /// + /// 工作区不可用: {Workspace}! + /// + public const string WorkspaceIsNotEnabled = Namespace + ":110001"; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json new file mode 100644 index 000000000..399e2a7c9 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json @@ -0,0 +1,6 @@ +{ + "culture": "en", + "texts": { + "Abp.AI:110001": "Workspace is not enabled: {Workspace}!" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..3fc69d30d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json @@ -0,0 +1,6 @@ +{ + "culture": "zh-Hans", + "texts": { + "Abp.AI:110001": "工作区不可用: {Workspace}!" + } +} \ No newline at end of file From 4f603429f44d2552021fcb18f2258654468237c0 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 15 Jan 2026 08:53:48 +0800 Subject: [PATCH 02/25] feat(ai): check state of use workspace --- .../Abp/AI/Agent/ChatClientAgentFactory.cs | 50 ++++++++++++++----- .../LINGYUN.Abp.AI.Core.csproj | 5 ++ .../LINGYUN/Abp/AI/ChatClientFactory.cs | 20 +++++++- .../LINGYUN/Abp/AI/KernelFactory.cs | 19 ++++++- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs index 62dd55dca..5f233f4e2 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs @@ -5,24 +5,29 @@ using Microsoft.Extensions.Localization; using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp.AI; +using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; +using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AI.Agent; public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDependency { private readonly static ConcurrentDictionary _chatClientAgentCache = new(); - private readonly IChatClientFactory _chatClientFactory; - private readonly IStringLocalizerFactory _stringLocalizerFactory; - private readonly IWorkspaceDefinitionManager _workspaceDefinitionManager; + protected IChatClientFactory ChatClientFactory { get; } + protected IStringLocalizerFactory StringLocalizerFactory { get; } + protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } + protected ISimpleStateCheckerManager StateCheckerManager { get; } public ChatClientAgentFactory( IChatClientFactory chatClientFactory, IStringLocalizerFactory stringLocalizerFactory, - IWorkspaceDefinitionManager workspaceDefinitionManager) + IWorkspaceDefinitionManager workspaceDefinitionManager, + ISimpleStateCheckerManager stateCheckerManager) { - _chatClientFactory = chatClientFactory; - _stringLocalizerFactory = stringLocalizerFactory; - _workspaceDefinitionManager = workspaceDefinitionManager; + ChatClientFactory = chatClientFactory; + StringLocalizerFactory = stringLocalizerFactory; + WorkspaceDefinitionManager = workspaceDefinitionManager; + StateCheckerManager = stateCheckerManager; } public async virtual Task CreateAsync() @@ -33,14 +38,19 @@ public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDepende return chatClientAgent; } - var chatClient = await _chatClientFactory.CreateAsync(); + var chatClient = await ChatClientFactory.CreateAsync(); - var workspaceDefine = await _workspaceDefinitionManager.GetOrNullAsync(workspace); + var workspaceDefine = await WorkspaceDefinitionManager.GetOrNullAsync(workspace); + + if (workspaceDefine != null) + { + await CheckWorkspaceStateAsync(workspaceDefine); + } string? description = null; if (workspaceDefine?.Description != null) { - description = workspaceDefine.Description.Localize(_stringLocalizerFactory); + description = workspaceDefine.Description.Localize(StringLocalizerFactory); } chatClientAgent = chatClient.CreateAIAgent( @@ -59,13 +69,16 @@ public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDepende { return chatClientAgent; } - var workspaceDefine = await _workspaceDefinitionManager.GetAsync(workspace); - var chatClient = await _chatClientFactory.CreateAsync(workspace); + var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); + + await CheckWorkspaceStateAsync(workspaceDefine); + + var chatClient = await ChatClientFactory.CreateAsync(workspace); string? description = null; if (workspaceDefine.Description != null) { - description = workspaceDefine.Description.Localize(_stringLocalizerFactory); + description = workspaceDefine.Description.Localize(StringLocalizerFactory); } chatClientAgent = chatClient.CreateAIAgent( @@ -77,4 +90,15 @@ public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDepende return chatClientAgent; } + + protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) + { + if (!await StateCheckerManager.IsEnabledAsync(workspace)) + { + throw new AbpAuthorizationException( + $"Workspace is not enabled: {workspace.Name}!", + AbpAIErrorCodes.WorkspaceIsNotEnabled) + .WithData("Workspace", workspace.Name); + } + } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj index 1c67a9757..57d089b1c 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj @@ -14,6 +14,11 @@ + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs index bbdfa9c90..c9dd2e74c 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs @@ -5,21 +5,26 @@ using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AI; +using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; +using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AI; public class ChatClientFactory : IChatClientFactory, ISingletonDependency { private readonly static ConcurrentDictionary _chatClientCache = new(); + protected ISimpleStateCheckerManager StateCheckerManager { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected IChatClientProviderManager ChatClientProviderManager { get; } protected IServiceProvider ServiceProvider { get; } public ChatClientFactory( + ISimpleStateCheckerManager stateCheckerManager, IWorkspaceDefinitionManager workspaceDefinitionManager, IChatClientProviderManager chatClientProviderManager, IServiceProvider serviceProvider) { + StateCheckerManager = stateCheckerManager; WorkspaceDefinitionManager = workspaceDefinitionManager; ChatClientProviderManager = chatClientProviderManager; ServiceProvider = serviceProvider; @@ -58,6 +63,8 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); + await CheckWorkspaceStateAsync(workspaceDefine); + chatClient = await CreateChatClientAsync(workspaceDefine); _chatClientCache.TryAdd(workspace, chatClient); @@ -77,6 +84,17 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency return await provider.CreateAsync(workspace); } - throw new AbpException($"The ChatClient provider implementation named {workspace.Provider} was not found"); + throw new AbpException($"The ChatClient provider implementation named {workspace.Provider} was not found!"); + } + + protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) + { + if (!await StateCheckerManager.IsEnabledAsync(workspace)) + { + throw new AbpAuthorizationException( + $"Workspace is not enabled: {workspace.Name}!", + AbpAIErrorCodes.WorkspaceIsNotEnabled) + .WithData("Workspace", workspace.Name); + } } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs index 5709d9127..a7067651a 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs @@ -5,21 +5,26 @@ using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AI; +using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; +using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AI; public class KernelFactory : IKernelFactory, ISingletonDependency { private readonly static ConcurrentDictionary _kernelCache = new(); + protected ISimpleStateCheckerManager StateCheckerManager { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected IKernelProviderManager KernelProviderManager { get; } protected IServiceProvider ServiceProvider { get; } public KernelFactory( + ISimpleStateCheckerManager stateCheckerManager, IWorkspaceDefinitionManager workspaceDefinitionManager, IKernelProviderManager kernelProviderManager, IServiceProvider serviceProvider) { + StateCheckerManager = stateCheckerManager; WorkspaceDefinitionManager = workspaceDefinitionManager; KernelProviderManager = kernelProviderManager; ServiceProvider = serviceProvider; @@ -58,6 +63,8 @@ public class KernelFactory : IKernelFactory, ISingletonDependency var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); + await CheckWorkspaceStateAsync(workspaceDefine); + kernel = await CreateKernelAsync(workspaceDefine); _kernelCache.TryAdd(workspace, kernel); @@ -77,6 +84,16 @@ public class KernelFactory : IKernelFactory, ISingletonDependency return await provider.CreateAsync(workspace); } - throw new AbpException($"The Kernel provider implementation named {workspace.Provider} was not found"); + throw new AbpException($"The Kernel provider implementation named {workspace.Provider} was not found!"); + } + protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) + { + if (!await StateCheckerManager.IsEnabledAsync(workspace)) + { + throw new AbpAuthorizationException( + $"Workspace is not enabled: {workspace.Name}!", + AbpAIErrorCodes.WorkspaceIsNotEnabled) + .WithData("Workspace", workspace.Name); + } } } From 8bee13911a1846fc7474555e86d1253012008b37 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 15 Jan 2026 16:29:59 +0800 Subject: [PATCH 03/25] feat(ai): Add definition of workspace properties --- .../Abp/AI/Agent/ChatClientAgentFactory.cs | 60 ++++++++++--- .../Abp/AI/Agent/IChatClientAgentFactory.cs | 5 +- .../LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs | 59 +++++++++++++ .../LINGYUN/Abp/AI/ChatClientFactory.cs | 10 +-- .../LINGYUN/Abp/AI/ChatClientProvider.cs | 18 ++++ .../LINGYUN/Abp/AI/IChatClientFactory.cs | 5 +- .../LINGYUN/Abp/AI/IChatClientProvider.cs | 3 +- .../LINGYUN/Abp/AI/IWorkspaceChatClient.cs | 8 ++ .../Abp/AI/OpenAIChatClientProvider.cs | 24 ++++-- .../LINGYUN/Abp/AI/WorkspaceChatClient.cs | 69 +++++++++++++++ .../Abp/AI/Workspaces/WorkspaceDefinition.cs | 86 ++++++++++++++++++- 11 files changed, 313 insertions(+), 34 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs index 5f233f4e2..106e40768 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs @@ -2,6 +2,8 @@ using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp.AI; @@ -12,25 +14,27 @@ using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AI.Agent; public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDependency { - private readonly static ConcurrentDictionary _chatClientAgentCache = new(); - + private readonly static ConcurrentDictionary _chatClientAgentCache = new(); + protected IServiceProvider ServiceProvider { get; } protected IChatClientFactory ChatClientFactory { get; } protected IStringLocalizerFactory StringLocalizerFactory { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected ISimpleStateCheckerManager StateCheckerManager { get; } public ChatClientAgentFactory( + IServiceProvider serviceProvider, IChatClientFactory chatClientFactory, IStringLocalizerFactory stringLocalizerFactory, IWorkspaceDefinitionManager workspaceDefinitionManager, ISimpleStateCheckerManager stateCheckerManager) { + ServiceProvider = serviceProvider; ChatClientFactory = chatClientFactory; StringLocalizerFactory = stringLocalizerFactory; WorkspaceDefinitionManager = workspaceDefinitionManager; StateCheckerManager = stateCheckerManager; } - public async virtual Task CreateAsync() + public async virtual Task CreateAsync() { var workspace = WorkspaceNameAttribute.GetWorkspaceName(); if (_chatClientAgentCache.TryGetValue(workspace, out var chatClientAgent)) @@ -53,17 +57,33 @@ public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDepende description = workspaceDefine.Description.Localize(StringLocalizerFactory); } - chatClientAgent = chatClient.CreateAIAgent( - instructions: workspaceDefine?.SystemPrompt, - name: workspaceDefine?.Name, - description: description); + var clientAgentOptions = new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + Instructions = workspaceDefine?.Instructions, + Temperature = workspaceDefine?.Temperature, + MaxOutputTokens = workspaceDefine?.MaxOutputTokens, + PresencePenalty = workspaceDefine?.PresencePenalty, + FrequencyPenalty = workspaceDefine?.FrequencyPenalty, + }, + Name = workspaceDefine?.Name, + Description = description + }; + + chatClientAgent = new WorkspaceAIAgent( + new AIAgentBuilder(chatClient.CreateAIAgent(clientAgentOptions)) + .UseLogging() + .UseOpenTelemetry() + .Build(ServiceProvider), + workspaceDefine); _chatClientAgentCache.TryAdd(workspace, chatClientAgent); return chatClientAgent; } - public async virtual Task CreateAsync(string workspace) + public async virtual Task CreateAsync(string workspace) { if (_chatClientAgentCache.TryGetValue(workspace, out var chatClientAgent)) { @@ -81,10 +101,26 @@ public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDepende description = workspaceDefine.Description.Localize(StringLocalizerFactory); } - chatClientAgent = chatClient.CreateAIAgent( - instructions: workspaceDefine.SystemPrompt, - name: workspaceDefine.Name, - description: description); + var clientAgentOptions = new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + Instructions = workspaceDefine.Instructions, + Temperature = workspaceDefine.Temperature, + MaxOutputTokens = workspaceDefine.MaxOutputTokens, + PresencePenalty = workspaceDefine.PresencePenalty, + FrequencyPenalty = workspaceDefine.FrequencyPenalty, + }, + Name = workspaceDefine.Name, + Description = description + }; + + chatClientAgent = new WorkspaceAIAgent( + new AIAgentBuilder(chatClient.CreateAIAgent(clientAgentOptions)) + .UseLogging() + .UseOpenTelemetry() + .Build(ServiceProvider), + workspaceDefine); _chatClientAgentCache.TryAdd(workspace, chatClientAgent); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs index 0d15511fd..cf7f938ef 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs @@ -1,13 +1,12 @@ using JetBrains.Annotations; -using Microsoft.Agents.AI; using System.Threading.Tasks; namespace LINGYUN.Abp.AI.Agent; public interface IChatClientAgentFactory { [NotNull] - Task CreateAsync(); + Task CreateAsync(); [NotNull] - Task CreateAsync(string workspace); + Task CreateAsync(string workspace); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs new file mode 100644 index 000000000..41539ec09 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs @@ -0,0 +1,59 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Agent; +public class WorkspaceAIAgent : AIAgent +{ + protected AIAgent InnerAIAgent { get; } + public WorkspaceDefinition? Workspace { get; } + + public WorkspaceAIAgent( + AIAgent innerAIAgent, + WorkspaceDefinition? workspace = null) + { + InnerAIAgent = innerAIAgent; + Workspace = workspace; + } + + public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) + { + return InnerAIAgent.DeserializeThread(serializedThread, jsonSerializerOptions); + } + + public override AgentThread GetNewThread() + { + return InnerAIAgent.GetNewThread(); + } + + protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerAIAgent.RunAsync(GetChatMessages(messages), thread, options, cancellationToken); + } + + protected override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerAIAgent.RunStreamingAsync(GetChatMessages(messages), thread, options, cancellationToken); + } + + protected virtual IEnumerable GetChatMessages(IEnumerable messages) + { + if (Workspace?.SystemPrompt?.IsNullOrWhiteSpace() == false && + !messages.Any(msg => msg.Role == ChatRole.System)) + { + // 加入系统提示词 + messages = new List + { + new ChatMessage(ChatRole.System, Workspace.SystemPrompt) + }.Union(messages); + } + + return messages; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs index c9dd2e74c..04f59d3e3 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs @@ -12,7 +12,7 @@ using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AI; public class ChatClientFactory : IChatClientFactory, ISingletonDependency { - private readonly static ConcurrentDictionary _chatClientCache = new(); + private readonly static ConcurrentDictionary _chatClientCache = new(); protected ISimpleStateCheckerManager StateCheckerManager { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected IChatClientProviderManager ChatClientProviderManager { get; } @@ -30,7 +30,7 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency ServiceProvider = serviceProvider; } - public async virtual Task CreateAsync() + public async virtual Task CreateAsync() { var workspace = WorkspaceNameAttribute.GetWorkspaceName(); if (_chatClientCache.TryGetValue(workspace, out var chatClient)) @@ -44,7 +44,7 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency chatClientAccessor is IChatClientAccessor accessor && accessor.ChatClient != null) { - chatClient = accessor.ChatClient; + chatClient = new WorkspaceChatClient(accessor.ChatClient); _chatClientCache.TryAdd(workspace, chatClient); } else @@ -54,7 +54,7 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency return chatClient; } - public async virtual Task CreateAsync(string workspace) + public async virtual Task CreateAsync(string workspace) { if (_chatClientCache.TryGetValue(workspace, out var chatClient)) { @@ -72,7 +72,7 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency return chatClient; } - protected async virtual Task CreateChatClientAsync(WorkspaceDefinition workspace) + protected async virtual Task CreateChatClientAsync(WorkspaceDefinition workspace) { foreach (var provider in ChatClientProviderManager.Providers) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs new file mode 100644 index 000000000..34f536698 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs @@ -0,0 +1,18 @@ +using LINGYUN.Abp.AI.Workspaces; +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI; +public abstract class ChatClientProvider : IChatClientProvider, ITransientDependency +{ + public abstract string Name { get; } + + protected IServiceProvider ServiceProvider { get; } + protected ChatClientProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public abstract Task CreateAsync(WorkspaceDefinition workspace); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs index 3ab6e99bb..437197ebb 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs @@ -1,13 +1,12 @@ using JetBrains.Annotations; -using Microsoft.Extensions.AI; using System.Threading.Tasks; namespace LINGYUN.Abp.AI; public interface IChatClientFactory { [NotNull] - Task CreateAsync(); + Task CreateAsync(); [NotNull] - Task CreateAsync(string workspace); + Task CreateAsync(string workspace); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs index bdf5aeeb7..ebde49eec 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs @@ -1,5 +1,4 @@ using LINGYUN.Abp.AI.Workspaces; -using Microsoft.Extensions.AI; using System.Threading.Tasks; namespace LINGYUN.Abp.AI; @@ -7,5 +6,5 @@ public interface IChatClientProvider { string Name { get; } - Task CreateAsync(WorkspaceDefinition workspace); + Task CreateAsync(WorkspaceDefinition workspace); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs new file mode 100644 index 000000000..04592ec53 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs @@ -0,0 +1,8 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.AI; + +namespace LINGYUN.Abp.AI; +public interface IWorkspaceChatClient : IChatClient +{ + WorkspaceDefinition? Workspace { get; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs index f8d145334..6574c47a4 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs @@ -5,17 +5,20 @@ using System; using System.ClientModel; using System.Threading.Tasks; using Volo.Abp; -using Volo.Abp.DependencyInjection; namespace LINGYUN.Abp.AI; -public class OpenAIChatClientProvider : IChatClientProvider, ITransientDependency +public class OpenAIChatClientProvider : ChatClientProvider { private const string DefaultEndpoint = "https://api.openai.com/v1"; public const string ProviderName = "OpenAI"; - public virtual string Name => ProviderName; + public override string Name => ProviderName; + public OpenAIChatClientProvider(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } - public virtual Task CreateAsync(WorkspaceDefinition workspace) + public override Task CreateAsync(WorkspaceDefinition workspace) { Check.NotNull(workspace, nameof(workspace)); Check.NotNullOrWhiteSpace(workspace.ApiKey, nameof(WorkspaceDefinition.ApiKey)); @@ -29,8 +32,17 @@ public class OpenAIChatClientProvider : IChatClientProvider, ITransientDependenc var chatClient = openAIClient .GetChatClient(workspace.ModelName) - .AsIChatClient(); + .AsIChatClient() + .AsBuilder() + .UseLogging() + .UseOpenTelemetry() + .UseFunctionInvocation() + .UseDistributedCache() + .UseChatReducer() + .Build(ServiceProvider); + + IWorkspaceChatClient workspaceChatClient = new WorkspaceChatClient(chatClient, workspace); - return Task.FromResult(chatClient); + return Task.FromResult(workspaceChatClient); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs new file mode 100644 index 000000000..cc0563249 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs @@ -0,0 +1,69 @@ +using LINGYUN.Abp.AI.Tools; +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.AI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI; +public class WorkspaceChatClient : IWorkspaceChatClient +{ + public IChatClient Client { get; } + public WorkspaceDefinition? Workspace { get; } + public WorkspaceChatClient( + IChatClient chatClient, + WorkspaceDefinition? workspace = null) + { + Workspace = workspace; + Client = chatClient; + } + + public virtual Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + return Client.GetResponseAsync(GetChatMessages(messages), GetChatOptions(options), cancellationToken); + } + + public virtual IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + return Client.GetStreamingResponseAsync(GetChatMessages(messages), GetChatOptions(options), cancellationToken); + } + + public virtual object? GetService(Type serviceType, object? serviceKey = null) + { + return Client.GetService(serviceType, serviceKey); + } + + public virtual void Dispose() + { + Client.Dispose(); + } + + protected virtual ChatOptions GetChatOptions(ChatOptions? options) + { + options ??= new ChatOptions(); + options.Instructions = Workspace?.Instructions; + options.Temperature ??= Workspace?.Temperature; + options.MaxOutputTokens ??= Workspace?.MaxOutputTokens; + options.FrequencyPenalty ??= Workspace?.FrequencyPenalty; + options.PresencePenalty ??= Workspace?.PresencePenalty; + + return options; + } + + protected virtual IEnumerable GetChatMessages(IEnumerable messages) + { + if (Workspace?.SystemPrompt?.IsNullOrWhiteSpace() == false && + !messages.Any(msg => msg.Role == ChatRole.System)) + { + // 加入系统提示词 + messages = new List + { + new ChatMessage(ChatRole.System, Workspace.SystemPrompt) + }.Union(messages); + } + + return messages; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs index 56f37e127..2f3288616 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs @@ -41,16 +41,84 @@ public class WorkspaceDefinition : IHasSimpleStateCheckers /// /// API 身份验证密钥 /// - public string? ApiKey { get; set; } + public string? ApiKey { get; private set; } /// /// 自定义端点 URL /// - public string? ApiBaseUrl { get; set; } + public string? ApiBaseUrl { get; private set; } /// /// 系统提示词 /// public string? SystemPrompt { get; set; } /// + /// 附加系统提示词 + /// + public string? Instructions { get; set; } + /// + /// 聊天回复时所依据的温度值, 为空时由模型提供者决定默认值 + /// + /// + /// 范围在 0 到 2 之间, 数值越高(比如 0.8)会使输出更加随机,而数值越低(比如 0.2)则会使输出更加集中且更具确定性 + /// + public float? Temperature { + get => _temperature; + set { + if (value.HasValue) + { + _temperature = Check.Range(value.Value, nameof(value), 0, 2); + } + else + { + _temperature = value; + } + } + } + private float? _temperature; + /// + /// 限制一次请求中模型生成 completion 的最大 token 数 + /// + public int? MaxOutputTokens { get; set; } + /// + /// 介于 -2.0 和 2.0 之间的数字 + /// + /// + /// 如果该值为正,那么新 token 会根据其在已有文本中的出现频率受到相应的惩罚,降低模型重复相同内容的可能性 + /// + public float? FrequencyPenalty { + get => _frequencyPenalty; + set { + if (value.HasValue) + { + _frequencyPenalty = Check.Range(value.Value, nameof(value), -2, 2); + } + else + { + _frequencyPenalty = value; + } + } + } + private float? _frequencyPenalty; + /// + /// 介于 -2.0 和 2.0 之间的数字 + /// + /// + /// 如果该值为正,那么新 token 会根据其是否已在已有文本中出现受到相应的惩罚,从而增加模型谈论新主题的可能性 + /// + public float? PresencePenalty { + get => _presencePenalty; + set { + if (value.HasValue) + { + _presencePenalty = Check.Range(value.Value, nameof(value), -2, 2); + } + else + { + _presencePenalty = value; + } + } + } + private float? _presencePenalty; + /// /// 启用/禁用工作区 /// public bool IsEnabled { get; set; } @@ -65,7 +133,13 @@ public class WorkspaceDefinition : IHasSimpleStateCheckers string provider, string modelName, ILocalizableString displayName, - ILocalizableString? description = null) + ILocalizableString? description = null, + string? systemPrompt = null, + string? instructions = null, + float? temperature = null, + int? maxOutputTokens = null, + float? frequencyPenalty = null, + float? presencePenalty = null) { Name = name; Provider = provider; @@ -73,6 +147,12 @@ public class WorkspaceDefinition : IHasSimpleStateCheckers _displayName = displayName; _displayName = displayName; Description = description; + SystemPrompt = systemPrompt; + Instructions = instructions; + Temperature = temperature; + MaxOutputTokens = maxOutputTokens; + FrequencyPenalty = frequencyPenalty; + PresencePenalty = presencePenalty; IsEnabled = true; Properties = new Dictionary(); From 1d35fc6628f0b346592c7e14a2934d5f5ddc3874 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 15 Jan 2026 20:25:58 +0800 Subject: [PATCH 04/25] feat(ai): Add simple conversation service --- .../LINGYUN/Abp/AI/Agent/AgentFactory.cs | 110 ++++++++++++++ .../LINGYUN/Abp/AI/Agent/AgentService.cs | 91 ++++++++++++ .../Abp/AI/Agent/ChatClientAgentFactory.cs | 140 ------------------ .../LINGYUN/Abp/AI/Agent/IAgentFactory.cs | 13 ++ .../LINGYUN/Abp/AI/Agent/IAgentService.cs | 8 + .../Abp/AI/Agent/IChatClientAgentFactory.cs | 12 -- .../LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs | 38 ++--- .../LINGYUN/Abp/AI/AbpAICoreModule.cs | 1 + .../LINGYUN/Abp/AI/ChatClientFactory.cs | 35 +---- .../LINGYUN/Abp/AI/ChatClientProvider.cs | 3 +- .../LINGYUN/Abp/AI/IChatClientFactory.cs | 5 +- .../LINGYUN/Abp/AI/IChatClientProvider.cs | 3 +- .../LINGYUN/Abp/AI/IWorkspaceChatClient.cs | 8 - .../LINGYUN/Abp/AI/KernelFactory.cs | 28 +--- .../LINGYUN/Abp/AI/KernelProvider.cs | 19 +++ .../Abp/AI/Messages/IUserMessageStore.cs | 11 ++ .../AI/Messages/InMemoryUserMessageStore.cs | 40 +++++ .../LINGYUN/Abp/AI/Models/ChatMessageInfo.cs | 8 + .../LINGYUN/Abp/AI/Models/MediaMessage.cs | 13 ++ .../LINGYUN/Abp/AI/Models/TokenUsageInfo.cs | 17 +++ .../LINGYUN/Abp/AI/Models/UserMessage.cs | 37 +++++ .../Abp/AI/OpenAIChatClientProvider.cs | 6 +- .../LINGYUN/Abp/AI/OpenAIKernelProvider.cs | 39 +++++ .../LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs | 9 ++ .../Abp/AI/Tokens/InMemoryTokenUsageStore.cs | 50 +++++++ .../LINGYUN/Abp/AI/Tools/GlobalFunctions.cs | 11 ++ .../LINGYUN/Abp/AI/WorkspaceChatClient.cs | 69 --------- 27 files changed, 517 insertions(+), 307 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentFactory.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProvider.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/MediaMessage.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIKernelProvider.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tools/GlobalFunctions.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs new file mode 100644 index 000000000..3739fdfd3 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs @@ -0,0 +1,110 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Localization; +using System; +using System.Threading.Tasks; +using Volo.Abp.AI; +using Volo.Abp.Authorization; +using Volo.Abp.DependencyInjection; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AI.Agent; +public class AgentFactory : IAgentFactory, IScopedDependency +{ + protected IServiceProvider ServiceProvider { get; } + protected IChatClientFactory ChatClientFactory { get; } + protected IStringLocalizerFactory StringLocalizerFactory { get; } + protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } + protected ISimpleStateCheckerManager StateCheckerManager { get; } + public AgentFactory( + IServiceProvider serviceProvider, + IChatClientFactory chatClientFactory, + IStringLocalizerFactory stringLocalizerFactory, + IWorkspaceDefinitionManager workspaceDefinitionManager, + ISimpleStateCheckerManager stateCheckerManager) + { + ServiceProvider = serviceProvider; + ChatClientFactory = chatClientFactory; + StringLocalizerFactory = stringLocalizerFactory; + WorkspaceDefinitionManager = workspaceDefinitionManager; + StateCheckerManager = stateCheckerManager; + } + + public async virtual Task CreateAsync() + { + var workspace = WorkspaceNameAttribute.GetWorkspaceName(); + + var chatClient = await ChatClientFactory.CreateAsync(); + + var workspaceDefine = await WorkspaceDefinitionManager.GetOrNullAsync(workspace); + + if (workspaceDefine != null) + { + await CheckWorkspaceStateAsync(workspaceDefine); + } + + return await CreateAgentAsync(chatClient, workspaceDefine); + } + + public async virtual Task CreateAsync(string workspace) + { + var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); + + await CheckWorkspaceStateAsync(workspaceDefine); + + var chatClient = await ChatClientFactory.CreateAsync(workspace); + + return await CreateAgentAsync(chatClient, workspaceDefine); + } + + protected async virtual Task CreateAgentAsync(IChatClient chatClient, WorkspaceDefinition? workspace) + { + string? description = null; + if (workspace?.Description != null) + { + description = workspace.Description.Localize(StringLocalizerFactory); + } + + var tools = await GetAgentToolsAsync(workspace); + + var clientAgentOptions = new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + Instructions = workspace?.Instructions, + Temperature = workspace?.Temperature, + MaxOutputTokens = workspace?.MaxOutputTokens, + PresencePenalty = workspace?.PresencePenalty, + FrequencyPenalty = workspace?.FrequencyPenalty, + Tools = tools, + }, + Name = workspace?.Name, + Description = description + }; + + var aiAgent = chatClient.CreateAIAgent(clientAgentOptions) + .AsBuilder() + .UseLogging() + .UseOpenTelemetry() + .Build(ServiceProvider); + + return new WorkspaceAIAgent(aiAgent, workspace); + } + + protected virtual Task GetAgentToolsAsync(WorkspaceDefinition? workspace) + { + return Task.FromResult([]); + } + + protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) + { + if (!await StateCheckerManager.IsEnabledAsync(workspace)) + { + throw new AbpAuthorizationException( + $"Workspace is not enabled: {workspace.Name}!", + AbpAIErrorCodes.WorkspaceIsNotEnabled) + .WithData("Workspace", workspace.Name); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs new file mode 100644 index 000000000..ed34c4200 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs @@ -0,0 +1,91 @@ +using LINGYUN.Abp.AI.Messages; +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AI.Tokens; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Agent; +public class AgentService : IAgentService, IScopedDependency +{ + private readonly IAgentFactory _agentFactory; + private readonly ITokenUsageStore _tokenUsageStore; + private readonly IUserMessageStore _userMessageStore; + public AgentService( + IAgentFactory agentFactory, + ITokenUsageStore tokenUsageStore, + IUserMessageStore userMessageStore) + { + _agentFactory = agentFactory; + _tokenUsageStore = tokenUsageStore; + _userMessageStore = userMessageStore; + } + + public async virtual IAsyncEnumerable SendMessageAsync(UserMessage message) + { + var messages = await BuildChatMessages(message); + + var agent = await _agentFactory.CreateAsync(message.Workspace); + + var agentRunRes = agent.RunStreamingAsync(messages); + + var agentMessageBuilder = new StringBuilder(); + + await foreach (var item in agentRunRes) + { + agentMessageBuilder.Append(item); + yield return item.Text; + + await StoreTokenUsageInfo(message, item.RawRepresentation); + } + + await StoreChatMessage(message, agentMessageBuilder.ToString()); + } + + protected virtual async Task> BuildChatMessages(UserMessage message) + { + var messages = new List(); + + var historyMessages = await _userMessageStore.GetHistoryMessagesAsync(message.ChatId); + + foreach (var chatMessage in historyMessages) + { + messages.Add(new ChatMessage(ChatRole.System, chatMessage.Content)); + } + + messages.Add(new ChatMessage(ChatRole.User, message.Content)); + + return messages; + } + + protected async virtual Task StoreChatMessage(UserMessage message, string agentMessage) + { + message.WithReply(agentMessage); + + await _userMessageStore.SaveMessageAsync(message); + } + + protected async virtual Task StoreTokenUsageInfo(UserMessage message, object? rawRepresentation) + { + if (rawRepresentation is ChatResponseUpdate update) + { + var tokenUsageInfos = update.Contents + .OfType() + .Where(usage => usage.Details != null) + .Select(usage => new TokenUsageInfo(message.Workspace) + { + TotalTokenCount = usage.Details.TotalTokenCount, + CachedInputTokenCount = usage.Details.CachedInputTokenCount, + InputTokenCount = usage.Details.InputTokenCount, + AdditionalCounts = usage.Details.AdditionalCounts, + OutputTokenCount = usage.Details.OutputTokenCount, + ReasoningTokenCount = usage.Details.ReasoningTokenCount, + }); + + await _tokenUsageStore.SaveTokenUsagesAsync(tokenUsageInfos); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs deleted file mode 100644 index 106e40768..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs +++ /dev/null @@ -1,140 +0,0 @@ -using LINGYUN.Abp.AI.Workspaces; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using Volo.Abp.AI; -using Volo.Abp.Authorization; -using Volo.Abp.DependencyInjection; -using Volo.Abp.SimpleStateChecking; - -namespace LINGYUN.Abp.AI.Agent; -public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDependency -{ - private readonly static ConcurrentDictionary _chatClientAgentCache = new(); - protected IServiceProvider ServiceProvider { get; } - protected IChatClientFactory ChatClientFactory { get; } - protected IStringLocalizerFactory StringLocalizerFactory { get; } - protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } - protected ISimpleStateCheckerManager StateCheckerManager { get; } - public ChatClientAgentFactory( - IServiceProvider serviceProvider, - IChatClientFactory chatClientFactory, - IStringLocalizerFactory stringLocalizerFactory, - IWorkspaceDefinitionManager workspaceDefinitionManager, - ISimpleStateCheckerManager stateCheckerManager) - { - ServiceProvider = serviceProvider; - ChatClientFactory = chatClientFactory; - StringLocalizerFactory = stringLocalizerFactory; - WorkspaceDefinitionManager = workspaceDefinitionManager; - StateCheckerManager = stateCheckerManager; - } - - public async virtual Task CreateAsync() - { - var workspace = WorkspaceNameAttribute.GetWorkspaceName(); - if (_chatClientAgentCache.TryGetValue(workspace, out var chatClientAgent)) - { - return chatClientAgent; - } - - var chatClient = await ChatClientFactory.CreateAsync(); - - var workspaceDefine = await WorkspaceDefinitionManager.GetOrNullAsync(workspace); - - if (workspaceDefine != null) - { - await CheckWorkspaceStateAsync(workspaceDefine); - } - - string? description = null; - if (workspaceDefine?.Description != null) - { - description = workspaceDefine.Description.Localize(StringLocalizerFactory); - } - - var clientAgentOptions = new ChatClientAgentOptions - { - ChatOptions = new ChatOptions - { - Instructions = workspaceDefine?.Instructions, - Temperature = workspaceDefine?.Temperature, - MaxOutputTokens = workspaceDefine?.MaxOutputTokens, - PresencePenalty = workspaceDefine?.PresencePenalty, - FrequencyPenalty = workspaceDefine?.FrequencyPenalty, - }, - Name = workspaceDefine?.Name, - Description = description - }; - - chatClientAgent = new WorkspaceAIAgent( - new AIAgentBuilder(chatClient.CreateAIAgent(clientAgentOptions)) - .UseLogging() - .UseOpenTelemetry() - .Build(ServiceProvider), - workspaceDefine); - - _chatClientAgentCache.TryAdd(workspace, chatClientAgent); - - return chatClientAgent; - } - - public async virtual Task CreateAsync(string workspace) - { - if (_chatClientAgentCache.TryGetValue(workspace, out var chatClientAgent)) - { - return chatClientAgent; - } - var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); - - await CheckWorkspaceStateAsync(workspaceDefine); - - var chatClient = await ChatClientFactory.CreateAsync(workspace); - - string? description = null; - if (workspaceDefine.Description != null) - { - description = workspaceDefine.Description.Localize(StringLocalizerFactory); - } - - var clientAgentOptions = new ChatClientAgentOptions - { - ChatOptions = new ChatOptions - { - Instructions = workspaceDefine.Instructions, - Temperature = workspaceDefine.Temperature, - MaxOutputTokens = workspaceDefine.MaxOutputTokens, - PresencePenalty = workspaceDefine.PresencePenalty, - FrequencyPenalty = workspaceDefine.FrequencyPenalty, - }, - Name = workspaceDefine.Name, - Description = description - }; - - chatClientAgent = new WorkspaceAIAgent( - new AIAgentBuilder(chatClient.CreateAIAgent(clientAgentOptions)) - .UseLogging() - .UseOpenTelemetry() - .Build(ServiceProvider), - workspaceDefine); - - _chatClientAgentCache.TryAdd(workspace, chatClientAgent); - - return chatClientAgent; - } - - protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) - { - if (!await StateCheckerManager.IsEnabledAsync(workspace)) - { - throw new AbpAuthorizationException( - $"Workspace is not enabled: {workspace.Name}!", - AbpAIErrorCodes.WorkspaceIsNotEnabled) - .WithData("Workspace", workspace.Name); - } - } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentFactory.cs new file mode 100644 index 000000000..b969e8f67 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentFactory.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using Microsoft.Agents.AI; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Agent; +public interface IAgentFactory +{ + [NotNull] + Task CreateAsync(); + + [NotNull] + Task CreateAsync(string workspace); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs new file mode 100644 index 000000000..24ad9e19f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs @@ -0,0 +1,8 @@ +using LINGYUN.Abp.AI.Models; +using System.Collections.Generic; + +namespace LINGYUN.Abp.AI.Agent; +public interface IAgentService +{ + IAsyncEnumerable SendMessageAsync(UserMessage message); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs deleted file mode 100644 index cf7f938ef..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -using JetBrains.Annotations; -using System.Threading.Tasks; - -namespace LINGYUN.Abp.AI.Agent; -public interface IChatClientAgentFactory -{ - [NotNull] - Task CreateAsync(); - - [NotNull] - Task CreateAsync(string workspace); -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs index 41539ec09..03998df61 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs @@ -11,49 +11,49 @@ using System.Threading.Tasks; namespace LINGYUN.Abp.AI.Agent; public class WorkspaceAIAgent : AIAgent { - protected AIAgent InnerAIAgent { get; } - public WorkspaceDefinition? Workspace { get; } - - public WorkspaceAIAgent( - AIAgent innerAIAgent, - WorkspaceDefinition? workspace = null) + protected AIAgent InnerAgent { get; } + protected WorkspaceDefinition? Workspace { get; } + public WorkspaceAIAgent(AIAgent innerAgent, WorkspaceDefinition? workspace) { - InnerAIAgent = innerAIAgent; + InnerAgent = innerAgent; Workspace = workspace; } public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) { - return InnerAIAgent.DeserializeThread(serializedThread, jsonSerializerOptions); + return InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions); } public override AgentThread GetNewThread() { - return InnerAIAgent.GetNewThread(); + return InnerAgent.GetNewThread(); } protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - return InnerAIAgent.RunAsync(GetChatMessages(messages), thread, options, cancellationToken); + return InnerAgent.RunAsync(GetChatMessages(messages), thread, options, cancellationToken); } protected override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - return InnerAIAgent.RunStreamingAsync(GetChatMessages(messages), thread, options, cancellationToken); + return InnerAgent.RunStreamingAsync(GetChatMessages(messages), thread, options, cancellationToken); } protected virtual IEnumerable GetChatMessages(IEnumerable messages) { - if (Workspace?.SystemPrompt?.IsNullOrWhiteSpace() == false && - !messages.Any(msg => msg.Role == ChatRole.System)) + var unionMessages = new List(); + + if (Workspace != null) { - // 加入系统提示词 - messages = new List + if (!Workspace.SystemPrompt.IsNullOrWhiteSpace()) { - new ChatMessage(ChatRole.System, Workspace.SystemPrompt) - }.Union(messages); + unionMessages.Add(new ChatMessage(ChatRole.System, Workspace.SystemPrompt)); + } + if (!Workspace.Instructions.IsNullOrWhiteSpace()) + { + unionMessages.Add(new ChatMessage(ChatRole.System, Workspace.Instructions)); + } } - - return messages; + return unionMessages.Union(messages); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs index 8bdcf3754..3f81bd409 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs @@ -38,6 +38,7 @@ public class AbpAICoreModule : AbpModule Configure(options => { options.ChatClientProviders.Add(); + options.KernelProviders.Add(); }); Configure(options => diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs index 04f59d3e3..3f5280dee 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs @@ -1,7 +1,6 @@ using LINGYUN.Abp.AI.Workspaces; using Microsoft.Extensions.AI; using System; -using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AI; @@ -10,9 +9,8 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AI; -public class ChatClientFactory : IChatClientFactory, ISingletonDependency +public class ChatClientFactory : IChatClientFactory, IScopedDependency { - private readonly static ConcurrentDictionary _chatClientCache = new(); protected ISimpleStateCheckerManager StateCheckerManager { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected IChatClientProviderManager ChatClientProviderManager { get; } @@ -30,13 +28,9 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency ServiceProvider = serviceProvider; } - public async virtual Task CreateAsync() + public async virtual Task CreateAsync() { var workspace = WorkspaceNameAttribute.GetWorkspaceName(); - if (_chatClientCache.TryGetValue(workspace, out var chatClient)) - { - return chatClient; - } var chatClientAccessorType = typeof(IChatClientAccessor<>).MakeGenericType(typeof(TWorkspace)); var chatClientAccessor = ServiceProvider.GetService(chatClientAccessorType); @@ -44,35 +38,22 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency chatClientAccessor is IChatClientAccessor accessor && accessor.ChatClient != null) { - chatClient = new WorkspaceChatClient(accessor.ChatClient); - _chatClientCache.TryAdd(workspace, chatClient); + return accessor.ChatClient; } - else - { - chatClient = await CreateAsync(workspace); - } - return chatClient; + + return await CreateAsync(workspace); } - public async virtual Task CreateAsync(string workspace) + public async virtual Task CreateAsync(string workspace) { - if (_chatClientCache.TryGetValue(workspace, out var chatClient)) - { - return chatClient; - } - var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); await CheckWorkspaceStateAsync(workspaceDefine); - chatClient = await CreateChatClientAsync(workspaceDefine); - - _chatClientCache.TryAdd(workspace, chatClient); - - return chatClient; + return await CreateChatClientAsync(workspaceDefine); } - protected async virtual Task CreateChatClientAsync(WorkspaceDefinition workspace) + protected async virtual Task CreateChatClientAsync(WorkspaceDefinition workspace) { foreach (var provider in ChatClientProviderManager.Providers) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs index 34f536698..c15b7875f 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.AI; using System; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -14,5 +15,5 @@ public abstract class ChatClientProvider : IChatClientProvider, ITransientDepend ServiceProvider = serviceProvider; } - public abstract Task CreateAsync(WorkspaceDefinition workspace); + public abstract Task CreateAsync(WorkspaceDefinition workspace); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs index 437197ebb..3ab6e99bb 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientFactory.cs @@ -1,12 +1,13 @@ using JetBrains.Annotations; +using Microsoft.Extensions.AI; using System.Threading.Tasks; namespace LINGYUN.Abp.AI; public interface IChatClientFactory { [NotNull] - Task CreateAsync(); + Task CreateAsync(); [NotNull] - Task CreateAsync(string workspace); + Task CreateAsync(string workspace); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs index ebde49eec..bdf5aeeb7 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IChatClientProvider.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.AI; using System.Threading.Tasks; namespace LINGYUN.Abp.AI; @@ -6,5 +7,5 @@ public interface IChatClientProvider { string Name { get; } - Task CreateAsync(WorkspaceDefinition workspace); + Task CreateAsync(WorkspaceDefinition workspace); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs deleted file mode 100644 index 04592ec53..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/IWorkspaceChatClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using LINGYUN.Abp.AI.Workspaces; -using Microsoft.Extensions.AI; - -namespace LINGYUN.Abp.AI; -public interface IWorkspaceChatClient : IChatClient -{ - WorkspaceDefinition? Workspace { get; } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs index a7067651a..5778e7921 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs @@ -1,7 +1,6 @@ using LINGYUN.Abp.AI.Workspaces; using Microsoft.SemanticKernel; using System; -using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AI; @@ -10,9 +9,8 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AI; -public class KernelFactory : IKernelFactory, ISingletonDependency +public class KernelFactory : IKernelFactory, IScopedDependency { - private readonly static ConcurrentDictionary _kernelCache = new(); protected ISimpleStateCheckerManager StateCheckerManager { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected IKernelProviderManager KernelProviderManager { get; } @@ -33,10 +31,6 @@ public class KernelFactory : IKernelFactory, ISingletonDependency public async virtual Task CreateAsync() { var workspace = WorkspaceNameAttribute.GetWorkspaceName(); - if (_kernelCache.TryGetValue(workspace, out var kernel)) - { - return kernel; - } var kernelAccessorType = typeof(IKernelAccessor<>).MakeGenericType(typeof(TWorkspace)); var kernelAccessor = ServiceProvider.GetService(kernelAccessorType); @@ -44,32 +38,18 @@ public class KernelFactory : IKernelFactory, ISingletonDependency kernelAccessor is IKernelAccessor accessor && accessor.Kernel != null) { - kernel = accessor.Kernel; - _kernelCache.TryAdd(workspace, kernel); + return accessor.Kernel; } - else - { - kernel = await CreateAsync(workspace); - } - return kernel; + return await CreateAsync(workspace); } public async virtual Task CreateAsync(string workspace) { - if (_kernelCache.TryGetValue(workspace, out var kernel)) - { - return kernel; - } - var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); await CheckWorkspaceStateAsync(workspaceDefine); - kernel = await CreateKernelAsync(workspaceDefine); - - _kernelCache.TryAdd(workspace, kernel); - - return kernel; + return await CreateKernelAsync(workspaceDefine); } protected async virtual Task CreateKernelAsync(WorkspaceDefinition workspace) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProvider.cs new file mode 100644 index 000000000..957f2a43c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProvider.cs @@ -0,0 +1,19 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.SemanticKernel; +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI; +public abstract class KernelProvider : IKernelProvider, ITransientDependency +{ + public abstract string Name { get; } + + protected IServiceProvider ServiceProvider { get; } + protected KernelProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public abstract Task CreateAsync(WorkspaceDefinition workspace); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs new file mode 100644 index 000000000..f9d8baf54 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.AI.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Messages; +public interface IUserMessageStore +{ + Task SaveMessageAsync(UserMessage message); + + Task> GetHistoryMessagesAsync(string chatId); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs new file mode 100644 index 000000000..2492a51ae --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs @@ -0,0 +1,40 @@ +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Messages; + +[Dependency(ServiceLifetime.Singleton, TryRegister = true)] +public class InMemoryUserMessageStore : IUserMessageStore +{ + private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); + + public Task> GetHistoryMessagesAsync(string chatId) + { + if (_userMessageCache.TryGetValue(chatId, out var messages)) + { + return Task.FromResult(messages.Take(5)); + } + + return Task.FromResult>(Array.Empty()); + } + + public Task SaveMessageAsync(UserMessage message) + { + if (_userMessageCache.ContainsKey(message.ChatId)) + { + _userMessageCache[message.ChatId].Add(message); + } + else + { + _userMessageCache[message.ChatId] = new List() { message }; + } + + return Task.FromResult(message.Id); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs new file mode 100644 index 000000000..279c9ba9b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LINGYUN.Abp.AI.Models; +internal class ChatMessageInfo +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/MediaMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/MediaMessage.cs new file mode 100644 index 000000000..f851d303f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/MediaMessage.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Content; + +namespace LINGYUN.Abp.AI.Models; +public class MediaMessage +{ + public string Id { get; } + public IRemoteStreamContent Content { get; } + public MediaMessage(string id, IRemoteStreamContent content) + { + Id = id; + Content = content; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs new file mode 100644 index 000000000..cdac08043 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace LINGYUN.Abp.AI.Models; +public class TokenUsageInfo +{ + public string Workspace { get; } + public long? InputTokenCount { get; set; } + public long? OutputTokenCount { get; set; } + public long? TotalTokenCount { get; set; } + public long? CachedInputTokenCount { get; set; } + public long? ReasoningTokenCount { get; set; } + public IDictionary? AdditionalCounts { get; set; } + public TokenUsageInfo(string workspace) + { + Workspace = workspace; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs new file mode 100644 index 000000000..f902b0c67 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs @@ -0,0 +1,37 @@ +using System.Linq; + +namespace LINGYUN.Abp.AI.Models; +public class UserMessage +{ + public string Id { get; } + public string ChatId { get; } + public string Content { get; } + public string Workspace { get; } + public string ReplyMessage { get; private set; } + public MediaMessage[]? Medias { get; private set; } + public UserMessage( + string workspace, + string id, + string chatId, + string content) + { + Workspace = workspace; + Id = id; + ChatId = chatId; + Content = content; + } + + public UserMessage WithMedia(MediaMessage media) + { + Medias ??= []; + Medias = Medias.Union([media]).ToArray(); + + return this; + } + + public UserMessage WithReply(string replyMessage) + { + ReplyMessage = replyMessage; + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs index 6574c47a4..d13ac08a1 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs @@ -18,7 +18,7 @@ public class OpenAIChatClientProvider : ChatClientProvider { } - public override Task CreateAsync(WorkspaceDefinition workspace) + public override Task CreateAsync(WorkspaceDefinition workspace) { Check.NotNull(workspace, nameof(workspace)); Check.NotNullOrWhiteSpace(workspace.ApiKey, nameof(WorkspaceDefinition.ApiKey)); @@ -41,8 +41,6 @@ public class OpenAIChatClientProvider : ChatClientProvider .UseChatReducer() .Build(ServiceProvider); - IWorkspaceChatClient workspaceChatClient = new WorkspaceChatClient(chatClient, workspace); - - return Task.FromResult(workspaceChatClient); + return Task.FromResult(chatClient); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIKernelProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIKernelProvider.cs new file mode 100644 index 000000000..2f1bb8f61 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIKernelProvider.cs @@ -0,0 +1,39 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.SemanticKernel; +using OpenAI; +using System; +using System.ClientModel; +using System.Threading.Tasks; +using Volo.Abp; + +namespace LINGYUN.Abp.AI; +public class OpenAIKernelProvider : KernelProvider +{ + private const string DefaultEndpoint = "https://api.openai.com/v1"; + + public const string ProviderName = "OpenAI"; + public override string Name => ProviderName; + public OpenAIKernelProvider(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + public override Task CreateAsync(WorkspaceDefinition workspace) + { + Check.NotNull(workspace, nameof(workspace)); + Check.NotNullOrWhiteSpace(workspace.ApiKey, nameof(WorkspaceDefinition.ApiKey)); + + var openAIClient = new OpenAIClient( + new ApiKeyCredential(workspace.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(workspace.ApiBaseUrl ?? DefaultEndpoint), + }); + + var kernel = Kernel.CreateBuilder() + .AddOpenAIChatClient(workspace.ModelName, openAIClient) + .Build(); + + return Task.FromResult(kernel); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs new file mode 100644 index 000000000..32b4d0718 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs @@ -0,0 +1,9 @@ +using LINGYUN.Abp.AI.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Tokens; +public interface ITokenUsageStore +{ + Task SaveTokenUsagesAsync(IEnumerable usageInfos); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs new file mode 100644 index 000000000..621f39c06 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs @@ -0,0 +1,50 @@ +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Tokens; + +[Dependency(ServiceLifetime.Singleton, TryRegister = true)] +public class InMemoryTokenUsageStore : ITokenUsageStore +{ + private static readonly ConcurrentDictionary _tokenUsageCache = new ConcurrentDictionary(); + + public Task SaveTokenUsagesAsync(IEnumerable usageInfos) + { + foreach (var usageInfo in usageInfos.GroupBy(x => x.Workspace)) + { + var tokenUsageInfo = new TokenUsageInfo(usageInfo.Key) + { + InputTokenCount = usageInfo.Sum(x => x.InputTokenCount), + TotalTokenCount = usageInfo.Sum(x => x.TotalTokenCount), + OutputTokenCount = usageInfo.Sum(x => x.OutputTokenCount), + ReasoningTokenCount = usageInfo.Sum(x => x.ReasoningTokenCount), + CachedInputTokenCount = usageInfo.Sum(x => x.CachedInputTokenCount), + }; + tokenUsageInfo.AdditionalCounts ??= new Dictionary(); + foreach (var item in usageInfo) + { + if (item.AdditionalCounts == null) + { + continue; + } + tokenUsageInfo.AdditionalCounts.AddIfNotContains(item.AdditionalCounts); + } + + if (!_tokenUsageCache.ContainsKey(usageInfo.Key)) + { + _tokenUsageCache.TryAdd(usageInfo.Key, tokenUsageInfo); + } + else + { + _tokenUsageCache[usageInfo.Key] = tokenUsageInfo; + } + } + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tools/GlobalFunctions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tools/GlobalFunctions.cs new file mode 100644 index 000000000..cc52e1fd7 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tools/GlobalFunctions.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.AI; +using System; + +namespace LINGYUN.Abp.AI.Tools; +public static class GlobalFunctions +{ + public static AITool Now => AIFunctionFactory.Create( + () => DateTime.Now, + nameof(Now), + "Get now time"); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs deleted file mode 100644 index cc0563249..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/WorkspaceChatClient.cs +++ /dev/null @@ -1,69 +0,0 @@ -using LINGYUN.Abp.AI.Tools; -using LINGYUN.Abp.AI.Workspaces; -using Microsoft.Extensions.AI; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace LINGYUN.Abp.AI; -public class WorkspaceChatClient : IWorkspaceChatClient -{ - public IChatClient Client { get; } - public WorkspaceDefinition? Workspace { get; } - public WorkspaceChatClient( - IChatClient chatClient, - WorkspaceDefinition? workspace = null) - { - Workspace = workspace; - Client = chatClient; - } - - public virtual Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - return Client.GetResponseAsync(GetChatMessages(messages), GetChatOptions(options), cancellationToken); - } - - public virtual IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - return Client.GetStreamingResponseAsync(GetChatMessages(messages), GetChatOptions(options), cancellationToken); - } - - public virtual object? GetService(Type serviceType, object? serviceKey = null) - { - return Client.GetService(serviceType, serviceKey); - } - - public virtual void Dispose() - { - Client.Dispose(); - } - - protected virtual ChatOptions GetChatOptions(ChatOptions? options) - { - options ??= new ChatOptions(); - options.Instructions = Workspace?.Instructions; - options.Temperature ??= Workspace?.Temperature; - options.MaxOutputTokens ??= Workspace?.MaxOutputTokens; - options.FrequencyPenalty ??= Workspace?.FrequencyPenalty; - options.PresencePenalty ??= Workspace?.PresencePenalty; - - return options; - } - - protected virtual IEnumerable GetChatMessages(IEnumerable messages) - { - if (Workspace?.SystemPrompt?.IsNullOrWhiteSpace() == false && - !messages.Any(msg => msg.Role == ChatRole.System)) - { - // 加入系统提示词 - messages = new List - { - new ChatMessage(ChatRole.System, Workspace.SystemPrompt) - }.Union(messages); - } - - return messages; - } -} From f6726548cdc6766c070aa1f546a3e9198fef767a Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 08:39:33 +0800 Subject: [PATCH 05/25] feat: The ConversationId is not mandatory. --- .../LINGYUN/Abp/AI/Agent/AgentFactory.cs | 2 +- .../LINGYUN/Abp/AI/Agent/AgentService.cs | 12 +++-- .../Abp/AI/Messages/IUserMessageStore.cs | 2 +- .../AI/Messages/InMemoryUserMessageStore.cs | 18 ++++--- .../LINGYUN/Abp/AI/Models/UserMessage.cs | 47 ++++++++++++++++--- .../Abp/AI/OpenAIChatClientProvider.cs | 1 - 6 files changed, 63 insertions(+), 19 deletions(-) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs index 3739fdfd3..95b93584f 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs @@ -80,7 +80,7 @@ public class AgentFactory : IAgentFactory, IScopedDependency Tools = tools, }, Name = workspace?.Name, - Description = description + Description = description, }; var aiAgent = chatClient.CreateAIAgent(clientAgentOptions) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs index ed34c4200..899689c88 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs @@ -2,6 +2,7 @@ using LINGYUN.Abp.AI.Models; using LINGYUN.Abp.AI.Tokens; using Microsoft.Extensions.AI; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -49,11 +50,14 @@ public class AgentService : IAgentService, IScopedDependency { var messages = new List(); - var historyMessages = await _userMessageStore.GetHistoryMessagesAsync(message.ChatId); - - foreach (var chatMessage in historyMessages) + if (!message.ConversationId.IsNullOrWhiteSpace()) { - messages.Add(new ChatMessage(ChatRole.System, chatMessage.Content)); + var historyMessages = await _userMessageStore.GetHistoryMessagesAsync(message.ConversationId); + + foreach (var chatMessage in historyMessages) + { + messages.Add(new ChatMessage(ChatRole.System, chatMessage.Content)); + } } messages.Add(new ChatMessage(ChatRole.User, message.Content)); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs index f9d8baf54..b56406d5f 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs @@ -7,5 +7,5 @@ public interface IUserMessageStore { Task SaveMessageAsync(UserMessage message); - Task> GetHistoryMessagesAsync(string chatId); + Task> GetHistoryMessagesAsync(string conversationId); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs index 2492a51ae..a12cf1de4 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs @@ -14,9 +14,9 @@ public class InMemoryUserMessageStore : IUserMessageStore { private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); - public Task> GetHistoryMessagesAsync(string chatId) + public Task> GetHistoryMessagesAsync(string conversationId) { - if (_userMessageCache.TryGetValue(chatId, out var messages)) + if (_userMessageCache.TryGetValue(conversationId, out var messages)) { return Task.FromResult(messages.Take(5)); } @@ -26,15 +26,21 @@ public class InMemoryUserMessageStore : IUserMessageStore public Task SaveMessageAsync(UserMessage message) { - if (_userMessageCache.ContainsKey(message.ChatId)) + var messageId = message.Id; + if (messageId.IsNullOrWhiteSpace()) { - _userMessageCache[message.ChatId].Add(message); + messageId = Guid.NewGuid().ToString(); + message.WithMessageId(messageId); + } + if (_userMessageCache.ContainsKey(messageId)) + { + _userMessageCache[messageId].Add(message); } else { - _userMessageCache[message.ChatId] = new List() { message }; + _userMessageCache[messageId] = new List() { message }; } - return Task.FromResult(message.Id); + return Task.FromResult(messageId); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs index f902b0c67..1c6a37557 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs @@ -3,24 +3,59 @@ namespace LINGYUN.Abp.AI.Models; public class UserMessage { - public string Id { get; } - public string ChatId { get; } + /// + /// 消息内容 + /// public string Content { get; } + /// + /// 工作区 + /// public string Workspace { get; } + /// + /// 消息Id + /// + /// + /// 在持久化设施处更新 + /// + public string? Id { get; private set; } + /// + /// 对话Id + /// + /// + /// 用于从客户端存储中持久化和检索聊天历史的唯一标识符,如果未指定则与AI对话时无上下文关联 + /// + public string? ConversationId { get; private set; } + /// + /// AI回复消息 + /// public string ReplyMessage { get; private set; } + /// + /// 媒体附件 + /// + /// + /// 暂未实现 + /// public MediaMessage[]? Medias { get; private set; } public UserMessage( string workspace, - string id, - string chatId, string content) { Workspace = workspace; - Id = id; - ChatId = chatId; Content = content; } + public UserMessage WithMessageId(string id) + { + Id = id; + return this; + } + + public UserMessage WithConversationId(string conversationId) + { + ConversationId = conversationId; + return this; + } + public UserMessage WithMedia(MediaMessage media) { Medias ??= []; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs index d13ac08a1..3501993b1 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs @@ -38,7 +38,6 @@ public class OpenAIChatClientProvider : ChatClientProvider .UseOpenTelemetry() .UseFunctionInvocation() .UseDistributedCache() - .UseChatReducer() .Build(ServiceProvider); return Task.FromResult(chatClient); From 389ad566fbefeabfec01ef860498f07726cd7c2e Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 09:12:31 +0800 Subject: [PATCH 06/25] feat: Reconstruct the general user message --- .../LINGYUN/Abp/AI/Agent/AgentService.cs | 5 +-- .../LINGYUN/Abp/AI/AbpAICoreModule.cs | 6 ++- .../AI/{ => Internal}/ChatClientFactory.cs | 2 +- .../ChatClientProviderManager.cs | 2 +- .../AI/Internal/DeepSeekChatClientProvider.cs | 15 ++++++++ .../Abp/AI/Internal/DeepSeekKernelProvider.cs | 13 +++++++ .../Abp/AI/{ => Internal}/KernelFactory.cs | 2 +- .../{ => Internal}/KernelProviderManager.cs | 2 +- .../OpenAIChatClientProvider.cs | 6 +-- .../AI/{ => Internal}/OpenAIKernelProvider.cs | 4 +- .../LINGYUN/Abp/AI/Models/TokenUsageInfo.cs | 5 +-- .../LINGYUN/Abp/AI/Models/UserMessage.cs | 37 +++++-------------- .../LINGYUN/Abp/AI/Models/UserTextMessage.cs | 20 ++++++++++ .../Abp/AI/Tokens/InMemoryTokenUsageStore.cs | 9 ----- 14 files changed, 74 insertions(+), 54 deletions(-) rename aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/{ => Internal}/ChatClientFactory.cs (98%) rename aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/{ => Internal}/ChatClientProviderManager.cs (97%) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekChatClientProvider.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekKernelProvider.cs rename aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/{ => Internal}/KernelFactory.cs (98%) rename aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/{ => Internal}/KernelProviderManager.cs (97%) rename aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/{ => Internal}/OpenAIChatClientProvider.cs (91%) rename aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/{ => Internal}/OpenAIKernelProvider.cs (89%) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs index 899689c88..17ea6b7a9 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs @@ -56,11 +56,11 @@ public class AgentService : IAgentService, IScopedDependency foreach (var chatMessage in historyMessages) { - messages.Add(new ChatMessage(ChatRole.System, chatMessage.Content)); + messages.Add(new ChatMessage(ChatRole.System, chatMessage.GetMessagePrompt())); } } - messages.Add(new ChatMessage(ChatRole.User, message.Content)); + messages.Add(new ChatMessage(ChatRole.User, message.GetMessagePrompt())); return messages; } @@ -84,7 +84,6 @@ public class AgentService : IAgentService, IScopedDependency TotalTokenCount = usage.Details.TotalTokenCount, CachedInputTokenCount = usage.Details.CachedInputTokenCount, InputTokenCount = usage.Details.InputTokenCount, - AdditionalCounts = usage.Details.AdditionalCounts, OutputTokenCount = usage.Details.OutputTokenCount, ReasoningTokenCount = usage.Details.ReasoningTokenCount, }); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs index 3f81bd409..2ab1185a5 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs @@ -1,4 +1,5 @@ -using LINGYUN.Abp.AI.Localization; +using LINGYUN.Abp.AI.Internal; +using LINGYUN.Abp.AI.Localization; using LINGYUN.Abp.AI.Workspaces; using Microsoft.Extensions.DependencyInjection; using System; @@ -38,7 +39,10 @@ public class AbpAICoreModule : AbpModule Configure(options => { options.ChatClientProviders.Add(); + options.ChatClientProviders.Add(); + options.KernelProviders.Add(); + options.KernelProviders.Add(); }); Configure(options => diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientFactory.cs similarity index 98% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientFactory.cs index 3f5280dee..a369c05e0 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientFactory.cs @@ -8,7 +8,7 @@ using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; using Volo.Abp.SimpleStateChecking; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class ChatClientFactory : IChatClientFactory, IScopedDependency { protected ISimpleStateCheckerManager StateCheckerManager { get; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProviderManager.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientProviderManager.cs similarity index 97% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProviderManager.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientProviderManager.cs index 8d3cc80ec..09495b231 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProviderManager.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientProviderManager.cs @@ -7,7 +7,7 @@ using System.Linq; using Volo.Abp; using Volo.Abp.DependencyInjection; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class ChatClientProviderManager : IChatClientProviderManager, ISingletonDependency { public List Providers => _lazyProviders.Value; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekChatClientProvider.cs new file mode 100644 index 000000000..bc0863f33 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekChatClientProvider.cs @@ -0,0 +1,15 @@ +using System; + +namespace LINGYUN.Abp.AI.Internal; +public class DeepSeekChatClientProvider : OpenAIChatClientProvider +{ + protected override string DefaultEndpoint => "https://api.deepseek.com/v1"; + + public new const string ProviderName = "DeepSeek"; + public override string Name => ProviderName; + public DeepSeekChatClientProvider( + IServiceProvider serviceProvider) + : base(serviceProvider) + { + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekKernelProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekKernelProvider.cs new file mode 100644 index 000000000..c27cb0e41 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekKernelProvider.cs @@ -0,0 +1,13 @@ +using System; + +namespace LINGYUN.Abp.AI.Internal; +public class DeepSeekKernelProvider : OpenAIKernelProvider +{ + protected override string DefaultEndpoint => "https://api.deepseek.com/v1"; + + public new const string ProviderName = "DeepSeek"; + public override string Name => ProviderName; + public DeepSeekKernelProvider(IServiceProvider serviceProvider) : base(serviceProvider) + { + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelFactory.cs similarity index 98% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelFactory.cs index 5778e7921..23c6aeda7 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelFactory.cs @@ -8,7 +8,7 @@ using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; using Volo.Abp.SimpleStateChecking; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class KernelFactory : IKernelFactory, IScopedDependency { protected ISimpleStateCheckerManager StateCheckerManager { get; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProviderManager.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelProviderManager.cs similarity index 97% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProviderManager.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelProviderManager.cs index f2beb0025..ab4bd6221 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProviderManager.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelProviderManager.cs @@ -6,7 +6,7 @@ using System.Linq; using Volo.Abp; using Volo.Abp.DependencyInjection; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class KernelProviderManager : IKernelProviderManager, ISingletonDependency { public List Providers => _lazyProviders.Value; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIChatClientProvider.cs similarity index 91% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIChatClientProvider.cs index 3501993b1..6f2ab1234 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIChatClientProvider.cs @@ -6,12 +6,12 @@ using System.ClientModel; using System.Threading.Tasks; using Volo.Abp; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class OpenAIChatClientProvider : ChatClientProvider { - private const string DefaultEndpoint = "https://api.openai.com/v1"; - public const string ProviderName = "OpenAI"; + protected virtual string DefaultEndpoint => "https://api.openai.com/v1"; + public const string ProviderName = "OpenAI"; public override string Name => ProviderName; public OpenAIChatClientProvider(IServiceProvider serviceProvider) : base(serviceProvider) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIKernelProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIKernelProvider.cs similarity index 89% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIKernelProvider.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIKernelProvider.cs index 2f1bb8f61..2eb16859e 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIKernelProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIKernelProvider.cs @@ -6,10 +6,10 @@ using System.ClientModel; using System.Threading.Tasks; using Volo.Abp; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class OpenAIKernelProvider : KernelProvider { - private const string DefaultEndpoint = "https://api.openai.com/v1"; + protected virtual string DefaultEndpoint { get; set; } = "https://api.openai.com/v1"; public const string ProviderName = "OpenAI"; public override string Name => ProviderName; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs index cdac08043..61c8e0457 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace LINGYUN.Abp.AI.Models; +namespace LINGYUN.Abp.AI.Models; public class TokenUsageInfo { public string Workspace { get; } @@ -9,7 +7,6 @@ public class TokenUsageInfo public long? TotalTokenCount { get; set; } public long? CachedInputTokenCount { get; set; } public long? ReasoningTokenCount { get; set; } - public IDictionary? AdditionalCounts { get; set; } public TokenUsageInfo(string workspace) { Workspace = workspace; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs index 1c6a37557..7681ae260 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs @@ -1,12 +1,6 @@ -using System.Linq; - -namespace LINGYUN.Abp.AI.Models; -public class UserMessage +namespace LINGYUN.Abp.AI.Models; +public abstract class UserMessage { - /// - /// 消息内容 - /// - public string Content { get; } /// /// 工作区 /// @@ -29,44 +23,31 @@ public class UserMessage /// AI回复消息 /// public string ReplyMessage { get; private set; } - /// - /// 媒体附件 - /// - /// - /// 暂未实现 - /// - public MediaMessage[]? Medias { get; private set; } - public UserMessage( - string workspace, - string content) + protected UserMessage(string workspace) { Workspace = workspace; - Content = content; } - public UserMessage WithMessageId(string id) + public virtual UserMessage WithMessageId(string id) { Id = id; return this; } - public UserMessage WithConversationId(string conversationId) + public virtual UserMessage WithConversationId(string conversationId) { ConversationId = conversationId; return this; } - public UserMessage WithMedia(MediaMessage media) + public virtual UserMessage WithReply(string replyMessage) { - Medias ??= []; - Medias = Medias.Union([media]).ToArray(); - + ReplyMessage = replyMessage; return this; } - public UserMessage WithReply(string replyMessage) + public virtual string GetMessagePrompt() { - ReplyMessage = replyMessage; - return this; + return string.Empty; } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs new file mode 100644 index 000000000..55cb12ae6 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs @@ -0,0 +1,20 @@ +namespace LINGYUN.Abp.AI.Models; +public class UserTextMessage : UserMessage +{ + /// + /// 消息内容 + /// + public string Content { get; } + public UserTextMessage( + string workspace, + string content) + : base(workspace) + { + Content = content; + } + + public override string GetMessagePrompt() + { + return Content; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs index 621f39c06..9c7862de8 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs @@ -25,15 +25,6 @@ public class InMemoryTokenUsageStore : ITokenUsageStore ReasoningTokenCount = usageInfo.Sum(x => x.ReasoningTokenCount), CachedInputTokenCount = usageInfo.Sum(x => x.CachedInputTokenCount), }; - tokenUsageInfo.AdditionalCounts ??= new Dictionary(); - foreach (var item in usageInfo) - { - if (item.AdditionalCounts == null) - { - continue; - } - tokenUsageInfo.AdditionalCounts.AddIfNotContains(item.AdditionalCounts); - } if (!_tokenUsageCache.ContainsKey(usageInfo.Key)) { From 81e7bdfe8094ed73bd12473a07d1c96d9bfd701f Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 10:51:06 +0800 Subject: [PATCH 07/25] feat: Add AIManagement Projects --- .../FodyWeavers.xml | 3 ++ ....AIManagement.Application.Contracts.csproj | 25 ++++++++++++++++ .../AbpAIManagementDomainSharedModule.cs | 8 +++++ .../FodyWeavers.xml | 3 ++ ...INGYUN.Abp.AIManagement.Application.csproj | 26 +++++++++++++++++ .../AbpAIManagementDomainSharedModule.cs | 8 +++++ .../FodyWeavers.xml | 3 ++ ...GYUN.Abp.AIManagement.Domain.Shared.csproj | 21 ++++++++++++++ .../AbpAIManagementDomainSharedModule.cs | 10 +++++++ .../Workspaces/WorkspaceConsts.cs | 14 +++++++++ .../FodyWeavers.xml | 3 ++ .../LINGYUN.Abp.AIManagement.Domain.csproj | 29 +++++++++++++++++++ .../Abp/AIManagement/AIManagementOptions.cs | 10 +++++++ .../AbpAIManagementDomainModule.cs | 17 +++++++++++ .../FodyWeavers.xml | 3 ++ ...bp.AIManagement.EntityFrameworkCore.csproj | 25 ++++++++++++++++ .../AbpAIManagementDomainSharedModule.cs | 8 +++++ .../FodyWeavers.xml | 3 ++ ...YUN.Abp.AIManagement.HttpApi.Client.csproj | 25 ++++++++++++++++ .../AbpAIManagementDomainSharedModule.cs | 8 +++++ .../FodyWeavers.xml | 3 ++ .../LINGYUN.Abp.AIManagement.HttpApi.csproj | 25 ++++++++++++++++ .../AbpAIManagementDomainSharedModule.cs | 8 +++++ 23 files changed, 288 insertions(+) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xml create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xml create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN.Abp.AIManagement.Application.csproj create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xml create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceConsts.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xml create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xml create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN.Abp.AIManagement.EntityFrameworkCore.csproj create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xml create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN.Abp.AIManagement.HttpApi.Client.csproj create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xml create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN.Abp.AIManagement.HttpApi.csproj create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj new file mode 100644 index 000000000..c756d035b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj @@ -0,0 +1,25 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + LINGYUN.Abp.AIManagement.Domain + LINGYUN.Abp.AIManagement.Domain + false + false + false + enable + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..751b9bfaa --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +public class AbpAIManagementDomainSharedModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN.Abp.AIManagement.Application.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN.Abp.AIManagement.Application.csproj new file mode 100644 index 000000000..5706b07d9 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN.Abp.AIManagement.Application.csproj @@ -0,0 +1,26 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.Application + LINGYUN.Abp.AIManagement.Application + false + false + false + enable + + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..751b9bfaa --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +public class AbpAIManagementDomainSharedModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj new file mode 100644 index 000000000..148c75c26 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj @@ -0,0 +1,21 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + LINGYUN.Abp.AIManagement.Domain.Shared + LINGYUN.Abp.AIManagement.Domain.Shared + false + false + false + enable + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..5515864d6 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,10 @@ +using Volo.Abp.Domain; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn(typeof(AbpDddDomainSharedModule))] +public class AbpAIManagementDomainSharedModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceConsts.cs new file mode 100644 index 000000000..c1dc19dc7 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceConsts.cs @@ -0,0 +1,14 @@ +namespace LINGYUN.Abp.AIManagement.Workspaces; +public static class WorkspaceConsts +{ + public static int MaxNameLength { get; set; } = 64; + public static int MaxProviderLength { get; set; } = 20; + public static int MaxModelNameLength { get; set; } = 64; + public static int MaxDisplayNameLength { get; set; } = 128; + public static int MaxDescriptionLength { get; set; } = 128; + public static int MaxApiKeyLength { get; set; } = 64; + public static int MaxApiBaseUrlLength { get; set; } = 128; + public static int MaxSystemPromptLength { get; set; } = 512; + public static int MaxInstructionsLength { get; set; } = 512; + public static int MaxStateCheckersLength { get; set; } = 256; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj new file mode 100644 index 000000000..3a58fe260 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj @@ -0,0 +1,29 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.Domain + LINGYUN.Abp.AIManagement.Domain + false + false + false + enable + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs new file mode 100644 index 000000000..493fcb14a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs @@ -0,0 +1,10 @@ +namespace LINGYUN.Abp.AIManagement; +public class AIManagementOptions +{ + public bool IsDynamicWorkspaceStoreEnabled { get; set; } + public bool SaveStaticWorkspacesToDatabase { get; set; } + public AIManagementOptions() + { + + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs new file mode 100644 index 000000000..8bed08b41 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs @@ -0,0 +1,17 @@ +using LINGYUN.Abp.AI; +using Volo.Abp.Caching; +using Volo.Abp.Domain; +using Volo.Abp.Mapperly; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementDomainSharedModule), + typeof(AbpAICoreModule), + typeof(AbpCachingModule), + typeof(AbpMapperlyModule), + typeof(AbpDddDomainModule))] +public class AbpAIManagementDomainModule : AbpModule +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN.Abp.AIManagement.EntityFrameworkCore.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN.Abp.AIManagement.EntityFrameworkCore.csproj new file mode 100644 index 000000000..32e7bafeb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN.Abp.AIManagement.EntityFrameworkCore.csproj @@ -0,0 +1,25 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.EntityFrameworkCore + LINGYUN.Abp.AIManagement.EntityFrameworkCore + false + false + false + enable + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..751b9bfaa --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +public class AbpAIManagementDomainSharedModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN.Abp.AIManagement.HttpApi.Client.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN.Abp.AIManagement.HttpApi.Client.csproj new file mode 100644 index 000000000..dc145f2f1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN.Abp.AIManagement.HttpApi.Client.csproj @@ -0,0 +1,25 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + LINGYUN.Abp.AIManagement.HttpApi.Client + LINGYUN.Abp.AIManagement.HttpApi.Client + false + false + false + enable + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..751b9bfaa --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +public class AbpAIManagementDomainSharedModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN.Abp.AIManagement.HttpApi.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN.Abp.AIManagement.HttpApi.csproj new file mode 100644 index 000000000..00f6afa11 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN.Abp.AIManagement.HttpApi.csproj @@ -0,0 +1,25 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.HttpApi + LINGYUN.Abp.AIManagement.HttpApi + false + false + false + enable + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..751b9bfaa --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +public class AbpAIManagementDomainSharedModule : AbpModule +{ + +} From 6bf0c7b9600dc35806a0db12aa9576582a2c1b0c Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 10:56:46 +0800 Subject: [PATCH 08/25] feat: Add AIManagementDbContext --- .../AbpAIManagementDbProperties.cs | 11 ++++++++++ .../AbpAIManagementDomainSharedModule.cs | 8 ------- .../AIManagementDbContext.cs | 21 +++++++++++++++++++ ...nagementDbContextModelBuilderExtensions.cs | 21 +++++++++++++++++++ ...bpAIManagementEntityFrameworkCoreModule.cs | 19 +++++++++++++++++ .../IAIManagementDbContext.cs | 10 +++++++++ 6 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs new file mode 100644 index 000000000..852fbd74a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Data; + +namespace LINGYUN.Abp.AIManagement; +public static class AbpAIManagementDbProperties +{ + public static string DbTablePrefix { get; set; } = AbpCommonDbProperties.DbTablePrefix; + + public static string? DbSchema { get; set; } = AbpCommonDbProperties.DbSchema; + + public const string ConnectionStringName = "AbpAIManagement"; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs deleted file mode 100644 index 751b9bfaa..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Volo.Abp.Modularity; - -namespace LINGYUN.Abp.AIManagement; - -public class AbpAIManagementDomainSharedModule : AbpModule -{ - -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs new file mode 100644 index 000000000..d1bfd18d6 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; + +[ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] +public class AIManagementDbContext : AbpDbContext, IAIManagementDbContext +{ + public AIManagementDbContext( + DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigureAIManagement(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs new file mode 100644 index 000000000..2bf2e574a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -0,0 +1,21 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public static class AIManagementDbContextModelBuilderExtensions +{ + public static void ConfigureAIManagement( + [NotNull] this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + if (builder.IsTenantOnlyDatabase()) + { + return; + } + + builder.TryConfigureObjectExtensions(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs new file mode 100644 index 000000000..adbf7146d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; + +[DependsOn( + typeof(AbpAIManagementDomainModule), + typeof(AbpEntityFrameworkCoreModule))] +public class AbpAIManagementEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + options.AddDefaultRepositories(); + }); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs new file mode 100644 index 000000000..16b860ed9 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs @@ -0,0 +1,10 @@ +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; + +[ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] +public interface IAIManagementDbContext : IEfCoreDbContext +{ + +} From c1bf459685b184b750f3b083646f8525cc82e915 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 10:57:32 +0800 Subject: [PATCH 09/25] feat: Implement dynamic workspace --- .../AbpAIManagementDomainModule.cs | 42 +++ .../DynamicWorkspaceDefinitionStore.cs | 161 +++++++++++ ...icWorkspaceDefinitionStoreInMemoryCache.cs | 95 +++++++ ...icWorkspaceDefinitionStoreInMemoryCache.cs | 22 ++ .../Workspaces/IStaticWorkspaceSaver.cs | 7 + .../IWorkspaceDefinitionSerializer.cs | 11 + .../Workspaces/IWorkspaceRepository.cs | 12 + .../Workspaces/StaticWorkspaceSaver.cs | 242 +++++++++++++++++ .../Abp/AIManagement/Workspaces/Workspace.cs | 255 ++++++++++++++++++ .../WorkspaceDefinitionSerializer.cs | 72 +++++ .../Workspaces/WorkspaceDynamicInitializer.cs | 149 ++++++++++ 11 files changed, 1068 insertions(+) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IStaticWorkspaceSaver.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs index 8bed08b41..8d139de9b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs @@ -1,8 +1,16 @@ using LINGYUN.Abp.AI; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp; using Volo.Abp.Caching; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; using Volo.Abp.Domain; using Volo.Abp.Mapperly; using Volo.Abp.Modularity; +using Volo.Abp.Threading; namespace LINGYUN.Abp.AIManagement; @@ -14,4 +22,38 @@ namespace LINGYUN.Abp.AIManagement; typeof(AbpDddDomainModule))] public class AbpAIManagementDomainModule : AbpModule { + + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + + if (context.Services.IsDataMigrationEnvironment()) + { + Configure(options => + { + options.SaveStaticWorkspacesToDatabase = false; + options.IsDynamicWorkspaceStoreEnabled = false; + }); + } + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context)); + } + + public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var rootServiceProvider = context.ServiceProvider.GetRequiredService(); + var initializer = rootServiceProvider.GetRequiredService(); + await initializer.InitializeAsync(true, _cancellationTokenSource.Token); + } + + public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) + { + _cancellationTokenSource.Cancel(); + return Task.CompletedTask; + } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs new file mode 100644 index 000000000..6c7225b8f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs @@ -0,0 +1,161 @@ +using JetBrains.Annotations; +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Threading; + +namespace LINGYUN.Abp.AIManagement.Workspaces; + +[Dependency(ReplaceServices = true)] +public class DynamicWorkspaceDefinitionStore : IDynamicWorkspaceDefinitionStore, ITransientDependency +{ + protected IWorkspaceRepository WorkspaceRepository { get; } + protected IWorkspaceDefinitionSerializer WorkspaceDefinitionSerializer { get; } + protected IDynamicWorkspaceDefinitionStoreInMemoryCache StoreCache { get; } + protected IDistributedCache DistributedCache { get; } + protected IAbpDistributedLock DistributedLock { get; } + public AIManagementOptions AIManagementOptions { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + + public DynamicWorkspaceDefinitionStore( + IWorkspaceRepository workspaceRepository, + IWorkspaceDefinitionSerializer workspaceDefinitionSerializer, + IDynamicWorkspaceDefinitionStoreInMemoryCache storeCache, + IDistributedCache distributedCache, + IOptions cacheOptions, + IOptions aiManagementOptions, + IAbpDistributedLock distributedLock) + { + WorkspaceRepository = workspaceRepository; + WorkspaceDefinitionSerializer = workspaceDefinitionSerializer; + StoreCache = storeCache; + DistributedCache = distributedCache; + DistributedLock = distributedLock; + AIManagementOptions = aiManagementOptions.Value; + CacheOptions = cacheOptions.Value; + } + + public async virtual Task> GetAllAsync() + { + if (!AIManagementOptions.IsDynamicWorkspaceStoreEnabled) + { + return Array.Empty(); + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetWorkspaces(); + } + } + + public async virtual Task GetAsync([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + return await GetOrNullAsync(name) ?? throw new AbpException("Undefined workspace: " + name); + } + + public async virtual Task GetOrNullAsync([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + if (!AIManagementOptions.IsDynamicWorkspaceStoreEnabled) + { + return null; + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetWorkspaceOrNull(name); + } + } + + protected virtual async Task EnsureCacheIsUptoDateAsync() + { + if (StoreCache.LastCheckTime.HasValue && + DateTime.Now.Subtract(StoreCache.LastCheckTime.Value).TotalSeconds < 30) + { + return; + } + + var stampInDistributedCache = await GetOrSetStampInDistributedCache(); + + if (stampInDistributedCache == StoreCache.CacheStamp) + { + StoreCache.LastCheckTime = DateTime.Now; + return; + } + + await UpdateInMemoryStoreCache(); + + StoreCache.CacheStamp = stampInDistributedCache; + StoreCache.LastCheckTime = DateTime.Now; + } + + protected virtual async Task UpdateInMemoryStoreCache() + { + var workspaces = await WorkspaceRepository.GetListAsync(); + + await StoreCache.FillAsync(workspaces); + } + + protected virtual async Task GetOrSetStampInDistributedCache() + { + var cacheKey = GetCommonStampCacheKey(); + + var stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); + if (stampInDistributedCache != null) + { + return stampInDistributedCache; + } + + await using (var commonLockHandle = await DistributedLock + .TryAcquireAsync(GetCommonDistributedLockKey(), TimeSpan.FromMinutes(2))) + { + if (commonLockHandle == null) + { + throw new AbpException( + "Could not acquire distributed lock for workspace definition common stamp check!" + ); + } + + stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); + if (stampInDistributedCache != null) + { + return stampInDistributedCache; + } + + stampInDistributedCache = Guid.NewGuid().ToString(); + + await DistributedCache.SetStringAsync( + cacheKey, + stampInDistributedCache, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromDays(30) + } + ); + } + + return stampInDistributedCache; + } + + protected virtual string GetCommonStampCacheKey() + { + return $"{CacheOptions.KeyPrefix}_AbpInMemoryWorkspaceCacheStamp"; + } + + protected virtual string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpWorkspaceUpdateLock"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs new file mode 100644 index 000000000..43314dfed --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -0,0 +1,95 @@ +using LINGYUN.Abp.AI.Workspaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class DynamicWorkspaceDefinitionStoreInMemoryCache : IDynamicWorkspaceDefinitionStoreInMemoryCache, ISingletonDependency +{ + public string CacheStamp { get; set; } + protected IDictionary WorkspaceDefinitions { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public SemaphoreSlim SyncSemaphore { get; } = new(1, 1); + + public DateTime? LastCheckTime { get; set; } + + public DynamicWorkspaceDefinitionStoreInMemoryCache( + ISimpleStateCheckerSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + StateCheckerSerializer = stateCheckerSerializer; + LocalizableStringSerializer = localizableStringSerializer; + + WorkspaceDefinitions = new Dictionary(); + } + + public Task FillAsync(List workspaces) + { + WorkspaceDefinitions.Clear(); + + foreach (var workspace in workspaces) + { + var workspaceDef = new WorkspaceDefinition( + workspace.Name, + workspace.Provider, + workspace.ModelName, + LocalizableStringSerializer.Deserialize(workspace.DisplayName), + !workspace.Description.IsNullOrWhiteSpace() ? LocalizableStringSerializer.Deserialize(workspace.Description) : null, + workspace.SystemPrompt, + workspace.Instructions, + workspace.Temperature, + workspace.MaxOutputTokens, + workspace.FrequencyPenalty, + workspace.PresencePenalty); + + if (!workspace.ApiKey.IsNullOrWhiteSpace()) + { + workspaceDef.WithApiKey(workspace.ApiKey); + } + if (!workspace.ApiBaseUrl.IsNullOrWhiteSpace()) + { + workspaceDef.WithApiBaseUrl(workspace.ApiBaseUrl); + } + workspaceDef.IsEnabled = workspace.IsEnabled; + + if (!workspace.StateCheckers.IsNullOrWhiteSpace()) + { + var checkers = StateCheckerSerializer + .DeserializeArray( + workspace.StateCheckers, + workspaceDef + ); + workspaceDef.StateCheckers.AddRange(checkers); + } + + foreach (var property in workspace.ExtraProperties) + { + if (property.Value != null) + { + workspaceDef.WithProperty(property.Key, property.Value); + } + } + + WorkspaceDefinitions[workspace.Name] = workspaceDef; + } + + return Task.CompletedTask; + } + + public WorkspaceDefinition? GetWorkspaceOrNull(string name) + { + return WorkspaceDefinitions.GetOrDefault(name); + } + + public IReadOnlyList GetWorkspaces() + { + return WorkspaceDefinitions.Values.ToList(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs new file mode 100644 index 000000000..c2d2a4b12 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -0,0 +1,22 @@ +using LINGYUN.Abp.AI.Workspaces; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Workspaces; + +public interface IDynamicWorkspaceDefinitionStoreInMemoryCache +{ + string CacheStamp { get; set; } + + SemaphoreSlim SyncSemaphore { get; } + + DateTime? LastCheckTime { get; set; } + + Task FillAsync(List permissions); + + WorkspaceDefinition? GetWorkspaceOrNull(string name); + + IReadOnlyList GetWorkspaces(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IStaticWorkspaceSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IStaticWorkspaceSaver.cs new file mode 100644 index 000000000..32ba0a028 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IStaticWorkspaceSaver.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IStaticWorkspaceSaver +{ + Task SaveAsync(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs new file mode 100644 index 000000000..fd1106fcd --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.AI.Workspaces; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IWorkspaceDefinitionSerializer +{ + Task SerializeAsync(IEnumerable definitions); + + Task SerializeAsync(WorkspaceDefinition definition); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs new file mode 100644 index 000000000..5405b0b0a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IWorkspaceRepository : IBasicRepository +{ + Task FindByNameAsync( + string name, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs new file mode 100644 index 000000000..df306125c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs @@ -0,0 +1,242 @@ +using LINGYUN.Abp.AI; +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Guids; +using Volo.Abp.Json.SystemTextJson.Modifiers; +using Volo.Abp.Threading; +using Volo.Abp.Uow; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency +{ + protected IStaticWorkspaceDefinitionStore StaticStore { get; } + protected IWorkspaceRepository WorkspaceRepository { get; } + protected IWorkspaceDefinitionSerializer WorkspaceSerializer { get; } + protected IDistributedCache Cache { get; } + protected IApplicationInfoAccessor ApplicationInfoAccessor { get; } + protected IAbpDistributedLock DistributedLock { get; } + protected AbpAICoreOptions AIOptions { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected IGuidGenerator GuidGenerator { get; } + + public StaticWorkspaceSaver( + IStaticWorkspaceDefinitionStore staticStore, + IWorkspaceRepository workspaceRepository, + IWorkspaceDefinitionSerializer workspaceSerializer, + IDistributedCache cache, + IOptions cacheOptions, + IApplicationInfoAccessor applicationInfoAccessor, + IAbpDistributedLock distributedLock, + IOptions settingOptions, + ICancellationTokenProvider cancellationTokenProvider, + IUnitOfWorkManager unitOfWorkManager, + IGuidGenerator guidGenerator) + { + StaticStore = staticStore; + WorkspaceRepository = workspaceRepository; + WorkspaceSerializer = workspaceSerializer; + Cache = cache; + ApplicationInfoAccessor = applicationInfoAccessor; + DistributedLock = distributedLock; + CancellationTokenProvider = cancellationTokenProvider; + AIOptions = settingOptions.Value; + CacheOptions = cacheOptions.Value; + UnitOfWorkManager = unitOfWorkManager; + GuidGenerator = guidGenerator; + } + + [UnitOfWork] + public async Task SaveAsync() + { + await using var applicationLockHandle = await DistributedLock.TryAcquireAsync( + GetApplicationDistributedLockKey() + ); + + if (applicationLockHandle == null) + { + return; + } + + var cacheKey = GetApplicationHashCacheKey(); + var cachedHash = await Cache.GetStringAsync(cacheKey, CancellationTokenProvider.Token); + + var workspaces = await WorkspaceSerializer.SerializeAsync(await StaticStore.GetAllAsync()); + var currentHash = CalculateHash(workspaces, AIOptions.DeletedWorkspaces); + + if (cachedHash == currentHash) + { + return; + } + + await using (var commonLockHandle = await DistributedLock.TryAcquireAsync( + GetCommonDistributedLockKey(), + TimeSpan.FromMinutes(5))) + { + if (commonLockHandle == null) + { + /* It will re-try */ + throw new AbpException("Could not acquire distributed lock for saving static Workspaces!"); + } + + using (var unitOfWork = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: true)) + { + try + { + var hasChangesInWorkspaces = await UpdateChangedWorkspacesAsync(workspaces); + + if (hasChangesInWorkspaces) + { + await Cache.SetStringAsync( + GetCommonStampCacheKey(), + Guid.NewGuid().ToString(), + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromDays(30) + }, + CancellationTokenProvider.Token + ); + } + } + catch + { + try + { + await unitOfWork.RollbackAsync(); + } + catch + { + /* ignored */ + } + + throw; + } + + await unitOfWork.CompleteAsync(); + } + } + + await Cache.SetStringAsync( + cacheKey, + currentHash, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromDays(30) + }, + CancellationTokenProvider.Token + ); + } + + private async Task UpdateChangedWorkspacesAsync(Workspace[] workspaces) + { + var newRecords = new List(); + var changedRecords = new List(); + + var workspaceRecordsInDatabase = (await WorkspaceRepository.GetListAsync()).ToDictionary(x => x.Name); + + foreach (var record in workspaces) + { + var workspaceRecordInDatabase = workspaceRecordsInDatabase.GetOrDefault(record.Name); + if (workspaceRecordInDatabase == null) + { + /* New group */ + newRecords.Add(record); + continue; + } + + if (record.HasSameData(workspaceRecordInDatabase)) + { + /* Not changed */ + continue; + } + + /* Changed */ + workspaceRecordInDatabase.Patch(record); + changedRecords.Add(workspaceRecordInDatabase); + } + + /* Deleted */ + var deletedRecords = new List(); + + if (AIOptions.DeletedWorkspaces.Any()) + { + deletedRecords.AddRange(workspaceRecordsInDatabase.Values.Where(x => AIOptions.DeletedWorkspaces.Contains(x.Name))); + } + + if (newRecords.Any()) + { + await WorkspaceRepository.InsertManyAsync(newRecords); + } + + if (changedRecords.Any()) + { + await WorkspaceRepository.UpdateManyAsync(changedRecords); + } + + if (deletedRecords.Any()) + { + await WorkspaceRepository.DeleteManyAsync(deletedRecords); + } + + return newRecords.Any() || changedRecords.Any() || deletedRecords.Any(); + } + + private string GetApplicationDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpWorkspaceUpdateLock"; + } + + private string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpWorkspaceUpdateLock"; + } + + private string GetApplicationHashCacheKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpWorkspacesHash"; + } + + private string GetCommonStampCacheKey() + { + return $"{CacheOptions.KeyPrefix}_AbpInMemoryWorkspaceCacheStamp"; + } + + private string CalculateHash(Workspace[] workspaces, IEnumerable deletedWorkspaces) + { + var jsonSerializerOptions = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + new AbpIgnorePropertiesModifiers().CreateModifyAction(x => x.Id), + } + } + }; + + var stringBuilder = new StringBuilder(); + + stringBuilder.Append("Workspaces:"); + stringBuilder.AppendLine(JsonSerializer.Serialize(workspaces, jsonSerializerOptions)); + + stringBuilder.Append("DeletedWorkspace:"); + stringBuilder.Append(deletedWorkspaces.JoinAsString(",")); + + return stringBuilder + .ToString() + .ToMd5(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs new file mode 100644 index 000000000..bb09b2f0f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs @@ -0,0 +1,255 @@ +using System; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Auditing; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class Workspace : AuditedAggregateRoot +{ + public string Name { get; private set; } + + public string Provider { get; private set; } + + public string ModelName { get; private set; } + + public string DisplayName { get; private set; } + + public string? Description { get; private set; } + + public string? ApiKey { get; private set; } + + public string? ApiBaseUrl { get; private set; } + + public string? SystemPrompt { get; set; } + + public string? Instructions { get; set; } + + public float? Temperature { get; set; } + + public int? MaxOutputTokens { get; set; } + + public float? FrequencyPenalty { get; set; } + + public float? PresencePenalty { get; set; } + + public bool IsEnabled { get; set; } + + public string? StateCheckers { get; set; } + + protected Workspace() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public Workspace( + Guid id, + string name, + string provider, + string modelName, + string displayName, + string? description = null, + string? apiKey = null, + string? apiBaseUrl = null, + string? systemPrompt = null, + string? instructions = null, + float? temperature = null, + int? maxOutputTokens = null, + float? frequencyPenalty = null, + float? presencePenalty = null, + string? stateCheckers = null) + : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), WorkspaceConsts.MaxNameLength); + Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), WorkspaceConsts.MaxProviderLength); + ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceConsts.MaxModelNameLength); + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceConsts.MaxDisplayNameLength); + Description = Check.Length(description, nameof(description), WorkspaceConsts.MaxDescriptionLength); + ApiKey = Check.Length(apiKey, nameof(apiKey), WorkspaceConsts.MaxApiKeyLength); + ApiBaseUrl = Check.Length(apiBaseUrl, nameof(apiBaseUrl), WorkspaceConsts.MaxApiBaseUrlLength); + SystemPrompt = Check.Length(systemPrompt, nameof(systemPrompt), WorkspaceConsts.MaxSystemPromptLength); + Instructions = Check.Length(instructions, nameof(instructions), WorkspaceConsts.MaxInstructionsLength); + StateCheckers = Check.Length(stateCheckers, nameof(stateCheckers), WorkspaceConsts.MaxStateCheckersLength); + Temperature = temperature; + MaxOutputTokens = maxOutputTokens; + FrequencyPenalty = frequencyPenalty; + PresencePenalty = presencePenalty; + + IsEnabled = true; + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public bool HasSameData(Workspace otherWorkspace) + { + if (Name != otherWorkspace.Name) + { + return false; + } + + if (Provider != otherWorkspace.Provider) + { + return false; + } + + if (ModelName != otherWorkspace.ModelName) + { + return false; + } + + if (DisplayName != otherWorkspace.DisplayName) + { + return false; + } + + if (Description != otherWorkspace.Description) + { + return false; + } + + if (ApiKey != otherWorkspace.ApiKey) + { + return false; + } + + if (ApiBaseUrl != otherWorkspace.ApiBaseUrl) + { + return false; + } + + if (SystemPrompt != otherWorkspace.SystemPrompt) + { + return false; + } + + if (Instructions != otherWorkspace.Instructions) + { + return false; + } + + if (IsEnabled != otherWorkspace.IsEnabled) + { + return false; + } + + if (Temperature != otherWorkspace.Temperature) + { + return false; + } + + if (MaxOutputTokens != otherWorkspace.MaxOutputTokens) + { + return false; + } + + if (FrequencyPenalty != otherWorkspace.FrequencyPenalty) + { + return false; + } + + if (PresencePenalty != otherWorkspace.PresencePenalty) + { + return false; + } + + if (StateCheckers != otherWorkspace.StateCheckers) + { + return false; + } + + if (!this.HasSameExtraProperties(otherWorkspace)) + { + return false; + } + + return true; + } + + public void Patch(Workspace otherWorkspace) + { + if (Name != otherWorkspace.Name) + { + Name = otherWorkspace.Name; + } + + if (Provider != otherWorkspace.Provider) + { + Provider = otherWorkspace.Provider; + } + + if (ModelName != otherWorkspace.ModelName) + { + ModelName = otherWorkspace.ModelName; + } + + if (DisplayName != otherWorkspace.DisplayName) + { + DisplayName = otherWorkspace.DisplayName; + } + + if (Description != otherWorkspace.Description) + { + Description = otherWorkspace.Description; + } + + if (ApiKey != otherWorkspace.ApiKey) + { + ApiKey = otherWorkspace.ApiKey; + } + + if (ApiBaseUrl != otherWorkspace.ApiBaseUrl) + { + ApiBaseUrl = otherWorkspace.ApiBaseUrl; + } + + if (SystemPrompt != otherWorkspace.SystemPrompt) + { + SystemPrompt = otherWorkspace.SystemPrompt; + } + + if (Instructions != otherWorkspace.Instructions) + { + Instructions = otherWorkspace.Instructions; + } + + if (IsEnabled != otherWorkspace.IsEnabled) + { + IsEnabled = otherWorkspace.IsEnabled; + } + + if (Temperature != otherWorkspace.Temperature) + { + Temperature = otherWorkspace.Temperature; + } + + if (MaxOutputTokens != otherWorkspace.MaxOutputTokens) + { + MaxOutputTokens = otherWorkspace.MaxOutputTokens; + } + + if (FrequencyPenalty != otherWorkspace.FrequencyPenalty) + { + FrequencyPenalty = otherWorkspace.FrequencyPenalty; + } + + if (PresencePenalty != otherWorkspace.PresencePenalty) + { + PresencePenalty = otherWorkspace.PresencePenalty; + } + + if (StateCheckers != otherWorkspace.StateCheckers) + { + StateCheckers = otherWorkspace.StateCheckers; + } + + if (!this.HasSameExtraProperties(otherWorkspace)) + { + ExtraProperties.Clear(); + + foreach (var property in otherWorkspace.ExtraProperties) + { + ExtraProperties.Add(property.Key, property.Value); + } + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs new file mode 100644 index 000000000..351ffa6ca --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs @@ -0,0 +1,72 @@ +using LINGYUN.Abp.AI.Workspaces; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.Localization; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITransientDependency +{ + protected IGuidGenerator GuidGenerator { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public WorkspaceDefinitionSerializer( + IGuidGenerator guidGenerator, + ISimpleStateCheckerSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + GuidGenerator = guidGenerator; + StateCheckerSerializer = stateCheckerSerializer; + LocalizableStringSerializer = localizableStringSerializer; + } + + public async virtual Task SerializeAsync(IEnumerable definitions) + { + var records = new List(); + foreach (var workspaceDef in definitions) + { + records.Add(await SerializeAsync(workspaceDef)); + } + + return records.ToArray(); + } + + public virtual Task SerializeAsync(WorkspaceDefinition definition) + { + using (CultureHelper.Use(CultureInfo.InvariantCulture)) + { + var workspace = new Workspace( + GuidGenerator.Create(), + definition.Name, + definition.Provider, + definition.ModelName, + LocalizableStringSerializer.Serialize(definition.DisplayName)!, + definition.Description != null ? LocalizableStringSerializer.Serialize(definition.Description) : null, + definition.ApiKey, + definition.ApiBaseUrl, + definition.SystemPrompt, + definition.Instructions, + definition.Temperature, + definition.MaxOutputTokens, + definition.FrequencyPenalty, + definition.PresencePenalty, + SerializeStateCheckers(definition.StateCheckers)); + + foreach (var property in definition.Properties) + { + workspace.SetProperty(property.Key, property.Value); + } + + return Task.FromResult(workspace); + } + } + protected virtual string? SerializeStateCheckers(List> stateCheckers) + { + return StateCheckerSerializer.Serialize(stateCheckers); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs new file mode 100644 index 000000000..591535580 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs @@ -0,0 +1,149 @@ +using JetBrains.Annotations; +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Polly; +using System; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDynamicInitializer : ITransientDependency +{ + public ILogger Logger { get; set; } + + protected IServiceProvider ServiceProvider { get; } + protected IOptions Options { get; } + [CanBeNull] + protected IHostApplicationLifetime ApplicationLifetime { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IDynamicWorkspaceDefinitionStore DynamicWorkspaceDefinitionStore { get; } + protected IStaticWorkspaceSaver StaticWorkspaceSaver { get; } + + public WorkspaceDynamicInitializer( + IServiceProvider serviceProvider, + IOptions options, + ICancellationTokenProvider cancellationTokenProvider, + IDynamicWorkspaceDefinitionStore dynamicWorkspaceDefinitionStore, + IStaticWorkspaceSaver staticWorkspaceSaver) + { + Logger = NullLogger.Instance; + + ServiceProvider = serviceProvider; + Options = options; + CancellationTokenProvider = cancellationTokenProvider; + DynamicWorkspaceDefinitionStore = dynamicWorkspaceDefinitionStore; + StaticWorkspaceSaver = staticWorkspaceSaver; + ApplicationLifetime = ServiceProvider.GetRequiredService(); + } + + public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) + { + var options = Options.Value; + + if (!options.SaveStaticWorkspacesToDatabase && !options.IsDynamicWorkspaceStoreEnabled) + { + return Task.CompletedTask; + } + + if (runInBackground) + { + Task.Run(async () => + { + if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + { + cancellationToken = ApplicationLifetime.ApplicationStopping; + } + await ExecuteInitializationAsync(options, cancellationToken); + }, cancellationToken); + + return Task.CompletedTask; + } + + return ExecuteInitializationAsync(options, cancellationToken); + } + + protected virtual async Task ExecuteInitializationAsync(AIManagementOptions options, CancellationToken cancellationToken) + { + try + { + using (CancellationTokenProvider.Use(cancellationToken)) + { + if (CancellationTokenProvider.Token.IsCancellationRequested) + { + return; + } + + await SaveStaticWorkspacesToDatabaseAsync(options, cancellationToken); + + if (CancellationTokenProvider.Token.IsCancellationRequested) + { + return; + } + + await PreCacheDynamicWorkspacesAsync(options); + } + } + catch + { + // No need to log here since inner calls log + } + } + + protected virtual async Task SaveStaticWorkspacesToDatabaseAsync( + AIManagementOptions options, + CancellationToken cancellationToken) + { + if (!options.SaveStaticWorkspacesToDatabase) + { + return; + } + + await Policy + .Handle(ex => ex is not OperationCanceledException) + .WaitAndRetryAsync( + 8, + retryAttempt => TimeSpan.FromSeconds( + Volo.Abp.RandomHelper.GetRandom( + (int)Math.Pow(2, retryAttempt) * 8, + (int)Math.Pow(2, retryAttempt) * 12) + ) + ) + .ExecuteAsync(async _ => + { + try + { + await StaticWorkspaceSaver.SaveAsync(); + } + catch (Exception ex) + { + Logger.LogException(ex); + throw; // Polly will catch it + } + }, cancellationToken); + } + + protected virtual async Task PreCacheDynamicWorkspacesAsync(AIManagementOptions options) + { + if (!options.IsDynamicWorkspaceStoreEnabled) + { + return; + } + + try + { + // Pre-cache Workspaces, so first request doesn't wait + await DynamicWorkspaceDefinitionStore.GetAllAsync(); + } + catch (Exception ex) + { + Logger.LogException(ex); + throw; // It will be cached in Initialize() + } + } +} From d5fe69de58b2b012a237aa1b2c50e406815cc45d Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 11:01:10 +0800 Subject: [PATCH 10/25] feat: Add EF Workspace Model Config --- .../AIManagementDbContext.cs | 4 +++- ...nagementDbContextModelBuilderExtensions.cs | 23 +++++++++++++++++++ .../IAIManagementDbContext.cs | 6 +++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs index d1bfd18d6..823e7cc71 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; @@ -7,6 +8,7 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; [ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] public class AIManagementDbContext : AbpDbContext, IAIManagementDbContext { + public DbSet Workspaces { get; set; } public AIManagementDbContext( DbContextOptions options) : base(options) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 2bf2e574a..8512e2199 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp; using Volo.Abp.EntityFrameworkCore.Modeling; @@ -16,6 +17,28 @@ public static class AIManagementDbContextModelBuilderExtensions return; } + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "Workspaces", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name).HasMaxLength(WorkspaceConsts.MaxNameLength).IsRequired(); + b.Property(x => x.Provider).HasMaxLength(WorkspaceConsts.MaxProviderLength).IsRequired(); + b.Property(x => x.ModelName).HasMaxLength(WorkspaceConsts.MaxModelNameLength).IsRequired(); + b.Property(x => x.DisplayName).HasMaxLength(WorkspaceConsts.MaxDisplayNameLength).IsRequired(); + b.Property(x => x.Description).HasMaxLength(WorkspaceConsts.MaxDescriptionLength); + b.Property(x => x.ApiKey).HasMaxLength(WorkspaceConsts.MaxApiKeyLength); + b.Property(x => x.ApiBaseUrl).HasMaxLength(WorkspaceConsts.MaxApiKeyLength); + b.Property(x => x.SystemPrompt).HasMaxLength(WorkspaceConsts.MaxSystemPromptLength); + b.Property(x => x.Instructions).HasMaxLength(WorkspaceConsts.MaxInstructionsLength); + b.Property(x => x.StateCheckers).HasMaxLength(WorkspaceConsts.MaxStateCheckersLength); + + b.HasIndex(x => new { x.Name }).IsUnique(); + + b.ApplyObjectExtensionMappings(); + }); + builder.TryConfigureObjectExtensions(); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs index 16b860ed9..234148a27 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs @@ -1,4 +1,6 @@ -using Volo.Abp.Data; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; @@ -6,5 +8,5 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; [ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] public interface IAIManagementDbContext : IEfCoreDbContext { - + DbSet Workspaces { get; } } From 5168b9d3755153b11cf9f94eac48e51b6fedd3cf Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 11:03:34 +0800 Subject: [PATCH 11/25] feat: Add Workspace EFCore Repository Impl --- .../Workspaces/IWorkspaceRepository.cs | 2 +- ...bpAIManagementEntityFrameworkCoreModule.cs | 5 +++- .../EfCoreWorkspaceRepository.cs | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceRepository.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs index 5405b0b0a..db729e5a5 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs @@ -6,7 +6,7 @@ using Volo.Abp.Domain.Repositories; namespace LINGYUN.Abp.AIManagement.Workspaces; public interface IWorkspaceRepository : IBasicRepository { - Task FindByNameAsync( + Task FindByNameAsync( string name, CancellationToken cancellationToken = default); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs index adbf7146d..566db1fc1 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Modularity; @@ -14,6 +15,8 @@ public class AbpAIManagementEntityFrameworkCoreModule : AbpModule context.Services.AddAbpDbContext(options => { options.AddDefaultRepositories(); + + options.AddRepository(); }); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceRepository.cs new file mode 100644 index 000000000..c0fcc2cb9 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceRepository.cs @@ -0,0 +1,26 @@ +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public class EfCoreWorkspaceRepository : EfCoreRepository, IWorkspaceRepository +{ + public EfCoreWorkspaceRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task FindByNameAsync(string name, CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(x => x.Name == name) + .FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); ; + } +} From 11dfe9af095495a070164b4825d49524a3e110d7 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 11:06:05 +0800 Subject: [PATCH 12/25] feat: Rename Workspace to WorkspaceDefinitionRecord --- ....cs => WorkspaceDefinitionRecordConsts.cs} | 2 +- .../DynamicWorkspaceDefinitionStore.cs | 8 ++--- ...icWorkspaceDefinitionStoreInMemoryCache.cs | 2 +- ...icWorkspaceDefinitionStoreInMemoryCache.cs | 2 +- ...> IWorkspaceDefinitionRecordRepository.cs} | 4 +-- .../IWorkspaceDefinitionSerializer.cs | 4 +-- .../Workspaces/StaticWorkspaceSaver.cs | 16 +++++----- ...kspace.cs => WorkspaceDefinitionRecord.cs} | 30 +++++++++---------- .../WorkspaceDefinitionSerializer.cs | 8 ++--- .../AIManagementDbContext.cs | 2 +- ...nagementDbContextModelBuilderExtensions.cs | 24 +++++++-------- ...bpAIManagementEntityFrameworkCoreModule.cs | 2 +- ...oreWorkspaceDefinitionRecordRepository.cs} | 6 ++-- .../IAIManagementDbContext.cs | 2 +- 14 files changed, 56 insertions(+), 56 deletions(-) rename aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/{WorkspaceConsts.cs => WorkspaceDefinitionRecordConsts.cs} (92%) rename aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/{IWorkspaceRepository.cs => IWorkspaceDefinitionRecordRepository.cs} (59%) rename aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/{Workspace.cs => WorkspaceDefinitionRecord.cs} (86%) rename aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/{EfCoreWorkspaceRepository.cs => EfCoreWorkspaceDefinitionRecordRepository.cs} (64%) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecordConsts.cs similarity index 92% rename from aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceConsts.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecordConsts.cs index c1dc19dc7..f8a2e6bd1 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceConsts.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecordConsts.cs @@ -1,5 +1,5 @@ namespace LINGYUN.Abp.AIManagement.Workspaces; -public static class WorkspaceConsts +public static class WorkspaceDefinitionRecordConsts { public static int MaxNameLength { get; set; } = 64; public static int MaxProviderLength { get; set; } = 20; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs index 6c7225b8f..4550973af 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs @@ -16,7 +16,7 @@ namespace LINGYUN.Abp.AIManagement.Workspaces; [Dependency(ReplaceServices = true)] public class DynamicWorkspaceDefinitionStore : IDynamicWorkspaceDefinitionStore, ITransientDependency { - protected IWorkspaceRepository WorkspaceRepository { get; } + protected IWorkspaceDefinitionRecordRepository WorkspaceDefinitionRecordRepository { get; } protected IWorkspaceDefinitionSerializer WorkspaceDefinitionSerializer { get; } protected IDynamicWorkspaceDefinitionStoreInMemoryCache StoreCache { get; } protected IDistributedCache DistributedCache { get; } @@ -25,7 +25,7 @@ public class DynamicWorkspaceDefinitionStore : IDynamicWorkspaceDefinitionStore, protected AbpDistributedCacheOptions CacheOptions { get; } public DynamicWorkspaceDefinitionStore( - IWorkspaceRepository workspaceRepository, + IWorkspaceDefinitionRecordRepository workspaceRepository, IWorkspaceDefinitionSerializer workspaceDefinitionSerializer, IDynamicWorkspaceDefinitionStoreInMemoryCache storeCache, IDistributedCache distributedCache, @@ -33,7 +33,7 @@ public class DynamicWorkspaceDefinitionStore : IDynamicWorkspaceDefinitionStore, IOptions aiManagementOptions, IAbpDistributedLock distributedLock) { - WorkspaceRepository = workspaceRepository; + WorkspaceDefinitionRecordRepository = workspaceRepository; WorkspaceDefinitionSerializer = workspaceDefinitionSerializer; StoreCache = storeCache; DistributedCache = distributedCache; @@ -103,7 +103,7 @@ public class DynamicWorkspaceDefinitionStore : IDynamicWorkspaceDefinitionStore, protected virtual async Task UpdateInMemoryStoreCache() { - var workspaces = await WorkspaceRepository.GetListAsync(); + var workspaces = await WorkspaceDefinitionRecordRepository.GetListAsync(); await StoreCache.FillAsync(workspaces); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs index 43314dfed..478ededa9 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -30,7 +30,7 @@ public class DynamicWorkspaceDefinitionStoreInMemoryCache : IDynamicWorkspaceDef WorkspaceDefinitions = new Dictionary(); } - public Task FillAsync(List workspaces) + public Task FillAsync(List workspaces) { WorkspaceDefinitions.Clear(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs index c2d2a4b12..ec3c6b82b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -14,7 +14,7 @@ public interface IDynamicWorkspaceDefinitionStoreInMemoryCache DateTime? LastCheckTime { get; set; } - Task FillAsync(List permissions); + Task FillAsync(List permissions); WorkspaceDefinition? GetWorkspaceOrNull(string name); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs similarity index 59% rename from aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs index db729e5a5..e4b566cee 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs @@ -4,9 +4,9 @@ using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; namespace LINGYUN.Abp.AIManagement.Workspaces; -public interface IWorkspaceRepository : IBasicRepository +public interface IWorkspaceDefinitionRecordRepository : IBasicRepository { - Task FindByNameAsync( + Task FindByNameAsync( string name, CancellationToken cancellationToken = default); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs index fd1106fcd..f2e186875 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace LINGYUN.Abp.AIManagement.Workspaces; public interface IWorkspaceDefinitionSerializer { - Task SerializeAsync(IEnumerable definitions); + Task SerializeAsync(IEnumerable definitions); - Task SerializeAsync(WorkspaceDefinition definition); + Task SerializeAsync(WorkspaceDefinition definition); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs index df306125c..255d65e41 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs @@ -22,7 +22,7 @@ namespace LINGYUN.Abp.AIManagement.Workspaces; public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency { protected IStaticWorkspaceDefinitionStore StaticStore { get; } - protected IWorkspaceRepository WorkspaceRepository { get; } + protected IWorkspaceDefinitionRecordRepository WorkspaceRepository { get; } protected IWorkspaceDefinitionSerializer WorkspaceSerializer { get; } protected IDistributedCache Cache { get; } protected IApplicationInfoAccessor ApplicationInfoAccessor { get; } @@ -35,7 +35,7 @@ public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency public StaticWorkspaceSaver( IStaticWorkspaceDefinitionStore staticStore, - IWorkspaceRepository workspaceRepository, + IWorkspaceDefinitionRecordRepository workspaceRepository, IWorkspaceDefinitionSerializer workspaceSerializer, IDistributedCache cache, IOptions cacheOptions, @@ -140,10 +140,10 @@ public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency ); } - private async Task UpdateChangedWorkspacesAsync(Workspace[] workspaces) + private async Task UpdateChangedWorkspacesAsync(WorkspaceDefinitionRecord[] workspaces) { - var newRecords = new List(); - var changedRecords = new List(); + var newRecords = new List(); + var changedRecords = new List(); var workspaceRecordsInDatabase = (await WorkspaceRepository.GetListAsync()).ToDictionary(x => x.Name); @@ -169,7 +169,7 @@ public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency } /* Deleted */ - var deletedRecords = new List(); + var deletedRecords = new List(); if (AIOptions.DeletedWorkspaces.Any()) { @@ -214,7 +214,7 @@ public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency return $"{CacheOptions.KeyPrefix}_AbpInMemoryWorkspaceCacheStamp"; } - private string CalculateHash(Workspace[] workspaces, IEnumerable deletedWorkspaces) + private string CalculateHash(WorkspaceDefinitionRecord[] workspaces, IEnumerable deletedWorkspaces) { var jsonSerializerOptions = new JsonSerializerOptions { @@ -222,7 +222,7 @@ public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency { Modifiers = { - new AbpIgnorePropertiesModifiers().CreateModifyAction(x => x.Id), + new AbpIgnorePropertiesModifiers().CreateModifyAction(x => x.Id), } } }; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs similarity index 86% rename from aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs index bb09b2f0f..1e75cba0b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs @@ -4,7 +4,7 @@ using Volo.Abp.Data; using Volo.Abp.Domain.Entities.Auditing; namespace LINGYUN.Abp.AIManagement.Workspaces; -public class Workspace : AuditedAggregateRoot +public class WorkspaceDefinitionRecord : AuditedAggregateRoot { public string Name { get; private set; } @@ -36,13 +36,13 @@ public class Workspace : AuditedAggregateRoot public string? StateCheckers { get; set; } - protected Workspace() + protected WorkspaceDefinitionRecord() { ExtraProperties = new ExtraPropertyDictionary(); this.SetDefaultsForExtraProperties(); } - public Workspace( + public WorkspaceDefinitionRecord( Guid id, string name, string provider, @@ -60,16 +60,16 @@ public class Workspace : AuditedAggregateRoot string? stateCheckers = null) : base(id) { - Name = Check.NotNullOrWhiteSpace(name, nameof(name), WorkspaceConsts.MaxNameLength); - Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), WorkspaceConsts.MaxProviderLength); - ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceConsts.MaxModelNameLength); - DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceConsts.MaxDisplayNameLength); - Description = Check.Length(description, nameof(description), WorkspaceConsts.MaxDescriptionLength); - ApiKey = Check.Length(apiKey, nameof(apiKey), WorkspaceConsts.MaxApiKeyLength); - ApiBaseUrl = Check.Length(apiBaseUrl, nameof(apiBaseUrl), WorkspaceConsts.MaxApiBaseUrlLength); - SystemPrompt = Check.Length(systemPrompt, nameof(systemPrompt), WorkspaceConsts.MaxSystemPromptLength); - Instructions = Check.Length(instructions, nameof(instructions), WorkspaceConsts.MaxInstructionsLength); - StateCheckers = Check.Length(stateCheckers, nameof(stateCheckers), WorkspaceConsts.MaxStateCheckersLength); + Name = Check.NotNullOrWhiteSpace(name, nameof(name), WorkspaceDefinitionRecordConsts.MaxNameLength); + Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), WorkspaceDefinitionRecordConsts.MaxProviderLength); + ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceDefinitionRecordConsts.MaxModelNameLength); + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceDefinitionRecordConsts.MaxDisplayNameLength); + Description = Check.Length(description, nameof(description), WorkspaceDefinitionRecordConsts.MaxDescriptionLength); + ApiKey = Check.Length(apiKey, nameof(apiKey), WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + ApiBaseUrl = Check.Length(apiBaseUrl, nameof(apiBaseUrl), WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength); + SystemPrompt = Check.Length(systemPrompt, nameof(systemPrompt), WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); + Instructions = Check.Length(instructions, nameof(instructions), WorkspaceDefinitionRecordConsts.MaxInstructionsLength); + StateCheckers = Check.Length(stateCheckers, nameof(stateCheckers), WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); Temperature = temperature; MaxOutputTokens = maxOutputTokens; FrequencyPenalty = frequencyPenalty; @@ -80,7 +80,7 @@ public class Workspace : AuditedAggregateRoot this.SetDefaultsForExtraProperties(); } - public bool HasSameData(Workspace otherWorkspace) + public bool HasSameData(WorkspaceDefinitionRecord otherWorkspace) { if (Name != otherWorkspace.Name) { @@ -165,7 +165,7 @@ public class Workspace : AuditedAggregateRoot return true; } - public void Patch(Workspace otherWorkspace) + public void Patch(WorkspaceDefinitionRecord otherWorkspace) { if (Name != otherWorkspace.Name) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs index 351ffa6ca..3c69f73fd 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs @@ -25,9 +25,9 @@ public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITr LocalizableStringSerializer = localizableStringSerializer; } - public async virtual Task SerializeAsync(IEnumerable definitions) + public async virtual Task SerializeAsync(IEnumerable definitions) { - var records = new List(); + var records = new List(); foreach (var workspaceDef in definitions) { records.Add(await SerializeAsync(workspaceDef)); @@ -36,11 +36,11 @@ public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITr return records.ToArray(); } - public virtual Task SerializeAsync(WorkspaceDefinition definition) + public virtual Task SerializeAsync(WorkspaceDefinition definition) { using (CultureHelper.Use(CultureInfo.InvariantCulture)) { - var workspace = new Workspace( + var workspace = new WorkspaceDefinitionRecord( GuidGenerator.Create(), definition.Name, definition.Provider, diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs index 823e7cc71..790174fb2 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs @@ -8,7 +8,7 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; [ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] public class AIManagementDbContext : AbpDbContext, IAIManagementDbContext { - public DbSet Workspaces { get; set; } + public DbSet Workspaces { get; set; } public AIManagementDbContext( DbContextOptions options) : base(options) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 8512e2199..7496ce829 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -17,22 +17,22 @@ public static class AIManagementDbContextModelBuilderExtensions return; } - builder.Entity(b => + builder.Entity(b => { - b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "Workspaces", AbpAIManagementDbProperties.DbSchema); + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "WorkspaceDefinitions", AbpAIManagementDbProperties.DbSchema); b.ConfigureByConvention(); - b.Property(x => x.Name).HasMaxLength(WorkspaceConsts.MaxNameLength).IsRequired(); - b.Property(x => x.Provider).HasMaxLength(WorkspaceConsts.MaxProviderLength).IsRequired(); - b.Property(x => x.ModelName).HasMaxLength(WorkspaceConsts.MaxModelNameLength).IsRequired(); - b.Property(x => x.DisplayName).HasMaxLength(WorkspaceConsts.MaxDisplayNameLength).IsRequired(); - b.Property(x => x.Description).HasMaxLength(WorkspaceConsts.MaxDescriptionLength); - b.Property(x => x.ApiKey).HasMaxLength(WorkspaceConsts.MaxApiKeyLength); - b.Property(x => x.ApiBaseUrl).HasMaxLength(WorkspaceConsts.MaxApiKeyLength); - b.Property(x => x.SystemPrompt).HasMaxLength(WorkspaceConsts.MaxSystemPromptLength); - b.Property(x => x.Instructions).HasMaxLength(WorkspaceConsts.MaxInstructionsLength); - b.Property(x => x.StateCheckers).HasMaxLength(WorkspaceConsts.MaxStateCheckersLength); + b.Property(x => x.Name).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength).IsRequired(); + b.Property(x => x.Provider).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxProviderLength).IsRequired(); + b.Property(x => x.ModelName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxModelNameLength).IsRequired(); + b.Property(x => x.DisplayName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength).IsRequired(); + b.Property(x => x.Description).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDescriptionLength); + b.Property(x => x.ApiKey).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + b.Property(x => x.ApiBaseUrl).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + b.Property(x => x.SystemPrompt).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); + b.Property(x => x.Instructions).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxInstructionsLength); + b.Property(x => x.StateCheckers).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); b.HasIndex(x => new { x.Name }).IsUnique(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs index 566db1fc1..870cad669 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -16,7 +16,7 @@ public class AbpAIManagementEntityFrameworkCoreModule : AbpModule { options.AddDefaultRepositories(); - options.AddRepository(); + options.AddRepository(); }); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceDefinitionRecordRepository.cs similarity index 64% rename from aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceRepository.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceDefinitionRecordRepository.cs index c0fcc2cb9..7c0bcd3b5 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceDefinitionRecordRepository.cs @@ -9,15 +9,15 @@ using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; -public class EfCoreWorkspaceRepository : EfCoreRepository, IWorkspaceRepository +public class EfCoreWorkspaceDefinitionRecordRepository : EfCoreRepository, IWorkspaceDefinitionRecordRepository { - public EfCoreWorkspaceRepository( + public EfCoreWorkspaceDefinitionRecordRepository( IDbContextProvider dbContextProvider) : base(dbContextProvider) { } - public async virtual Task FindByNameAsync(string name, CancellationToken cancellationToken = default) + public async virtual Task FindByNameAsync(string name, CancellationToken cancellationToken = default) { return await (await GetQueryableAsync()) .Where(x => x.Name == name) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs index 234148a27..8a6f5fa6f 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs @@ -8,5 +8,5 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; [ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] public interface IAIManagementDbContext : IEfCoreDbContext { - DbSet Workspaces { get; } + DbSet Workspaces { get; } } From ee374cf5767098b93f76dd3a455e5166afa781b9 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 11:10:49 +0800 Subject: [PATCH 13/25] feat: replace IsTenantOnlyDatabase to IsHostDatabase --- ...nagementDbContextModelBuilderExtensions.cs | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 7496ce829..46de47d01 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -12,33 +12,31 @@ public static class AIManagementDbContextModelBuilderExtensions { Check.NotNull(builder, nameof(builder)); - if (builder.IsTenantOnlyDatabase()) + if (builder.IsHostDatabase()) { - return; + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "WorkspaceDefinitions", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength).IsRequired(); + b.Property(x => x.Provider).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxProviderLength).IsRequired(); + b.Property(x => x.ModelName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxModelNameLength).IsRequired(); + b.Property(x => x.DisplayName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength).IsRequired(); + b.Property(x => x.Description).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDescriptionLength); + b.Property(x => x.ApiKey).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + b.Property(x => x.ApiBaseUrl).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + b.Property(x => x.SystemPrompt).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); + b.Property(x => x.Instructions).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxInstructionsLength); + b.Property(x => x.StateCheckers).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); + + b.HasIndex(x => new { x.Name }).IsUnique(); + + b.ApplyObjectExtensionMappings(); + }); } - builder.Entity(b => - { - b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "WorkspaceDefinitions", AbpAIManagementDbProperties.DbSchema); - - b.ConfigureByConvention(); - - b.Property(x => x.Name).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength).IsRequired(); - b.Property(x => x.Provider).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxProviderLength).IsRequired(); - b.Property(x => x.ModelName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxModelNameLength).IsRequired(); - b.Property(x => x.DisplayName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength).IsRequired(); - b.Property(x => x.Description).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDescriptionLength); - b.Property(x => x.ApiKey).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); - b.Property(x => x.ApiBaseUrl).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); - b.Property(x => x.SystemPrompt).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); - b.Property(x => x.Instructions).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxInstructionsLength); - b.Property(x => x.StateCheckers).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); - - b.HasIndex(x => new { x.Name }).IsUnique(); - - b.ApplyObjectExtensionMappings(); - }); - builder.TryConfigureObjectExtensions(); } } From 3ae8ce0b674baf0dbd945281b5c07d07da91366e Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 11:33:38 +0800 Subject: [PATCH 14/25] feat: Add AIManagement localization resource --- .../AbpAIManagementDomainSharedModule.cs | 17 ++++++++++++++++- .../Localization/AIManagementResource.cs | 8 ++++++++ .../AIManagement/Localization/Resources/en.json | 5 +++++ .../Localization/Resources/zh-Hans.json | 5 +++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/AIManagementResource.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs index 5515864d6..5c0168a94 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -1,10 +1,25 @@ -using Volo.Abp.Domain; +using LINGYUN.Abp.AIManagement.Localization; +using Volo.Abp.Domain; +using Volo.Abp.Localization; using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.AIManagement; [DependsOn(typeof(AbpDddDomainSharedModule))] public class AbpAIManagementDomainSharedModule : AbpModule { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + Configure(options => + { + options.Resources.Add() + .AddVirtualJson("/LINGYUN/Abp/AIManagement/Localization/Resources"); + }); + } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/AIManagementResource.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/AIManagementResource.cs new file mode 100644 index 000000000..d31c83f9a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/AIManagementResource.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.AIManagement.Localization; + +[LocalizationResourceName("AIManagement")] +public class AIManagementResource +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json new file mode 100644 index 000000000..a9c8dcc3f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json @@ -0,0 +1,5 @@ +{ + "culture": "en", + "texts": { + } +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..c5ad81326 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json @@ -0,0 +1,5 @@ +{ + "culture": "zh-Hans", + "texts": { + } +} \ No newline at end of file From 6d51f28f30ffb2b90e6d0afdbca9d15ff0de078e Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 13:03:39 +0800 Subject: [PATCH 15/25] feat: Add UseTextMessageStore Impl --- ...GYUN.Abp.AIManagement.Domain.Shared.csproj | 5 + .../Localization/Resources/en.json | 2 + .../Localization/Resources/zh-Hans.json | 2 + .../Messages/UserMessageRecordConsts.cs | 5 + .../Messages/UserTextMessageRecordConsts.cs | 5 + .../LINGYUN.Abp.AIManagement.Domain.csproj | 1 + .../AbpAIManagementDomainMappers.cs | 15 +++ .../IUserTextMessageRecordRepository.cs | 14 +++ .../Messages/UserMessageRecord.cs | 69 +++++++++++ .../AIManagement/Messages/UserMessageStore.cs | 111 ++++++++++++++++++ .../Messages/UserTextMessageRecord.cs | 27 +++++ .../AIManagementSettingDefinitionProvider.cs | 22 ++++ .../Settings/AIManagementSettingNames.cs | 10 ++ .../AIManagementDbContext.cs | 6 +- ...nagementDbContextModelBuilderExtensions.cs | 16 +++ ...bpAIManagementEntityFrameworkCoreModule.cs | 5 +- .../EfCoreUserTextMessageRecordRepository.cs | 31 +++++ .../IAIManagementDbContext.cs | 6 +- 18 files changed, 347 insertions(+), 5 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/IUserTextMessageRecordRepository.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreUserTextMessageRecordRepository.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj index 148c75c26..9f7983f9b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj @@ -14,6 +14,11 @@ + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json index a9c8dcc3f..d29ec98c1 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json @@ -1,5 +1,7 @@ { "culture": "en", "texts": { + "DisplayName:MaxLatestHistoryMessagesToKeep": "Carry the recent conversation records", + "Description:MaxLatestHistoryMessagesToKeep": "When a user initiates a conversation with a large model, the upper limit of the user's recent conversation history that is carried along." } } \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json index c5ad81326..6eb63499b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json @@ -1,5 +1,7 @@ { "culture": "zh-Hans", "texts": { + "DisplayName:MaxLatestHistoryMessagesToKeep": "携带最近对话记录", + "Description:MaxLatestHistoryMessagesToKeep": "用户发起与大模型对话时,携带用户最近对话记录的上限." } } \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs new file mode 100644 index 000000000..e51d5365a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AIManagement.Messages; +public static class UserMessageRecordConsts +{ + public static int MaxConversationIdLength { get; set; } = 64; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs new file mode 100644 index 000000000..184b93ab0 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AIManagement.Messages; +public static class UserTextMessageRecordConsts +{ + public static int MaxContentLength { get; set; } = 1024; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj index 3a58fe260..2cc7884dc 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj @@ -18,6 +18,7 @@ + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs new file mode 100644 index 000000000..1993a46f5 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs @@ -0,0 +1,15 @@ +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AIManagement.Messages; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.ObjectExtending; + +namespace LINGYUN.Abp.AIManagement; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties(DefinitionChecks = MappingPropertyDefinitionChecks.None)] +public partial class UserTextMessageRecordToUserTextMessageMapper : MapperBase +{ + public override partial UserTextMessage Map(UserTextMessageRecord source); + public override partial void Map(UserTextMessageRecord source, UserTextMessage destination); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/IUserTextMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/IUserTextMessageRecordRepository.cs new file mode 100644 index 000000000..6ecb5ab8a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/IUserTextMessageRecordRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace LINGYUN.Abp.AIManagement.Messages; +public interface IUserTextMessageRecordRepository : IBasicRepository +{ + Task> GetHistoryMessagesAsync( + string conversationId, + int maxResultCount = 0, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs new file mode 100644 index 000000000..559a4f189 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs @@ -0,0 +1,69 @@ +using LINGYUN.Abp.AIManagement.Workspaces; +using System; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Messages; +public abstract class UserMessageRecord : AuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; private set; } + + public string Workspace { get; private set; } + + public string? ConversationId { get; private set; } + + public string? ReplyMessage { get; private set; } + + protected UserMessageRecord() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public UserMessageRecord( + Guid id, + string workspace, + Guid? tenantId = null) + : base(id) + { + TenantId = tenantId; + Workspace = workspace; + } + + public virtual UserMessageRecord WithConversationId(string conversationId) + { + ConversationId = Check.NotNullOrWhiteSpace(conversationId, nameof(conversationId), UserMessageRecordConsts.MaxConversationIdLength); + return this; + } + + public virtual UserMessageRecord WithReply(string replyMessage) + { + ReplyMessage = replyMessage; + return this; + } + + public void Patch(UserMessageRecord otherMessage) + { + if (ConversationId != otherMessage.ConversationId) + { + ConversationId = otherMessage.ConversationId; + } + + if (ReplyMessage != otherMessage.ReplyMessage) + { + ReplyMessage = otherMessage.ReplyMessage; + } + + if (!this.HasSameExtraProperties(otherMessage)) + { + ExtraProperties.Clear(); + + foreach (var property in otherMessage.ExtraProperties) + { + ExtraProperties.Add(property.Key, property.Value); + } + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageStore.cs new file mode 100644 index 000000000..04dd720a3 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageStore.cs @@ -0,0 +1,111 @@ +using LINGYUN.Abp.AI.Messages; +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AIManagement.Settings; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.AIManagement.Messages; + +[Dependency(ReplaceServices = true)] +public class UserMessageStore : IUserMessageStore, ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + private readonly IGuidGenerator _guidGenerator; + private readonly ISettingProvider _settingProvider; + private readonly IObjectMapper _objectMapper; + private readonly IUserTextMessageRecordRepository _messageRecordRepository; + + public UserMessageStore( + ICurrentTenant currentTenant, + IGuidGenerator guidGenerator, + ISettingProvider settingProvider, + IObjectMapper objectMapper, + IUserTextMessageRecordRepository messageRecordRepository) + { + _currentTenant = currentTenant; + _guidGenerator = guidGenerator; + _settingProvider = settingProvider; + _objectMapper = objectMapper; + _messageRecordRepository = messageRecordRepository; + } + + public async virtual Task> GetHistoryMessagesAsync(string conversationId) + { + var maxLatestHistoryMessagesToKeep = await _settingProvider.GetAsync(AIManagementSettingNames.UserMessage.MaxLatestHistoryMessagesToKeep, 0); + if (maxLatestHistoryMessagesToKeep < 1) + { + return Array.Empty(); + } + + var userTextMessages = await _messageRecordRepository.GetHistoryMessagesAsync(conversationId, maxLatestHistoryMessagesToKeep); + + return _objectMapper.Map, IEnumerable>(userTextMessages); + } + + public async virtual Task SaveMessageAsync(UserMessage message) + { + var messageId = message.Id; + if (messageId.IsNullOrWhiteSpace()) + { + messageId = _guidGenerator.Create().ToString(); + message.WithMessageId(messageId); + } + + await StoreMessageAsync(Guid.Parse(messageId), message); + + return messageId; + } + + protected async virtual Task StoreMessageAsync(Guid messageId, UserMessage message) + { + switch (message) + { + case UserTextMessage textMessage: + await StoreUserTextMessageAsync(messageId, textMessage); + break; + } + } + + protected async virtual Task StoreUserTextMessageAsync(Guid messageId, UserTextMessage textMessage) + { + var textMessageRecord = await _messageRecordRepository.FindAsync(messageId); + if (textMessageRecord == null) + { + textMessageRecord = new UserTextMessageRecord( + messageId, + textMessage.Workspace, + textMessage.Content, + _currentTenant.Id); + + UpdateUserMessageRecord(textMessageRecord, textMessage); + + await _messageRecordRepository.InsertAsync(textMessageRecord); + } + else + { + textMessageRecord.WithContent(textMessage.Content); + + UpdateUserMessageRecord(textMessageRecord, textMessage); + + await _messageRecordRepository.UpdateAsync(textMessageRecord); + } + } + + private static void UpdateUserMessageRecord(UserMessageRecord messageRecord, UserMessage message) + { + if (!message.ConversationId.IsNullOrWhiteSpace()) + { + messageRecord.WithConversationId(message.ConversationId); + } + if (!message.ReplyMessage.IsNullOrWhiteSpace()) + { + messageRecord.WithConversationId(message.ReplyMessage); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs new file mode 100644 index 000000000..874790b9b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs @@ -0,0 +1,27 @@ +using System; +using Volo.Abp; + +namespace LINGYUN.Abp.AIManagement.Messages; +public class UserTextMessageRecord : UserMessageRecord +{ + public string Content { get; private set; } + + public UserTextMessageRecord() + { + } + + public UserTextMessageRecord( + Guid id, + string workspace, + string content, + Guid? tenantId = null) : base(id, workspace, tenantId) + { + WithContent(content); + } + + public virtual UserTextMessageRecord WithContent(string content) + { + Content = Check.NotNullOrWhiteSpace(content, nameof(content), UserTextMessageRecordConsts.MaxContentLength); + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs new file mode 100644 index 000000000..c862256fc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs @@ -0,0 +1,22 @@ +using LINGYUN.Abp.AIManagement.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.AIManagement.Settings; +public class AIManagementSettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add( + new SettingDefinition( + AIManagementSettingNames.UserMessage.MaxLatestHistoryMessagesToKeep, + defaultValue: "5", + displayName: L("DisplayName:MaxLatestHistoryMessagesToKeep"), + description: L("Description:MaxLatestHistoryMessagesToKeep"))); + } + + private static ILocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs new file mode 100644 index 000000000..3cdb8355d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs @@ -0,0 +1,10 @@ +namespace LINGYUN.Abp.AIManagement.Settings; +public static class AIManagementSettingNames +{ + public const string Prefix = "Abp.AIManagement"; + + public static class UserMessage + { + public const string MaxLatestHistoryMessagesToKeep = Prefix + ".MaxLatestHistoryMessagesToKeep"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs index 790174fb2..865365326 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs @@ -1,4 +1,5 @@ -using LINGYUN.Abp.AIManagement.Workspaces; +using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; @@ -8,7 +9,8 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; [ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] public class AIManagementDbContext : AbpDbContext, IAIManagementDbContext { - public DbSet Workspaces { get; set; } + public DbSet WorkspaceDefinitions { get; set; } + public DbSet UserTextMessageRecords { get; set; } public AIManagementDbContext( DbContextOptions options) : base(options) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 46de47d01..181054e54 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using LINGYUN.Abp.AIManagement.Messages; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp; @@ -12,6 +13,21 @@ public static class AIManagementDbContextModelBuilderExtensions { Check.NotNull(builder, nameof(builder)); + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "UserTextMessages", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Workspace).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength).IsRequired(); + b.Property(x => x.ConversationId).HasMaxLength(UserMessageRecordConsts.MaxConversationIdLength); + b.Property(x => x.Content).HasMaxLength(UserTextMessageRecordConsts.MaxContentLength).IsRequired(); + + b.HasIndex(x => new { x.TenantId, x.ConversationId }); + + b.ApplyObjectExtensionMappings(); + }); + if (builder.IsHostDatabase()) { builder.Entity(b => diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs index 870cad669..0371fb3fd 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -1,4 +1,5 @@ -using LINGYUN.Abp.AIManagement.Workspaces; +using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Modularity; @@ -16,6 +17,8 @@ public class AbpAIManagementEntityFrameworkCoreModule : AbpModule { options.AddDefaultRepositories(); + options.AddRepository(); + options.AddRepository(); }); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreUserTextMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreUserTextMessageRecordRepository.cs new file mode 100644 index 000000000..6d1ddc943 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreUserTextMessageRecordRepository.cs @@ -0,0 +1,31 @@ +using LINGYUN.Abp.AIManagement.Messages; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public class EfCoreUserTextMessageRecordRepository : EfCoreRepository, IUserTextMessageRecordRepository +{ + public EfCoreUserTextMessageRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task> GetHistoryMessagesAsync( + string conversationId, + int maxResultCount = 0, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(x => x.ConversationId == conversationId) + .OrderByDescending(x => x.CreationTime) + .Take(maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs index 8a6f5fa6f..d253a4aa6 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs @@ -1,4 +1,5 @@ -using LINGYUN.Abp.AIManagement.Workspaces; +using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; @@ -8,5 +9,6 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; [ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] public interface IAIManagementDbContext : IEfCoreDbContext { - DbSet Workspaces { get; } + DbSet WorkspaceDefinitions { get; } + DbSet UserTextMessageRecords { get; } } From 3ab9926bfc08514ec57fef4acc1351391f37b447 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 17:35:54 +0800 Subject: [PATCH 16/25] feat: Rename UserMessage to ChatMessage --- .../LINGYUN/Abp/AI/Agent/AgentService.cs | 82 +++++++++++-------- .../LINGYUN/Abp/AI/Agent/IAgentService.cs | 2 +- .../LINGYUN.Abp.AI.Core.csproj | 1 + .../LINGYUN/Abp/AI/Chats/IChatMessageStore.cs | 11 +++ .../Abp/AI/Chats/InMemoryChatMessageStore.cs | 52 ++++++++++++ .../Abp/AI/Messages/IUserMessageStore.cs | 11 --- .../AI/Messages/InMemoryUserMessageStore.cs | 46 ----------- .../LINGYUN/Abp/AI/Models/ChatMessage.cs | 53 ++++++++++++ .../LINGYUN/Abp/AI/Models/TextChatMessage.cs | 25 ++++++ .../LINGYUN/Abp/AI/Models/TokenUsageInfo.cs | 34 +++++++- .../LINGYUN/Abp/AI/Models/UserMessage.cs | 53 ------------ .../LINGYUN/Abp/AI/Models/UserTextMessage.cs | 20 ----- .../LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs | 3 +- .../Abp/AI/Tokens/InMemoryTokenUsageStore.cs | 31 ++----- .../Chats/ChatMessageRecordConsts.cs | 6 ++ .../Chats/TextChatMessageRecordConsts.cs | 5 ++ .../Messages/UserMessageRecordConsts.cs | 5 -- .../Messages/UserTextMessageRecordConsts.cs | 5 -- .../AbpAIManagementDomainMappers.cs | 6 +- .../AIManagement/Chats/ChatMessageRecord.cs | 57 +++++++++++++ .../ChatMessageStore.cs} | 41 +++++----- .../ITextChatMessageRecordRepository.cs} | 6 +- .../Chats/TextChatMessageRecord.cs | 31 +++++++ .../Messages/UserMessageRecord.cs | 69 ---------------- .../Messages/UserTextMessageRecord.cs | 27 ------ .../AIManagementSettingDefinitionProvider.cs | 2 +- .../Settings/AIManagementSettingNames.cs | 2 +- .../AIManagement/Tokens/TokenUsageRecord.cs} | 4 +- ...nagementDbContextModelBuilderExtensions.cs | 56 +++++++++---- ...bpAIManagementEntityFrameworkCoreModule.cs | 4 +- ... EfCoreTextChatMessageRecordRepository.cs} | 9 +- .../ChatRoleValueConverter.cs | 11 +++ 32 files changed, 423 insertions(+), 347 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TextChatMessage.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordConsts.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs rename aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/{Messages/UserMessageStore.cs => Chats/ChatMessageStore.cs} (68%) rename aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/{Messages/IUserTextMessageRecordRepository.cs => Chats/ITextChatMessageRecordRepository.cs} (56%) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecord.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs rename aspnet-core/modules/ai/{LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs => LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs} (50%) rename aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/{EfCoreUserTextMessageRecordRepository.cs => EfCoreTextChatMessageRecordRepository.cs} (70%) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/ValueConversions/ChatRoleValueConverter.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs index 17ea6b7a9..3d4f11183 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs @@ -1,6 +1,7 @@ -using LINGYUN.Abp.AI.Messages; +using LINGYUN.Abp.AI.Chats; using LINGYUN.Abp.AI.Models; using LINGYUN.Abp.AI.Tokens; +using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using System; using System.Collections.Generic; @@ -8,24 +9,29 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.Timing; +using AIChatMessage = Microsoft.Extensions.AI.ChatMessage; namespace LINGYUN.Abp.AI.Agent; public class AgentService : IAgentService, IScopedDependency { + private readonly IClock _clock; private readonly IAgentFactory _agentFactory; private readonly ITokenUsageStore _tokenUsageStore; - private readonly IUserMessageStore _userMessageStore; + private readonly IChatMessageStore _chatMessageStore; public AgentService( + IClock clock, IAgentFactory agentFactory, ITokenUsageStore tokenUsageStore, - IUserMessageStore userMessageStore) + IChatMessageStore chatMessageStore) { + _clock = clock; _agentFactory = agentFactory; _tokenUsageStore = tokenUsageStore; - _userMessageStore = userMessageStore; + _chatMessageStore = chatMessageStore; } - public async virtual IAsyncEnumerable SendMessageAsync(UserMessage message) + public async virtual IAsyncEnumerable SendMessageAsync(Models.ChatMessage message) { var messages = await BuildChatMessages(message); @@ -33,62 +39,70 @@ public class AgentService : IAgentService, IScopedDependency var agentRunRes = agent.RunStreamingAsync(messages); + var tokenUsageInfo = new TokenUsageInfo(message.Workspace); var agentMessageBuilder = new StringBuilder(); - await foreach (var item in agentRunRes) + await foreach (var response in agentRunRes) { - agentMessageBuilder.Append(item); - yield return item.Text; - - await StoreTokenUsageInfo(message, item.RawRepresentation); + UpdateTokenUsageInfo(tokenUsageInfo, response); + agentMessageBuilder.Append(response.Text); + yield return response.Text; } - await StoreChatMessage(message, agentMessageBuilder.ToString()); + var messageId = await StoreChatMessage(message, agentMessageBuilder.ToString()); + + tokenUsageInfo.WithConversationId(message.ConversationId); + tokenUsageInfo.WithMessageId(messageId); + + Console.WriteLine(); + Console.WriteLine($"消耗Token: {tokenUsageInfo}"); + + await StoreTokenUsageInfo(tokenUsageInfo); } - protected virtual async Task> BuildChatMessages(UserMessage message) + protected virtual async Task> BuildChatMessages(Models.ChatMessage message) { - var messages = new List(); + var messages = new List(); if (!message.ConversationId.IsNullOrWhiteSpace()) { - var historyMessages = await _userMessageStore.GetHistoryMessagesAsync(message.ConversationId); + var historyMessages = await _chatMessageStore.GetHistoryMessagesAsync(message.ConversationId); foreach (var chatMessage in historyMessages) { - messages.Add(new ChatMessage(ChatRole.System, chatMessage.GetMessagePrompt())); + messages.Add(new AIChatMessage(ChatRole.System, chatMessage.GetMessagePrompt())); } } - messages.Add(new ChatMessage(ChatRole.User, message.GetMessagePrompt())); + messages.Add(new AIChatMessage(ChatRole.User, message.GetMessagePrompt())); return messages; } - protected async virtual Task StoreChatMessage(UserMessage message, string agentMessage) + protected async virtual Task StoreChatMessage(Models.ChatMessage message, string agentMessage) { - message.WithReply(agentMessage); + message.WithReply(agentMessage, _clock.Now); - await _userMessageStore.SaveMessageAsync(message); + return await _chatMessageStore.SaveMessageAsync(message); } - protected async virtual Task StoreTokenUsageInfo(UserMessage message, object? rawRepresentation) + protected async virtual Task StoreTokenUsageInfo(TokenUsageInfo tokenUsageInfo) { - if (rawRepresentation is ChatResponseUpdate update) + await _tokenUsageStore.SaveTokenUsageAsync(tokenUsageInfo); + } + + private static void UpdateTokenUsageInfo(TokenUsageInfo tokenUsageInfo, AgentRunResponseUpdate response) + { + if (response.RawRepresentation != null && + response.RawRepresentation is ChatResponseUpdate update) { - var tokenUsageInfos = update.Contents - .OfType() - .Where(usage => usage.Details != null) - .Select(usage => new TokenUsageInfo(message.Workspace) - { - TotalTokenCount = usage.Details.TotalTokenCount, - CachedInputTokenCount = usage.Details.CachedInputTokenCount, - InputTokenCount = usage.Details.InputTokenCount, - OutputTokenCount = usage.Details.OutputTokenCount, - ReasoningTokenCount = usage.Details.ReasoningTokenCount, - }); - - await _tokenUsageStore.SaveTokenUsagesAsync(tokenUsageInfos); + var usageContents = update.Contents.OfType(); + + tokenUsageInfo.InputTokenCount = usageContents.Max(x => x.Details.InputTokenCount); + tokenUsageInfo.OutputTokenCount = usageContents.Max(x => x.Details.OutputTokenCount); + tokenUsageInfo.TotalTokenCount = usageContents.Max(x => x.Details.TotalTokenCount); + tokenUsageInfo.ReasoningTokenCount = usageContents.Max(x => x.Details.ReasoningTokenCount); + tokenUsageInfo.CachedInputTokenCount = usageContents.Max(x => x.Details.CachedInputTokenCount); } } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs index 24ad9e19f..109f267d3 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs @@ -4,5 +4,5 @@ using System.Collections.Generic; namespace LINGYUN.Abp.AI.Agent; public interface IAgentService { - IAsyncEnumerable SendMessageAsync(UserMessage message); + IAsyncEnumerable SendMessageAsync(ChatMessage message); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj index 57d089b1c..4dc562c9c 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs new file mode 100644 index 000000000..023f79b20 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.AI.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Chats; +public interface IChatMessageStore +{ + Task SaveMessageAsync(ChatMessage message); + + Task> GetHistoryMessagesAsync(string conversationId); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs new file mode 100644 index 000000000..293e57202 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs @@ -0,0 +1,52 @@ +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Chats; + +[Dependency(ServiceLifetime.Singleton, TryRegister = true)] +public class InMemoryChatMessageStore : IChatMessageStore +{ + private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); + + public Task> GetHistoryMessagesAsync(string conversationId) + { + var messages = new List(); + + foreach (var userMessages in _userMessageCache.Values) + { + messages.AddRange(userMessages.Where(x => x.ConversationId == conversationId)); + } + + return Task.FromResult>( + messages + .OrderByDescending(x => x.CreatedAt) + .Take(5) + .OrderBy(x => x.CreatedAt)); + } + + public Task SaveMessageAsync(ChatMessage message) + { + var messageId = message.Id; + if (messageId.IsNullOrWhiteSpace()) + { + messageId = Guid.NewGuid().ToString(); + message.WithMessageId(messageId); + } + if (_userMessageCache.ContainsKey(messageId)) + { + _userMessageCache[messageId].Add(message); + } + else + { + _userMessageCache[messageId] = new List() { message }; + } + + return Task.FromResult(messageId); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs deleted file mode 100644 index b56406d5f..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/IUserMessageStore.cs +++ /dev/null @@ -1,11 +0,0 @@ -using LINGYUN.Abp.AI.Models; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace LINGYUN.Abp.AI.Messages; -public interface IUserMessageStore -{ - Task SaveMessageAsync(UserMessage message); - - Task> GetHistoryMessagesAsync(string conversationId); -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs deleted file mode 100644 index a12cf1de4..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Messages/InMemoryUserMessageStore.cs +++ /dev/null @@ -1,46 +0,0 @@ -using LINGYUN.Abp.AI.Models; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; - -namespace LINGYUN.Abp.AI.Messages; - -[Dependency(ServiceLifetime.Singleton, TryRegister = true)] -public class InMemoryUserMessageStore : IUserMessageStore -{ - private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); - - public Task> GetHistoryMessagesAsync(string conversationId) - { - if (_userMessageCache.TryGetValue(conversationId, out var messages)) - { - return Task.FromResult(messages.Take(5)); - } - - return Task.FromResult>(Array.Empty()); - } - - public Task SaveMessageAsync(UserMessage message) - { - var messageId = message.Id; - if (messageId.IsNullOrWhiteSpace()) - { - messageId = Guid.NewGuid().ToString(); - message.WithMessageId(messageId); - } - if (_userMessageCache.ContainsKey(messageId)) - { - _userMessageCache[messageId].Add(message); - } - else - { - _userMessageCache[messageId] = new List() { message }; - } - - return Task.FromResult(messageId); - } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs new file mode 100644 index 000000000..e173d053a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.AI; +using System; + +namespace LINGYUN.Abp.AI.Models; +public abstract class ChatMessage +{ + public string Workspace { get; } + + public string? Id { get; private set; } + + public string? ConversationId { get; private set; } + + public string? ReplyMessage { get; private set; } + + public DateTime? ReplyAt { get; private set; } + + public ChatRole Role { get; private set; } + + public DateTime CreatedAt { get; private set; } + protected ChatMessage( + string workspace, + ChatRole? role = null, + DateTime? createdAt = null) + { + Workspace = workspace; + Role = role ?? ChatRole.User; + CreatedAt = createdAt ?? DateTime.Now; + } + + public virtual ChatMessage WithMessageId(string id) + { + Id = id; + return this; + } + + public virtual ChatMessage WithConversationId(string conversationId) + { + ConversationId = conversationId; + return this; + } + + public virtual ChatMessage WithReply(string replyMessage, DateTime replyAt) + { + ReplyMessage = replyMessage; + ReplyAt = replyAt; + return this; + } + + public virtual string GetMessagePrompt() + { + return string.Empty; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TextChatMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TextChatMessage.cs new file mode 100644 index 000000000..20ea67e1c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TextChatMessage.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.AI; +using System; + +namespace LINGYUN.Abp.AI.Models; +public class TextChatMessage : ChatMessage +{ + /// + /// 消息内容 + /// + public string Content { get; } + public TextChatMessage( + string workspace, + string content, + ChatRole? role = null, + DateTime? createdAt = null) + : base(workspace, role, createdAt) + { + Content = content; + } + + public override string GetMessagePrompt() + { + return Content; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs index 61c8e0457..08238d79b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs @@ -1,7 +1,11 @@ -namespace LINGYUN.Abp.AI.Models; +using System.Text; + +namespace LINGYUN.Abp.AI.Models; public class TokenUsageInfo { public string Workspace { get; } + public string? MessageId { get; private set; } + public string? ConversationId { get; private set; } public long? InputTokenCount { get; set; } public long? OutputTokenCount { get; set; } public long? TotalTokenCount { get; set; } @@ -11,4 +15,32 @@ public class TokenUsageInfo { Workspace = workspace; } + public virtual TokenUsageInfo WithMessageId(string id) + { + MessageId = id; + return this; + } + + public virtual TokenUsageInfo WithConversationId(string? conversationId) + { + ConversationId = conversationId; + return this; + } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine("---------------------- TokenUsage Begin ----------------------"); + sb.AppendLine($"====== Workspace - {Workspace}"); + sb.AppendLine($"====== MessageId - {MessageId}"); + sb.AppendLine($"====== ConversationId - {ConversationId}"); + sb.AppendLine($"====== InputTokenCount - {InputTokenCount}"); + sb.AppendLine($"====== OutputTokenCount - {OutputTokenCount}"); + sb.AppendLine($"====== TotalTokenCount - {TotalTokenCount}"); + sb.AppendLine($"====== ReasoningTokenCount - {ReasoningTokenCount}"); + sb.AppendLine("---------------------- TokenUsage End ----------------------"); + + return sb.ToString(); + } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs deleted file mode 100644 index 7681ae260..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserMessage.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace LINGYUN.Abp.AI.Models; -public abstract class UserMessage -{ - /// - /// 工作区 - /// - public string Workspace { get; } - /// - /// 消息Id - /// - /// - /// 在持久化设施处更新 - /// - public string? Id { get; private set; } - /// - /// 对话Id - /// - /// - /// 用于从客户端存储中持久化和检索聊天历史的唯一标识符,如果未指定则与AI对话时无上下文关联 - /// - public string? ConversationId { get; private set; } - /// - /// AI回复消息 - /// - public string ReplyMessage { get; private set; } - protected UserMessage(string workspace) - { - Workspace = workspace; - } - - public virtual UserMessage WithMessageId(string id) - { - Id = id; - return this; - } - - public virtual UserMessage WithConversationId(string conversationId) - { - ConversationId = conversationId; - return this; - } - - public virtual UserMessage WithReply(string replyMessage) - { - ReplyMessage = replyMessage; - return this; - } - - public virtual string GetMessagePrompt() - { - return string.Empty; - } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs deleted file mode 100644 index 55cb12ae6..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/UserTextMessage.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace LINGYUN.Abp.AI.Models; -public class UserTextMessage : UserMessage -{ - /// - /// 消息内容 - /// - public string Content { get; } - public UserTextMessage( - string workspace, - string content) - : base(workspace) - { - Content = content; - } - - public override string GetMessagePrompt() - { - return Content; - } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs index 32b4d0718..dd44f988a 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs @@ -1,9 +1,8 @@ using LINGYUN.Abp.AI.Models; -using System.Collections.Generic; using System.Threading.Tasks; namespace LINGYUN.Abp.AI.Tokens; public interface ITokenUsageStore { - Task SaveTokenUsagesAsync(IEnumerable usageInfos); + Task SaveTokenUsageAsync(TokenUsageInfo tokenUsageInfo); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs index 9c7862de8..9f492b8b2 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -11,31 +10,19 @@ namespace LINGYUN.Abp.AI.Tokens; [Dependency(ServiceLifetime.Singleton, TryRegister = true)] public class InMemoryTokenUsageStore : ITokenUsageStore { - private static readonly ConcurrentDictionary _tokenUsageCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary> _tokenUsageCache = new ConcurrentDictionary>(); - public Task SaveTokenUsagesAsync(IEnumerable usageInfos) + public Task SaveTokenUsageAsync(TokenUsageInfo tokenUsageInfo) { - foreach (var usageInfo in usageInfos.GroupBy(x => x.Workspace)) + if (_tokenUsageCache.TryGetValue(tokenUsageInfo.Workspace, out var tokenUsageInfos)) { - var tokenUsageInfo = new TokenUsageInfo(usageInfo.Key) - { - InputTokenCount = usageInfo.Sum(x => x.InputTokenCount), - TotalTokenCount = usageInfo.Sum(x => x.TotalTokenCount), - OutputTokenCount = usageInfo.Sum(x => x.OutputTokenCount), - ReasoningTokenCount = usageInfo.Sum(x => x.ReasoningTokenCount), - CachedInputTokenCount = usageInfo.Sum(x => x.CachedInputTokenCount), - }; - - if (!_tokenUsageCache.ContainsKey(usageInfo.Key)) - { - _tokenUsageCache.TryAdd(usageInfo.Key, tokenUsageInfo); - } - else - { - _tokenUsageCache[usageInfo.Key] = tokenUsageInfo; - } + tokenUsageInfos.Add(tokenUsageInfo); } - + else + { + _tokenUsageCache.TryAdd(tokenUsageInfo.Workspace, [tokenUsageInfo]); + } + return Task.CompletedTask; } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs new file mode 100644 index 000000000..3af24e28f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.AIManagement.Chats; +public static class ChatMessageRecordConsts +{ + public static int MaxConversationIdLength { get; set; } = 64; + public static int MaxChatRoleLength { get; set; } = 20; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordConsts.cs new file mode 100644 index 000000000..a603c0e87 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordConsts.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AIManagement.Chats; +public static class TextChatMessageRecordConsts +{ + public static int MaxContentLength { get; set; } = 1024; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs deleted file mode 100644 index e51d5365a..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserMessageRecordConsts.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace LINGYUN.Abp.AIManagement.Messages; -public static class UserMessageRecordConsts -{ - public static int MaxConversationIdLength { get; set; } = 64; -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs deleted file mode 100644 index 184b93ab0..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecordConsts.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace LINGYUN.Abp.AIManagement.Messages; -public static class UserTextMessageRecordConsts -{ - public static int MaxContentLength { get; set; } = 1024; -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs index 1993a46f5..75b113067 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs @@ -8,8 +8,8 @@ namespace LINGYUN.Abp.AIManagement; [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [MapExtraProperties(DefinitionChecks = MappingPropertyDefinitionChecks.None)] -public partial class UserTextMessageRecordToUserTextMessageMapper : MapperBase +public partial class UserTextMessageRecordToUserTextMessageMapper : MapperBase { - public override partial UserTextMessage Map(UserTextMessageRecord source); - public override partial void Map(UserTextMessageRecord source, UserTextMessage destination); + public override partial TextChatMessage Map(UserTextMessageRecord source); + public override partial void Map(UserTextMessageRecord source, TextChatMessage destination); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs new file mode 100644 index 000000000..2b08d4c67 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.AI; +using System; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Chats; +public abstract class ChatMessageRecord : AuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; private set; } + + public string Workspace { get; private set; } + + public ChatRole Role { get; private set; } + + public DateTime CreatedAt { get; private set; } + + public string? ConversationId { get; private set; } + + public string? ReplyMessage { get; private set; } + + public DateTime? ReplyAt { get; private set; } + + protected ChatMessageRecord() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public ChatMessageRecord( + Guid id, + string workspace, + ChatRole role, + DateTime createdAt, + Guid? tenantId = null) + : base(id) + { + Workspace = workspace; + Role = role; + CreatedAt = createdAt; + TenantId = tenantId; + } + + public virtual ChatMessageRecord SetConversationId(string conversationId) + { + ConversationId = Check.NotNullOrWhiteSpace(conversationId, nameof(conversationId), ChatMessageRecordConsts.MaxConversationIdLength); + return this; + } + + public virtual ChatMessageRecord SetReply(string replyMessage, DateTime? replyAt) + { + ReplyMessage = replyMessage; + ReplyAt = replyAt; + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs similarity index 68% rename from aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageStore.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs index 04dd720a3..feb7b9f80 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs @@ -1,4 +1,4 @@ -using LINGYUN.Abp.AI.Messages; +using LINGYUN.Abp.AI.Chats; using LINGYUN.Abp.AI.Models; using LINGYUN.Abp.AIManagement.Settings; using System; @@ -10,23 +10,23 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.ObjectMapping; using Volo.Abp.Settings; -namespace LINGYUN.Abp.AIManagement.Messages; +namespace LINGYUN.Abp.AIManagement.Chats; [Dependency(ReplaceServices = true)] -public class UserMessageStore : IUserMessageStore, ITransientDependency +public class ChatMessageStore : IChatMessageStore, ITransientDependency { private readonly ICurrentTenant _currentTenant; private readonly IGuidGenerator _guidGenerator; private readonly ISettingProvider _settingProvider; private readonly IObjectMapper _objectMapper; - private readonly IUserTextMessageRecordRepository _messageRecordRepository; + private readonly ITextChatMessageRecordRepository _messageRecordRepository; - public UserMessageStore( + public ChatMessageStore( ICurrentTenant currentTenant, IGuidGenerator guidGenerator, ISettingProvider settingProvider, IObjectMapper objectMapper, - IUserTextMessageRecordRepository messageRecordRepository) + ITextChatMessageRecordRepository messageRecordRepository) { _currentTenant = currentTenant; _guidGenerator = guidGenerator; @@ -35,20 +35,21 @@ public class UserMessageStore : IUserMessageStore, ITransientDependency _messageRecordRepository = messageRecordRepository; } - public async virtual Task> GetHistoryMessagesAsync(string conversationId) + public async virtual Task> GetHistoryMessagesAsync(string conversationId) { - var maxLatestHistoryMessagesToKeep = await _settingProvider.GetAsync(AIManagementSettingNames.UserMessage.MaxLatestHistoryMessagesToKeep, 0); + var maxLatestHistoryMessagesToKeep = await _settingProvider.GetAsync( + AIManagementSettingNames.ChatMessage.MaxLatestHistoryMessagesToKeep, 0); if (maxLatestHistoryMessagesToKeep < 1) { - return Array.Empty(); + return Array.Empty(); } var userTextMessages = await _messageRecordRepository.GetHistoryMessagesAsync(conversationId, maxLatestHistoryMessagesToKeep); - return _objectMapper.Map, IEnumerable>(userTextMessages); + return _objectMapper.Map, IEnumerable>(userTextMessages); } - public async virtual Task SaveMessageAsync(UserMessage message) + public async virtual Task SaveMessageAsync(ChatMessage message) { var messageId = message.Id; if (messageId.IsNullOrWhiteSpace()) @@ -62,25 +63,27 @@ public class UserMessageStore : IUserMessageStore, ITransientDependency return messageId; } - protected async virtual Task StoreMessageAsync(Guid messageId, UserMessage message) + protected async virtual Task StoreMessageAsync(Guid messageId, ChatMessage message) { switch (message) { - case UserTextMessage textMessage: + case TextChatMessage textMessage: await StoreUserTextMessageAsync(messageId, textMessage); break; } } - protected async virtual Task StoreUserTextMessageAsync(Guid messageId, UserTextMessage textMessage) + protected async virtual Task StoreUserTextMessageAsync(Guid messageId, TextChatMessage textMessage) { var textMessageRecord = await _messageRecordRepository.FindAsync(messageId); if (textMessageRecord == null) { - textMessageRecord = new UserTextMessageRecord( + textMessageRecord = new TextChatMessageRecord( messageId, textMessage.Workspace, textMessage.Content, + textMessage.Role, + textMessage.CreatedAt, _currentTenant.Id); UpdateUserMessageRecord(textMessageRecord, textMessage); @@ -89,7 +92,7 @@ public class UserMessageStore : IUserMessageStore, ITransientDependency } else { - textMessageRecord.WithContent(textMessage.Content); + textMessageRecord.SetContent(textMessage.Content); UpdateUserMessageRecord(textMessageRecord, textMessage); @@ -97,15 +100,15 @@ public class UserMessageStore : IUserMessageStore, ITransientDependency } } - private static void UpdateUserMessageRecord(UserMessageRecord messageRecord, UserMessage message) + private static void UpdateUserMessageRecord(ChatMessageRecord messageRecord, ChatMessage message) { if (!message.ConversationId.IsNullOrWhiteSpace()) { - messageRecord.WithConversationId(message.ConversationId); + messageRecord.SetConversationId(message.ConversationId); } if (!message.ReplyMessage.IsNullOrWhiteSpace()) { - messageRecord.WithConversationId(message.ReplyMessage); + messageRecord.SetReply(message.ReplyMessage, message.ReplyAt); } } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/IUserTextMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs similarity index 56% rename from aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/IUserTextMessageRecordRepository.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs index 6ecb5ab8a..ad641cb43 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/IUserTextMessageRecordRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs @@ -4,10 +4,10 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; -namespace LINGYUN.Abp.AIManagement.Messages; -public interface IUserTextMessageRecordRepository : IBasicRepository +namespace LINGYUN.Abp.AIManagement.Chats; +public interface ITextChatMessageRecordRepository : IBasicRepository { - Task> GetHistoryMessagesAsync( + Task> GetHistoryMessagesAsync( string conversationId, int maxResultCount = 0, CancellationToken cancellationToken = default); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecord.cs new file mode 100644 index 000000000..16efa2653 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecord.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.AI; +using System; +using Volo.Abp; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class TextChatMessageRecord : ChatMessageRecord +{ + public string Content { get; private set; } + + public TextChatMessageRecord() + { + } + + public TextChatMessageRecord( + Guid id, + string workspace, + string content, + ChatRole role, + DateTime createdAt, + Guid? tenantId = null) + : base(id, workspace, role, createdAt, tenantId) + { + SetContent(content); + } + + public virtual TextChatMessageRecord SetContent(string content) + { + Content = Check.NotNullOrWhiteSpace(content, nameof(content), TextChatMessageRecordConsts.MaxContentLength); + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs deleted file mode 100644 index 559a4f189..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserMessageRecord.cs +++ /dev/null @@ -1,69 +0,0 @@ -using LINGYUN.Abp.AIManagement.Workspaces; -using System; -using Volo.Abp; -using Volo.Abp.Data; -using Volo.Abp.Domain.Entities.Auditing; -using Volo.Abp.MultiTenancy; - -namespace LINGYUN.Abp.AIManagement.Messages; -public abstract class UserMessageRecord : AuditedAggregateRoot, IMultiTenant -{ - public Guid? TenantId { get; private set; } - - public string Workspace { get; private set; } - - public string? ConversationId { get; private set; } - - public string? ReplyMessage { get; private set; } - - protected UserMessageRecord() - { - ExtraProperties = new ExtraPropertyDictionary(); - this.SetDefaultsForExtraProperties(); - } - - public UserMessageRecord( - Guid id, - string workspace, - Guid? tenantId = null) - : base(id) - { - TenantId = tenantId; - Workspace = workspace; - } - - public virtual UserMessageRecord WithConversationId(string conversationId) - { - ConversationId = Check.NotNullOrWhiteSpace(conversationId, nameof(conversationId), UserMessageRecordConsts.MaxConversationIdLength); - return this; - } - - public virtual UserMessageRecord WithReply(string replyMessage) - { - ReplyMessage = replyMessage; - return this; - } - - public void Patch(UserMessageRecord otherMessage) - { - if (ConversationId != otherMessage.ConversationId) - { - ConversationId = otherMessage.ConversationId; - } - - if (ReplyMessage != otherMessage.ReplyMessage) - { - ReplyMessage = otherMessage.ReplyMessage; - } - - if (!this.HasSameExtraProperties(otherMessage)) - { - ExtraProperties.Clear(); - - foreach (var property in otherMessage.ExtraProperties) - { - ExtraProperties.Add(property.Key, property.Value); - } - } - } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs deleted file mode 100644 index 874790b9b..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Messages/UserTextMessageRecord.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Volo.Abp; - -namespace LINGYUN.Abp.AIManagement.Messages; -public class UserTextMessageRecord : UserMessageRecord -{ - public string Content { get; private set; } - - public UserTextMessageRecord() - { - } - - public UserTextMessageRecord( - Guid id, - string workspace, - string content, - Guid? tenantId = null) : base(id, workspace, tenantId) - { - WithContent(content); - } - - public virtual UserTextMessageRecord WithContent(string content) - { - Content = Check.NotNullOrWhiteSpace(content, nameof(content), UserTextMessageRecordConsts.MaxContentLength); - return this; - } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs index c862256fc..e36886420 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs @@ -9,7 +9,7 @@ public class AIManagementSettingDefinitionProvider : SettingDefinitionProvider { context.Add( new SettingDefinition( - AIManagementSettingNames.UserMessage.MaxLatestHistoryMessagesToKeep, + AIManagementSettingNames.ChatMessage.MaxLatestHistoryMessagesToKeep, defaultValue: "5", displayName: L("DisplayName:MaxLatestHistoryMessagesToKeep"), description: L("Description:MaxLatestHistoryMessagesToKeep"))); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs index 3cdb8355d..5e87515f6 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs @@ -3,7 +3,7 @@ public static class AIManagementSettingNames { public const string Prefix = "Abp.AIManagement"; - public static class UserMessage + public static class ChatMessage { public const string MaxLatestHistoryMessagesToKeep = Prefix + ".MaxLatestHistoryMessagesToKeep"; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs similarity index 50% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs index 279c9ba9b..fedf9a194 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessageInfo.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace LINGYUN.Abp.AI.Models; -internal class ChatMessageInfo +namespace LINGYUN.Abp.AIManagement.Tokens; +public class TokenUsageRecord { } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 181054e54..90a2d3fc9 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; -using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.EntityFrameworkCore.ValueConversions; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp; @@ -13,15 +14,24 @@ public static class AIManagementDbContextModelBuilderExtensions { Check.NotNull(builder, nameof(builder)); - builder.Entity(b => + builder.Entity(b => { - b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "UserTextMessages", AbpAIManagementDbProperties.DbSchema); + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "TextChatMessages", AbpAIManagementDbProperties.DbSchema); b.ConfigureByConvention(); - b.Property(x => x.Workspace).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength).IsRequired(); - b.Property(x => x.ConversationId).HasMaxLength(UserMessageRecordConsts.MaxConversationIdLength); - b.Property(x => x.Content).HasMaxLength(UserTextMessageRecordConsts.MaxContentLength).IsRequired(); + b.Property(x => x.Workspace) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength) + .IsRequired(); + b.Property(x => x.Role) + .HasMaxLength(ChatMessageRecordConsts.MaxChatRoleLength) + .HasConversion(new ChatRoleValueConverter()) + .IsRequired(); + b.Property(x => x.ConversationId) + .HasMaxLength(ChatMessageRecordConsts.MaxConversationIdLength); + b.Property(x => x.Content) + .HasMaxLength(TextChatMessageRecordConsts.MaxContentLength) + .IsRequired(); b.HasIndex(x => new { x.TenantId, x.ConversationId }); @@ -36,16 +46,30 @@ public static class AIManagementDbContextModelBuilderExtensions b.ConfigureByConvention(); - b.Property(x => x.Name).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength).IsRequired(); - b.Property(x => x.Provider).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxProviderLength).IsRequired(); - b.Property(x => x.ModelName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxModelNameLength).IsRequired(); - b.Property(x => x.DisplayName).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength).IsRequired(); - b.Property(x => x.Description).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDescriptionLength); - b.Property(x => x.ApiKey).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); - b.Property(x => x.ApiBaseUrl).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); - b.Property(x => x.SystemPrompt).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); - b.Property(x => x.Instructions).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxInstructionsLength); - b.Property(x => x.StateCheckers).HasMaxLength(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); + b.Property(x => x.Name) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength) + .IsRequired(); + b.Property(x => x.Provider) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxProviderLength) + .IsRequired(); + b.Property(x => x.ModelName) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxModelNameLength) + .IsRequired(); + b.Property(x => x.DisplayName) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength) + .IsRequired(); + b.Property(x => x.Description) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDescriptionLength); + b.Property(x => x.ApiKey) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + b.Property(x => x.ApiBaseUrl) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + b.Property(x => x.SystemPrompt) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); + b.Property(x => x.Instructions) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxInstructionsLength); + b.Property(x => x.StateCheckers) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); b.HasIndex(x => new { x.Name }).IsUnique(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs index 0371fb3fd..f372b441f 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -1,4 +1,4 @@ -using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Chats; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.EntityFrameworkCore; @@ -17,7 +17,7 @@ public class AbpAIManagementEntityFrameworkCoreModule : AbpModule { options.AddDefaultRepositories(); - options.AddRepository(); + options.AddRepository(); options.AddRepository(); }); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreUserTextMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs similarity index 70% rename from aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreUserTextMessageRecordRepository.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs index 6d1ddc943..3377e6304 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreUserTextMessageRecordRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs @@ -1,4 +1,4 @@ -using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Chats; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; @@ -9,15 +9,15 @@ using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; -public class EfCoreUserTextMessageRecordRepository : EfCoreRepository, IUserTextMessageRecordRepository +public class EfCoreTextChatMessageRecordRepository : EfCoreRepository, ITextChatMessageRecordRepository { - public EfCoreUserTextMessageRecordRepository( + public EfCoreTextChatMessageRecordRepository( IDbContextProvider dbContextProvider) : base(dbContextProvider) { } - public async virtual Task> GetHistoryMessagesAsync( + public async virtual Task> GetHistoryMessagesAsync( string conversationId, int maxResultCount = 0, CancellationToken cancellationToken = default) @@ -26,6 +26,7 @@ public class EfCoreUserTextMessageRecordRepository : EfCoreRepository x.ConversationId == conversationId) .OrderByDescending(x => x.CreationTime) .Take(maxResultCount) + .OrderBy(x => x.CreationTime) .ToListAsync(GetCancellationToken(cancellationToken)); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/ValueConversions/ChatRoleValueConverter.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/ValueConversions/ChatRoleValueConverter.cs new file mode 100644 index 000000000..c4a1faf2c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/ValueConversions/ChatRoleValueConverter.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.Extensions.AI; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore.ValueConversions; +public class ChatRoleValueConverter(ConverterMappingHints? mappingHints = null) : ValueConverter( + value => value.Value, + value => new ChatRole(value), + mappingHints + ) +{ +} From f97d5171dcfaba456a5d95bec01cc9b33a566ee4 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 16 Jan 2026 17:43:08 +0800 Subject: [PATCH 17/25] feat: Reset ConversationId type to Guid --- .../LINGYUN/Abp/AI/Agent/AgentService.cs | 8 +++++--- .../LINGYUN/Abp/AI/Chats/IChatMessageStore.cs | 3 ++- .../LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs | 2 +- .../LINGYUN/Abp/AI/Models/ChatMessage.cs | 4 ++-- .../LINGYUN/Abp/AI/Models/TokenUsageInfo.cs | 7 ++++--- .../Abp/AIManagement/Chats/ChatMessageRecordConsts.cs | 1 - .../LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs | 7 +++---- .../LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs | 2 +- .../Chats/ITextChatMessageRecordRepository.cs | 2 +- .../AIManagementDbContextModelBuilderExtensions.cs | 2 -- .../EfCoreTextChatMessageRecordRepository.cs | 2 +- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs index 3d4f11183..6fe8db044 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs @@ -54,8 +54,10 @@ public class AgentService : IAgentService, IScopedDependency tokenUsageInfo.WithConversationId(message.ConversationId); tokenUsageInfo.WithMessageId(messageId); +#if DEBUG Console.WriteLine(); - Console.WriteLine($"消耗Token: {tokenUsageInfo}"); + Console.WriteLine(tokenUsageInfo); +#endif await StoreTokenUsageInfo(tokenUsageInfo); } @@ -64,9 +66,9 @@ public class AgentService : IAgentService, IScopedDependency { var messages = new List(); - if (!message.ConversationId.IsNullOrWhiteSpace()) + if (message.ConversationId.HasValue) { - var historyMessages = await _chatMessageStore.GetHistoryMessagesAsync(message.ConversationId); + var historyMessages = await _chatMessageStore.GetHistoryMessagesAsync(message.ConversationId.Value); foreach (var chatMessage in historyMessages) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs index 023f79b20..886a0e953 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.AI.Models; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -7,5 +8,5 @@ public interface IChatMessageStore { Task SaveMessageAsync(ChatMessage message); - Task> GetHistoryMessagesAsync(string conversationId); + Task> GetHistoryMessagesAsync(Guid conversationId); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs index 293e57202..b81ccef2b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs @@ -14,7 +14,7 @@ public class InMemoryChatMessageStore : IChatMessageStore { private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); - public Task> GetHistoryMessagesAsync(string conversationId) + public Task> GetHistoryMessagesAsync(Guid conversationId) { var messages = new List(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs index e173d053a..46770a3f5 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs @@ -8,7 +8,7 @@ public abstract class ChatMessage public string? Id { get; private set; } - public string? ConversationId { get; private set; } + public Guid? ConversationId { get; private set; } public string? ReplyMessage { get; private set; } @@ -33,7 +33,7 @@ public abstract class ChatMessage return this; } - public virtual ChatMessage WithConversationId(string conversationId) + public virtual ChatMessage WithConversationId(Guid conversationId) { ConversationId = conversationId; return this; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs index 08238d79b..770454a3a 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs @@ -1,11 +1,12 @@ -using System.Text; +using System; +using System.Text; namespace LINGYUN.Abp.AI.Models; public class TokenUsageInfo { public string Workspace { get; } public string? MessageId { get; private set; } - public string? ConversationId { get; private set; } + public Guid? ConversationId { get; private set; } public long? InputTokenCount { get; set; } public long? OutputTokenCount { get; set; } public long? TotalTokenCount { get; set; } @@ -21,7 +22,7 @@ public class TokenUsageInfo return this; } - public virtual TokenUsageInfo WithConversationId(string? conversationId) + public virtual TokenUsageInfo WithConversationId(Guid? conversationId) { ConversationId = conversationId; return this; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs index 3af24e28f..34283daae 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs @@ -1,6 +1,5 @@ namespace LINGYUN.Abp.AIManagement.Chats; public static class ChatMessageRecordConsts { - public static int MaxConversationIdLength { get; set; } = 64; public static int MaxChatRoleLength { get; set; } = 20; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs index 2b08d4c67..91bd1c0f1 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.AI; using System; -using Volo.Abp; using Volo.Abp.Data; using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.MultiTenancy; @@ -16,7 +15,7 @@ public abstract class ChatMessageRecord : AuditedAggregateRoot, IMultiTena public DateTime CreatedAt { get; private set; } - public string? ConversationId { get; private set; } + public Guid? ConversationId { get; private set; } public string? ReplyMessage { get; private set; } @@ -42,9 +41,9 @@ public abstract class ChatMessageRecord : AuditedAggregateRoot, IMultiTena TenantId = tenantId; } - public virtual ChatMessageRecord SetConversationId(string conversationId) + public virtual ChatMessageRecord SetConversationId(Guid conversationId) { - ConversationId = Check.NotNullOrWhiteSpace(conversationId, nameof(conversationId), ChatMessageRecordConsts.MaxConversationIdLength); + ConversationId = conversationId; return this; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs index feb7b9f80..add01791f 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs @@ -35,7 +35,7 @@ public class ChatMessageStore : IChatMessageStore, ITransientDependency _messageRecordRepository = messageRecordRepository; } - public async virtual Task> GetHistoryMessagesAsync(string conversationId) + public async virtual Task> GetHistoryMessagesAsync(Guid conversationId) { var maxLatestHistoryMessagesToKeep = await _settingProvider.GetAsync( AIManagementSettingNames.ChatMessage.MaxLatestHistoryMessagesToKeep, 0); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs index ad641cb43..de20f6e27 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs @@ -8,7 +8,7 @@ namespace LINGYUN.Abp.AIManagement.Chats; public interface ITextChatMessageRecordRepository : IBasicRepository { Task> GetHistoryMessagesAsync( - string conversationId, + Guid conversationId, int maxResultCount = 0, CancellationToken cancellationToken = default); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 90a2d3fc9..185fd3c86 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -27,8 +27,6 @@ public static class AIManagementDbContextModelBuilderExtensions .HasMaxLength(ChatMessageRecordConsts.MaxChatRoleLength) .HasConversion(new ChatRoleValueConverter()) .IsRequired(); - b.Property(x => x.ConversationId) - .HasMaxLength(ChatMessageRecordConsts.MaxConversationIdLength); b.Property(x => x.Content) .HasMaxLength(TextChatMessageRecordConsts.MaxContentLength) .IsRequired(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs index 3377e6304..d5d075bd4 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs @@ -18,7 +18,7 @@ public class EfCoreTextChatMessageRecordRepository : EfCoreRepository> GetHistoryMessagesAsync( - string conversationId, + Guid conversationId, int maxResultCount = 0, CancellationToken cancellationToken = default) { From d64571d18cb358b1eed5fc6fb27f2bf57bcfe75b Mon Sep 17 00:00:00 2001 From: colin Date: Tue, 27 Jan 2026 14:43:53 +0800 Subject: [PATCH 18/25] feat: Add persistent conversation --- .../LINGYUN/Abp/AI/Agent/AgentService.cs | 57 ++++++++++- .../LINGYUN.Abp.AI.Core.csproj | 1 + .../LINGYUN/Abp/AI/AbpAICoreModule.cs | 4 + .../LINGYUN/Abp/AI/AbpAIErrorCodes.cs | 4 + .../LINGYUN/Abp/AI/Chats/IChatMessageStore.cs | 2 +- .../Abp/AI/Chats/IConversationStore.cs | 13 +++ .../Abp/AI/Chats/InMemoryChatMessageStore.cs | 18 ++-- .../Abp/AI/Chats/InMemoryConversationStore.cs | 48 ++++++++++ .../Abp/AI/Localization/Resources/en.json | 4 +- .../AI/Localization/Resources/zh-Hans.json | 4 +- .../LINGYUN/Abp/AI/Models/ChatMessage.cs | 4 +- .../LINGYUN/Abp/AI/Models/Conversation.cs | 28 ++++++ .../LINGYUN/Abp/AI/Models/TokenUsageInfo.cs | 15 +-- .../FodyWeavers.xsd | 30 ++++++ .../FodyWeavers.xsd | 30 ++++++ .../Chats/ConversationRecordConsts.cs | 5 + .../AbpAIManagementDomainMappers.cs | 8 +- .../AIManagement/Chats/ChatMessageRecord.cs | 8 ++ .../AIManagement/Chats/ChatMessageStore.cs | 33 +++++-- .../Chats/ConversationCleanupOptions.cs | 15 +++ .../AIManagement/Chats/ConversationRecord.cs | 34 +++++++ .../AIManagement/Chats/ConversationStore.cs | 95 +++++++++++++++++++ .../Chats/IConversationRecordRepository.cs | 21 ++++ .../AIManagementDbContext.cs | 5 +- ...nagementDbContextModelBuilderExtensions.cs | 10 ++ ...bpAIManagementEntityFrameworkCoreModule.cs | 1 + .../EfCoreConversationRecordRepository.cs | 44 +++++++++ .../IAIManagementDbContext.cs | 5 +- .../FodyWeavers.xsd | 30 ++++++ .../FodyWeavers.xsd | 30 ++++++ 30 files changed, 560 insertions(+), 46 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs index 6fe8db044..78c9f0508 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs @@ -1,14 +1,18 @@ using LINGYUN.Abp.AI.Chats; +using LINGYUN.Abp.AI.Localization; using LINGYUN.Abp.AI.Models; using LINGYUN.Abp.AI.Tokens; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Localization; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Volo.Abp; using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; using Volo.Abp.Timing; using AIChatMessage = Microsoft.Extensions.AI.ChatMessage; @@ -16,30 +20,43 @@ namespace LINGYUN.Abp.AI.Agent; public class AgentService : IAgentService, IScopedDependency { private readonly IClock _clock; + private readonly IGuidGenerator _guidGenerator; private readonly IAgentFactory _agentFactory; private readonly ITokenUsageStore _tokenUsageStore; private readonly IChatMessageStore _chatMessageStore; + private readonly IConversationStore _conversationStore; + private readonly IStringLocalizer _localizerResource; public AgentService( IClock clock, + IGuidGenerator guidGenerator, IAgentFactory agentFactory, ITokenUsageStore tokenUsageStore, - IChatMessageStore chatMessageStore) + IChatMessageStore chatMessageStore, + IConversationStore conversationStore, + IStringLocalizer localizerResource) { _clock = clock; + _guidGenerator = guidGenerator; _agentFactory = agentFactory; _tokenUsageStore = tokenUsageStore; _chatMessageStore = chatMessageStore; + _conversationStore = conversationStore; + _localizerResource = localizerResource; } public async virtual IAsyncEnumerable SendMessageAsync(Models.ChatMessage message) { + var conversationId = await StoreConversation(message); + + message.WithConversationId(conversationId); + var messages = await BuildChatMessages(message); var agent = await _agentFactory.CreateAsync(message.Workspace); var agentRunRes = agent.RunStreamingAsync(messages); - var tokenUsageInfo = new TokenUsageInfo(message.Workspace); + var tokenUsageInfo = new TokenUsageInfo(message.Workspace, conversationId); var agentMessageBuilder = new StringBuilder(); await foreach (var response in agentRunRes) @@ -51,7 +68,6 @@ public class AgentService : IAgentService, IScopedDependency var messageId = await StoreChatMessage(message, agentMessageBuilder.ToString()); - tokenUsageInfo.WithConversationId(message.ConversationId); tokenUsageInfo.WithMessageId(messageId); #if DEBUG @@ -70,9 +86,10 @@ public class AgentService : IAgentService, IScopedDependency { var historyMessages = await _chatMessageStore.GetHistoryMessagesAsync(message.ConversationId.Value); + // TODO: 应用摘要提示压缩 foreach (var chatMessage in historyMessages) { - messages.Add(new AIChatMessage(ChatRole.System, chatMessage.GetMessagePrompt())); + messages.Add(new AIChatMessage(chatMessage.Role, chatMessage.GetMessagePrompt())); } } @@ -81,13 +98,43 @@ public class AgentService : IAgentService, IScopedDependency return messages; } - protected async virtual Task StoreChatMessage(Models.ChatMessage message, string agentMessage) + protected async virtual Task StoreChatMessage(Models.ChatMessage message, string agentMessage) { message.WithReply(agentMessage, _clock.Now); return await _chatMessageStore.SaveMessageAsync(message); } + protected async virtual Task StoreConversation(Models.ChatMessage message) + { + if (message.ConversationId.HasValue) + { + var conversation = await _conversationStore.FindAsync(message.ConversationId.Value); + if (conversation == null || conversation.ExpiredAt <= _clock.Now) + { + throw new BusinessException( + AbpAIErrorCodes.ConversationHasExpired, + "The conversation has expired. Please create a new one!"); + } + + conversation.UpdateAt = _clock.Now; + await _conversationStore.SaveAsync(conversation); + + return conversation.Id; + } + else + { + var conversation = new Conversation( + _guidGenerator.Create(), + _localizerResource["NewConversation"], + _clock.Now); + + await _conversationStore.SaveAsync(conversation); + + return conversation.Id; + } + } + protected async virtual Task StoreTokenUsageInfo(TokenUsageInfo tokenUsageInfo) { await _tokenUsageStore.SaveTokenUsageAsync(tokenUsageInfo); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj index 4dc562c9c..b6e35c892 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs index 2ab1185a5..9bd80e7b7 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs @@ -5,15 +5,19 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using Volo.Abp.AI; +using Volo.Abp.Guids; using Volo.Abp.Localization; using Volo.Abp.Localization.ExceptionHandling; using Volo.Abp.Modularity; +using Volo.Abp.Timing; using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.AI; [DependsOn( typeof(AbpAIModule), + typeof(AbpGuidsModule), + typeof(AbpTimingModule), typeof(AbpLocalizationModule))] public class AbpAICoreModule : AbpModule { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs index bb261dea1..e5538e47e 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs @@ -6,4 +6,8 @@ public static class AbpAIErrorCodes /// 工作区不可用: {Workspace}! /// public const string WorkspaceIsNotEnabled = Namespace + ":110001"; + /// + /// 对话已过期, 请重新创建会话! + /// + public const string ConversationHasExpired = Namespace + ":110101"; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs index 886a0e953..724eb5a43 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace LINGYUN.Abp.AI.Chats; public interface IChatMessageStore { - Task SaveMessageAsync(ChatMessage message); + Task SaveMessageAsync(ChatMessage message); Task> GetHistoryMessagesAsync(Guid conversationId); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs new file mode 100644 index 000000000..e13119c9d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs @@ -0,0 +1,13 @@ +using LINGYUN.Abp.AI.Models; +using System; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Chats; +public interface IConversationStore +{ + Task SaveAsync(Conversation conversation); + + Task FindAsync(Guid conversationId); + + Task CleanupAsync(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs index b81ccef2b..3519ea30c 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs @@ -12,7 +12,7 @@ namespace LINGYUN.Abp.AI.Chats; [Dependency(ServiceLifetime.Singleton, TryRegister = true)] public class InMemoryChatMessageStore : IChatMessageStore { - private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); public Task> GetHistoryMessagesAsync(Guid conversationId) { @@ -30,23 +30,23 @@ public class InMemoryChatMessageStore : IChatMessageStore .OrderBy(x => x.CreatedAt)); } - public Task SaveMessageAsync(ChatMessage message) + public Task SaveMessageAsync(ChatMessage message) { var messageId = message.Id; - if (messageId.IsNullOrWhiteSpace()) + if (!messageId.HasValue) { - messageId = Guid.NewGuid().ToString(); - message.WithMessageId(messageId); + messageId = Guid.NewGuid(); + message.WithMessageId(messageId.Value); } - if (_userMessageCache.ContainsKey(messageId)) + if (_userMessageCache.ContainsKey(messageId.Value)) { - _userMessageCache[messageId].Add(message); + _userMessageCache[messageId.Value].Add(message); } else { - _userMessageCache[messageId] = new List() { message }; + _userMessageCache[messageId.Value] = new List() { message }; } - return Task.FromResult(messageId); + return Task.FromResult(messageId.Value); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs new file mode 100644 index 000000000..d273e47eb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs @@ -0,0 +1,48 @@ +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Chats; + +[Dependency(ServiceLifetime.Singleton, TryRegister = true)] +public class InMemoryConversationStore : IConversationStore +{ + private static readonly ConcurrentDictionary _conversationCache = new ConcurrentDictionary(); + public Task SaveAsync(Conversation conversation) + { + if (_conversationCache.ContainsKey(conversation.Id)) + { + conversation.ExpiredAt = DateTime.Now.AddHours(2); + _conversationCache[conversation.Id] = conversation; + } + else + { + _conversationCache.TryAdd(conversation.Id, conversation); + } + + return Task.CompletedTask; + } + + public Task FindAsync(Guid conversationId) + { + _conversationCache.TryGetValue(conversationId, out var conversation); + return Task.FromResult(conversation); + } + + public Task CleanupAsync() + { + // Configure it... + var expiredTime = DateTime.Now.AddHours(-2); + var expiredConversationIds = _conversationCache.Values + .Where(x => x.UpdateAt <= expiredTime) + .Select(x => x.Id); + _conversationCache.RemoveAll(x => expiredConversationIds.Contains(x.Key)); + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json index 399e2a7c9..5e631f34c 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json @@ -1,6 +1,8 @@ { "culture": "en", "texts": { - "Abp.AI:110001": "Workspace is not enabled: {Workspace}!" + "Abp.AI:110001": "Workspace is not enabled: {Workspace}!", + "Abp.AI:110101": "The conversation has expired. Please create a new one!", + "NewConversation": "New Conversation" } } \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json index 3fc69d30d..8681eedaf 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json @@ -1,6 +1,8 @@ { "culture": "zh-Hans", "texts": { - "Abp.AI:110001": "工作区不可用: {Workspace}!" + "Abp.AI:110001": "工作区不可用: {Workspace}!", + "Abp.AI:110101": "对话已过期, 请重新创建会话!", + "NewConversation": "新对话" } } \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs index 46770a3f5..143a537de 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs @@ -6,7 +6,7 @@ public abstract class ChatMessage { public string Workspace { get; } - public string? Id { get; private set; } + public Guid? Id { get; private set; } public Guid? ConversationId { get; private set; } @@ -27,7 +27,7 @@ public abstract class ChatMessage CreatedAt = createdAt ?? DateTime.Now; } - public virtual ChatMessage WithMessageId(string id) + public virtual ChatMessage WithMessageId(Guid id) { Id = id; return this; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs new file mode 100644 index 000000000..52943a7a1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs @@ -0,0 +1,28 @@ +using System; + +namespace LINGYUN.Abp.AI.Models; +public class Conversation +{ + public Guid Id { get; private set; } + public string Name { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime? ExpiredAt { get; set; } + public DateTime? UpdateAt { get; set; } + public Conversation( + Guid id, + string name, + DateTime createdAt) + { + Id = id; + Name = name; + CreatedAt = createdAt; + UpdateAt = createdAt; + } + + public Conversation WithName(string name) + { + Name = name; + + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs index 770454a3a..f74b1867e 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs @@ -5,29 +5,24 @@ namespace LINGYUN.Abp.AI.Models; public class TokenUsageInfo { public string Workspace { get; } - public string? MessageId { get; private set; } - public Guid? ConversationId { get; private set; } + public Guid? MessageId { get; private set; } + public Guid ConversationId { get; private set; } public long? InputTokenCount { get; set; } public long? OutputTokenCount { get; set; } public long? TotalTokenCount { get; set; } public long? CachedInputTokenCount { get; set; } public long? ReasoningTokenCount { get; set; } - public TokenUsageInfo(string workspace) + public TokenUsageInfo(string workspace, Guid conversationId) { Workspace = workspace; + ConversationId = conversationId; } - public virtual TokenUsageInfo WithMessageId(string id) + public virtual TokenUsageInfo WithMessageId(Guid id) { MessageId = id; return this; } - public virtual TokenUsageInfo WithConversationId(Guid? conversationId) - { - ConversationId = conversationId; - return this; - } - public override string ToString() { var sb = new StringBuilder(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs new file mode 100644 index 000000000..30c3868cb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AIManagement.Chats; +public static class ConversationRecordConsts +{ + public static int MaxNameLength { get; set; } = 50; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs index 75b113067..93cdb0261 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs @@ -1,5 +1,5 @@ using LINGYUN.Abp.AI.Models; -using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Chats; using Riok.Mapperly.Abstractions; using Volo.Abp.Mapperly; using Volo.Abp.ObjectExtending; @@ -8,8 +8,8 @@ namespace LINGYUN.Abp.AIManagement; [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [MapExtraProperties(DefinitionChecks = MappingPropertyDefinitionChecks.None)] -public partial class UserTextMessageRecordToUserTextMessageMapper : MapperBase +public partial class TextChatMessageRecordToUserTextMessageMapper : MapperBase { - public override partial TextChatMessage Map(UserTextMessageRecord source); - public override partial void Map(UserTextMessageRecord source, TextChatMessage destination); + public override partial TextChatMessage Map(TextChatMessageRecord source); + public override partial void Map(TextChatMessageRecord source, TextChatMessage destination); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs index 91bd1c0f1..805aaadd3 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs @@ -15,6 +15,8 @@ public abstract class ChatMessageRecord : AuditedAggregateRoot, IMultiTena public DateTime CreatedAt { get; private set; } + public Guid? UserId { get; private set; } + public Guid? ConversationId { get; private set; } public string? ReplyMessage { get; private set; } @@ -41,6 +43,12 @@ public abstract class ChatMessageRecord : AuditedAggregateRoot, IMultiTena TenantId = tenantId; } + public virtual ChatMessageRecord SetUserId(Guid userId) + { + UserId = userId; + return this; + } + public virtual ChatMessageRecord SetConversationId(Guid conversationId) { ConversationId = conversationId; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs index add01791f..724af7db4 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs @@ -3,6 +3,7 @@ using LINGYUN.Abp.AI.Models; using LINGYUN.Abp.AIManagement.Settings; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Guids; @@ -46,21 +47,35 @@ public class ChatMessageStore : IChatMessageStore, ITransientDependency var userTextMessages = await _messageRecordRepository.GetHistoryMessagesAsync(conversationId, maxLatestHistoryMessagesToKeep); - return _objectMapper.Map, IEnumerable>(userTextMessages); + return userTextMessages.Select(msg => + { + var chatMessage = new TextChatMessage(msg.Workspace, msg.Content, msg.Role, msg.CreatedAt); + chatMessage.WithMessageId(msg.Id); + if (msg.ConversationId.HasValue) + { + chatMessage.WithConversationId(msg.ConversationId.Value); + } + if (!msg.ReplyMessage.IsNullOrWhiteSpace() && msg.ReplyAt.HasValue) + { + chatMessage.WithReply(msg.ReplyMessage, msg.ReplyAt.Value); + } + + return chatMessage; + }); } - public async virtual Task SaveMessageAsync(ChatMessage message) + public async virtual Task SaveMessageAsync(ChatMessage message) { var messageId = message.Id; - if (messageId.IsNullOrWhiteSpace()) + if (!messageId.HasValue) { - messageId = _guidGenerator.Create().ToString(); - message.WithMessageId(messageId); + messageId = _guidGenerator.Create(); + message.WithMessageId(messageId.Value); } - await StoreMessageAsync(Guid.Parse(messageId), message); + await StoreMessageAsync(messageId.Value, message); - return messageId; + return messageId.Value; } protected async virtual Task StoreMessageAsync(Guid messageId, ChatMessage message) @@ -102,9 +117,9 @@ public class ChatMessageStore : IChatMessageStore, ITransientDependency private static void UpdateUserMessageRecord(ChatMessageRecord messageRecord, ChatMessage message) { - if (!message.ConversationId.IsNullOrWhiteSpace()) + if (message.ConversationId.HasValue) { - messageRecord.SetConversationId(message.ConversationId); + messageRecord.SetConversationId(message.ConversationId.Value); } if (!message.ReplyMessage.IsNullOrWhiteSpace()) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs new file mode 100644 index 000000000..22f6cbe26 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs @@ -0,0 +1,15 @@ +using System; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ConversationCleanupOptions +{ + public bool IsCleanupEnabled { get; set; } + public TimeSpan ExpiredTime { get; set; } + public int CleanupPeriod { get; set; } + public ConversationCleanupOptions() + { + IsCleanupEnabled = true; + CleanupPeriod = 3_600_000; + ExpiredTime = TimeSpan.FromHours(2); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs new file mode 100644 index 000000000..cd81fcac6 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs @@ -0,0 +1,34 @@ +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ConversationRecord : AuditedEntity, IMultiTenant +{ + public Guid? TenantId { get; private set; } + + public string Name { get; private set; } + + public DateTime CreatedAt { get; private set; } + + public DateTime ExpiredAt { get; set; } + + public DateTime? UpdateAt { get; set; } + public ConversationRecord( + Guid id, + string name, + DateTime createdAt, + DateTime expiredAt, + Guid? tenantId = null) + : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), ConversationRecordConsts.MaxNameLength); + CreatedAt = createdAt; + ExpiredAt = expiredAt; + + UpdateAt = createdAt; + + TenantId = tenantId; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs new file mode 100644 index 000000000..3d7d2ed5e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs @@ -0,0 +1,95 @@ +using LINGYUN.Abp.AI.Chats; +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Specifications; +using Volo.Abp.Timing; + +namespace LINGYUN.Abp.AIManagement.Chats; + +[Dependency(ReplaceServices = true)] +public class ConversationStore : IConversationStore, ITransientDependency +{ + private readonly IClock _clock; + private readonly ICurrentTenant _currentTenant; + private readonly ConversationCleanupOptions _cleanupOptions; + private readonly IConversationRecordRepository _conversationRecordRepository; + + public ConversationStore( + IClock clock, + ICurrentTenant currentTenant, + IOptions cleanupOptions, + IConversationRecordRepository conversationRecordRepository) + { + _clock = clock; + _currentTenant = currentTenant; + _cleanupOptions = cleanupOptions.Value; + _conversationRecordRepository = conversationRecordRepository; + } + + public async virtual Task CleanupAsync() + { + if (!_cleanupOptions.IsCleanupEnabled) + { + return; + } + + var specification = new ExpressionSpecification( + x => x.ExpiredAt <= _clock.Now); + + var totalCount = await _conversationRecordRepository.GetCountAsync(specification); + var expiredRecords = await _conversationRecordRepository.GetListAsync(specification, maxResultCount: totalCount); + + await _conversationRecordRepository.DeleteManyAsync(expiredRecords); + } + + public async virtual Task FindAsync(Guid conversationId) + { + var conversationRecord = await _conversationRecordRepository.FindAsync(conversationId); + if (conversationRecord == null) + { + return null; + } + + var conversation = new Conversation( + conversationRecord.Id, + conversationRecord.Name, + conversationRecord.CreatedAt) + { + UpdateAt = conversationRecord.UpdateAt, + ExpiredAt = conversationRecord.ExpiredAt, + }; + + + return conversation; + } + + public async virtual Task SaveAsync(Conversation conversation) + { + var conversationRecord = await _conversationRecordRepository.FindAsync(conversation.Id); + if (conversationRecord == null) + { + var expiredTime = conversation.CreatedAt.Add(_cleanupOptions.ExpiredTime); + conversationRecord = new ConversationRecord( + conversation.Id, + conversation.Name, + conversation.CreatedAt, + expiredTime, + _currentTenant.Id); + + await _conversationRecordRepository.InsertAsync(conversationRecord); + } + else + { + var expiredTime = (conversation.UpdateAt ?? _clock.Now).Add(_cleanupOptions.ExpiredTime); + conversationRecord.UpdateAt = conversation.UpdateAt; + conversationRecord.ExpiredAt = expiredTime; + + await _conversationRecordRepository.UpdateAsync(conversationRecord); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs new file mode 100644 index 000000000..6ed7e0f1a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Specifications; + +namespace LINGYUN.Abp.AIManagement.Chats; +public interface IConversationRecordRepository : IBasicRepository +{ + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string? sorting = $"{nameof(ConversationRecord.CreatedAt)} DESC", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs index 865365326..b43c95e49 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs @@ -1,4 +1,4 @@ -using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Chats; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; @@ -10,7 +10,8 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; public class AIManagementDbContext : AbpDbContext, IAIManagementDbContext { public DbSet WorkspaceDefinitions { get; set; } - public DbSet UserTextMessageRecords { get; set; } + public DbSet TextChatMessageRecords { get; set; } + public DbSet ConversationRecords { get; set; } public AIManagementDbContext( DbContextOptions options) : base(options) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 185fd3c86..79612bacf 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -14,6 +14,16 @@ public static class AIManagementDbContextModelBuilderExtensions { Check.NotNull(builder, nameof(builder)); + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "Conversations", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name) + .HasMaxLength(ConversationRecordConsts.MaxNameLength) + .IsRequired(); + }); builder.Entity(b => { b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "TextChatMessages", AbpAIManagementDbProperties.DbSchema); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs index f372b441f..0802cbc47 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -17,6 +17,7 @@ public class AbpAIManagementEntityFrameworkCoreModule : AbpModule { options.AddDefaultRepositories(); + options.AddRepository(); options.AddRepository(); options.AddRepository(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs new file mode 100644 index 000000000..d79ea9709 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs @@ -0,0 +1,44 @@ +using LINGYUN.Abp.AIManagement.Chats; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Specifications; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public class EfCoreConversationRecordRepository : EfCoreRepository, IConversationRecordRepository +{ + public EfCoreConversationRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .CountAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string? sorting = $"{nameof(ConversationRecord.CreatedAt)} DESC", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .OrderBy(!sorting.IsNullOrWhiteSpace() ? sorting : $"{nameof(ConversationRecord.CreatedAt)} DESC") + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs index d253a4aa6..33d66185c 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs @@ -1,4 +1,4 @@ -using LINGYUN.Abp.AIManagement.Messages; +using LINGYUN.Abp.AIManagement.Chats; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; @@ -10,5 +10,6 @@ namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; public interface IAIManagementDbContext : IEfCoreDbContext { DbSet WorkspaceDefinitions { get; } - DbSet UserTextMessageRecords { get; } + DbSet TextChatMessageRecords { get; } + DbSet ConversationRecords { get; } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file From 1aad1f0231e460a42edcbb59cf8cb35f691781b2 Mon Sep 17 00:00:00 2001 From: colin Date: Tue, 27 Jan 2026 14:44:34 +0800 Subject: [PATCH 19/25] feat: Add persistent TokenUsage --- .../Abp/AIManagement/AIManagementOptions.cs | 4 +- .../Tokens/ITokenUsageRecordRepository.cs | 26 +++++++ .../AIManagement/Tokens/TokenUsageRecord.cs | 40 ++++++++++- .../AIManagement/Tokens/TokenUsageStore.cs | 68 +++++++++++++++++++ .../AIManagementDbContext.cs | 2 + ...nagementDbContextModelBuilderExtensions.cs | 9 +++ ...bpAIManagementEntityFrameworkCoreModule.cs | 2 + .../EfCoreTokenUsageRecordRepository.cs | 54 +++++++++++++++ .../IAIManagementDbContext.cs | 2 + 9 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/ITokenUsageRecordRepository.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageStore.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTokenUsageRecordRepository.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs index 493fcb14a..72ab61ffd 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs @@ -1,4 +1,6 @@ -namespace LINGYUN.Abp.AIManagement; +using System; + +namespace LINGYUN.Abp.AIManagement; public class AIManagementOptions { public bool IsDynamicWorkspaceStoreEnabled { get; set; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/ITokenUsageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/ITokenUsageRecordRepository.cs new file mode 100644 index 000000000..11724dda4 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/ITokenUsageRecordRepository.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Specifications; + +namespace LINGYUN.Abp.AIManagement.Tokens; +public interface ITokenUsageRecordRepository : IBasicRepository +{ + Task FindByMessageIdAsync( + Guid conversationId, + Guid? messageId, + CancellationToken cancellationToken = default); + + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(TokenUsageRecord.CreationTime)}", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs index fedf9a194..136747a39 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs @@ -1,8 +1,42 @@ using System; -using System.Collections.Generic; -using System.Text; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; namespace LINGYUN.Abp.AIManagement.Tokens; -public class TokenUsageRecord +public class TokenUsageRecord : AuditedEntity, IMultiTenant { + public Guid? TenantId { get; private set; } + public Guid? MessageId { get; private set; } + public Guid? ConversationId { get; private set; } + public long? InputTokenCount { get; set; } + public long? OutputTokenCount { get; set; } + public long? TotalTokenCount { get; set; } + public long? CachedInputTokenCount { get; set; } + public long? ReasoningTokenCount { get; set; } + protected TokenUsageRecord() + { + + } + + public TokenUsageRecord( + Guid id, + Guid? messageId = null, + Guid? conversationId = null, + long? inputTokenCount = null, + long? outputTokenCount = null, + long? totalTokenCount = null, + long? cachedInputTokenCount = null, + long? reasoningTokenCount = null, + Guid? tenantId = null) + : base(id) + { + MessageId = messageId; + ConversationId = conversationId; + InputTokenCount = inputTokenCount; + OutputTokenCount = outputTokenCount; + TotalTokenCount = totalTokenCount; + CachedInputTokenCount = cachedInputTokenCount; + ReasoningTokenCount = reasoningTokenCount; + TenantId = tenantId; + } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageStore.cs new file mode 100644 index 000000000..026ba82cd --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageStore.cs @@ -0,0 +1,68 @@ +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AI.Tokens; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Tokens; + +[Dependency(ReplaceServices = true)] +public class TokenUsageStore : ITokenUsageStore, ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + private readonly IGuidGenerator _guidGenerator; + private readonly ITokenUsageRecordRepository _tokenUsageRecordRepository; + + public TokenUsageStore( + ICurrentTenant currentTenant, + IGuidGenerator guidGenerator, + ITokenUsageRecordRepository tokenUsageRecordRepository) + { + _currentTenant = currentTenant; + _guidGenerator = guidGenerator; + _tokenUsageRecordRepository = tokenUsageRecordRepository; + } + + public async virtual Task SaveTokenUsageAsync(TokenUsageInfo tokenUsageInfo) + { + var tokenUsageRecord = await _tokenUsageRecordRepository.FindByMessageIdAsync( + tokenUsageInfo.ConversationId, + tokenUsageInfo.MessageId); + + if (tokenUsageRecord == null) + { + tokenUsageRecord = new TokenUsageRecord( + _guidGenerator.Create(), + tokenUsageInfo.MessageId, + tokenUsageInfo.ConversationId, + tokenUsageInfo.InputTokenCount, + tokenUsageInfo.OutputTokenCount, + tokenUsageInfo.TotalTokenCount, + tokenUsageInfo.CachedInputTokenCount, + tokenUsageInfo.ReasoningTokenCount, + _currentTenant.Id); + + await _tokenUsageRecordRepository.InsertAsync(tokenUsageRecord); + } + else + { + tokenUsageRecord.InputTokenCount ??= 0; + tokenUsageRecord.InputTokenCount += tokenUsageInfo.InputTokenCount; + + tokenUsageRecord.OutputTokenCount ??= 0; + tokenUsageRecord.OutputTokenCount += tokenUsageInfo.OutputTokenCount; + + tokenUsageRecord.CachedInputTokenCount ??= 0; + tokenUsageRecord.CachedInputTokenCount += tokenUsageInfo.CachedInputTokenCount; + + tokenUsageRecord.ReasoningTokenCount ??= 0; + tokenUsageRecord.ReasoningTokenCount += tokenUsageInfo.ReasoningTokenCount; + + tokenUsageRecord.TotalTokenCount ??= 0; + tokenUsageRecord.TotalTokenCount += tokenUsageInfo.TotalTokenCount; + + await _tokenUsageRecordRepository.UpdateAsync(tokenUsageRecord); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs index b43c95e49..fb3b6a9f2 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.Tokens; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; @@ -12,6 +13,7 @@ public class AIManagementDbContext : AbpDbContext, IAIMan public DbSet WorkspaceDefinitions { get; set; } public DbSet TextChatMessageRecords { get; set; } public DbSet ConversationRecords { get; set; } + public DbSet TokenUsageRecords { get; set; } public AIManagementDbContext( DbContextOptions options) : base(options) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index 79612bacf..b6e8d2110 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -1,6 +1,7 @@ using JetBrains.Annotations; using LINGYUN.Abp.AIManagement.Chats; using LINGYUN.Abp.AIManagement.EntityFrameworkCore.ValueConversions; +using LINGYUN.Abp.AIManagement.Tokens; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp; @@ -45,6 +46,14 @@ public static class AIManagementDbContextModelBuilderExtensions b.ApplyObjectExtensionMappings(); }); + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "TokenUsages", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.HasIndex(x => new { x.TenantId, x.ConversationId }); + }); if (builder.IsHostDatabase()) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs index 0802cbc47..bb4cf3964 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.Tokens; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.EntityFrameworkCore; @@ -19,6 +20,7 @@ public class AbpAIManagementEntityFrameworkCoreModule : AbpModule options.AddRepository(); options.AddRepository(); + options.AddRepository(); options.AddRepository(); }); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTokenUsageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTokenUsageRecordRepository.cs new file mode 100644 index 000000000..2bb211cc6 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTokenUsageRecordRepository.cs @@ -0,0 +1,54 @@ +using LINGYUN.Abp.AIManagement.Tokens; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Specifications; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public class EfCoreTokenUsageRecordRepository : EfCoreRepository, ITokenUsageRecordRepository +{ + public EfCoreTokenUsageRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task FindByMessageIdAsync( + Guid conversationId, + Guid? messageId, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(x => x.ConversationId == conversationId && x.MessageId == messageId) + .FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .CountAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(TokenUsageRecord.CreationTime)}", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .OrderBy(!sorting.IsNullOrWhiteSpace() ? sorting : $"{nameof(TokenUsageRecord.CreationTime)}") + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs index 33d66185c..edc46235d 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.Tokens; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; @@ -12,4 +13,5 @@ public interface IAIManagementDbContext : IEfCoreDbContext DbSet WorkspaceDefinitions { get; } DbSet TextChatMessageRecords { get; } DbSet ConversationRecords { get; } + DbSet TokenUsageRecords { get; } } From e102e6c8f930d7739563a7fa1da246e972c687a7 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 28 Jan 2026 11:46:55 +0800 Subject: [PATCH 20/25] feat: Add Workspace management interface implementation --- ....AIManagement.Application.Contracts.csproj | 5 +- .../AIManagementRemoteServiceConsts.cs | 7 + ...pAIManagementApplicationContractsModule.cs | 14 ++ .../AbpAIManagementDomainSharedModule.cs | 8 - .../WorkspaceDefinitionRecordCreateDto.cs | 10 ++ ...kspaceDefinitionRecordCreateOrUpdateDto.cs | 47 ++++++ .../Dtos/WorkspaceDefinitionRecordDto.cs | 39 +++++ .../WorkspaceDefinitionRecordGetListInput.cs | 14 ++ .../WorkspaceDefinitionRecordUpdateDto.cs | 10 ++ .../IWorkspaceDefinitionAppService.cs | 14 ++ .../FodyWeavers.xsd | 30 ++++ .../AIManagementApplicationServiceBase.cs | 12 ++ .../AbpAIManagementApplicationMappers.cs | 15 ++ .../AbpAIManagementApplicationModule.cs | 17 ++ .../AbpAIManagementDomainSharedModule.cs | 8 - .../WorkspaceDefinitionAppService.cs | 152 ++++++++++++++++++ .../AIManagement/AIManagementErrorCodes.cs | 12 ++ .../WorkspaceAlreadyExistsException.cs | 16 ++ .../AbpAIManagementDbProperties.cs | 2 +- .../FodyWeavers.xsd | 30 ++++ ...nagementDbContextModelBuilderExtensions.cs | 2 +- .../AbpAIManagementDomainSharedModule.cs | 8 - .../WorkspaceDefinitionController.cs | 52 ++++++ 23 files changed, 496 insertions(+), 28 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AIManagementRemoteServiceConsts.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationContractsModule.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateOrUpdateDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordGetListInput.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordUpdateDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionAppService.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xsd create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AIManagementApplicationServiceBase.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationMappers.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationModule.cs delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionAppService.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AIManagementErrorCodes.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceAlreadyExistsException.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xsd delete mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionController.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj index c756d035b..1d2169985 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj @@ -5,8 +5,8 @@ netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 - LINGYUN.Abp.AIManagement.Domain - LINGYUN.Abp.AIManagement.Domain + LINGYUN.Abp.AIManagement.Application.Contracts + LINGYUN.Abp.AIManagement.Application.Contracts false false false @@ -16,6 +16,7 @@ + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AIManagementRemoteServiceConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AIManagementRemoteServiceConsts.cs new file mode 100644 index 000000000..b68f59f17 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AIManagementRemoteServiceConsts.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Abp.AIManagement; +public static class AIManagementRemoteServiceConsts +{ + public const string RemoteServiceName = "AbpAIManagement"; + + public const string ModuleName = "ai-management"; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationContractsModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationContractsModule.cs new file mode 100644 index 000000000..289fb8ed2 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationContractsModule.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Application; +using Volo.Abp.Authorization; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementDomainSharedModule), + typeof(AbpDddApplicationContractsModule), + typeof(AbpAuthorizationAbstractionsModule))] +public class AbpAIManagementApplicationContractsModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs deleted file mode 100644 index 751b9bfaa..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Volo.Abp.Modularity; - -namespace LINGYUN.Abp.AIManagement; - -public class AbpAIManagementDomainSharedModule : AbpModule -{ - -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateDto.cs new file mode 100644 index 000000000..9591c2773 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; +public class WorkspaceDefinitionRecordCreateDto : WorkspaceDefinitionRecordCreateOrUpdateDto +{ + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxNameLength))] + public string Name { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateOrUpdateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateOrUpdateDto.cs new file mode 100644 index 000000000..dcb866294 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateOrUpdateDto.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.ObjectExtending; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; +public abstract class WorkspaceDefinitionRecordCreateOrUpdateDto : ExtensibleObject +{ + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxProviderLength))] + public string Provider { get; set; } + + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxModelNameLength))] + public string ModelName { get; set; } + + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength))] + public string DisplayName { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxDescriptionLength))] + public string? Description { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxApiKeyLength))] + public string? ApiKey { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength))] + public string? ApiBaseUrl { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength))] + public string? SystemPrompt { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxInstructionsLength))] + public string? Instructions { get; set; } + + public float? Temperature { get; set; } + + public int? MaxOutputTokens { get; set; } + + public float? FrequencyPenalty { get; set; } + + public float? PresencePenalty { get; set; } + + public bool IsEnabled { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength))] + public string? StateCheckers { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordDto.cs new file mode 100644 index 000000000..f65dd0ee9 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordDto.cs @@ -0,0 +1,39 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Entities; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; + +[Serializable] +public class WorkspaceDefinitionRecordDto : ExtensibleAuditedEntityDto, IHasConcurrencyStamp +{ + public string Name { get; set; } + + public string Provider { get; set; } + + public string ModelName { get; set; } + + public string DisplayName { get; set; } + + public string? Description { get; set; } + + public string? ApiBaseUrl { get; set; } + + public string? SystemPrompt { get; set; } + + public string? Instructions { get; set; } + + public float? Temperature { get; set; } + + public int? MaxOutputTokens { get; set; } + + public float? FrequencyPenalty { get; set; } + + public float? PresencePenalty { get; set; } + + public bool IsEnabled { get; set; } + + public string? StateCheckers { get; set; } + + public string ConcurrencyStamp { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordGetListInput.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordGetListInput.cs new file mode 100644 index 000000000..a4f99fb6c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordGetListInput.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; + +[Serializable] +public class WorkspaceDefinitionRecordGetListInput : PagedAndSortedResultRequestDto +{ + public string? Filter { get; set; } + + public string? Provider { get; set; } + + public string? ModelName { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordUpdateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordUpdateDto.cs new file mode 100644 index 000000000..848ef7394 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordUpdateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Domain.Entities; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; +public class WorkspaceDefinitionRecordUpdateDto : WorkspaceDefinitionRecordCreateOrUpdateDto, IHasConcurrencyStamp +{ + [Required] + [StringLength(40)] + public string ConcurrencyStamp { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionAppService.cs new file mode 100644 index 000000000..6ebb52c95 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionAppService.cs @@ -0,0 +1,14 @@ +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using System; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IWorkspaceDefinitionAppService : + ICrudAppService< + WorkspaceDefinitionRecordDto, + Guid, + WorkspaceDefinitionRecordGetListInput, + WorkspaceDefinitionRecordCreateDto, + WorkspaceDefinitionRecordUpdateDto> +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AIManagementApplicationServiceBase.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AIManagementApplicationServiceBase.cs new file mode 100644 index 000000000..291b27256 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AIManagementApplicationServiceBase.cs @@ -0,0 +1,12 @@ +using LINGYUN.Abp.AIManagement.Localization; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement; +public abstract class AIManagementApplicationServiceBase : ApplicationService +{ + protected AIManagementApplicationServiceBase() + { + LocalizationResource = typeof(AIManagementResource); + ObjectMapperContext = typeof(AbpAIManagementApplicationModule); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationMappers.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationMappers.cs new file mode 100644 index 000000000..adc71e599 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationMappers.cs @@ -0,0 +1,15 @@ +using LINGYUN.Abp.AIManagement.Workspaces; +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.ObjectExtending; + +namespace LINGYUN.Abp.AIManagement; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties(DefinitionChecks = MappingPropertyDefinitionChecks.None)] +public partial class WorkspaceDefinitionRecordToWorkspaceDefinitionRecordDtoMapper : MapperBase +{ + public override partial WorkspaceDefinitionRecordDto Map(WorkspaceDefinitionRecord source); + public override partial void Map(WorkspaceDefinitionRecord source, WorkspaceDefinitionRecordDto destination); +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationModule.cs new file mode 100644 index 000000000..4b5413042 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationModule.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Application; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementApplicationContractsModule), + typeof(AbpAIManagementDomainModule), + typeof(AbpDddApplicationModule))] +public class AbpAIManagementApplicationModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs deleted file mode 100644 index 751b9bfaa..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Volo.Abp.Modularity; - -namespace LINGYUN.Abp.AIManagement; - -public class AbpAIManagementDomainSharedModule : AbpModule -{ - -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionAppService.cs new file mode 100644 index 000000000..e249d025b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionAppService.cs @@ -0,0 +1,152 @@ +using LINGYUN.Abp.AIManagement.Localization; +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Data; +using Volo.Abp.Security.Encryption; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDefinitionAppService : + CrudAppService< + WorkspaceDefinitionRecord, + WorkspaceDefinitionRecordDto, + Guid, + WorkspaceDefinitionRecordGetListInput, + WorkspaceDefinitionRecordCreateDto, + WorkspaceDefinitionRecordUpdateDto>, + IWorkspaceDefinitionAppService +{ + protected IStringEncryptionService StringEncryptionService { get; } + protected IWorkspaceDefinitionRecordRepository WorkspaceDefinitionRecordRepository { get; } + public WorkspaceDefinitionAppService( + IStringEncryptionService stringEncryptionService, + IWorkspaceDefinitionRecordRepository repository) : base(repository) + { + StringEncryptionService = stringEncryptionService; + WorkspaceDefinitionRecordRepository = repository; + + LocalizationResource = typeof(AIManagementResource); + ObjectMapperContext = typeof(AbpAIManagementApplicationModule); + } + + protected async override Task> CreateFilteredQueryAsync(WorkspaceDefinitionRecordGetListInput input) + { + var queryable = await base.CreateFilteredQueryAsync(input); + + return queryable + .WhereIf(!input.Provider.IsNullOrWhiteSpace(), x => x.Provider == input.Provider) + .WhereIf(!input.ModelName.IsNullOrWhiteSpace(), x => x.ModelName == input.ModelName) + .WhereIf(!input.Filter.IsNullOrWhiteSpace(), x => x.Provider.Contains(input.Filter!) || + x.ModelName.Contains(input.Filter!) || x.DisplayName.Contains(input.Filter!) || + (!x.Description.IsNullOrWhiteSpace() && x.Description.Contains(input.Filter!)) || + (!x.SystemPrompt.IsNullOrWhiteSpace() && x.SystemPrompt.Contains(input.Filter!)) || + (!x.Instructions.IsNullOrWhiteSpace() && x.Instructions.Contains(input.Filter!))); + } + + protected async override Task MapToEntityAsync(WorkspaceDefinitionRecordCreateDto createInput) + { + if (await WorkspaceDefinitionRecordRepository.FindByNameAsync(createInput.Name) != null) + { + throw new WorkspaceAlreadyExistsException(createInput.Name); + } + + var record = new WorkspaceDefinitionRecord( + GuidGenerator.Create(), + createInput.Name, + createInput.Provider, + createInput.ModelName, + createInput.DisplayName, + createInput.Description, + createInput.SystemPrompt, + createInput.Instructions, + createInput.Temperature, + createInput.MaxOutputTokens, + createInput.FrequencyPenalty, + createInput.PresencePenalty, + createInput.StateCheckers); + + if (!createInput.ApiKey.IsNullOrWhiteSpace()) + { + var encryptApiKey = StringEncryptionService.Encrypt(createInput.ApiKey); + record.SetApiKey(encryptApiKey, createInput.ApiBaseUrl); + } + + return record; + } + + protected override void MapToEntity(WorkspaceDefinitionRecordUpdateDto updateInput, WorkspaceDefinitionRecord entity) + { + if (entity.DisplayName != updateInput.DisplayName) + { + entity.SetDisplayName(updateInput.DisplayName); + } + + if (entity.Description != updateInput.Description) + { + entity.Description = updateInput.Description; + } + + if (entity.Provider != updateInput.Provider || entity.ModelName != updateInput.ModelName) + { + entity.SetModel(updateInput.Provider, updateInput.ModelName); + } + + if (entity.SystemPrompt != updateInput.SystemPrompt) + { + entity.SystemPrompt = updateInput.SystemPrompt; + } + + if (entity.Instructions != updateInput.Instructions) + { + entity.Instructions = updateInput.Instructions; + } + + if (entity.IsEnabled != updateInput.IsEnabled) + { + entity.IsEnabled = updateInput.IsEnabled; + } + + if (entity.Temperature != updateInput.Temperature) + { + entity.Temperature = updateInput.Temperature; + } + + if (entity.MaxOutputTokens != updateInput.MaxOutputTokens) + { + entity.MaxOutputTokens = updateInput.MaxOutputTokens; + } + + if (entity.FrequencyPenalty != updateInput.FrequencyPenalty) + { + entity.FrequencyPenalty = updateInput.FrequencyPenalty; + } + + if (entity.PresencePenalty != updateInput.PresencePenalty) + { + entity.PresencePenalty = updateInput.PresencePenalty; + } + + if (entity.StateCheckers != updateInput.StateCheckers) + { + entity.StateCheckers = updateInput.StateCheckers; + } + + if (!updateInput.ApiKey.IsNullOrWhiteSpace()) + { + var encryptApiKey = StringEncryptionService.Encrypt(updateInput.ApiKey); + entity.SetApiKey(encryptApiKey, updateInput.ApiBaseUrl); + } + + if (!entity.HasSameExtraProperties(updateInput)) + { + entity.ExtraProperties.Clear(); + + foreach (var property in updateInput.ExtraProperties) + { + entity.ExtraProperties.Add(property.Key, property.Value); + } + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AIManagementErrorCodes.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AIManagementErrorCodes.cs new file mode 100644 index 000000000..ae295d8db --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AIManagementErrorCodes.cs @@ -0,0 +1,12 @@ +namespace LINGYUN.Abp.AIManagement; +public static class AIManagementErrorCodes +{ + public const string Namespace = "AIManagement"; + + public static class Workspace + { + public const string Prefix = Namespace + ":100"; + + public const string NameAlreadyExists = Prefix + "001"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceAlreadyExistsException.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceAlreadyExistsException.cs new file mode 100644 index 000000000..5e23fd8b3 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceAlreadyExistsException.cs @@ -0,0 +1,16 @@ +using Volo.Abp; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceAlreadyExistsException : BusinessException +{ + public string Workspace { get; } + public WorkspaceAlreadyExistsException(string workspace) + : base( + AIManagementErrorCodes.Workspace.NameAlreadyExists, + $"A Workspace named {workspace} already exists!") + { + Workspace = workspace; + + WithData(nameof(Workspace), workspace); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs index 852fbd74a..947baaa18 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs @@ -3,7 +3,7 @@ namespace LINGYUN.Abp.AIManagement; public static class AbpAIManagementDbProperties { - public static string DbTablePrefix { get; set; } = AbpCommonDbProperties.DbTablePrefix; + public static string DbTablePrefix { get; set; } = AbpCommonDbProperties.DbTablePrefix + "AI"; public static string? DbSchema { get; set; } = AbpCommonDbProperties.DbSchema; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs index b6e8d2110..f3f5e9f9a 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -80,7 +80,7 @@ public static class AIManagementDbContextModelBuilderExtensions b.Property(x => x.ApiKey) .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); b.Property(x => x.ApiBaseUrl) - .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength); b.Property(x => x.SystemPrompt) .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); b.Property(x => x.Instructions) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs deleted file mode 100644 index 751b9bfaa..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Volo.Abp.Modularity; - -namespace LINGYUN.Abp.AIManagement; - -public class AbpAIManagementDomainSharedModule : AbpModule -{ - -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionController.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionController.cs new file mode 100644 index 000000000..58444f420 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionController.cs @@ -0,0 +1,52 @@ +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Abp.AIManagement.Workspaces; + +[Controller] +[RemoteService(Name = AIManagementRemoteServiceConsts.RemoteServiceName)] +[Area(AIManagementRemoteServiceConsts.ModuleName)] +[Route($"api/{AIManagementRemoteServiceConsts.ModuleName}/workspaces")] +public class WorkspaceDefinitionController : AbpControllerBase, IWorkspaceDefinitionAppService +{ + private readonly IWorkspaceDefinitionAppService _service; + public WorkspaceDefinitionController(IWorkspaceDefinitionAppService service) + { + _service = service; + } + + [HttpPost] + public virtual Task CreateAsync(WorkspaceDefinitionRecordCreateDto input) + { + return _service.CreateAsync(input); + } + + [HttpDelete("{id}")] + public virtual Task DeleteAsync(Guid id) + { + return _service.DeleteAsync(id); + } + + [HttpGet("{id}")] + public virtual Task GetAsync(Guid id) + { + return _service.GetAsync(id); + } + + [HttpGet] + public virtual Task> GetListAsync(WorkspaceDefinitionRecordGetListInput input) + { + return _service.GetListAsync(input); + } + + [HttpPut("{id}")] + public virtual Task UpdateAsync(Guid id, WorkspaceDefinitionRecordUpdateDto input) + { + return _service.UpdateAsync(id, input); + } +} From c64935f5c4b163dbedd63b8526eeb9752b116cb9 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 28 Jan 2026 11:48:02 +0800 Subject: [PATCH 21/25] Add Conversation management interface implementation --- .../Chats/Dtos/ConversationCreateDto.cs | 8 ++ .../Chats/Dtos/ConversationDto.cs | 14 ++++ .../Chats/Dtos/ConversationGetListInput.cs | 7 ++ .../Chats/Dtos/ConversationUpdateDto.cs | 10 +++ .../Chats/IConversationAppService.cs | 14 ++++ .../Chats/ConversationAppService.cs | 82 +++++++++++++++++++ .../AIManagement/Chats/ConversationRecord.cs | 5 ++ .../Chats/ConversationController.cs | 52 ++++++++++++ 8 files changed, 192 insertions(+) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationCreateDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationGetListInput.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationUpdateDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IConversationAppService.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ConversationAppService.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ConversationController.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationCreateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationCreateDto.cs new file mode 100644 index 000000000..8112305b2 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationCreateDto.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationCreateDto +{ + [DynamicStringLength(typeof(ConversationRecordConsts), nameof(ConversationRecordConsts.MaxNameLength))] + public string? Name { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationDto.cs new file mode 100644 index 000000000..8f5945c95 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationDto : AuditedEntityDto +{ + public string Name { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime ExpiredAt { get; set; } + + public DateTime? UpdateAt { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationGetListInput.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationGetListInput.cs new file mode 100644 index 000000000..d3b889f48 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationGetListInput.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationGetListInput : PagedAndSortedResultRequestDto +{ + public string? Filter { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationUpdateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationUpdateDto.cs new file mode 100644 index 000000000..4469eba6d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationUpdateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationUpdateDto +{ + [Required] + [DynamicStringLength(typeof(ConversationRecordConsts), nameof(ConversationRecordConsts.MaxNameLength))] + public string Name { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IConversationAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IConversationAppService.cs new file mode 100644 index 000000000..a59c397cb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IConversationAppService.cs @@ -0,0 +1,14 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using System; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement.Chats; +public interface IConversationAppService : + ICrudAppService< + ConversationDto, + Guid, + ConversationGetListInput, + ConversationCreateDto, + ConversationUpdateDto> +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ConversationAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ConversationAppService.cs new file mode 100644 index 000000000..203747602 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ConversationAppService.cs @@ -0,0 +1,82 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using LINGYUN.Abp.AIManagement.Localization; +using Microsoft.Extensions.Options; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ConversationAppService : + CrudAppService< + ConversationRecord, + ConversationDto, + Guid, + ConversationGetListInput, + ConversationCreateDto, + ConversationUpdateDto>, + IConversationAppService +{ + private readonly ConversationCleanupOptions _cleanupOptions; + public ConversationAppService( + IRepository repository, + IOptions cleanupOptions) + : base(repository) + { + _cleanupOptions = cleanupOptions.Value; + + LocalizationResource = typeof(AIManagementResource); + ObjectMapperContext = typeof(AbpAIManagementApplicationModule); + } + + protected async override Task> CreateFilteredQueryAsync(ConversationGetListInput input) + { + var queryable = await base.CreateFilteredQueryAsync(input); + + return queryable + .WhereIf(!input.Filter.IsNullOrWhiteSpace(), x => x.Name.Contains(input.Filter!)); + } + + protected override ConversationRecord MapToEntity(ConversationCreateDto createInput) + { + var createdAt = Clock.Now; + var expiredTime = createdAt.Add(_cleanupOptions.ExpiredTime); + var conversationName = createInput.Name ?? L["NewConversation"]; + return new ConversationRecord( + GuidGenerator.Create(), + conversationName, + createdAt, + expiredTime, + CurrentTenant.Id); + } + + protected override void MapToEntity(ConversationUpdateDto updateInput, ConversationRecord entity) + { + if (!string.Equals(entity.Name, updateInput.Name, StringComparison.InvariantCultureIgnoreCase)) + { + entity.SetName(updateInput.Name); + } + } + + protected override ConversationDto MapToGetOutputDto(ConversationRecord entity) + { + return new ConversationDto + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + ExpiredAt = entity.ExpiredAt, + CreationTime = entity.CreationTime, + CreatorId = entity.CreatorId, + LastModificationTime = entity.LastModificationTime, + LastModifierId = entity.LastModifierId, + Name = entity.Name, + UpdateAt = entity.UpdateAt, + }; + } + + protected override ConversationDto MapToGetListOutputDto(ConversationRecord entity) + { + return MapToGetOutputDto(entity); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs index cd81fcac6..10192ab8b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs @@ -31,4 +31,9 @@ public class ConversationRecord : AuditedEntity, IMultiTenant TenantId = tenantId; } + + public void SetName(string name) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), ConversationRecordConsts.MaxNameLength); + } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ConversationController.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ConversationController.cs new file mode 100644 index 000000000..e90aced0b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ConversationController.cs @@ -0,0 +1,52 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Abp.AIManagement.Chats; + +[Controller] +[RemoteService(Name = AIManagementRemoteServiceConsts.RemoteServiceName)] +[Area(AIManagementRemoteServiceConsts.ModuleName)] +[Route($"api/{AIManagementRemoteServiceConsts.ModuleName}/chats/conversations")] +public class ConversationController : AbpControllerBase, IConversationAppService +{ + private readonly IConversationAppService _service; + public ConversationController(IConversationAppService service) + { + _service = service; + } + + [HttpPost] + public virtual Task CreateAsync(ConversationCreateDto input) + { + return _service.CreateAsync(input); + } + + [HttpDelete("{id}")] + public virtual Task DeleteAsync(Guid id) + { + return _service.DeleteAsync(id); + } + + [HttpGet("{id}")] + public virtual Task GetAsync(Guid id) + { + return _service.GetAsync(id); + } + + [HttpGet] + public virtual Task> GetListAsync(ConversationGetListInput input) + { + return _service.GetListAsync(input); + } + + [HttpPut("{id}")] + public virtual Task UpdateAsync(Guid id, ConversationUpdateDto input) + { + return _service.UpdateAsync(id, input); + } +} From 024d90472238393a0025f7a59cedde1ab4b59b5e Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 28 Jan 2026 11:48:55 +0800 Subject: [PATCH 22/25] feat: Encrypt the Workspace ApiKey --- ...icWorkspaceDefinitionStoreInMemoryCache.cs | 7 ++- .../IWorkspaceDefinitionRecordRepository.cs | 2 +- .../Workspaces/WorkspaceDefinitionRecord.cs | 23 +++++++--- .../WorkspaceDefinitionSerializer.cs | 13 +++++- .../Workspaces/WorkspaceDynamicInitializer.cs | 45 +++++++------------ 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs index 478ededa9..3d7d0d142 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Localization; +using Volo.Abp.Security.Encryption; using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AIManagement.Workspaces; @@ -13,6 +14,7 @@ public class DynamicWorkspaceDefinitionStoreInMemoryCache : IDynamicWorkspaceDef { public string CacheStamp { get; set; } protected IDictionary WorkspaceDefinitions { get; } + protected IStringEncryptionService StringEncryptionService { get; } protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } protected ILocalizableStringSerializer LocalizableStringSerializer { get; } @@ -21,9 +23,11 @@ public class DynamicWorkspaceDefinitionStoreInMemoryCache : IDynamicWorkspaceDef public DateTime? LastCheckTime { get; set; } public DynamicWorkspaceDefinitionStoreInMemoryCache( + IStringEncryptionService stringEncryptionService, ISimpleStateCheckerSerializer stateCheckerSerializer, ILocalizableStringSerializer localizableStringSerializer) { + StringEncryptionService = stringEncryptionService; StateCheckerSerializer = stateCheckerSerializer; LocalizableStringSerializer = localizableStringSerializer; @@ -51,7 +55,8 @@ public class DynamicWorkspaceDefinitionStoreInMemoryCache : IDynamicWorkspaceDef if (!workspace.ApiKey.IsNullOrWhiteSpace()) { - workspaceDef.WithApiKey(workspace.ApiKey); + var decryptApiKey = StringEncryptionService.Decrypt(workspace.ApiKey); + workspaceDef.WithApiKey(decryptApiKey!); } if (!workspace.ApiBaseUrl.IsNullOrWhiteSpace()) { diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs index e4b566cee..4c893dd04 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; namespace LINGYUN.Abp.AIManagement.Workspaces; -public interface IWorkspaceDefinitionRecordRepository : IBasicRepository +public interface IWorkspaceDefinitionRecordRepository : IRepository { Task FindByNameAsync( string name, diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs index 1e75cba0b..e93500b17 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs @@ -14,7 +14,7 @@ public class WorkspaceDefinitionRecord : AuditedAggregateRoot public string DisplayName { get; private set; } - public string? Description { get; private set; } + public string? Description { get; set; } public string? ApiKey { get; private set; } @@ -49,8 +49,6 @@ public class WorkspaceDefinitionRecord : AuditedAggregateRoot string modelName, string displayName, string? description = null, - string? apiKey = null, - string? apiBaseUrl = null, string? systemPrompt = null, string? instructions = null, float? temperature = null, @@ -65,8 +63,6 @@ public class WorkspaceDefinitionRecord : AuditedAggregateRoot ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceDefinitionRecordConsts.MaxModelNameLength); DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceDefinitionRecordConsts.MaxDisplayNameLength); Description = Check.Length(description, nameof(description), WorkspaceDefinitionRecordConsts.MaxDescriptionLength); - ApiKey = Check.Length(apiKey, nameof(apiKey), WorkspaceDefinitionRecordConsts.MaxApiKeyLength); - ApiBaseUrl = Check.Length(apiBaseUrl, nameof(apiBaseUrl), WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength); SystemPrompt = Check.Length(systemPrompt, nameof(systemPrompt), WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); Instructions = Check.Length(instructions, nameof(instructions), WorkspaceDefinitionRecordConsts.MaxInstructionsLength); StateCheckers = Check.Length(stateCheckers, nameof(stateCheckers), WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); @@ -80,6 +76,23 @@ public class WorkspaceDefinitionRecord : AuditedAggregateRoot this.SetDefaultsForExtraProperties(); } + public void SetDisplayName(string displayName) + { + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceDefinitionRecordConsts.MaxDisplayNameLength); + } + + public void SetModel(string provider, string modelName) + { + Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), WorkspaceDefinitionRecordConsts.MaxProviderLength); + ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceDefinitionRecordConsts.MaxModelNameLength); + } + + public void SetApiKey(string? apiKey = null, string? apiBaseUrl = null) + { + ApiKey = Check.Length(apiKey, nameof(apiKey), WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + ApiBaseUrl = Check.Length(apiBaseUrl, nameof(apiBaseUrl), WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength); + } + public bool HasSameData(WorkspaceDefinitionRecord otherWorkspace) { if (Name != otherWorkspace.Name) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs index 3c69f73fd..94096f797 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.AI.Workspaces; +using System; using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; @@ -6,21 +7,25 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Guids; using Volo.Abp.Localization; +using Volo.Abp.Security.Encryption; using Volo.Abp.SimpleStateChecking; namespace LINGYUN.Abp.AIManagement.Workspaces; public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITransientDependency { protected IGuidGenerator GuidGenerator { get; } + protected IStringEncryptionService StringEncryptionService { get; } protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } protected ILocalizableStringSerializer LocalizableStringSerializer { get; } public WorkspaceDefinitionSerializer( IGuidGenerator guidGenerator, + IStringEncryptionService stringEncryptionService, ISimpleStateCheckerSerializer stateCheckerSerializer, ILocalizableStringSerializer localizableStringSerializer) { GuidGenerator = guidGenerator; + StringEncryptionService = stringEncryptionService; StateCheckerSerializer = stateCheckerSerializer; LocalizableStringSerializer = localizableStringSerializer; } @@ -47,8 +52,6 @@ public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITr definition.ModelName, LocalizableStringSerializer.Serialize(definition.DisplayName)!, definition.Description != null ? LocalizableStringSerializer.Serialize(definition.Description) : null, - definition.ApiKey, - definition.ApiBaseUrl, definition.SystemPrompt, definition.Instructions, definition.Temperature, @@ -57,6 +60,12 @@ public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITr definition.PresencePenalty, SerializeStateCheckers(definition.StateCheckers)); + if (!definition.ApiKey.IsNullOrWhiteSpace()) + { + var encryptApiKey = StringEncryptionService.Encrypt(definition.ApiKey); + workspace.SetApiKey(encryptApiKey, definition.ApiBaseUrl); + } + foreach (var property in definition.Properties) { workspace.SetProperty(property.Key, property.Value); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs index 591535580..793814d3a 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs @@ -1,5 +1,4 @@ -using JetBrains.Annotations; -using LINGYUN.Abp.AI.Workspaces; +using LINGYUN.Abp.AI.Workspaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -18,33 +17,17 @@ public class WorkspaceDynamicInitializer : ITransientDependency public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } - protected IOptions Options { get; } - [CanBeNull] - protected IHostApplicationLifetime ApplicationLifetime { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected IDynamicWorkspaceDefinitionStore DynamicWorkspaceDefinitionStore { get; } - protected IStaticWorkspaceSaver StaticWorkspaceSaver { get; } - - public WorkspaceDynamicInitializer( - IServiceProvider serviceProvider, - IOptions options, - ICancellationTokenProvider cancellationTokenProvider, - IDynamicWorkspaceDefinitionStore dynamicWorkspaceDefinitionStore, - IStaticWorkspaceSaver staticWorkspaceSaver) + + public WorkspaceDynamicInitializer(IServiceProvider serviceProvider) { Logger = NullLogger.Instance; ServiceProvider = serviceProvider; - Options = options; - CancellationTokenProvider = cancellationTokenProvider; - DynamicWorkspaceDefinitionStore = dynamicWorkspaceDefinitionStore; - StaticWorkspaceSaver = staticWorkspaceSaver; - ApplicationLifetime = ServiceProvider.GetRequiredService(); } public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) { - var options = Options.Value; + var options = ServiceProvider.GetRequiredService>().Value; if (!options.SaveStaticWorkspacesToDatabase && !options.IsDynamicWorkspaceStoreEnabled) { @@ -53,11 +36,12 @@ public class WorkspaceDynamicInitializer : ITransientDependency if (runInBackground) { + var applicationLifetime = ServiceProvider.GetService(); Task.Run(async () => { - if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + if (cancellationToken == default && applicationLifetime?.ApplicationStopping != null) { - cancellationToken = ApplicationLifetime.ApplicationStopping; + cancellationToken = applicationLifetime.ApplicationStopping; } await ExecuteInitializationAsync(options, cancellationToken); }, cancellationToken); @@ -72,16 +56,17 @@ public class WorkspaceDynamicInitializer : ITransientDependency { try { - using (CancellationTokenProvider.Use(cancellationToken)) + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) { - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } await SaveStaticWorkspacesToDatabaseAsync(options, cancellationToken); - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } @@ -104,6 +89,8 @@ public class WorkspaceDynamicInitializer : ITransientDependency return; } + var staticWorkspaceSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle(ex => ex is not OperationCanceledException) .WaitAndRetryAsync( @@ -118,7 +105,7 @@ public class WorkspaceDynamicInitializer : ITransientDependency { try { - await StaticWorkspaceSaver.SaveAsync(); + await staticWorkspaceSaver.SaveAsync(); } catch (Exception ex) { @@ -135,10 +122,12 @@ public class WorkspaceDynamicInitializer : ITransientDependency return; } + var dynamicWorkspaceDefinitionStore = ServiceProvider.GetRequiredService(); + try { // Pre-cache Workspaces, so first request doesn't wait - await DynamicWorkspaceDefinitionStore.GetAllAsync(); + await dynamicWorkspaceDefinitionStore.GetAllAsync(); } catch (Exception ex) { From 01e61c29a661712460cbc98fd434970bf96035b3 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 28 Jan 2026 11:50:21 +0800 Subject: [PATCH 23/25] feat: Add Chat interface implementation --- .../AIManagement/Chats/Dtos/ChatMessageDto.cs | 14 +++++++ .../Chats/Dtos/SendTextChatMessageDto.cs | 19 +++++++++ .../Chats/Dtos/TextChatMessageDto.cs | 8 ++++ .../Abp/AIManagement/Chats/IChatAppService.cs | 9 ++++ .../Abp/AIManagement/Chats/ChatAppService.cs | 28 +++++++++++++ .../FodyWeavers.xsd | 30 +++++++++++++ .../LINGYUN.Abp.AIManagement.Domain.csproj | 2 +- .../AbpAIManagementDomainModule.cs | 14 ++++++- .../AbpAIManagementHttpApiModule.cs | 41 ++++++++++++++++++ .../Abp/AIManagement/Chats/ChatController.cs | 30 +++++++++++++ .../Mvc/SseAsyncEnumerableResult.cs | 42 +++++++++++++++++++ .../Mvc/SseAsyncEnumerableResultFilter.cs | 18 ++++++++ 12 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ChatMessageDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/SendTextChatMessageDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/TextChatMessageDto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IChatAppService.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ChatAppService.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xsd create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementHttpApiModule.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ChatController.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResult.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResultFilter.cs diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ChatMessageDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ChatMessageDto.cs new file mode 100644 index 000000000..035fc1464 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ChatMessageDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public abstract class ChatMessageDto : ExtensibleAuditedEntityDto +{ + public string Workspace { get; set; } + + public DateTime CreatedAt { get; set; } + + public Guid? UserId { get; set; } + + public Guid? ConversationId { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/SendTextChatMessageDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/SendTextChatMessageDto.cs new file mode 100644 index 000000000..ad0372260 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/SendTextChatMessageDto.cs @@ -0,0 +1,19 @@ +using LINGYUN.Abp.AIManagement.Workspaces; +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class SendTextChatMessageDto +{ + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxNameLength))] + public string Workspace { get; set; } + + [Required] + public Guid ConversationId { get; set; } + + [Required] + [DynamicStringLength(typeof(TextChatMessageRecordConsts), nameof(TextChatMessageRecordConsts.MaxContentLength))] + public string Content { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/TextChatMessageDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/TextChatMessageDto.cs new file mode 100644 index 000000000..1ef328229 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/TextChatMessageDto.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class TextChatMessageDto +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IChatAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IChatAppService.cs new file mode 100644 index 000000000..dcfaa06ca --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IChatAppService.cs @@ -0,0 +1,9 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using System.Collections.Generic; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement.Chats; +public interface IChatAppService : IApplicationService +{ + IAsyncEnumerable SendMessageAsync(SendTextChatMessageDto input); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ChatAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ChatAppService.cs new file mode 100644 index 000000000..fe420a66a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ChatAppService.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.AI.Agent; +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using Microsoft.Extensions.AI; +using System.Collections.Generic; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ChatAppService : AIManagementApplicationServiceBase, IChatAppService +{ + private readonly IAgentService _agentService; + public ChatAppService(IAgentService agentService) + { + _agentService = agentService; + } + + public IAsyncEnumerable SendMessageAsync(SendTextChatMessageDto input) + { + var chatMessage = new TextChatMessage( + input.Workspace, + input.Content, + ChatRole.User, + Clock.Now); + + chatMessage.WithConversationId(input.ConversationId); + + return _agentService.SendMessageAsync(chatMessage); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj index 2cc7884dc..813624bf7 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj @@ -23,7 +23,7 @@ - + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs index 8d139de9b..75ba502f1 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs @@ -1,4 +1,6 @@ -using LINGYUN.Abp.AI; +using LINGYUN.Abp.AI.Agent; +using LINGYUN.Abp.AI.Localization; +using LINGYUN.Abp.AIManagement.Localization; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.Extensions.DependencyInjection; using System.Threading; @@ -8,6 +10,7 @@ using Volo.Abp.Caching; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain; +using Volo.Abp.Localization; using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.Threading; @@ -16,7 +19,7 @@ namespace LINGYUN.Abp.AIManagement; [DependsOn( typeof(AbpAIManagementDomainSharedModule), - typeof(AbpAICoreModule), + typeof(AbpAIAgentModule), typeof(AbpCachingModule), typeof(AbpMapperlyModule), typeof(AbpDddDomainModule))] @@ -37,6 +40,13 @@ public class AbpAIManagementDomainModule : AbpModule options.IsDynamicWorkspaceStoreEnabled = false; }); } + + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes(typeof(AbpAIResource)); + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementHttpApiModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementHttpApiModule.cs new file mode 100644 index 000000000..7e119483f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementHttpApiModule.cs @@ -0,0 +1,41 @@ +using LINGYUN.Abp.AIManagement.Localization; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementApplicationContractsModule), + typeof(AbpAspNetCoreMvcModule))] +public class AbpAIManagementHttpApiModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(options => + { + options.AddAssemblyResource(typeof(AIManagementResource), typeof(AbpAIManagementApplicationContractsModule).Assembly); + }); + + PreConfigure(mvcBuilder => + { + mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAIManagementHttpApiModule).Assembly); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes(typeof(AbpUiResource)); + }); + + context.Services.AddTransient(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ChatController.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ChatController.cs new file mode 100644 index 000000000..811a29c56 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ChatController.cs @@ -0,0 +1,30 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Abp.AIManagement.Chats; + +[Controller] +[RemoteService(Name = AIManagementRemoteServiceConsts.RemoteServiceName)] +[Area(AIManagementRemoteServiceConsts.ModuleName)] +[Route($"api/{AIManagementRemoteServiceConsts.ModuleName}/chats")] +public class ChatController : AbpControllerBase, IChatAppService +{ + private readonly IChatAppService _service; + public ChatController(IChatAppService service) + { + _service = service; + } + + [HttpPost] + [ServiceFilter] + public async virtual IAsyncEnumerable SendMessageAsync(SendTextChatMessageDto input) + { + await foreach (var content in _service.SendMessageAsync(input)) + { + yield return content; + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResult.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResult.cs new file mode 100644 index 000000000..f44e8f911 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResult.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc; +public class SseAsyncEnumerableResult : IActionResult +{ + private readonly IAsyncEnumerable _asyncEnumerable; + public SseAsyncEnumerableResult(IAsyncEnumerable asyncEnumerable) + { + _asyncEnumerable = asyncEnumerable; + } + public async Task ExecuteResultAsync(ActionContext context) + { + var response = context.HttpContext.Response; + + response.Headers.Append("Content-Type", "text/event-stream"); + response.Headers.Append("Cache-Control", "no-cache"); + response.Headers.Append("Connection", "keep-alive"); + response.Headers.Append("X-Accel-Buffering", "no"); + + try + { + await foreach (var content in _asyncEnumerable) + { + if (!string.IsNullOrEmpty(content)) + { + await response.WriteAsync($"data: {content}\n\n"); + await response.Body.FlushAsync(); + } + } + + await response.WriteAsync("data: [DONE]\n\n"); + await response.Body.FlushAsync(); + } + catch (OperationCanceledException) + { + // ignore + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResultFilter.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResultFilter.cs new file mode 100644 index 000000000..55d20014e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResultFilter.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc; +public class SseAsyncEnumerableResultFilter : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var executedContext = await next(); + + if (executedContext.Result is ObjectResult objectResult && + objectResult.Value is IAsyncEnumerable asyncEnumerable) + { + executedContext.Result = new SseAsyncEnumerableResult(asyncEnumerable); + } + } +} From ce7d9a6c120cff94b66b0be508ce05abee3bd37c Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 28 Jan 2026 11:51:21 +0800 Subject: [PATCH 24/25] feat: Introduce AI management for microservices --- .../AIServiceDbMigratorHostedService.cs | 53 +++ .../AIServiceDbMigratorModule.cs | 13 + .../FodyWeavers.xml | 3 + ...p.MicroService.AIService.DbMigrator.csproj | 40 ++ .../Program.cs | 43 ++ .../appsettings.json | 5 + .../AIServiceDataSeeder.cs | 26 ++ .../AIServiceDbMigrationEventHandler.cs | 44 ++ .../AIServiceDbMigrationService.cs | 36 ++ .../AIServiceMigrationsDbContext.cs | 32 ++ .../AIServiceMigrationsDbContextFactory.cs | 28 ++ ...viceMigrationsEntityFrameworkCoreModule.cs | 45 ++ .../FodyWeavers.xml | 3 + .../FodyWeavers.xsd | 30 ++ ...rvice.AIService.EntityFrameworkCore.csproj | 35 ++ ...60127083027_Initial_AI_Service.Designer.cs | 303 +++++++++++++ .../20260127083027_Initial_AI_Service.cs | 148 ++++++ ...ServiceMigrationsDbContextModelSnapshot.cs | 300 +++++++++++++ .../AIServiceModule.Configure.cs | 423 ++++++++++++++++++ .../AIServiceModule.cs | 103 +++++ .../FodyWeavers.xml | 3 + .../LINGYUN.Abp.MicroService.AIService.csproj | 71 +++ .../Program.cs | 99 ++++ .../Properties/launchSettings.json | 12 + .../TenantHeaderParamter.cs | 35 ++ .../appsettings.Development.json | 116 +++++ .../appsettings.json | 91 ++++ .../yarp.json | 25 ++ .../AppHost.cs | 12 + .../LINGYUN.Abp.MicroService.AppHost.csproj | 2 + 30 files changed, 2179 insertions(+) create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorHostedService.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorModule.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/FodyWeavers.xml create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/LINGYUN.Abp.MicroService.AIService.DbMigrator.csproj create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/Program.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/appsettings.json create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDataSeeder.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationEventHandler.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationService.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContext.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContextFactory.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsEntityFrameworkCoreModule.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xml create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xsd create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore.csproj create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.Designer.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/AIServiceMigrationsDbContextModelSnapshot.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.Configure.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/FodyWeavers.xml create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/LINGYUN.Abp.MicroService.AIService.csproj create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Program.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Properties/launchSettings.json create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/TenantHeaderParamter.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.Development.json create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.json diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorHostedService.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorHostedService.cs new file mode 100644 index 000000000..2590ba079 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorHostedService.cs @@ -0,0 +1,53 @@ +using LINGYUN.Abp.MicroService.AIService.DbMigrator; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Data; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDbMigratorHostedService : IHostedService +{ + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly IConfiguration _configuration; + + public AIServiceDbMigratorHostedService( + IHostApplicationLifetime hostApplicationLifetime, + IConfiguration configuration) + { + _hostApplicationLifetime = hostApplicationLifetime; + _configuration = configuration; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var application = await AbpApplicationFactory + .CreateAsync(options => + { + options.Services.ReplaceConfiguration(_configuration); + options.UseAutofac(); + options.Services.AddLogging(c => c.AddSerilog()); + options.AddDataMigrationEnvironment(); + }); + + await application.InitializeAsync(); + + await application + .ServiceProvider + .GetRequiredService() + .CheckAndApplyDatabaseMigrationsAsync(); + + await application.ShutdownAsync(); + + _hostApplicationLifetime.StopApplication(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorModule.cs new file mode 100644 index 000000000..ff9dd1c85 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.MicroService.AIService.DbMigrator; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AIServiceMigrationsEntityFrameworkCoreModule) + )] +public class AIServiceDbMigratorModule : AbpModule +{ + +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/FodyWeavers.xml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/LINGYUN.Abp.MicroService.AIService.DbMigrator.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/LINGYUN.Abp.MicroService.AIService.DbMigrator.csproj new file mode 100644 index 000000000..662765cf3 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/LINGYUN.Abp.MicroService.AIService.DbMigrator.csproj @@ -0,0 +1,40 @@ + + + + + + Exe + net10.0 + enable + false + LINGYUN.Abp.MicroService.AIService + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + Always + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/Program.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/Program.cs new file mode 100644 index 000000000..135bb215b --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/Program.cs @@ -0,0 +1,43 @@ +using LINGYUN.Abp.MicroService.AIService; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using System; + +var defaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}"; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) +#if DEBUG + .MinimumLevel.Override("LINGYUN.Abp.MicroService.AIService", LogEventLevel.Debug) +#else + .MinimumLevel.Override("LINGYUN.Abp.MicroService.AIService", LogEventLevel.Information) +#endif + .Enrich.FromLogContext() + .WriteTo.Async(x => x.Console(outputTemplate: defaultOutputTemplate)) + .WriteTo.Async(x => x.File("Logs/migrations.txt", outputTemplate: defaultOutputTemplate)) + .CreateLogger(); + +try +{ + var builder = Host.CreateDefaultBuilder(args) + .AddAppSettingsSecretsJson() + .ConfigureLogging((context, logging) => logging.ClearProviders()) + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); + }); + await builder.RunConsoleAsync(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly!"); +} +finally +{ + await Log.CloseAndFlushAsync(); +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/appsettings.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/appsettings.json new file mode 100644 index 000000000..38212c579 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "Default": "Host=127.0.0.1;Database=abp;Username=postgres;Password=123456" + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDataSeeder.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDataSeeder.cs new file mode 100644 index 000000000..8a088b76d --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDataSeeder.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDataSeeder : ITransientDependency +{ + protected ILogger Logger { get; } + protected ICurrentTenant CurrentTenant { get; } + + public AIServiceDataSeeder( + ICurrentTenant currentTenant) + { + CurrentTenant = currentTenant; + + Logger = NullLogger.Instance; + } + + public virtual Task SeedAsync(DataSeedContext context) + { + return Task.CompletedTask; + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationEventHandler.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationEventHandler.cs new file mode 100644 index 000000000..b05842e54 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationEventHandler.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DistributedLocking; +using Volo.Abp.EntityFrameworkCore.Migrations; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDbMigrationEventHandler : EfCoreDatabaseMigrationEventHandlerBase +{ + protected AIServiceDataSeeder DataSeeder { get; } + + public AIServiceDbMigrationEventHandler( + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + ITenantStore tenantStore, + IAbpDistributedLock abpDistributedLock, + IDistributedEventBus distributedEventBus, + ILoggerFactory loggerFactory, + AIServiceDataSeeder dataSeeder) + : base( + ConnectionStringNameAttribute.GetConnStringName(), + currentTenant, unitOfWorkManager, tenantStore, abpDistributedLock, distributedEventBus, loggerFactory) + { + DataSeeder = dataSeeder; + } + + protected async override Task AfterTenantCreated(TenantCreatedEto eventData, bool schemaMigrated) + { + // 新租户数据种子 + var context = new DataSeedContext(eventData.Id); + if (eventData.Properties != null) + { + foreach (var property in eventData.Properties) + { + context.WithProperty(property.Key, property.Value); + } + } + + await DataSeeder.SeedAsync(context); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationService.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationService.cs new file mode 100644 index 000000000..445f0fd92 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationService.cs @@ -0,0 +1,36 @@ +using LINGYUN.Abp.Data.DbMigrator; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDbMigrationService : EfCoreRuntimeDbMigratorBase, ITransientDependency +{ + protected AIServiceDataSeeder DataSeeder { get; } + public AIServiceDbMigrationService( + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + IServiceProvider serviceProvider, + IAbpDistributedLock abpDistributedLock, + IDistributedEventBus distributedEventBus, + ILoggerFactory loggerFactory, + AIServiceDataSeeder dataSeeder) + : base( + ConnectionStringNameAttribute.GetConnStringName(), + unitOfWorkManager, serviceProvider, currentTenant, abpDistributedLock, distributedEventBus, loggerFactory) + { + DataSeeder = dataSeeder; + } + + protected async override Task SeedAsync() + { + // DbMigrator迁移数据种子 + await DataSeeder.SeedAsync(new DataSeedContext()); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContext.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContext.cs new file mode 100644 index 000000000..35d068464 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContext.cs @@ -0,0 +1,32 @@ +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.EntityFrameworkCore; +using LINGYUN.Abp.AIManagement.Tokens; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.MicroService.AIService; + +[ConnectionStringName("Default")] +public class AIServiceMigrationsDbContext : + AbpDbContext, + IAIManagementDbContext +{ + public DbSet WorkspaceDefinitions { get; set; } + public DbSet TextChatMessageRecords { get; set; } + public DbSet ConversationRecords { get; set; } + public DbSet TokenUsageRecords { get; set; } + + public AIServiceMigrationsDbContext( + DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigureAIManagement(); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContextFactory.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContextFactory.cs new file mode 100644 index 000000000..afd229619 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContextFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using System.IO; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceMigrationsDbContextFactory : IDesignTimeDbContextFactory +{ + public AIServiceMigrationsDbContext CreateDbContext(string[] args) + { + var configuration = BuildConfiguration(); + var connectionString = configuration.GetConnectionString("Default"); + + var builder = new DbContextOptionsBuilder() + .UseNpgsql(connectionString); + + return new AIServiceMigrationsDbContext(builder!.Options); + } + + private static IConfigurationRoot BuildConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../LINGYUN.Abp.MicroService.AIService.DbMigrator/")) + .AddJsonFile("appsettings.json", optional: false); + + return builder.Build(); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsEntityFrameworkCoreModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsEntityFrameworkCoreModule.cs new file mode 100644 index 000000000..1c84f02f9 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsEntityFrameworkCoreModule.cs @@ -0,0 +1,45 @@ +using LINGYUN.Abp.AIManagement.EntityFrameworkCore; +using LINGYUN.Abp.Data.DbMigrator; +using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore; +using LINGYUN.Abp.Saas.EntityFrameworkCore; +using LINGYUN.Abp.TextTemplating.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.PostgreSql; +using Volo.Abp.FeatureManagement.EntityFrameworkCore; +using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.EntityFrameworkCore; +using Volo.Abp.SettingManagement.EntityFrameworkCore; + +namespace LINGYUN.Abp.MicroService.AIService; + +[DependsOn( + typeof(AbpAIManagementEntityFrameworkCoreModule), + typeof(AbpSaasEntityFrameworkCoreModule), + typeof(AbpSettingManagementEntityFrameworkCoreModule), + typeof(AbpPermissionManagementEntityFrameworkCoreModule), + typeof(AbpFeatureManagementEntityFrameworkCoreModule), + typeof(AbpLocalizationManagementEntityFrameworkCoreModule), + typeof(AbpTextTemplatingEntityFrameworkCoreModule), + typeof(AbpEntityFrameworkCorePostgreSqlModule), + typeof(AbpDataDbMigratorModule) + )] +public class AIServiceMigrationsEntityFrameworkCoreModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + // https://www.npgsql.org/efcore/release-notes/6.0.html#opting-out-of-the-new-timestamp-mapping-logic + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(); + + Configure(options => + { + options.UseNpgsql(); + }); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xsd b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore.csproj new file mode 100644 index 000000000..c85236273 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore.csproj @@ -0,0 +1,35 @@ + + + + + + + false + net10.0 + enable + LINGYUN.Abp.MicroService.AIService + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.Designer.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.Designer.cs new file mode 100644 index 000000000..3b253c2ca --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.Designer.cs @@ -0,0 +1,303 @@ +// +using System; +using LINGYUN.Abp.MicroService.AIService; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace LINGYUN.Abp.MicroService.AIService.Migrations +{ + [DbContext(typeof(AIServiceMigrationsDbContext))] + [Migration("20260127083027_Initial_AI_Service")] + partial class Initial_AI_Service + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.ConversationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UpdateAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("AbpAIConversations", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.TextChatMessageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReplyAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyMessage") + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Workspace") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITextChatMessages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Tokens.TokenUsageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CachedInputTokenCount") + .HasColumnType("bigint"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("InputTokenCount") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("OutputTokenCount") + .HasColumnType("bigint"); + + b.Property("ReasoningTokenCount") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalTokenCount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITokenUsages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Workspaces.WorkspaceDefinitionRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiBaseUrl") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApiKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FrequencyPenalty") + .HasColumnType("real"); + + b.Property("Instructions") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MaxOutputTokens") + .HasColumnType("integer"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PresencePenalty") + .HasColumnType("real"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SystemPrompt") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Temperature") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpAIWorkspaceDefinitions", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.cs new file mode 100644 index 000000000..ed6c57888 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.cs @@ -0,0 +1,148 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LINGYUN.Abp.MicroService.AIService.Migrations +{ + /// + public partial class Initial_AI_Service : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AbpAIConversations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdateAt = table.Column(type: "timestamp with time zone", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAIConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AbpAITextChatMessages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: true), + Workspace = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Role = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + ConversationId = table.Column(type: "uuid", nullable: true), + ReplyMessage = table.Column(type: "text", nullable: true), + ReplyAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAITextChatMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AbpAITokenUsages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + MessageId = table.Column(type: "uuid", nullable: true), + ConversationId = table.Column(type: "uuid", nullable: true), + InputTokenCount = table.Column(type: "bigint", nullable: true), + OutputTokenCount = table.Column(type: "bigint", nullable: true), + TotalTokenCount = table.Column(type: "bigint", nullable: true), + CachedInputTokenCount = table.Column(type: "bigint", nullable: true), + ReasoningTokenCount = table.Column(type: "bigint", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAITokenUsages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AbpAIWorkspaceDefinitions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Provider = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + ModelName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + DisplayName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Description = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + ApiKey = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ApiBaseUrl = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + SystemPrompt = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + Instructions = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + Temperature = table.Column(type: "real", nullable: true), + MaxOutputTokens = table.Column(type: "integer", nullable: true), + FrequencyPenalty = table.Column(type: "real", nullable: true), + PresencePenalty = table.Column(type: "real", nullable: true), + IsEnabled = table.Column(type: "boolean", nullable: false), + StateCheckers = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAIWorkspaceDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AbpAITextChatMessages_TenantId_ConversationId", + table: "AbpAITextChatMessages", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AbpAITokenUsages_TenantId_ConversationId", + table: "AbpAITokenUsages", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AbpAIWorkspaceDefinitions_Name", + table: "AbpAIWorkspaceDefinitions", + column: "Name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AbpAIConversations"); + + migrationBuilder.DropTable( + name: "AbpAITextChatMessages"); + + migrationBuilder.DropTable( + name: "AbpAITokenUsages"); + + migrationBuilder.DropTable( + name: "AbpAIWorkspaceDefinitions"); + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/AIServiceMigrationsDbContextModelSnapshot.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/AIServiceMigrationsDbContextModelSnapshot.cs new file mode 100644 index 000000000..095a0771d --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/AIServiceMigrationsDbContextModelSnapshot.cs @@ -0,0 +1,300 @@ +// +using System; +using LINGYUN.Abp.MicroService.AIService; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace LINGYUN.Abp.MicroService.AIService.Migrations +{ + [DbContext(typeof(AIServiceMigrationsDbContext))] + partial class AIServiceMigrationsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.ConversationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UpdateAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("AbpAIConversations", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.TextChatMessageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReplyAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyMessage") + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Workspace") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITextChatMessages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Tokens.TokenUsageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CachedInputTokenCount") + .HasColumnType("bigint"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("InputTokenCount") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("OutputTokenCount") + .HasColumnType("bigint"); + + b.Property("ReasoningTokenCount") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalTokenCount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITokenUsages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Workspaces.WorkspaceDefinitionRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiBaseUrl") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApiKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FrequencyPenalty") + .HasColumnType("real"); + + b.Property("Instructions") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MaxOutputTokens") + .HasColumnType("integer"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PresencePenalty") + .HasColumnType("real"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SystemPrompt") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Temperature") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpAIWorkspaceDefinitions", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.Configure.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.Configure.cs new file mode 100644 index 000000000..f654a1bc1 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.Configure.cs @@ -0,0 +1,423 @@ +using DotNetCore.CAP; +using LINGYUN.Abp.AIManagement; +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.Localization.CultureMap; +using LINGYUN.Abp.LocalizationManagement; +using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +using LINGYUN.Abp.TextTemplating; +using LINGYUN.Abp.Wrapper; +using Medallion.Threading; +using Medallion.Threading.Redis; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Caching.StackExchangeRedis; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using StackExchange.Redis; +using System.Text.Encodings.Web; +using System.Text.Unicode; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.AntiForgery; +using Volo.Abp.Auditing; +using Volo.Abp.Caching; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.FeatureManagement; +using Volo.Abp.GlobalFeatures; +using Volo.Abp.Http.Client; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Json; +using Volo.Abp.Json.SystemTextJson; +using Volo.Abp.Localization; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement; +using Volo.Abp.Security.Claims; +using Volo.Abp.SettingManagement; +using Volo.Abp.Threading; +using Volo.Abp.Timing; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.MicroService.AIService; + +public partial class AIServiceModule +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + private void PreConfigureFeature() + { + OneTimeRunner.Run(() => + { + GlobalFeatureManager.Instance.Modules.Editions().EnableAll(); + }); + } + + private void PreConfigureApp(IConfiguration configuration) + { + PreConfigure(options => + { + // 以开放端口区别,应在0-31之间 + options.SnowflakeIdOptions.WorkerId = 19; + options.SnowflakeIdOptions.WorkerIdBits = 5; + options.SnowflakeIdOptions.DatacenterId = 1; + }); + + if (configuration.GetValue("App:ShowPii")) + { + IdentityModelEventSource.ShowPII = true; + } + } + + private void PreConfigureCAP(IConfiguration configuration) + { + PreConfigure(options => + { + options + .UsePostgreSql(mySqlOptions => + { + configuration.GetSection("CAP:PostgreSql").Bind(mySqlOptions); + }) + .UseRabbitMQ(rabbitMQOptions => + { + configuration.GetSection("CAP:RabbitMQ").Bind(rabbitMQOptions); + }) + .UseDashboard(); + }); + } + + private void ConfigureTextTemplating() + { + Configure(options => + { + options.IsDynamicTemplateDefinitionStoreEnabled = true; + }); + } + + private void ConfigureFeatureManagement() + { + Configure(options => + { + options.IsDynamicFeatureStoreEnabled = true; + }); + } + + private void ConfigureJsonSerializer(IConfiguration configuration) + { + // 统一时间日期格式 + Configure(options => + { + var jsonConfiguration = configuration.GetSection("Json"); + if (jsonConfiguration.Exists()) + { + jsonConfiguration.Bind(options); + } + }); + // 中文序列化的编码问题 + Configure(options => + { + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); + }); + } + + private void ConfigureAIManagement() + { + Configure(options => + { + options.IsDynamicWorkspaceStoreEnabled = true; + options.SaveStaticWorkspacesToDatabase = true; + }); + } + + private void ConfigurePermissionManagement() + { + Configure(options => + { + options.IsDynamicPermissionStoreEnabled = true; + }); + } + + private void ConfigureSettingManagement() + { + Configure(options => + { + options.IsDynamicSettingStoreEnabled = true; + }); + } + + private void ConfigureTiming(IConfiguration configuration) + { + Configure(options => + { + configuration.GetSection("Clock").Bind(options); + }); + } + + private void ConfigureCaching(IConfiguration configuration) + { + Configure(options => + { + configuration.GetSection("DistributedCache").Bind(options); + }); + + Configure(options => + { + options.AutoEventSelectors.AddNamespace("Volo.Abp.TenantManagement"); + }); + + Configure(options => + { + var redisConfig = ConfigurationOptions.Parse(options.Configuration!); + options.ConfigurationOptions = redisConfig; + options.InstanceName = configuration["Redis:InstanceName"]; + }); + } + + private void ConfigureDistributedLocking(IServiceCollection services, IConfiguration configuration) + { + var distributedLockEnabled = configuration["DistributedLock:IsEnabled"]; + if (distributedLockEnabled.IsNullOrEmpty() || bool.Parse(distributedLockEnabled)) + { + services.AddSingleton(sp => + { + var connectionMultiplexer = sp.GetRequiredService(); + return new RedisDistributedSynchronizationProvider(connectionMultiplexer.GetDatabase()); + }); + } + } + + private void ConfigureMvc(IServiceCollection services, IConfiguration configuration) + { + Configure(options => + { + options.ExposeIntegrationServices = true; + }); + } + + private void ConfigureVirtualFileSystem() + { + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.MicroService.AIService"); + }); + } + + private void ConfigureMultiTenancy(IConfiguration configuration) + { + // 多租户 + Configure(options => + { + options.IsEnabled = true; + }); + + var tenantResolveCfg = configuration.GetSection("App:Domains"); + if (tenantResolveCfg.Exists()) + { + Configure(options => + { + var domains = tenantResolveCfg.Get() ?? []; + foreach (var domain in domains) + { + options.AddDomainTenantResolver(domain); + } + }); + } + } + + private void ConfigureIdentity(IConfiguration configuration) + { + Configure(options => + { + options.IsDynamicClaimsEnabled = true; + options.RemoteRefreshUrl = configuration["App:RefreshClaimsUrl"] + options.RemoteRefreshUrl; + }); + } + + private void ConfigureAuditing(IConfiguration configuration) + { + Configure(options => + { + // 是否启用实体变更记录 + var allEntitiesSelectorIsEnabled = configuration["Auditing:AllEntitiesSelector"]; + if (allEntitiesSelectorIsEnabled.IsNullOrWhiteSpace() || + (bool.TryParse(allEntitiesSelectorIsEnabled, out var enabled) && enabled)) + { + options.EntityHistorySelectors.AddAllEntities(); + } + }); + } + + private void ConfigureSwagger(IServiceCollection services, IConfiguration configuration) + { + // Swagger + services.AddAbpSwaggerGenWithOAuth( + configuration["AuthServer:Authority"]!, + new Dictionary + { + { "AIService", "AI Service API"} + }, + options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "AI Service API", Version = "v1", + Contact = new OpenApiContact + { + Name = "colin", + Email = "colin.in@foxmail.com", + Url = new Uri("https://github.com/colinin") + }, + License = new OpenApiLicense + { + Name = "MIT", + Url = new Uri("https://github.com/colinin/abp-next-admin/blob/master/LICENSE") + } + }); + options.DocInclusionPredicate((docName, description) => true); + options.CustomSchemaIds(type => type.FullName); + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Scheme = "bearer", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT" + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + new string[] { } + } + }); + options.OperationFilter(); + }); + } + + private void ConfigureLocalization() + { + // 支持本地化语言类型 + Configure(options => + { + options.Languages.Add(new LanguageInfo("en", "en", "English")); + options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文")); + + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources"); + }); + + Configure(options => + { + var zhHansCultureMapInfo = new CultureMapInfo + { + TargetCulture = "zh-Hans", + SourceCultures = new string[] { "zh", "zh_CN", "zh-CN" } + }; + + options.CulturesMaps.Add(zhHansCultureMapInfo); + options.UiCulturesMaps.Add(zhHansCultureMapInfo); + }); + + Configure(options => + { + options.SaveStaticLocalizationsToDatabase = true; + }); + } + + private void ConfigureCors(IServiceCollection services, IConfiguration configuration) + { + services.AddCors(options => + { + options.AddDefaultPolicy(builder => + { + var corsOrigins = configuration.GetSection("App:CorsOrigins").Get>(); + if (corsOrigins == null || corsOrigins.Count == 0) + { + corsOrigins = configuration["App:CorsOrigins"]? + .Split(",", StringSplitOptions.RemoveEmptyEntries) + .Select(o => o.RemovePostFix("/")) + .ToList() ?? new List(); + } + builder + .WithOrigins(corsOrigins + .Select(o => o.RemovePostFix("/")) + .ToArray() + ) + .WithAbpExposedHeaders() + .WithAbpWrapExposedHeaders() + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + } + + private void ConfigureSecurity(IServiceCollection services, IConfiguration configuration, bool isDevelopment = false) + { + Configure(options => + { + options.TokenCookie.HttpOnly = false; + options.TokenCookie.SameSite = SameSiteMode.Lax; + }); + + services.AddAlwaysAllowAuthorization(); + services.AddAlwaysAllowSession(); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddAbpJwtBearer(options => + { + configuration.GetSection("AuthServer").Bind(options); + + var validIssuers = configuration.GetSection("AuthServer:ValidIssuers").Get>(); + if (validIssuers?.Count > 0) + { + options.TokenValidationParameters.ValidIssuers = validIssuers; + options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; + } + var validAudiences = configuration.GetSection("AuthServer:ValidAudiences").Get>(); + if (validAudiences?.Count > 0) + { + options.TokenValidationParameters.ValidAudiences = validAudiences; + } + }); + + services + .AddDataProtection() + .SetApplicationName("LINGYUN.Abp.Application") + .PersistKeysToStackExchangeRedis(() => + { + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]!); + + return redis.GetDatabase(); + }, + "LINGYUN.Abp.Application:DataProtection:Protection-Keys"); + } + + private void ConfigureWrapper() + { + Configure(options => + { + options.IsEnabled = true; + + options.IgnoreControllers.Add(); + }); + } + + private void PreConfigureWrapper() + { + // 服务间调用不包装 + PreConfigure(options => + { + options.ProxyClientActions.Add( + (_, _, client) => + { + client.DefaultRequestHeaders.TryAddWithoutValidation(AbpHttpWrapConsts.AbpDontWrapResult, "true"); + }); + }); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.cs new file mode 100644 index 000000000..e18ca18d8 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.cs @@ -0,0 +1,103 @@ +using LINGYUN.Abp.AIManagement; +using LINGYUN.Abp.AspNetCore.HttpOverrides; +using LINGYUN.Abp.AspNetCore.Mvc.Localization; +using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; +using LINGYUN.Abp.AuditLogging.Elasticsearch; +using LINGYUN.Abp.Claims.Mapping; +using LINGYUN.Abp.Data.DbMigrator; +using LINGYUN.Abp.Emailing.Platform; +using LINGYUN.Abp.EventBus.CAP; +using LINGYUN.Abp.ExceptionHandling.Emailing; +using LINGYUN.Abp.Identity.Session.AspNetCore; +using LINGYUN.Abp.Localization.CultureMap; +using LINGYUN.Abp.Logging.Serilog.Elasticsearch; +using LINGYUN.Abp.Serilog.Enrichers.Application; +using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +using LINGYUN.Abp.Sms.Platform; +using LINGYUN.Abp.TextTemplating.Scriban; +using Volo.Abp.AspNetCore.Authentication.JwtBearer; +using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; +using Volo.Abp.AspNetCore.Serilog; +using Volo.Abp.Autofac; +using Volo.Abp.Caching.StackExchangeRedis; +using Volo.Abp.Http.Client; +using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.Identity; +using Volo.Abp.PermissionManagement.OpenIddict; +using Volo.Abp.Swashbuckle; + +namespace LINGYUN.Abp.MicroService.AIService; + +[DependsOn( + typeof(AbpCAPEventBusModule), + typeof(AbpSerilogEnrichersApplicationModule), + typeof(AbpSerilogEnrichersUniqueIdModule), + typeof(AbpAspNetCoreSerilogModule), + typeof(AbpLoggingSerilogElasticsearchModule), + typeof(AbpAuditLoggingElasticsearchModule), + typeof(AbpAspNetCoreMvcUiMultiTenancyModule), + typeof(AbpAspNetCoreMvcLocalizationModule), + + typeof(AbpPermissionManagementDomainIdentityModule), + typeof(AbpPermissionManagementDomainOpenIddictModule), + + // 重写模板引擎支持外部本地化 + typeof(AbpTextTemplatingScribanModule), + + typeof(AbpIdentitySessionAspNetCoreModule), + + typeof(AbpAIManagementApplicationModule), + typeof(AbpAIManagementHttpApiModule), + typeof(AIServiceMigrationsEntityFrameworkCoreModule), + typeof(AbpDataDbMigratorModule), + typeof(AbpAspNetCoreAuthenticationJwtBearerModule), + typeof(AbpEmailingExceptionHandlingModule), + typeof(AbpHttpClientModule), + typeof(AbpSmsPlatformModule), + typeof(AbpEmailingPlatformModule), + typeof(AbpCachingStackExchangeRedisModule), + typeof(AbpLocalizationCultureMapModule), + typeof(AbpAspNetCoreMvcWrapperModule), + typeof(AbpAspNetCoreHttpOverridesModule), + typeof(AbpClaimsMappingModule), + typeof(AbpSwashbuckleModule), + typeof(AbpAutofacModule) + )] +public partial class AIServiceModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + PreConfigureWrapper(); + PreConfigureFeature(); + PreConfigureApp(configuration); + PreConfigureCAP(configuration); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + ConfigureWrapper(); + ConfigureLocalization(); + ConfigureVirtualFileSystem(); + ConfigureTextTemplating(); + ConfigureAIManagement(); + ConfigureSettingManagement(); + ConfigureFeatureManagement(); + ConfigurePermissionManagement(); + ConfigureIdentity(configuration); + ConfigureTiming(configuration); + ConfigureCaching(configuration); + ConfigureAuditing(configuration); + ConfigureMultiTenancy(configuration); + ConfigureJsonSerializer(configuration); + ConfigureMvc(context.Services, configuration); + ConfigureCors(context.Services, configuration); + ConfigureSwagger(context.Services, configuration); + ConfigureDistributedLocking(context.Services, configuration); + ConfigureSecurity(context.Services, configuration, hostingEnvironment.IsDevelopment()); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/FodyWeavers.xml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/LINGYUN.Abp.MicroService.AIService.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/LINGYUN.Abp.MicroService.AIService.csproj new file mode 100644 index 000000000..dafa6b426 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/LINGYUN.Abp.MicroService.AIService.csproj @@ -0,0 +1,71 @@ + + + + net10.0 + enable + enable + LINGYUN.Abp.MicroService.AIService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Program.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Program.cs new file mode 100644 index 000000000..85b652951 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Program.cs @@ -0,0 +1,99 @@ +using LINGYUN.Abp.Identity.Session.AspNetCore; +using LINGYUN.Abp.MicroService.AIService; +using LINGYUN.Abp.Serilog.Enrichers.Application; +using Serilog; +using Volo.Abp.IO; +using Volo.Abp.Modularity.PlugIns; + +Log.Information("Starting AIService Host..."); + +try +{ + var builder = WebApplication.CreateBuilder(args); + builder.Host.AddAppSettingsSecretsJson() + .UseAutofac() + .ConfigureAppConfiguration((context, config) => + { + if (context.Configuration.GetValue("AgileConfig:IsEnabled", false)) + { + config.AddAgileConfig(new AgileConfig.Client.ConfigClient(context.Configuration)); + } + }) + .UseSerilog((context, provider, config) => + { + config.ReadFrom.Configuration(context.Configuration); + }); + + builder.AddServiceDefaults(); + + await builder.AddApplicationAsync(options => + { + var applicationName = Environment.GetEnvironmentVariable("APPLICATION_NAME") ?? "AIService"; + options.ApplicationName = applicationName; + AbpSerilogEnrichersConsts.ApplicationName = applicationName; + + var pluginFolder = Path.Combine(Directory.GetCurrentDirectory(), "Modules"); + DirectoryHelper.CreateIfNotExists(pluginFolder); + options.PlugInSources.AddFolder(pluginFolder, SearchOption.AllDirectories); + }); + + var app = builder.Build(); + + await app.InitializeApplicationAsync(); + + app.MapDefaultEndpoints(); + + app.UseForwardedHeaders(); + // 本地化 + app.UseMapRequestLocalization(); + // http调用链 + app.UseCorrelationId(); + // 文件系统 + app.MapAbpStaticAssets(); + // 路由 + app.UseRouting(); + // 跨域 + app.UseCors(); + // 认证 + app.UseAuthentication(); + app.UseJwtTokenMiddleware(); + // 多租户 + app.UseMultiTenancy(); + // 会话 + app.UseAbpSession(); + // jwt + app.UseDynamicClaims(); + // 授权 + app.UseAuthorization(); + // Swagger + app.UseSwagger(); + // Swagger可视化界面 + app.UseAbpSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Support AI Service API"); + + var configuration = app.Configuration; + options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]); + options.OAuthScopes(configuration["AuthServer:Audience"]); + }); + // 审计日志 + app.UseAuditing(); + app.UseAbpSerilogEnrichers(); + // 路由 + app.UseConfiguredEndpoints(); + + await app.RunAsync(); +} +catch (Exception ex) +{ + if (ex is HostAbortedException) + { + throw; + } + + Log.Fatal(ex, "Host terminated unexpectedly!"); +} +finally +{ + await Log.CloseAndFlushAsync(); +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Properties/launchSettings.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Properties/launchSettings.json new file mode 100644 index 000000000..312571616 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "LINGYUN.Abp.MicroService.AIService": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:49787;http://localhost:49788" + } + } +} \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/TenantHeaderParamter.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/TenantHeaderParamter.cs new file mode 100644 index 000000000..f72407569 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/TenantHeaderParamter.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.MicroService.AIService; + +public class TenantHeaderParamter : IOperationFilter +{ + private readonly AbpMultiTenancyOptions _multiTenancyOptions; + private readonly AbpAspNetCoreMultiTenancyOptions _aspNetCoreMultiTenancyOptions; + public TenantHeaderParamter( + IOptions multiTenancyOptions, + IOptions aspNetCoreMultiTenancyOptions) + { + _multiTenancyOptions = multiTenancyOptions.Value; + _aspNetCoreMultiTenancyOptions = aspNetCoreMultiTenancyOptions.Value; + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (_multiTenancyOptions.IsEnabled) + { + operation.Parameters = operation.Parameters ?? new List(); + operation.Parameters.Add(new OpenApiParameter + { + Name = _aspNetCoreMultiTenancyOptions.TenantKey, + In = ParameterLocation.Header, + Description = "Tenant Id in http header", + Required = false + }); + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.Development.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.Development.json new file mode 100644 index 000000000..c9a9d8327 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.Development.json @@ -0,0 +1,116 @@ +{ + "App": { + "ShowPii": true, + "CorsOrigins": [ "http://localhost:5666", "http://localhost:30000" ], + "RefreshClaimsUrl": "http://localhost:30015" + }, + "Auditing": { + "AllEntitiesSelector": true + }, + "DistributedCache": { + "HideErrors": true, + "KeyPrefix": "LINGYUN.Abp.Application", + "GlobalCacheEntryOptions": { + "SlidingExpiration": "30:00:00", + "AbsoluteExpirationRelativeToNow": "60:00:00" + } + }, + "ConnectionStrings": { + "Default": "Host=127.0.0.1;Database=abp;Username=postgres;Password=123456" + }, + "CAP": { + "EventBus": { + "DefaultGroupName": "AIService", + "Version": "v1", + "FailedRetryInterval": 300, + "FailedRetryCount": 10, + "CollectorCleaningInterval": 3600000 + }, + "PostgreSql": { + "TableNamePrefix": "admin", + "ConnectionString": "Host=127.0.0.1;Database=abp;Username=postgres;Password=123456" + }, + "RabbitMQ": { + "HostName": "localhost", + "Port": 5672, + "UserName": "admin", + "Password": "123456", + "ExchangeName": "LINGYUN.Abp.Application", + "VirtualHost": "/" + } + }, + "DistributedLock": { + "IsEnabled": true, + "Redis": { + "Configuration": "localhost,defaultDatabase=13" + } + }, + "Redis": { + "Configuration": "localhost,defaultDatabase=10", + "InstanceName": "LINGYUN.Abp.Application" + }, + "AuthServer": { + "Authority": "http://localhost:44385/", + "Audience": "admin-service", + "ValidAudiences": [ "lingyun-abp-application" ], + "MapInboundClaims": false, + "RequireHttpsMetadata": false, + "SwaggerClientId": "vue-oauth-client" + }, + "RemoteServices": { + "Platform": { + "BaseUrl": "http://localhost:30025", + "UseCurrentAccessToken": false + } + }, + "Logging": { + "Serilog": { + "Elasticsearch": { + "IndexFormat": "abp.dev.logging-{0:yyyy.MM.dd}" + } + } + }, + "AuditLogging": { + "Elasticsearch": { + "IndexPrefix": "abp.dev.auditing" + } + }, + "Elasticsearch": { + "NodeUris": "http://elasticsearch" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "System": "Warning", + "Microsoft": "Warning", + "DotNetCore": "Debug" + } + }, + "WriteTo": [ + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "Console", + "Args": { + "restrictedToMinimumLevel": "Debug", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "Elasticsearch", + "Args": { + "nodeUris": "http://elasticsearch", + "indexFormat": "abp.dev.logging-{0:yyyy.MM.dd}", + "autoRegisterTemplate": true, + "autoRegisterTemplateVersion": "ESv7" + } + } + ] + } + } + ] + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.json new file mode 100644 index 000000000..b8e4da13c --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.json @@ -0,0 +1,91 @@ +{ + "Clock": { + "Kind": "Local" + }, + "Forwarded": { + "ForwardedHeaders": "XForwardedFor,XForwardedProto" + }, + "StringEncryption": { + "DefaultPassPhrase": "s46c5q55nxpeS8Ra", + "InitVectorBytes": "s83ng0abvd02js84", + "DefaultSalt": "sf&5)s3#" + }, + "Json": { + "InputDateTimeFormats": [ + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-ddTHH:mm:ss" + ] + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "System": "Warning", + "Microsoft": "Warning", + "DotNetCore": "Information" + } + }, + "Enrich": [ "FromLogContext", "WithProcessId", "WithThreadId", "WithEnvironmentName", "WithMachineName", "WithApplicationName", "WithUniqueId" ], + "WriteTo": [ + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "Console", + "Args": { + "restrictedToMinimumLevel": "Debug", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Debug-.log", + "restrictedToMinimumLevel": "Debug", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Info-.log", + "restrictedToMinimumLevel": "Information", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Warn-.log", + "restrictedToMinimumLevel": "Warning", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Error-.log", + "restrictedToMinimumLevel": "Error", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Fatal-.log", + "restrictedToMinimumLevel": "Fatal", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + } + ] + } + } + ] + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json index 61baae0e6..34e4940a0 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json @@ -1,6 +1,21 @@ { "ReverseProxy": { "Routes": { + "ai-management-route": { + "ClusterId": "ai-management-cluster", + "Match": { + "Path": "/api/ai-management/{**everything}" + }, + "Transforms": [ + { + "HeaderPrefix": "X-Forwarded-", + "X-Forwarded": "Append" + }, + { + "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" + } + ] + }, "abp-route": { "ClusterId": "admin-service-cluster", "Match": { @@ -451,6 +466,16 @@ } }, "Clusters": { + "ai-management-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:30070", + "Metadata": { + "SwaggerEndpoint": "http://localhost:30070" + } + } + } + }, "auth-server-cluster": { "Destinations": { "destination1": { diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs index 48a47b899..def8a420f 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs @@ -215,6 +215,18 @@ builder.AddProject("WorkflowS .WaitFor(rabbitmq) .WaitFor(taskService); +// AIService +AddDotNetProject< + Projects.LINGYUN_Abp_MicroService_AIService_DbMigrator, + Projects.LINGYUN_Abp_MicroService_AIService>( + builder: builder, + servicePrefix: "AI", + serviceSuffix: "Service", + migratorSuffix: "Migrator", + port: 30070, + portName: "ai", + waitProject: localizationService); + // ApiGateway var apigateway = builder.AddProject("ApiGateway") .WithHttpEndpoint(port: 30000, name: "gateway") diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj index cccaa6637..807b9b7a0 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj @@ -17,6 +17,8 @@ + + From 4ffa047d7292700f63f3c93643833562e45cb177 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 4 Feb 2026 11:55:01 +0800 Subject: [PATCH 25/25] feat: Add kibana components --- aspnet-core/LINGYUN.MicroService.Aspire.slnx | 561 ++++++++++++++++++ .../AppHost.cs | 13 +- ...INGYUN.Abp.MicroService.TaskService.csproj | 1 + .../TaskServiceModule.cs | 2 + 4 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 aspnet-core/LINGYUN.MicroService.Aspire.slnx diff --git a/aspnet-core/LINGYUN.MicroService.Aspire.slnx b/aspnet-core/LINGYUN.MicroService.Aspire.slnx new file mode 100644 index 000000000..670b925f3 --- /dev/null +++ b/aspnet-core/LINGYUN.MicroService.Aspire.slnx @@ -0,0 +1,561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs index def8a420f..dadaf8d82 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs @@ -10,8 +10,17 @@ var redis = builder.AddRedis("redis") // Elasticsearch var elasticsearch = builder.AddElasticsearch("elasticsearch") .WithContainerName("elasticsearch") + .WithImageTag("8.17.3") .WithDataVolume("elasticsearch-dev") - .WithEnvironment("ES_JAVA_OPTS", "-Xms2g -Xmx2g"); + .WithEnvironment("ES_JAVA_OPTS", "-Xms2g -Xmx2g") + // see: https://www.funkysi1701.com/posts/2025/adding-elasticsearch-with-aspire/ + .WithEnvironment("xpack.security.enabled", "false"); + +// Kibana +builder.AddContainer("kibana", "kibana", "8.17.3") + .WithReference(elasticsearch) + .WithEndpoint(5601, 5601) + .WaitFor(elasticsearch); // Postgres var postgres = builder.AddPostgres("postgres") @@ -229,7 +238,7 @@ AddDotNetProject< // ApiGateway var apigateway = builder.AddProject("ApiGateway") - .WithHttpEndpoint(port: 30000, name: "gateway") + // .WithHttpEndpoint(port: 30000, name: "gateway") .WithExternalHttpEndpoints() .WithReference(redis, "Redis") .WithReference(elasticsearch, "Elasticsearch") diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj index 2a4b28ffb..ba3e066a1 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj @@ -55,6 +55,7 @@ + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs index 42e80af93..4933dbb3b 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs @@ -10,6 +10,7 @@ using LINGYUN.Abp.BackgroundTasks.Notifications; using LINGYUN.Abp.BackgroundTasks.Quartz; using LINGYUN.Abp.Claims.Mapping; using LINGYUN.Abp.Data.DbMigrator; +using LINGYUN.Abp.Elasticsearch.Jobs; using LINGYUN.Abp.Emailing.Platform; using LINGYUN.Abp.EventBus.CAP; using LINGYUN.Abp.ExceptionHandling.Emailing; @@ -54,6 +55,7 @@ namespace LINGYUN.Abp.MicroService.TaskService; typeof(AbpHttpClientIdentityModelWebModule), typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAspNetCoreMvcLocalizationModule), + typeof(AbpElasticsearchJobsModule), typeof(AbpBackgroundTasksJobsModule), typeof(AbpBackgroundTasksQuartzModule), typeof(AbpBackgroundTasksDistributedLockingModule),