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", |
|||
"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", |
|||
"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