diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs index 8bed08b41..8d139de9b 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs @@ -1,8 +1,16 @@ using LINGYUN.Abp.AI; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp; using Volo.Abp.Caching; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; using Volo.Abp.Domain; using Volo.Abp.Mapperly; using Volo.Abp.Modularity; +using Volo.Abp.Threading; namespace LINGYUN.Abp.AIManagement; @@ -14,4 +22,38 @@ namespace LINGYUN.Abp.AIManagement; typeof(AbpDddDomainModule))] public class AbpAIManagementDomainModule : AbpModule { + + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + + if (context.Services.IsDataMigrationEnvironment()) + { + Configure(options => + { + options.SaveStaticWorkspacesToDatabase = false; + options.IsDynamicWorkspaceStoreEnabled = false; + }); + } + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context)); + } + + public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var rootServiceProvider = context.ServiceProvider.GetRequiredService(); + var initializer = rootServiceProvider.GetRequiredService(); + await initializer.InitializeAsync(true, _cancellationTokenSource.Token); + } + + public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) + { + _cancellationTokenSource.Cancel(); + return Task.CompletedTask; + } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs new file mode 100644 index 000000000..6c7225b8f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStore.cs @@ -0,0 +1,161 @@ +using JetBrains.Annotations; +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Threading; + +namespace LINGYUN.Abp.AIManagement.Workspaces; + +[Dependency(ReplaceServices = true)] +public class DynamicWorkspaceDefinitionStore : IDynamicWorkspaceDefinitionStore, ITransientDependency +{ + protected IWorkspaceRepository WorkspaceRepository { get; } + protected IWorkspaceDefinitionSerializer WorkspaceDefinitionSerializer { get; } + protected IDynamicWorkspaceDefinitionStoreInMemoryCache StoreCache { get; } + protected IDistributedCache DistributedCache { get; } + protected IAbpDistributedLock DistributedLock { get; } + public AIManagementOptions AIManagementOptions { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + + public DynamicWorkspaceDefinitionStore( + IWorkspaceRepository workspaceRepository, + IWorkspaceDefinitionSerializer workspaceDefinitionSerializer, + IDynamicWorkspaceDefinitionStoreInMemoryCache storeCache, + IDistributedCache distributedCache, + IOptions cacheOptions, + IOptions aiManagementOptions, + IAbpDistributedLock distributedLock) + { + WorkspaceRepository = workspaceRepository; + WorkspaceDefinitionSerializer = workspaceDefinitionSerializer; + StoreCache = storeCache; + DistributedCache = distributedCache; + DistributedLock = distributedLock; + AIManagementOptions = aiManagementOptions.Value; + CacheOptions = cacheOptions.Value; + } + + public async virtual Task> GetAllAsync() + { + if (!AIManagementOptions.IsDynamicWorkspaceStoreEnabled) + { + return Array.Empty(); + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetWorkspaces(); + } + } + + public async virtual Task GetAsync([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + return await GetOrNullAsync(name) ?? throw new AbpException("Undefined workspace: " + name); + } + + public async virtual Task GetOrNullAsync([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + if (!AIManagementOptions.IsDynamicWorkspaceStoreEnabled) + { + return null; + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetWorkspaceOrNull(name); + } + } + + protected virtual async Task EnsureCacheIsUptoDateAsync() + { + if (StoreCache.LastCheckTime.HasValue && + DateTime.Now.Subtract(StoreCache.LastCheckTime.Value).TotalSeconds < 30) + { + return; + } + + var stampInDistributedCache = await GetOrSetStampInDistributedCache(); + + if (stampInDistributedCache == StoreCache.CacheStamp) + { + StoreCache.LastCheckTime = DateTime.Now; + return; + } + + await UpdateInMemoryStoreCache(); + + StoreCache.CacheStamp = stampInDistributedCache; + StoreCache.LastCheckTime = DateTime.Now; + } + + protected virtual async Task UpdateInMemoryStoreCache() + { + var workspaces = await WorkspaceRepository.GetListAsync(); + + await StoreCache.FillAsync(workspaces); + } + + protected virtual async Task GetOrSetStampInDistributedCache() + { + var cacheKey = GetCommonStampCacheKey(); + + var stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); + if (stampInDistributedCache != null) + { + return stampInDistributedCache; + } + + await using (var commonLockHandle = await DistributedLock + .TryAcquireAsync(GetCommonDistributedLockKey(), TimeSpan.FromMinutes(2))) + { + if (commonLockHandle == null) + { + throw new AbpException( + "Could not acquire distributed lock for workspace definition common stamp check!" + ); + } + + stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); + if (stampInDistributedCache != null) + { + return stampInDistributedCache; + } + + stampInDistributedCache = Guid.NewGuid().ToString(); + + await DistributedCache.SetStringAsync( + cacheKey, + stampInDistributedCache, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromDays(30) + } + ); + } + + return stampInDistributedCache; + } + + protected virtual string GetCommonStampCacheKey() + { + return $"{CacheOptions.KeyPrefix}_AbpInMemoryWorkspaceCacheStamp"; + } + + protected virtual string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpWorkspaceUpdateLock"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs new file mode 100644 index 000000000..43314dfed --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -0,0 +1,95 @@ +using LINGYUN.Abp.AI.Workspaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class DynamicWorkspaceDefinitionStoreInMemoryCache : IDynamicWorkspaceDefinitionStoreInMemoryCache, ISingletonDependency +{ + public string CacheStamp { get; set; } + protected IDictionary WorkspaceDefinitions { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public SemaphoreSlim SyncSemaphore { get; } = new(1, 1); + + public DateTime? LastCheckTime { get; set; } + + public DynamicWorkspaceDefinitionStoreInMemoryCache( + ISimpleStateCheckerSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + StateCheckerSerializer = stateCheckerSerializer; + LocalizableStringSerializer = localizableStringSerializer; + + WorkspaceDefinitions = new Dictionary(); + } + + public Task FillAsync(List workspaces) + { + WorkspaceDefinitions.Clear(); + + foreach (var workspace in workspaces) + { + var workspaceDef = new WorkspaceDefinition( + workspace.Name, + workspace.Provider, + workspace.ModelName, + LocalizableStringSerializer.Deserialize(workspace.DisplayName), + !workspace.Description.IsNullOrWhiteSpace() ? LocalizableStringSerializer.Deserialize(workspace.Description) : null, + workspace.SystemPrompt, + workspace.Instructions, + workspace.Temperature, + workspace.MaxOutputTokens, + workspace.FrequencyPenalty, + workspace.PresencePenalty); + + if (!workspace.ApiKey.IsNullOrWhiteSpace()) + { + workspaceDef.WithApiKey(workspace.ApiKey); + } + if (!workspace.ApiBaseUrl.IsNullOrWhiteSpace()) + { + workspaceDef.WithApiBaseUrl(workspace.ApiBaseUrl); + } + workspaceDef.IsEnabled = workspace.IsEnabled; + + if (!workspace.StateCheckers.IsNullOrWhiteSpace()) + { + var checkers = StateCheckerSerializer + .DeserializeArray( + workspace.StateCheckers, + workspaceDef + ); + workspaceDef.StateCheckers.AddRange(checkers); + } + + foreach (var property in workspace.ExtraProperties) + { + if (property.Value != null) + { + workspaceDef.WithProperty(property.Key, property.Value); + } + } + + WorkspaceDefinitions[workspace.Name] = workspaceDef; + } + + return Task.CompletedTask; + } + + public WorkspaceDefinition? GetWorkspaceOrNull(string name) + { + return WorkspaceDefinitions.GetOrDefault(name); + } + + public IReadOnlyList GetWorkspaces() + { + return WorkspaceDefinitions.Values.ToList(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs new file mode 100644 index 000000000..c2d2a4b12 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IDynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -0,0 +1,22 @@ +using LINGYUN.Abp.AI.Workspaces; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Workspaces; + +public interface IDynamicWorkspaceDefinitionStoreInMemoryCache +{ + string CacheStamp { get; set; } + + SemaphoreSlim SyncSemaphore { get; } + + DateTime? LastCheckTime { get; set; } + + Task FillAsync(List permissions); + + WorkspaceDefinition? GetWorkspaceOrNull(string name); + + IReadOnlyList GetWorkspaces(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IStaticWorkspaceSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IStaticWorkspaceSaver.cs new file mode 100644 index 000000000..32ba0a028 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IStaticWorkspaceSaver.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IStaticWorkspaceSaver +{ + Task SaveAsync(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs new file mode 100644 index 000000000..fd1106fcd --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionSerializer.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.AI.Workspaces; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IWorkspaceDefinitionSerializer +{ + Task SerializeAsync(IEnumerable definitions); + + Task SerializeAsync(WorkspaceDefinition definition); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs new file mode 100644 index 000000000..5405b0b0a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IWorkspaceRepository : IBasicRepository +{ + Task FindByNameAsync( + string name, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs new file mode 100644 index 000000000..df306125c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs @@ -0,0 +1,242 @@ +using LINGYUN.Abp.AI; +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Guids; +using Volo.Abp.Json.SystemTextJson.Modifiers; +using Volo.Abp.Threading; +using Volo.Abp.Uow; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class StaticWorkspaceSaver : IStaticWorkspaceSaver, ITransientDependency +{ + protected IStaticWorkspaceDefinitionStore StaticStore { get; } + protected IWorkspaceRepository WorkspaceRepository { get; } + protected IWorkspaceDefinitionSerializer WorkspaceSerializer { get; } + protected IDistributedCache Cache { get; } + protected IApplicationInfoAccessor ApplicationInfoAccessor { get; } + protected IAbpDistributedLock DistributedLock { get; } + protected AbpAICoreOptions AIOptions { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected IGuidGenerator GuidGenerator { get; } + + public StaticWorkspaceSaver( + IStaticWorkspaceDefinitionStore staticStore, + IWorkspaceRepository workspaceRepository, + IWorkspaceDefinitionSerializer workspaceSerializer, + IDistributedCache cache, + IOptions cacheOptions, + IApplicationInfoAccessor applicationInfoAccessor, + IAbpDistributedLock distributedLock, + IOptions settingOptions, + ICancellationTokenProvider cancellationTokenProvider, + IUnitOfWorkManager unitOfWorkManager, + IGuidGenerator guidGenerator) + { + StaticStore = staticStore; + WorkspaceRepository = workspaceRepository; + WorkspaceSerializer = workspaceSerializer; + Cache = cache; + ApplicationInfoAccessor = applicationInfoAccessor; + DistributedLock = distributedLock; + CancellationTokenProvider = cancellationTokenProvider; + AIOptions = settingOptions.Value; + CacheOptions = cacheOptions.Value; + UnitOfWorkManager = unitOfWorkManager; + GuidGenerator = guidGenerator; + } + + [UnitOfWork] + public async Task SaveAsync() + { + await using var applicationLockHandle = await DistributedLock.TryAcquireAsync( + GetApplicationDistributedLockKey() + ); + + if (applicationLockHandle == null) + { + return; + } + + var cacheKey = GetApplicationHashCacheKey(); + var cachedHash = await Cache.GetStringAsync(cacheKey, CancellationTokenProvider.Token); + + var workspaces = await WorkspaceSerializer.SerializeAsync(await StaticStore.GetAllAsync()); + var currentHash = CalculateHash(workspaces, AIOptions.DeletedWorkspaces); + + if (cachedHash == currentHash) + { + return; + } + + await using (var commonLockHandle = await DistributedLock.TryAcquireAsync( + GetCommonDistributedLockKey(), + TimeSpan.FromMinutes(5))) + { + if (commonLockHandle == null) + { + /* It will re-try */ + throw new AbpException("Could not acquire distributed lock for saving static Workspaces!"); + } + + using (var unitOfWork = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: true)) + { + try + { + var hasChangesInWorkspaces = await UpdateChangedWorkspacesAsync(workspaces); + + if (hasChangesInWorkspaces) + { + await Cache.SetStringAsync( + GetCommonStampCacheKey(), + Guid.NewGuid().ToString(), + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromDays(30) + }, + CancellationTokenProvider.Token + ); + } + } + catch + { + try + { + await unitOfWork.RollbackAsync(); + } + catch + { + /* ignored */ + } + + throw; + } + + await unitOfWork.CompleteAsync(); + } + } + + await Cache.SetStringAsync( + cacheKey, + currentHash, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromDays(30) + }, + CancellationTokenProvider.Token + ); + } + + private async Task UpdateChangedWorkspacesAsync(Workspace[] workspaces) + { + var newRecords = new List(); + var changedRecords = new List(); + + var workspaceRecordsInDatabase = (await WorkspaceRepository.GetListAsync()).ToDictionary(x => x.Name); + + foreach (var record in workspaces) + { + var workspaceRecordInDatabase = workspaceRecordsInDatabase.GetOrDefault(record.Name); + if (workspaceRecordInDatabase == null) + { + /* New group */ + newRecords.Add(record); + continue; + } + + if (record.HasSameData(workspaceRecordInDatabase)) + { + /* Not changed */ + continue; + } + + /* Changed */ + workspaceRecordInDatabase.Patch(record); + changedRecords.Add(workspaceRecordInDatabase); + } + + /* Deleted */ + var deletedRecords = new List(); + + if (AIOptions.DeletedWorkspaces.Any()) + { + deletedRecords.AddRange(workspaceRecordsInDatabase.Values.Where(x => AIOptions.DeletedWorkspaces.Contains(x.Name))); + } + + if (newRecords.Any()) + { + await WorkspaceRepository.InsertManyAsync(newRecords); + } + + if (changedRecords.Any()) + { + await WorkspaceRepository.UpdateManyAsync(changedRecords); + } + + if (deletedRecords.Any()) + { + await WorkspaceRepository.DeleteManyAsync(deletedRecords); + } + + return newRecords.Any() || changedRecords.Any() || deletedRecords.Any(); + } + + private string GetApplicationDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpWorkspaceUpdateLock"; + } + + private string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpWorkspaceUpdateLock"; + } + + private string GetApplicationHashCacheKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpWorkspacesHash"; + } + + private string GetCommonStampCacheKey() + { + return $"{CacheOptions.KeyPrefix}_AbpInMemoryWorkspaceCacheStamp"; + } + + private string CalculateHash(Workspace[] workspaces, IEnumerable deletedWorkspaces) + { + var jsonSerializerOptions = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + new AbpIgnorePropertiesModifiers().CreateModifyAction(x => x.Id), + } + } + }; + + var stringBuilder = new StringBuilder(); + + stringBuilder.Append("Workspaces:"); + stringBuilder.AppendLine(JsonSerializer.Serialize(workspaces, jsonSerializerOptions)); + + stringBuilder.Append("DeletedWorkspace:"); + stringBuilder.Append(deletedWorkspaces.JoinAsString(",")); + + return stringBuilder + .ToString() + .ToMd5(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs new file mode 100644 index 000000000..bb09b2f0f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/Workspace.cs @@ -0,0 +1,255 @@ +using System; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Auditing; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class Workspace : AuditedAggregateRoot +{ + public string Name { get; private set; } + + public string Provider { get; private set; } + + public string ModelName { get; private set; } + + public string DisplayName { get; private set; } + + public string? Description { get; private set; } + + public string? ApiKey { get; private set; } + + public string? ApiBaseUrl { get; private set; } + + public string? SystemPrompt { get; set; } + + public string? Instructions { get; set; } + + public float? Temperature { get; set; } + + public int? MaxOutputTokens { get; set; } + + public float? FrequencyPenalty { get; set; } + + public float? PresencePenalty { get; set; } + + public bool IsEnabled { get; set; } + + public string? StateCheckers { get; set; } + + protected Workspace() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public Workspace( + Guid id, + string name, + string provider, + string modelName, + string displayName, + string? description = null, + string? apiKey = null, + string? apiBaseUrl = null, + string? systemPrompt = null, + string? instructions = null, + float? temperature = null, + int? maxOutputTokens = null, + float? frequencyPenalty = null, + float? presencePenalty = null, + string? stateCheckers = null) + : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), WorkspaceConsts.MaxNameLength); + Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), WorkspaceConsts.MaxProviderLength); + ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceConsts.MaxModelNameLength); + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceConsts.MaxDisplayNameLength); + Description = Check.Length(description, nameof(description), WorkspaceConsts.MaxDescriptionLength); + ApiKey = Check.Length(apiKey, nameof(apiKey), WorkspaceConsts.MaxApiKeyLength); + ApiBaseUrl = Check.Length(apiBaseUrl, nameof(apiBaseUrl), WorkspaceConsts.MaxApiBaseUrlLength); + SystemPrompt = Check.Length(systemPrompt, nameof(systemPrompt), WorkspaceConsts.MaxSystemPromptLength); + Instructions = Check.Length(instructions, nameof(instructions), WorkspaceConsts.MaxInstructionsLength); + StateCheckers = Check.Length(stateCheckers, nameof(stateCheckers), WorkspaceConsts.MaxStateCheckersLength); + Temperature = temperature; + MaxOutputTokens = maxOutputTokens; + FrequencyPenalty = frequencyPenalty; + PresencePenalty = presencePenalty; + + IsEnabled = true; + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public bool HasSameData(Workspace otherWorkspace) + { + if (Name != otherWorkspace.Name) + { + return false; + } + + if (Provider != otherWorkspace.Provider) + { + return false; + } + + if (ModelName != otherWorkspace.ModelName) + { + return false; + } + + if (DisplayName != otherWorkspace.DisplayName) + { + return false; + } + + if (Description != otherWorkspace.Description) + { + return false; + } + + if (ApiKey != otherWorkspace.ApiKey) + { + return false; + } + + if (ApiBaseUrl != otherWorkspace.ApiBaseUrl) + { + return false; + } + + if (SystemPrompt != otherWorkspace.SystemPrompt) + { + return false; + } + + if (Instructions != otherWorkspace.Instructions) + { + return false; + } + + if (IsEnabled != otherWorkspace.IsEnabled) + { + return false; + } + + if (Temperature != otherWorkspace.Temperature) + { + return false; + } + + if (MaxOutputTokens != otherWorkspace.MaxOutputTokens) + { + return false; + } + + if (FrequencyPenalty != otherWorkspace.FrequencyPenalty) + { + return false; + } + + if (PresencePenalty != otherWorkspace.PresencePenalty) + { + return false; + } + + if (StateCheckers != otherWorkspace.StateCheckers) + { + return false; + } + + if (!this.HasSameExtraProperties(otherWorkspace)) + { + return false; + } + + return true; + } + + public void Patch(Workspace otherWorkspace) + { + if (Name != otherWorkspace.Name) + { + Name = otherWorkspace.Name; + } + + if (Provider != otherWorkspace.Provider) + { + Provider = otherWorkspace.Provider; + } + + if (ModelName != otherWorkspace.ModelName) + { + ModelName = otherWorkspace.ModelName; + } + + if (DisplayName != otherWorkspace.DisplayName) + { + DisplayName = otherWorkspace.DisplayName; + } + + if (Description != otherWorkspace.Description) + { + Description = otherWorkspace.Description; + } + + if (ApiKey != otherWorkspace.ApiKey) + { + ApiKey = otherWorkspace.ApiKey; + } + + if (ApiBaseUrl != otherWorkspace.ApiBaseUrl) + { + ApiBaseUrl = otherWorkspace.ApiBaseUrl; + } + + if (SystemPrompt != otherWorkspace.SystemPrompt) + { + SystemPrompt = otherWorkspace.SystemPrompt; + } + + if (Instructions != otherWorkspace.Instructions) + { + Instructions = otherWorkspace.Instructions; + } + + if (IsEnabled != otherWorkspace.IsEnabled) + { + IsEnabled = otherWorkspace.IsEnabled; + } + + if (Temperature != otherWorkspace.Temperature) + { + Temperature = otherWorkspace.Temperature; + } + + if (MaxOutputTokens != otherWorkspace.MaxOutputTokens) + { + MaxOutputTokens = otherWorkspace.MaxOutputTokens; + } + + if (FrequencyPenalty != otherWorkspace.FrequencyPenalty) + { + FrequencyPenalty = otherWorkspace.FrequencyPenalty; + } + + if (PresencePenalty != otherWorkspace.PresencePenalty) + { + PresencePenalty = otherWorkspace.PresencePenalty; + } + + if (StateCheckers != otherWorkspace.StateCheckers) + { + StateCheckers = otherWorkspace.StateCheckers; + } + + if (!this.HasSameExtraProperties(otherWorkspace)) + { + ExtraProperties.Clear(); + + foreach (var property in otherWorkspace.ExtraProperties) + { + ExtraProperties.Add(property.Key, property.Value); + } + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs new file mode 100644 index 000000000..351ffa6ca --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs @@ -0,0 +1,72 @@ +using LINGYUN.Abp.AI.Workspaces; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.Localization; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITransientDependency +{ + protected IGuidGenerator GuidGenerator { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public WorkspaceDefinitionSerializer( + IGuidGenerator guidGenerator, + ISimpleStateCheckerSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + GuidGenerator = guidGenerator; + StateCheckerSerializer = stateCheckerSerializer; + LocalizableStringSerializer = localizableStringSerializer; + } + + public async virtual Task SerializeAsync(IEnumerable definitions) + { + var records = new List(); + foreach (var workspaceDef in definitions) + { + records.Add(await SerializeAsync(workspaceDef)); + } + + return records.ToArray(); + } + + public virtual Task SerializeAsync(WorkspaceDefinition definition) + { + using (CultureHelper.Use(CultureInfo.InvariantCulture)) + { + var workspace = new Workspace( + GuidGenerator.Create(), + definition.Name, + definition.Provider, + definition.ModelName, + LocalizableStringSerializer.Serialize(definition.DisplayName)!, + definition.Description != null ? LocalizableStringSerializer.Serialize(definition.Description) : null, + definition.ApiKey, + definition.ApiBaseUrl, + definition.SystemPrompt, + definition.Instructions, + definition.Temperature, + definition.MaxOutputTokens, + definition.FrequencyPenalty, + definition.PresencePenalty, + SerializeStateCheckers(definition.StateCheckers)); + + foreach (var property in definition.Properties) + { + workspace.SetProperty(property.Key, property.Value); + } + + return Task.FromResult(workspace); + } + } + protected virtual string? SerializeStateCheckers(List> stateCheckers) + { + return StateCheckerSerializer.Serialize(stateCheckers); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs new file mode 100644 index 000000000..591535580 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs @@ -0,0 +1,149 @@ +using JetBrains.Annotations; +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Polly; +using System; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDynamicInitializer : ITransientDependency +{ + public ILogger Logger { get; set; } + + protected IServiceProvider ServiceProvider { get; } + protected IOptions Options { get; } + [CanBeNull] + protected IHostApplicationLifetime ApplicationLifetime { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IDynamicWorkspaceDefinitionStore DynamicWorkspaceDefinitionStore { get; } + protected IStaticWorkspaceSaver StaticWorkspaceSaver { get; } + + public WorkspaceDynamicInitializer( + IServiceProvider serviceProvider, + IOptions options, + ICancellationTokenProvider cancellationTokenProvider, + IDynamicWorkspaceDefinitionStore dynamicWorkspaceDefinitionStore, + IStaticWorkspaceSaver staticWorkspaceSaver) + { + Logger = NullLogger.Instance; + + ServiceProvider = serviceProvider; + Options = options; + CancellationTokenProvider = cancellationTokenProvider; + DynamicWorkspaceDefinitionStore = dynamicWorkspaceDefinitionStore; + StaticWorkspaceSaver = staticWorkspaceSaver; + ApplicationLifetime = ServiceProvider.GetRequiredService(); + } + + public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) + { + var options = Options.Value; + + if (!options.SaveStaticWorkspacesToDatabase && !options.IsDynamicWorkspaceStoreEnabled) + { + return Task.CompletedTask; + } + + if (runInBackground) + { + Task.Run(async () => + { + if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + { + cancellationToken = ApplicationLifetime.ApplicationStopping; + } + await ExecuteInitializationAsync(options, cancellationToken); + }, cancellationToken); + + return Task.CompletedTask; + } + + return ExecuteInitializationAsync(options, cancellationToken); + } + + protected virtual async Task ExecuteInitializationAsync(AIManagementOptions options, CancellationToken cancellationToken) + { + try + { + using (CancellationTokenProvider.Use(cancellationToken)) + { + if (CancellationTokenProvider.Token.IsCancellationRequested) + { + return; + } + + await SaveStaticWorkspacesToDatabaseAsync(options, cancellationToken); + + if (CancellationTokenProvider.Token.IsCancellationRequested) + { + return; + } + + await PreCacheDynamicWorkspacesAsync(options); + } + } + catch + { + // No need to log here since inner calls log + } + } + + protected virtual async Task SaveStaticWorkspacesToDatabaseAsync( + AIManagementOptions options, + CancellationToken cancellationToken) + { + if (!options.SaveStaticWorkspacesToDatabase) + { + return; + } + + await Policy + .Handle(ex => ex is not OperationCanceledException) + .WaitAndRetryAsync( + 8, + retryAttempt => TimeSpan.FromSeconds( + Volo.Abp.RandomHelper.GetRandom( + (int)Math.Pow(2, retryAttempt) * 8, + (int)Math.Pow(2, retryAttempt) * 12) + ) + ) + .ExecuteAsync(async _ => + { + try + { + await StaticWorkspaceSaver.SaveAsync(); + } + catch (Exception ex) + { + Logger.LogException(ex); + throw; // Polly will catch it + } + }, cancellationToken); + } + + protected virtual async Task PreCacheDynamicWorkspacesAsync(AIManagementOptions options) + { + if (!options.IsDynamicWorkspaceStoreEnabled) + { + return; + } + + try + { + // Pre-cache Workspaces, so first request doesn't wait + await DynamicWorkspaceDefinitionStore.GetAllAsync(); + } + catch (Exception ex) + { + Logger.LogException(ex); + throw; // It will be cached in Initialize() + } + } +}