diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolDefinition.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolDefinition.cs index 5e4bc9a7e..9c65df093 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolDefinition.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolDefinition.cs @@ -26,6 +26,10 @@ public class AIToolDefinition : IHasSimpleStateCheckers /// [NotNull] public Dictionary Properties { get; } + /// + /// 启用/禁用工具 + /// + public bool IsEnabled { get; set; } public List> StateCheckers { get; } @@ -38,6 +42,7 @@ public class AIToolDefinition : IHasSimpleStateCheckers Provider = provider; Description = description; + IsEnabled = true; Properties = new Dictionary(); StateCheckers = new List>(); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolFactory.cs index beb0b1199..73c2005ea 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolFactory.cs @@ -44,7 +44,7 @@ public class AIToolFactory : IAIToolFactory, IScopedDependency foreach (var toolDefine in toolDefines) { - if (await StateCheckerManager.IsEnabledAsync(toolDefine)) + if (toolDefine.IsEnabled && await StateCheckerManager.IsEnabledAsync(toolDefine)) { aiTools.AddRange(await CreateTool(toolDefine)); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AbpAIToolsOptions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AbpAIToolsOptions.cs index c2d4d872c..fa552fa29 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AbpAIToolsOptions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AbpAIToolsOptions.cs @@ -1,13 +1,16 @@ -using Volo.Abp.Collections; +using System.Collections.Generic; +using Volo.Abp.Collections; namespace LINGYUN.Abp.AI.Tools; public class AbpAIToolsOptions { public ITypeList DefinitionProviders { get; } public ITypeList AIToolProviders { get; } + public HashSet DeletedAITools { get; } public AbpAIToolsOptions() { DefinitionProviders = new TypeList(); AIToolProviders = new TypeList(); + DeletedAITools = new HashSet(); } } 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 813624bf7..390c80eb8 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 @@ -24,6 +24,7 @@ + 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 72ab61ffd..44505739a 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,10 +1,10 @@ -using System; - -namespace LINGYUN.Abp.AIManagement; +namespace LINGYUN.Abp.AIManagement; public class AIManagementOptions { public bool IsDynamicWorkspaceStoreEnabled { get; set; } public bool SaveStaticWorkspacesToDatabase { get; set; } + public bool IsDynamicAIToolStoreEnabled { get; set; } + public bool SaveStaticAIToolsToDatabase { 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 index 75ba502f1..968dd7f2e 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,6 +1,8 @@ using LINGYUN.Abp.AI.Agent; using LINGYUN.Abp.AI.Localization; +using LINGYUN.Abp.AI.Tools; using LINGYUN.Abp.AIManagement.Localization; +using LINGYUN.Abp.AIManagement.Tools; using LINGYUN.Abp.AIManagement.Workspaces; using Microsoft.Extensions.DependencyInjection; using System.Threading; @@ -20,6 +22,7 @@ namespace LINGYUN.Abp.AIManagement; [DependsOn( typeof(AbpAIManagementDomainSharedModule), typeof(AbpAIAgentModule), + typeof(AbpAIToolsModule), typeof(AbpCachingModule), typeof(AbpMapperlyModule), typeof(AbpDddDomainModule))] @@ -57,8 +60,13 @@ public class AbpAIManagementDomainModule : AbpModule public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { var rootServiceProvider = context.ServiceProvider.GetRequiredService(); - var initializer = rootServiceProvider.GetRequiredService(); - await initializer.InitializeAsync(true, _cancellationTokenSource.Token); + var workspaceDynamicInitializer = rootServiceProvider.GetRequiredService(); + var aIToolDynamicInitializer = rootServiceProvider.GetRequiredService(); + + await Task.WhenAll([ + workspaceDynamicInitializer.InitializeAsync(true, _cancellationTokenSource.Token), + aIToolDynamicInitializer.InitializeAsync(true, _cancellationTokenSource.Token) + ]); } public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionRecord.cs index 9a19c8223..4886c7e0b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionRecord.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionRecord.cs @@ -1,6 +1,7 @@ using JetBrains.Annotations; using System; using Volo.Abp; +using Volo.Abp.Data; using Volo.Abp.Domain.Entities.Auditing; namespace LINGYUN.Abp.AIManagement.Tools; @@ -23,12 +24,89 @@ public class AIToolDefinitionRecord : AuditedAggregateRoot } public AIToolDefinitionRecord( + [NotNull] Guid id, [NotNull] string name, [NotNull] string provider, - [CanBeNull] string? description = null) + [CanBeNull] string? description = null, + [CanBeNull] string? stateCheckers = null) + : base(id) { Name = Check.NotNullOrWhiteSpace(name, nameof(name), AIToolDefinitionRecordConsts.MaxNameLength); Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), AIToolDefinitionRecordConsts.MaxProviderLength); Description = Check.Length(description, nameof(description), AIToolDefinitionRecordConsts.MaxDescriptionLength); + StateCheckers = Check.Length(stateCheckers, nameof(stateCheckers), AIToolDefinitionRecordConsts.MaxStateCheckersLength); + } + + public bool HasSameData(AIToolDefinitionRecord otherAITool) + { + if (Name != otherAITool.Name) + { + return false; + } + + if (Provider != otherAITool.Provider) + { + return false; + } + + if (Description != otherAITool.Description) + { + return false; + } + + if (IsEnabled != otherAITool.IsEnabled) + { + return false; + } + + if (StateCheckers != otherAITool.StateCheckers) + { + return false; + } + + if (!this.HasSameExtraProperties(otherAITool)) + { + return false; + } + + return true; + } + + public void Patch(AIToolDefinitionRecord otherAITool) + { + if (Name != otherAITool.Name) + { + Name = otherAITool.Name; + } + + if (Provider != otherAITool.Provider) + { + Provider = otherAITool.Provider; + } + + if (Description != otherAITool.Description) + { + Description = otherAITool.Description; + } + + if (IsEnabled != otherAITool.IsEnabled) + { + IsEnabled = otherAITool.IsEnabled; + } + + if (StateCheckers != otherAITool.StateCheckers) + { + StateCheckers = otherAITool.StateCheckers; + } + + if (!this.HasSameExtraProperties(otherAITool)) + { + ExtraProperties.Clear(); + + foreach (var property in otherAITool.ExtraProperties) + { + ExtraProperties.Add(property.Key, property.Value); + } + } } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionSerializer.cs new file mode 100644 index 000000000..a53513d0c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionSerializer.cs @@ -0,0 +1,67 @@ +using LINGYUN.Abp.AI.Tools; +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.Tools; +public class AIToolDefinitionSerializer : IAIToolDefinitionSerializer, ITransientDependency +{ + protected IGuidGenerator GuidGenerator { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public AIToolDefinitionSerializer( + 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 aiToolDef in definitions) + { + records.Add(await SerializeAsync(aiToolDef)); + } + + return records.ToArray(); + } + + public virtual Task SerializeAsync(AIToolDefinition definition) + { + using (CultureHelper.Use(CultureInfo.InvariantCulture)) + { + var aiToolRecord = new AIToolDefinitionRecord( + GuidGenerator.Create(), + definition.Name, + definition.Provider, + definition.Description != null ? LocalizableStringSerializer.Serialize(definition.Description) : null, + SerializeStateCheckers(definition.StateCheckers)); + + + foreach (var property in definition.Properties) + { + aiToolRecord.SetProperty(property.Key, property.Value); + } + + aiToolRecord.IsEnabled = definition.IsEnabled; + aiToolRecord.IsSystem = true; + + return Task.FromResult(aiToolRecord); + } + } + + protected virtual string? SerializeStateCheckers(List> stateCheckers) + { + return StateCheckerSerializer.Serialize(stateCheckers); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDynamicInitializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDynamicInitializer.cs new file mode 100644 index 000000000..7367be7e1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/AIToolDynamicInitializer.cs @@ -0,0 +1,138 @@ +using LINGYUN.Abp.AI.Tools; +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.Tools; +public class AIToolDynamicInitializer : ITransientDependency +{ + public ILogger Logger { get; set; } + + protected IServiceProvider ServiceProvider { get; } + + public AIToolDynamicInitializer(IServiceProvider serviceProvider) + { + Logger = NullLogger.Instance; + + ServiceProvider = serviceProvider; + } + + public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) + { + var options = ServiceProvider.GetRequiredService>().Value; + + if (!options.SaveStaticAIToolsToDatabase && !options.IsDynamicAIToolStoreEnabled) + { + return Task.CompletedTask; + } + + if (runInBackground) + { + var applicationLifetime = ServiceProvider.GetService(); + 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 + { + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) + { + if (cancellationTokenProvider.Token.IsCancellationRequested) + { + return; + } + + await SaveStaticAIToolsToDatabaseAsync(options, cancellationToken); + + if (cancellationTokenProvider.Token.IsCancellationRequested) + { + return; + } + + await PreCacheDynamicAIToolsAsync(options); + } + } + catch + { + // No need to log here since inner calls log + } + } + + protected virtual async Task SaveStaticAIToolsToDatabaseAsync( + AIManagementOptions options, + CancellationToken cancellationToken) + { + if (!options.SaveStaticAIToolsToDatabase) + { + return; + } + + var staticAIToolSaver = ServiceProvider.GetRequiredService(); + + 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 staticAIToolSaver.SaveAsync(); + } + catch (Exception ex) + { + Logger.LogException(ex); + throw; // Polly will catch it + } + }, cancellationToken); + } + + protected virtual async Task PreCacheDynamicAIToolsAsync(AIManagementOptions options) + { + if (!options.IsDynamicAIToolStoreEnabled) + { + return; + } + + var dynamicAIToolDefinitionStore = ServiceProvider.GetRequiredService(); + + try + { + // Pre-cache AITools, so first request doesn't wait + await dynamicAIToolDefinitionStore.GetAllAsync(); + } + catch (Exception ex) + { + Logger.LogException(ex); + throw; // It will be cached in Initialize() + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStore.cs new file mode 100644 index 000000000..21b468b60 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStore.cs @@ -0,0 +1,160 @@ +using JetBrains.Annotations; +using LINGYUN.Abp.AI.Tools; +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.Tools; + +[Dependency(ReplaceServices = true)] +public class DynamicAIToolDefinitionStore : IDynamicAIToolDefinitionStore, ITransientDependency +{ + protected IAIToolDefinitionRecordRepository AIToolDefinitionRecordRepository { get; } + protected IAIToolDefinitionSerializer AIToolDefinitionSerializer { get; } + protected IDynamicAIToolDefinitionStoreInMemoryCache StoreCache { get; } + protected IDistributedCache DistributedCache { get; } + protected IAbpDistributedLock DistributedLock { get; } + public AIManagementOptions AIManagementOptions { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + + public DynamicAIToolDefinitionStore( + IAIToolDefinitionRecordRepository aiToolDefinitionRecordRepository, + IAIToolDefinitionSerializer aiToolDefinitionSerializer, + IDynamicAIToolDefinitionStoreInMemoryCache storeCache, + IDistributedCache distributedCache, + IOptions cacheOptions, + IOptions aiManagementOptions, + IAbpDistributedLock distributedLock) + { + AIToolDefinitionRecordRepository = aiToolDefinitionRecordRepository; + AIToolDefinitionSerializer = aiToolDefinitionSerializer; + StoreCache = storeCache; + DistributedCache = distributedCache; + DistributedLock = distributedLock; + AIManagementOptions = aiManagementOptions.Value; + CacheOptions = cacheOptions.Value; + } + + public async virtual Task> GetAllAsync() + { + if (!AIManagementOptions.IsDynamicAIToolStoreEnabled) + { + return Array.Empty(); + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetAITools(); + } + } + + public async virtual Task GetAsync([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + return await GetOrNullAsync(name) ?? throw new AbpException("Undefined AITool: " + name); + } + + public async virtual Task GetOrNullAsync([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + if (!AIManagementOptions.IsDynamicAIToolStoreEnabled) + { + return null; + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetAIToolOrNull(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 AIToolDefinitionRecordRepository.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 AITool 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}_AbpInMemoryAIToolCacheStamp"; + } + + protected virtual string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpAIToolUpdateLock"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStoreCacheInvalidator.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStoreCacheInvalidator.cs new file mode 100644 index 000000000..4f52cfe2e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStoreCacheInvalidator.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.EventBus; +using Volo.Abp.Threading; +using Volo.Abp.Timing; + +namespace LINGYUN.Abp.AIManagement.Tools; +public class DynamicAIToolDefinitionStoreCacheInvalidator : + ILocalEventHandler>, + ITransientDependency +{ + private readonly IDynamicAIToolDefinitionStoreInMemoryCache _storeCache; + + private readonly IClock _clock; + private readonly IDistributedCache _distributedCache; + private readonly AbpDistributedCacheOptions _cacheOptions; + + public DynamicAIToolDefinitionStoreCacheInvalidator( + IClock clock, + IDistributedCache distributedCache, + IDynamicAIToolDefinitionStoreInMemoryCache storeCache, + IOptions cacheOptions) + { + _storeCache = storeCache; + _clock = clock; + _distributedCache = distributedCache; + _cacheOptions = cacheOptions.Value; + } + + public async virtual Task HandleEventAsync(EntityChangedEventData eventData) + { + await RemoveStampInDistributedCacheAsync(); + } + + protected async virtual Task RemoveStampInDistributedCacheAsync() + { + using (await _storeCache.SyncSemaphore.LockAsync()) + { + var cacheKey = GetCommonStampCacheKey(); + + await _distributedCache.RemoveAsync(cacheKey); + + _storeCache.CacheStamp = Guid.NewGuid().ToString(); + _storeCache.LastCheckTime = _clock.Now.AddMinutes(-5); + } + } + + protected virtual string GetCommonStampCacheKey() + { + return $"{_cacheOptions.KeyPrefix}_AbpInMemoryAIToolCacheStamp"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStoreInMemoryCache.cs new file mode 100644 index 000000000..e4dbca17a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/DynamicAIToolDefinitionStoreInMemoryCache.cs @@ -0,0 +1,79 @@ +using LINGYUN.Abp.AI.Tools; +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.Tools; +public class DynamicAIToolDefinitionStoreInMemoryCache : IDynamicAIToolDefinitionStoreInMemoryCache, ISingletonDependency +{ + public string CacheStamp { get; set; } + protected IDictionary AIToolDefinitions { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public SemaphoreSlim SyncSemaphore { get; } = new(1, 1); + + public DateTime? LastCheckTime { get; set; } + + public DynamicAIToolDefinitionStoreInMemoryCache( + ISimpleStateCheckerSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + StateCheckerSerializer = stateCheckerSerializer; + LocalizableStringSerializer = localizableStringSerializer; + + AIToolDefinitions = new Dictionary(); + } + + public Task FillAsync(List tools) + { + AIToolDefinitions.Clear(); + + foreach (var tool in tools) + { + var toolDef = new AIToolDefinition( + tool.Name, + tool.Provider, + !tool.Description.IsNullOrWhiteSpace() ? LocalizableStringSerializer.Deserialize(tool.Description) : null); + + toolDef.IsEnabled = tool.IsEnabled; + + if (!tool.StateCheckers.IsNullOrWhiteSpace()) + { + var checkers = StateCheckerSerializer + .DeserializeArray( + tool.StateCheckers, + toolDef + ); + toolDef.StateCheckers.AddRange(checkers); + } + + foreach (var property in tool.ExtraProperties) + { + if (property.Value != null) + { + toolDef.WithProperty(property.Key, property.Value); + } + } + + AIToolDefinitions[tool.Name] = toolDef; + } + + return Task.CompletedTask; + } + + public AIToolDefinition? GetAIToolOrNull(string name) + { + return AIToolDefinitions.GetOrDefault(name); + } + + public IReadOnlyList GetAITools() + { + return AIToolDefinitions.Values.ToList(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IAIToolDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IAIToolDefinitionSerializer.cs new file mode 100644 index 000000000..c7c322951 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IAIToolDefinitionSerializer.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.AI.Tools; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Tools; +public interface IAIToolDefinitionSerializer +{ + Task SerializeAsync(IEnumerable definitions); + + Task SerializeAsync(AIToolDefinition definition); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IDynamicAIToolDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IDynamicAIToolDefinitionStoreInMemoryCache.cs new file mode 100644 index 000000000..108dd095b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IDynamicAIToolDefinitionStoreInMemoryCache.cs @@ -0,0 +1,21 @@ +using LINGYUN.Abp.AI.Tools; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Tools; +public interface IDynamicAIToolDefinitionStoreInMemoryCache +{ + string CacheStamp { get; set; } + + SemaphoreSlim SyncSemaphore { get; } + + DateTime? LastCheckTime { get; set; } + + Task FillAsync(List tools); + + AIToolDefinition? GetAIToolOrNull(string name); + + IReadOnlyList GetAITools(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IStaticAIToolSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IStaticAIToolSaver.cs new file mode 100644 index 000000000..afe42957a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/IStaticAIToolSaver.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Tools; +public interface IStaticAIToolSaver +{ + Task SaveAsync(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/StaticAIToolSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/StaticAIToolSaver.cs new file mode 100644 index 000000000..f05988d9c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tools/StaticAIToolSaver.cs @@ -0,0 +1,241 @@ +using LINGYUN.Abp.AI.Tools; +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.Tools; +public class StaticAIToolSaver : IStaticAIToolSaver, ITransientDependency +{ + protected IStaticAIToolDefinitionStore StaticStore { get; } + protected IAIToolDefinitionRecordRepository AIToolDefinitionRecordRepository { get; } + protected IAIToolDefinitionSerializer AIToolDefinitionSerializer { get; } + protected IDistributedCache Cache { get; } + protected IApplicationInfoAccessor ApplicationInfoAccessor { get; } + protected IAbpDistributedLock DistributedLock { get; } + protected AbpAIToolsOptions AIToolOptions { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected IGuidGenerator GuidGenerator { get; } + + public StaticAIToolSaver( + IStaticAIToolDefinitionStore staticStore, + IAIToolDefinitionRecordRepository aiToolDefinitionRecordRepository, + IAIToolDefinitionSerializer aiToolDefinitionSerializer, + IDistributedCache cache, + IOptions cacheOptions, + IApplicationInfoAccessor applicationInfoAccessor, + IAbpDistributedLock distributedLock, + IOptions aiToolOptions, + ICancellationTokenProvider cancellationTokenProvider, + IUnitOfWorkManager unitOfWorkManager, + IGuidGenerator guidGenerator) + { + StaticStore = staticStore; + AIToolDefinitionRecordRepository = aiToolDefinitionRecordRepository; + AIToolDefinitionSerializer = aiToolDefinitionSerializer; + Cache = cache; + ApplicationInfoAccessor = applicationInfoAccessor; + DistributedLock = distributedLock; + CancellationTokenProvider = cancellationTokenProvider; + AIToolOptions = aiToolOptions.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 aiTools = await AIToolDefinitionSerializer.SerializeAsync(await StaticStore.GetAllAsync()); + var currentHash = CalculateHash(aiTools, AIToolOptions.DeletedAITools); + + 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 AITool!"); + } + + using (var unitOfWork = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: true)) + { + try + { + var hasChangesInAITools = await UpdateChangedAIToolsAsync(aiTools); + + if (hasChangesInAITools) + { + 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 UpdateChangedAIToolsAsync(AIToolDefinitionRecord[] aiToolRecords) + { + var newRecords = new List(); + var changedRecords = new List(); + + var aiToolRecordsInDatabase = (await AIToolDefinitionRecordRepository.GetListAsync()).ToDictionary(x => x.Name); + + foreach (var record in aiToolRecords) + { + var aiToolRecordInDatabase = aiToolRecordsInDatabase.GetOrDefault(record.Name); + if (aiToolRecordInDatabase == null) + { + /* New group */ + newRecords.Add(record); + continue; + } + + if (record.HasSameData(aiToolRecordInDatabase)) + { + /* Not changed */ + continue; + } + + /* Changed */ + aiToolRecordInDatabase.Patch(record); + changedRecords.Add(aiToolRecordInDatabase); + } + + /* Deleted */ + var deletedRecords = new List(); + + if (AIToolOptions.DeletedAITools.Any()) + { + deletedRecords.AddRange(aiToolRecordsInDatabase.Values.Where(x => AIToolOptions.DeletedAITools.Contains(x.Name))); + } + + if (newRecords.Any()) + { + await AIToolDefinitionRecordRepository.InsertManyAsync(newRecords); + } + + if (changedRecords.Any()) + { + await AIToolDefinitionRecordRepository.UpdateManyAsync(changedRecords); + } + + if (deletedRecords.Any()) + { + await AIToolDefinitionRecordRepository.DeleteManyAsync(deletedRecords); + } + + return newRecords.Any() || changedRecords.Any() || deletedRecords.Any(); + } + + private string GetApplicationDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpAIToolUpdateLock"; + } + + private string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpAIToolUpdateLock"; + } + + private string GetApplicationHashCacheKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpAIToolsHash"; + } + + private string GetCommonStampCacheKey() + { + return $"{CacheOptions.KeyPrefix}_AbpInMemoryAIToolCacheStamp"; + } + + private string CalculateHash(AIToolDefinitionRecord[] aiToolRecords, IEnumerable deletedAITool) + { + var jsonSerializerOptions = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + new AbpIgnorePropertiesModifiers().CreateModifyAction(x => x.Id), + } + } + }; + + var stringBuilder = new StringBuilder(); + + stringBuilder.Append("AITools:"); + stringBuilder.AppendLine(JsonSerializer.Serialize(aiToolRecords, jsonSerializerOptions)); + + stringBuilder.Append("DeletedAITool:"); + stringBuilder.Append(deletedAITool.JoinAsString(",")); + + return stringBuilder + .ToString() + .ToMd5(); + } +} 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 ec3c6b82b..3d3f1a1b9 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 workspaces); WorkspaceDefinition? GetWorkspaceOrNull(string name);