Browse Source

feat: Add persistent conversation

pull/1421/head
colin 1 week ago
parent
commit
d64571d18c
  1. 57
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs
  2. 1
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj
  3. 4
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs
  4. 4
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs
  5. 2
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs
  6. 13
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs
  7. 18
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs
  8. 48
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs
  9. 4
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json
  10. 4
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json
  11. 4
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs
  12. 28
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs
  13. 15
      aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs
  14. 30
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd
  15. 30
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd
  16. 5
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs
  17. 8
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs
  18. 8
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs
  19. 33
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs
  20. 15
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs
  21. 34
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs
  22. 95
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs
  23. 21
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs
  24. 5
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs
  25. 10
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs
  26. 1
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs
  27. 44
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs
  28. 5
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs
  29. 30
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd
  30. 30
      aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd

57
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<AbpAIResource> _localizerResource;
public AgentService(
IClock clock,
IGuidGenerator guidGenerator,
IAgentFactory agentFactory,
ITokenUsageStore tokenUsageStore,
IChatMessageStore chatMessageStore)
IChatMessageStore chatMessageStore,
IConversationStore conversationStore,
IStringLocalizer<AbpAIResource> localizerResource)
{
_clock = clock;
_guidGenerator = guidGenerator;
_agentFactory = agentFactory;
_tokenUsageStore = tokenUsageStore;
_chatMessageStore = chatMessageStore;
_conversationStore = conversationStore;
_localizerResource = localizerResource;
}
public async virtual IAsyncEnumerable<string> 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<string> StoreChatMessage(Models.ChatMessage message, string agentMessage)
protected async virtual Task<Guid> StoreChatMessage(Models.ChatMessage message, string agentMessage)
{
message.WithReply(agentMessage, _clock.Now);
return await _chatMessageStore.SaveMessageAsync(message);
}
protected async virtual Task<Guid> 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);

1
aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj

@ -22,6 +22,7 @@
<ItemGroup>
<PackageReference Include="Volo.Abp.AI" />
<PackageReference Include="Volo.Abp.Localization" />
<PackageReference Include="Volo.Abp.Guids" />
<PackageReference Include="Volo.Abp.Timing" />
</ItemGroup>

4
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
{

4
aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs

@ -6,4 +6,8 @@ public static class AbpAIErrorCodes
/// 工作区不可用: {Workspace}!
/// </summary>
public const string WorkspaceIsNotEnabled = Namespace + ":110001";
/// <summary>
/// 对话已过期, 请重新创建会话!
/// </summary>
public const string ConversationHasExpired = Namespace + ":110101";
}

2
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<string> SaveMessageAsync(ChatMessage message);
Task<Guid> SaveMessageAsync(ChatMessage message);
Task<IEnumerable<ChatMessage>> GetHistoryMessagesAsync(Guid conversationId);
}

13
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<Conversation?> FindAsync(Guid conversationId);
Task CleanupAsync();
}

18
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<string, List<ChatMessage>> _userMessageCache = new ConcurrentDictionary<string, List<ChatMessage>>();
private static readonly ConcurrentDictionary<Guid, List<ChatMessage>> _userMessageCache = new ConcurrentDictionary<Guid, List<ChatMessage>>();
public Task<IEnumerable<ChatMessage>> GetHistoryMessagesAsync(Guid conversationId)
{
@ -30,23 +30,23 @@ public class InMemoryChatMessageStore : IChatMessageStore
.OrderBy(x => x.CreatedAt));
}
public Task<string> SaveMessageAsync(ChatMessage message)
public Task<Guid> 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<ChatMessage>() { message };
_userMessageCache[messageId.Value] = new List<ChatMessage>() { message };
}
return Task.FromResult(messageId);
return Task.FromResult(messageId.Value);
}
}

48
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<Guid, Conversation> _conversationCache = new ConcurrentDictionary<Guid, Conversation>();
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<Conversation?> FindAsync(Guid conversationId)
{
_conversationCache.TryGetValue(conversationId, out var conversation);
return Task.FromResult<Conversation?>(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;
}
}

4
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"
}
}

4
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": "新对话"
}
}

4
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;

28
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;
}
}

15
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();

30
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

30
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

5
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;
}

8
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<UserTextMessageRecord, TextChatMessage>
public partial class TextChatMessageRecordToUserTextMessageMapper : MapperBase<TextChatMessageRecord, TextChatMessage>
{
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);
}

8
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs

@ -15,6 +15,8 @@ public abstract class ChatMessageRecord : AuditedAggregateRoot<Guid>, 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<Guid>, IMultiTena
TenantId = tenantId;
}
public virtual ChatMessageRecord SetUserId(Guid userId)
{
UserId = userId;
return this;
}
public virtual ChatMessageRecord SetConversationId(Guid conversationId)
{
ConversationId = conversationId;

33
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<TextChatMessageRecord>, IEnumerable<TextChatMessage>>(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<string> SaveMessageAsync(ChatMessage message)
public async virtual Task<Guid> 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())
{

15
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);
}
}

34
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<Guid>, 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;
}
}

95
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<ConversationCleanupOptions> 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<ConversationRecord>(
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<Conversation?> 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);
}
}
}

21
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<ConversationRecord, Guid>
{
Task<int> GetCountAsync(
ISpecification<ConversationRecord> specification,
CancellationToken cancellationToken = default);
Task<List<ConversationRecord>> GetListAsync(
ISpecification<ConversationRecord> specification,
string? sorting = $"{nameof(ConversationRecord.CreatedAt)} DESC",
int maxResultCount = 10,
int skipCount = 0,
CancellationToken cancellationToken = default);
}

5
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<AIManagementDbContext>, IAIManagementDbContext
{
public DbSet<WorkspaceDefinitionRecord> WorkspaceDefinitions { get; set; }
public DbSet<UserTextMessageRecord> UserTextMessageRecords { get; set; }
public DbSet<TextChatMessageRecord> TextChatMessageRecords { get; set; }
public DbSet<ConversationRecord> ConversationRecords { get; set; }
public AIManagementDbContext(
DbContextOptions<AIManagementDbContext> options) : base(options)
{

10
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<ConversationRecord>(b =>
{
b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "Conversations", AbpAIManagementDbProperties.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name)
.HasMaxLength(ConversationRecordConsts.MaxNameLength)
.IsRequired();
});
builder.Entity<TextChatMessageRecord>(b =>
{
b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "TextChatMessages", AbpAIManagementDbProperties.DbSchema);

1
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs

@ -17,6 +17,7 @@ public class AbpAIManagementEntityFrameworkCoreModule : AbpModule
{
options.AddDefaultRepositories<IAIManagementDbContext>();
options.AddRepository<ConversationRecord, EfCoreConversationRecordRepository>();
options.AddRepository<TextChatMessageRecord, EfCoreTextChatMessageRecordRepository>();
options.AddRepository<WorkspaceDefinitionRecord, EfCoreWorkspaceDefinitionRecordRepository>();

44
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<IAIManagementDbContext, ConversationRecord, Guid>, IConversationRecordRepository
{
public EfCoreConversationRecordRepository(
IDbContextProvider<IAIManagementDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async virtual Task<int> GetCountAsync(
ISpecification<ConversationRecord> specification,
CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync())
.Where(specification.ToExpression())
.CountAsync(GetCancellationToken(cancellationToken));
}
public async virtual Task<List<ConversationRecord>> GetListAsync(
ISpecification<ConversationRecord> 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));
}
}

5
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<WorkspaceDefinitionRecord> WorkspaceDefinitions { get; }
DbSet<UserTextMessageRecord> UserTextMessageRecords { get; }
DbSet<TextChatMessageRecord> TextChatMessageRecords { get; }
DbSet<ConversationRecord> ConversationRecords { get; }
}

30
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

30
aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
Loading…
Cancel
Save