30 changed files with 560 additions and 46 deletions
@ -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(); |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -1,6 +1,8 @@ |
|||||
{ |
{ |
||||
"culture": "en", |
"culture": "en", |
||||
"texts": { |
"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" |
||||
} |
} |
||||
} |
} |
||||
@ -1,6 +1,8 @@ |
|||||
{ |
{ |
||||
"culture": "zh-Hans", |
"culture": "zh-Hans", |
||||
"texts": { |
"texts": { |
||||
"Abp.AI:110001": "工作区不可用: {Workspace}!" |
"Abp.AI:110001": "工作区不可用: {Workspace}!", |
||||
|
"Abp.AI:110101": "对话已过期, 请重新创建会话!", |
||||
|
"NewConversation": "新对话" |
||||
} |
} |
||||
} |
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,5 @@ |
|||||
|
namespace LINGYUN.Abp.AIManagement.Chats; |
||||
|
public static class ConversationRecordConsts |
||||
|
{ |
||||
|
public static int MaxNameLength { get; set; } = 50; |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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…
Reference in new issue