diff --git a/aspnet-core/LINGYUN.MicroService.Aspire.slnx b/aspnet-core/LINGYUN.MicroService.Aspire.slnx new file mode 100644 index 000000000..670b925f3 --- /dev/null +++ b/aspnet-core/LINGYUN.MicroService.Aspire.slnx @@ -0,0 +1,561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorHostedService.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorHostedService.cs new file mode 100644 index 000000000..2590ba079 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorHostedService.cs @@ -0,0 +1,53 @@ +using LINGYUN.Abp.MicroService.AIService.DbMigrator; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Data; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDbMigratorHostedService : IHostedService +{ + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly IConfiguration _configuration; + + public AIServiceDbMigratorHostedService( + IHostApplicationLifetime hostApplicationLifetime, + IConfiguration configuration) + { + _hostApplicationLifetime = hostApplicationLifetime; + _configuration = configuration; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var application = await AbpApplicationFactory + .CreateAsync(options => + { + options.Services.ReplaceConfiguration(_configuration); + options.UseAutofac(); + options.Services.AddLogging(c => c.AddSerilog()); + options.AddDataMigrationEnvironment(); + }); + + await application.InitializeAsync(); + + await application + .ServiceProvider + .GetRequiredService() + .CheckAndApplyDatabaseMigrationsAsync(); + + await application.ShutdownAsync(); + + _hostApplicationLifetime.StopApplication(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorModule.cs new file mode 100644 index 000000000..ff9dd1c85 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/AIServiceDbMigratorModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.MicroService.AIService.DbMigrator; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AIServiceMigrationsEntityFrameworkCoreModule) + )] +public class AIServiceDbMigratorModule : AbpModule +{ + +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/FodyWeavers.xml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/LINGYUN.Abp.MicroService.AIService.DbMigrator.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/LINGYUN.Abp.MicroService.AIService.DbMigrator.csproj new file mode 100644 index 000000000..662765cf3 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/LINGYUN.Abp.MicroService.AIService.DbMigrator.csproj @@ -0,0 +1,40 @@ + + + + + + Exe + net10.0 + enable + false + LINGYUN.Abp.MicroService.AIService + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + Always + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/Program.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/Program.cs new file mode 100644 index 000000000..135bb215b --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/Program.cs @@ -0,0 +1,43 @@ +using LINGYUN.Abp.MicroService.AIService; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using System; + +var defaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}"; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) +#if DEBUG + .MinimumLevel.Override("LINGYUN.Abp.MicroService.AIService", LogEventLevel.Debug) +#else + .MinimumLevel.Override("LINGYUN.Abp.MicroService.AIService", LogEventLevel.Information) +#endif + .Enrich.FromLogContext() + .WriteTo.Async(x => x.Console(outputTemplate: defaultOutputTemplate)) + .WriteTo.Async(x => x.File("Logs/migrations.txt", outputTemplate: defaultOutputTemplate)) + .CreateLogger(); + +try +{ + var builder = Host.CreateDefaultBuilder(args) + .AddAppSettingsSecretsJson() + .ConfigureLogging((context, logging) => logging.ClearProviders()) + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); + }); + await builder.RunConsoleAsync(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly!"); +} +finally +{ + await Log.CloseAndFlushAsync(); +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/appsettings.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/appsettings.json new file mode 100644 index 000000000..38212c579 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.DbMigrator/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "Default": "Host=127.0.0.1;Database=abp;Username=postgres;Password=123456" + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDataSeeder.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDataSeeder.cs new file mode 100644 index 000000000..8a088b76d --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDataSeeder.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDataSeeder : ITransientDependency +{ + protected ILogger Logger { get; } + protected ICurrentTenant CurrentTenant { get; } + + public AIServiceDataSeeder( + ICurrentTenant currentTenant) + { + CurrentTenant = currentTenant; + + Logger = NullLogger.Instance; + } + + public virtual Task SeedAsync(DataSeedContext context) + { + return Task.CompletedTask; + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationEventHandler.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationEventHandler.cs new file mode 100644 index 000000000..b05842e54 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationEventHandler.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DistributedLocking; +using Volo.Abp.EntityFrameworkCore.Migrations; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDbMigrationEventHandler : EfCoreDatabaseMigrationEventHandlerBase +{ + protected AIServiceDataSeeder DataSeeder { get; } + + public AIServiceDbMigrationEventHandler( + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + ITenantStore tenantStore, + IAbpDistributedLock abpDistributedLock, + IDistributedEventBus distributedEventBus, + ILoggerFactory loggerFactory, + AIServiceDataSeeder dataSeeder) + : base( + ConnectionStringNameAttribute.GetConnStringName(), + currentTenant, unitOfWorkManager, tenantStore, abpDistributedLock, distributedEventBus, loggerFactory) + { + DataSeeder = dataSeeder; + } + + protected async override Task AfterTenantCreated(TenantCreatedEto eventData, bool schemaMigrated) + { + // 新租户数据种子 + var context = new DataSeedContext(eventData.Id); + if (eventData.Properties != null) + { + foreach (var property in eventData.Properties) + { + context.WithProperty(property.Key, property.Value); + } + } + + await DataSeeder.SeedAsync(context); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationService.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationService.cs new file mode 100644 index 000000000..445f0fd92 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceDbMigrationService.cs @@ -0,0 +1,36 @@ +using LINGYUN.Abp.Data.DbMigrator; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceDbMigrationService : EfCoreRuntimeDbMigratorBase, ITransientDependency +{ + protected AIServiceDataSeeder DataSeeder { get; } + public AIServiceDbMigrationService( + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + IServiceProvider serviceProvider, + IAbpDistributedLock abpDistributedLock, + IDistributedEventBus distributedEventBus, + ILoggerFactory loggerFactory, + AIServiceDataSeeder dataSeeder) + : base( + ConnectionStringNameAttribute.GetConnStringName(), + unitOfWorkManager, serviceProvider, currentTenant, abpDistributedLock, distributedEventBus, loggerFactory) + { + DataSeeder = dataSeeder; + } + + protected async override Task SeedAsync() + { + // DbMigrator迁移数据种子 + await DataSeeder.SeedAsync(new DataSeedContext()); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContext.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContext.cs new file mode 100644 index 000000000..35d068464 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContext.cs @@ -0,0 +1,32 @@ +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.EntityFrameworkCore; +using LINGYUN.Abp.AIManagement.Tokens; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.MicroService.AIService; + +[ConnectionStringName("Default")] +public class AIServiceMigrationsDbContext : + AbpDbContext, + IAIManagementDbContext +{ + public DbSet WorkspaceDefinitions { get; set; } + public DbSet TextChatMessageRecords { get; set; } + public DbSet ConversationRecords { get; set; } + public DbSet TokenUsageRecords { get; set; } + + public AIServiceMigrationsDbContext( + DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigureAIManagement(); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContextFactory.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContextFactory.cs new file mode 100644 index 000000000..afd229619 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsDbContextFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using System.IO; + +namespace LINGYUN.Abp.MicroService.AIService; +public class AIServiceMigrationsDbContextFactory : IDesignTimeDbContextFactory +{ + public AIServiceMigrationsDbContext CreateDbContext(string[] args) + { + var configuration = BuildConfiguration(); + var connectionString = configuration.GetConnectionString("Default"); + + var builder = new DbContextOptionsBuilder() + .UseNpgsql(connectionString); + + return new AIServiceMigrationsDbContext(builder!.Options); + } + + private static IConfigurationRoot BuildConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../LINGYUN.Abp.MicroService.AIService.DbMigrator/")) + .AddJsonFile("appsettings.json", optional: false); + + return builder.Build(); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsEntityFrameworkCoreModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsEntityFrameworkCoreModule.cs new file mode 100644 index 000000000..1c84f02f9 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/AIServiceMigrationsEntityFrameworkCoreModule.cs @@ -0,0 +1,45 @@ +using LINGYUN.Abp.AIManagement.EntityFrameworkCore; +using LINGYUN.Abp.Data.DbMigrator; +using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore; +using LINGYUN.Abp.Saas.EntityFrameworkCore; +using LINGYUN.Abp.TextTemplating.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.PostgreSql; +using Volo.Abp.FeatureManagement.EntityFrameworkCore; +using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.EntityFrameworkCore; +using Volo.Abp.SettingManagement.EntityFrameworkCore; + +namespace LINGYUN.Abp.MicroService.AIService; + +[DependsOn( + typeof(AbpAIManagementEntityFrameworkCoreModule), + typeof(AbpSaasEntityFrameworkCoreModule), + typeof(AbpSettingManagementEntityFrameworkCoreModule), + typeof(AbpPermissionManagementEntityFrameworkCoreModule), + typeof(AbpFeatureManagementEntityFrameworkCoreModule), + typeof(AbpLocalizationManagementEntityFrameworkCoreModule), + typeof(AbpTextTemplatingEntityFrameworkCoreModule), + typeof(AbpEntityFrameworkCorePostgreSqlModule), + typeof(AbpDataDbMigratorModule) + )] +public class AIServiceMigrationsEntityFrameworkCoreModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + // https://www.npgsql.org/efcore/release-notes/6.0.html#opting-out-of-the-new-timestamp-mapping-logic + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(); + + Configure(options => + { + options.UseNpgsql(); + }); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xsd b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore.csproj new file mode 100644 index 000000000..c85236273 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore.csproj @@ -0,0 +1,35 @@ + + + + + + + false + net10.0 + enable + LINGYUN.Abp.MicroService.AIService + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.Designer.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.Designer.cs new file mode 100644 index 000000000..3b253c2ca --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.Designer.cs @@ -0,0 +1,303 @@ +// +using System; +using LINGYUN.Abp.MicroService.AIService; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace LINGYUN.Abp.MicroService.AIService.Migrations +{ + [DbContext(typeof(AIServiceMigrationsDbContext))] + [Migration("20260127083027_Initial_AI_Service")] + partial class Initial_AI_Service + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.ConversationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UpdateAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("AbpAIConversations", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.TextChatMessageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReplyAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyMessage") + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Workspace") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITextChatMessages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Tokens.TokenUsageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CachedInputTokenCount") + .HasColumnType("bigint"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("InputTokenCount") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("OutputTokenCount") + .HasColumnType("bigint"); + + b.Property("ReasoningTokenCount") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalTokenCount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITokenUsages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Workspaces.WorkspaceDefinitionRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiBaseUrl") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApiKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FrequencyPenalty") + .HasColumnType("real"); + + b.Property("Instructions") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MaxOutputTokens") + .HasColumnType("integer"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PresencePenalty") + .HasColumnType("real"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SystemPrompt") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Temperature") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpAIWorkspaceDefinitions", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.cs new file mode 100644 index 000000000..ed6c57888 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/20260127083027_Initial_AI_Service.cs @@ -0,0 +1,148 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LINGYUN.Abp.MicroService.AIService.Migrations +{ + /// + public partial class Initial_AI_Service : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AbpAIConversations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdateAt = table.Column(type: "timestamp with time zone", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAIConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AbpAITextChatMessages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: true), + Workspace = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Role = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + ConversationId = table.Column(type: "uuid", nullable: true), + ReplyMessage = table.Column(type: "text", nullable: true), + ReplyAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAITextChatMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AbpAITokenUsages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + MessageId = table.Column(type: "uuid", nullable: true), + ConversationId = table.Column(type: "uuid", nullable: true), + InputTokenCount = table.Column(type: "bigint", nullable: true), + OutputTokenCount = table.Column(type: "bigint", nullable: true), + TotalTokenCount = table.Column(type: "bigint", nullable: true), + CachedInputTokenCount = table.Column(type: "bigint", nullable: true), + ReasoningTokenCount = table.Column(type: "bigint", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAITokenUsages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AbpAIWorkspaceDefinitions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Provider = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + ModelName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + DisplayName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Description = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + ApiKey = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ApiBaseUrl = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + SystemPrompt = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + Instructions = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + Temperature = table.Column(type: "real", nullable: true), + MaxOutputTokens = table.Column(type: "integer", nullable: true), + FrequencyPenalty = table.Column(type: "real", nullable: true), + PresencePenalty = table.Column(type: "real", nullable: true), + IsEnabled = table.Column(type: "boolean", nullable: false), + StateCheckers = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpAIWorkspaceDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AbpAITextChatMessages_TenantId_ConversationId", + table: "AbpAITextChatMessages", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AbpAITokenUsages_TenantId_ConversationId", + table: "AbpAITokenUsages", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AbpAIWorkspaceDefinitions_Name", + table: "AbpAIWorkspaceDefinitions", + column: "Name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AbpAIConversations"); + + migrationBuilder.DropTable( + name: "AbpAITextChatMessages"); + + migrationBuilder.DropTable( + name: "AbpAITokenUsages"); + + migrationBuilder.DropTable( + name: "AbpAIWorkspaceDefinitions"); + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/AIServiceMigrationsDbContextModelSnapshot.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/AIServiceMigrationsDbContextModelSnapshot.cs new file mode 100644 index 000000000..095a0771d --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService.EntityFrameworkCore/Migrations/AIServiceMigrationsDbContextModelSnapshot.cs @@ -0,0 +1,300 @@ +// +using System; +using LINGYUN.Abp.MicroService.AIService; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace LINGYUN.Abp.MicroService.AIService.Migrations +{ + [DbContext(typeof(AIServiceMigrationsDbContext))] + partial class AIServiceMigrationsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.ConversationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UpdateAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("AbpAIConversations", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Chats.TextChatMessageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReplyAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyMessage") + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Workspace") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITextChatMessages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Tokens.TokenUsageRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CachedInputTokenCount") + .HasColumnType("bigint"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("InputTokenCount") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("OutputTokenCount") + .HasColumnType("bigint"); + + b.Property("ReasoningTokenCount") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalTokenCount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AbpAITokenUsages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.AIManagement.Workspaces.WorkspaceDefinitionRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiBaseUrl") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApiKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FrequencyPenalty") + .HasColumnType("real"); + + b.Property("Instructions") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MaxOutputTokens") + .HasColumnType("integer"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PresencePenalty") + .HasColumnType("real"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SystemPrompt") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Temperature") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpAIWorkspaceDefinitions", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.Configure.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.Configure.cs new file mode 100644 index 000000000..f654a1bc1 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.Configure.cs @@ -0,0 +1,423 @@ +using DotNetCore.CAP; +using LINGYUN.Abp.AIManagement; +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.Localization.CultureMap; +using LINGYUN.Abp.LocalizationManagement; +using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +using LINGYUN.Abp.TextTemplating; +using LINGYUN.Abp.Wrapper; +using Medallion.Threading; +using Medallion.Threading.Redis; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Caching.StackExchangeRedis; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using StackExchange.Redis; +using System.Text.Encodings.Web; +using System.Text.Unicode; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.AntiForgery; +using Volo.Abp.Auditing; +using Volo.Abp.Caching; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.FeatureManagement; +using Volo.Abp.GlobalFeatures; +using Volo.Abp.Http.Client; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Json; +using Volo.Abp.Json.SystemTextJson; +using Volo.Abp.Localization; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement; +using Volo.Abp.Security.Claims; +using Volo.Abp.SettingManagement; +using Volo.Abp.Threading; +using Volo.Abp.Timing; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.MicroService.AIService; + +public partial class AIServiceModule +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + private void PreConfigureFeature() + { + OneTimeRunner.Run(() => + { + GlobalFeatureManager.Instance.Modules.Editions().EnableAll(); + }); + } + + private void PreConfigureApp(IConfiguration configuration) + { + PreConfigure(options => + { + // 以开放端口区别,应在0-31之间 + options.SnowflakeIdOptions.WorkerId = 19; + options.SnowflakeIdOptions.WorkerIdBits = 5; + options.SnowflakeIdOptions.DatacenterId = 1; + }); + + if (configuration.GetValue("App:ShowPii")) + { + IdentityModelEventSource.ShowPII = true; + } + } + + private void PreConfigureCAP(IConfiguration configuration) + { + PreConfigure(options => + { + options + .UsePostgreSql(mySqlOptions => + { + configuration.GetSection("CAP:PostgreSql").Bind(mySqlOptions); + }) + .UseRabbitMQ(rabbitMQOptions => + { + configuration.GetSection("CAP:RabbitMQ").Bind(rabbitMQOptions); + }) + .UseDashboard(); + }); + } + + private void ConfigureTextTemplating() + { + Configure(options => + { + options.IsDynamicTemplateDefinitionStoreEnabled = true; + }); + } + + private void ConfigureFeatureManagement() + { + Configure(options => + { + options.IsDynamicFeatureStoreEnabled = true; + }); + } + + private void ConfigureJsonSerializer(IConfiguration configuration) + { + // 统一时间日期格式 + Configure(options => + { + var jsonConfiguration = configuration.GetSection("Json"); + if (jsonConfiguration.Exists()) + { + jsonConfiguration.Bind(options); + } + }); + // 中文序列化的编码问题 + Configure(options => + { + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); + }); + } + + private void ConfigureAIManagement() + { + Configure(options => + { + options.IsDynamicWorkspaceStoreEnabled = true; + options.SaveStaticWorkspacesToDatabase = true; + }); + } + + private void ConfigurePermissionManagement() + { + Configure(options => + { + options.IsDynamicPermissionStoreEnabled = true; + }); + } + + private void ConfigureSettingManagement() + { + Configure(options => + { + options.IsDynamicSettingStoreEnabled = true; + }); + } + + private void ConfigureTiming(IConfiguration configuration) + { + Configure(options => + { + configuration.GetSection("Clock").Bind(options); + }); + } + + private void ConfigureCaching(IConfiguration configuration) + { + Configure(options => + { + configuration.GetSection("DistributedCache").Bind(options); + }); + + Configure(options => + { + options.AutoEventSelectors.AddNamespace("Volo.Abp.TenantManagement"); + }); + + Configure(options => + { + var redisConfig = ConfigurationOptions.Parse(options.Configuration!); + options.ConfigurationOptions = redisConfig; + options.InstanceName = configuration["Redis:InstanceName"]; + }); + } + + private void ConfigureDistributedLocking(IServiceCollection services, IConfiguration configuration) + { + var distributedLockEnabled = configuration["DistributedLock:IsEnabled"]; + if (distributedLockEnabled.IsNullOrEmpty() || bool.Parse(distributedLockEnabled)) + { + services.AddSingleton(sp => + { + var connectionMultiplexer = sp.GetRequiredService(); + return new RedisDistributedSynchronizationProvider(connectionMultiplexer.GetDatabase()); + }); + } + } + + private void ConfigureMvc(IServiceCollection services, IConfiguration configuration) + { + Configure(options => + { + options.ExposeIntegrationServices = true; + }); + } + + private void ConfigureVirtualFileSystem() + { + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.MicroService.AIService"); + }); + } + + private void ConfigureMultiTenancy(IConfiguration configuration) + { + // 多租户 + Configure(options => + { + options.IsEnabled = true; + }); + + var tenantResolveCfg = configuration.GetSection("App:Domains"); + if (tenantResolveCfg.Exists()) + { + Configure(options => + { + var domains = tenantResolveCfg.Get() ?? []; + foreach (var domain in domains) + { + options.AddDomainTenantResolver(domain); + } + }); + } + } + + private void ConfigureIdentity(IConfiguration configuration) + { + Configure(options => + { + options.IsDynamicClaimsEnabled = true; + options.RemoteRefreshUrl = configuration["App:RefreshClaimsUrl"] + options.RemoteRefreshUrl; + }); + } + + private void ConfigureAuditing(IConfiguration configuration) + { + Configure(options => + { + // 是否启用实体变更记录 + var allEntitiesSelectorIsEnabled = configuration["Auditing:AllEntitiesSelector"]; + if (allEntitiesSelectorIsEnabled.IsNullOrWhiteSpace() || + (bool.TryParse(allEntitiesSelectorIsEnabled, out var enabled) && enabled)) + { + options.EntityHistorySelectors.AddAllEntities(); + } + }); + } + + private void ConfigureSwagger(IServiceCollection services, IConfiguration configuration) + { + // Swagger + services.AddAbpSwaggerGenWithOAuth( + configuration["AuthServer:Authority"]!, + new Dictionary + { + { "AIService", "AI Service API"} + }, + options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "AI Service API", Version = "v1", + Contact = new OpenApiContact + { + Name = "colin", + Email = "colin.in@foxmail.com", + Url = new Uri("https://github.com/colinin") + }, + License = new OpenApiLicense + { + Name = "MIT", + Url = new Uri("https://github.com/colinin/abp-next-admin/blob/master/LICENSE") + } + }); + options.DocInclusionPredicate((docName, description) => true); + options.CustomSchemaIds(type => type.FullName); + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Scheme = "bearer", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT" + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + new string[] { } + } + }); + options.OperationFilter(); + }); + } + + private void ConfigureLocalization() + { + // 支持本地化语言类型 + Configure(options => + { + options.Languages.Add(new LanguageInfo("en", "en", "English")); + options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文")); + + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources"); + }); + + Configure(options => + { + var zhHansCultureMapInfo = new CultureMapInfo + { + TargetCulture = "zh-Hans", + SourceCultures = new string[] { "zh", "zh_CN", "zh-CN" } + }; + + options.CulturesMaps.Add(zhHansCultureMapInfo); + options.UiCulturesMaps.Add(zhHansCultureMapInfo); + }); + + Configure(options => + { + options.SaveStaticLocalizationsToDatabase = true; + }); + } + + private void ConfigureCors(IServiceCollection services, IConfiguration configuration) + { + services.AddCors(options => + { + options.AddDefaultPolicy(builder => + { + var corsOrigins = configuration.GetSection("App:CorsOrigins").Get>(); + if (corsOrigins == null || corsOrigins.Count == 0) + { + corsOrigins = configuration["App:CorsOrigins"]? + .Split(",", StringSplitOptions.RemoveEmptyEntries) + .Select(o => o.RemovePostFix("/")) + .ToList() ?? new List(); + } + builder + .WithOrigins(corsOrigins + .Select(o => o.RemovePostFix("/")) + .ToArray() + ) + .WithAbpExposedHeaders() + .WithAbpWrapExposedHeaders() + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + } + + private void ConfigureSecurity(IServiceCollection services, IConfiguration configuration, bool isDevelopment = false) + { + Configure(options => + { + options.TokenCookie.HttpOnly = false; + options.TokenCookie.SameSite = SameSiteMode.Lax; + }); + + services.AddAlwaysAllowAuthorization(); + services.AddAlwaysAllowSession(); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddAbpJwtBearer(options => + { + configuration.GetSection("AuthServer").Bind(options); + + var validIssuers = configuration.GetSection("AuthServer:ValidIssuers").Get>(); + if (validIssuers?.Count > 0) + { + options.TokenValidationParameters.ValidIssuers = validIssuers; + options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; + } + var validAudiences = configuration.GetSection("AuthServer:ValidAudiences").Get>(); + if (validAudiences?.Count > 0) + { + options.TokenValidationParameters.ValidAudiences = validAudiences; + } + }); + + services + .AddDataProtection() + .SetApplicationName("LINGYUN.Abp.Application") + .PersistKeysToStackExchangeRedis(() => + { + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]!); + + return redis.GetDatabase(); + }, + "LINGYUN.Abp.Application:DataProtection:Protection-Keys"); + } + + private void ConfigureWrapper() + { + Configure(options => + { + options.IsEnabled = true; + + options.IgnoreControllers.Add(); + }); + } + + private void PreConfigureWrapper() + { + // 服务间调用不包装 + PreConfigure(options => + { + options.ProxyClientActions.Add( + (_, _, client) => + { + client.DefaultRequestHeaders.TryAddWithoutValidation(AbpHttpWrapConsts.AbpDontWrapResult, "true"); + }); + }); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.cs new file mode 100644 index 000000000..e18ca18d8 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/AIServiceModule.cs @@ -0,0 +1,103 @@ +using LINGYUN.Abp.AIManagement; +using LINGYUN.Abp.AspNetCore.HttpOverrides; +using LINGYUN.Abp.AspNetCore.Mvc.Localization; +using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; +using LINGYUN.Abp.AuditLogging.Elasticsearch; +using LINGYUN.Abp.Claims.Mapping; +using LINGYUN.Abp.Data.DbMigrator; +using LINGYUN.Abp.Emailing.Platform; +using LINGYUN.Abp.EventBus.CAP; +using LINGYUN.Abp.ExceptionHandling.Emailing; +using LINGYUN.Abp.Identity.Session.AspNetCore; +using LINGYUN.Abp.Localization.CultureMap; +using LINGYUN.Abp.Logging.Serilog.Elasticsearch; +using LINGYUN.Abp.Serilog.Enrichers.Application; +using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +using LINGYUN.Abp.Sms.Platform; +using LINGYUN.Abp.TextTemplating.Scriban; +using Volo.Abp.AspNetCore.Authentication.JwtBearer; +using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; +using Volo.Abp.AspNetCore.Serilog; +using Volo.Abp.Autofac; +using Volo.Abp.Caching.StackExchangeRedis; +using Volo.Abp.Http.Client; +using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.Identity; +using Volo.Abp.PermissionManagement.OpenIddict; +using Volo.Abp.Swashbuckle; + +namespace LINGYUN.Abp.MicroService.AIService; + +[DependsOn( + typeof(AbpCAPEventBusModule), + typeof(AbpSerilogEnrichersApplicationModule), + typeof(AbpSerilogEnrichersUniqueIdModule), + typeof(AbpAspNetCoreSerilogModule), + typeof(AbpLoggingSerilogElasticsearchModule), + typeof(AbpAuditLoggingElasticsearchModule), + typeof(AbpAspNetCoreMvcUiMultiTenancyModule), + typeof(AbpAspNetCoreMvcLocalizationModule), + + typeof(AbpPermissionManagementDomainIdentityModule), + typeof(AbpPermissionManagementDomainOpenIddictModule), + + // 重写模板引擎支持外部本地化 + typeof(AbpTextTemplatingScribanModule), + + typeof(AbpIdentitySessionAspNetCoreModule), + + typeof(AbpAIManagementApplicationModule), + typeof(AbpAIManagementHttpApiModule), + typeof(AIServiceMigrationsEntityFrameworkCoreModule), + typeof(AbpDataDbMigratorModule), + typeof(AbpAspNetCoreAuthenticationJwtBearerModule), + typeof(AbpEmailingExceptionHandlingModule), + typeof(AbpHttpClientModule), + typeof(AbpSmsPlatformModule), + typeof(AbpEmailingPlatformModule), + typeof(AbpCachingStackExchangeRedisModule), + typeof(AbpLocalizationCultureMapModule), + typeof(AbpAspNetCoreMvcWrapperModule), + typeof(AbpAspNetCoreHttpOverridesModule), + typeof(AbpClaimsMappingModule), + typeof(AbpSwashbuckleModule), + typeof(AbpAutofacModule) + )] +public partial class AIServiceModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + PreConfigureWrapper(); + PreConfigureFeature(); + PreConfigureApp(configuration); + PreConfigureCAP(configuration); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + ConfigureWrapper(); + ConfigureLocalization(); + ConfigureVirtualFileSystem(); + ConfigureTextTemplating(); + ConfigureAIManagement(); + ConfigureSettingManagement(); + ConfigureFeatureManagement(); + ConfigurePermissionManagement(); + ConfigureIdentity(configuration); + ConfigureTiming(configuration); + ConfigureCaching(configuration); + ConfigureAuditing(configuration); + ConfigureMultiTenancy(configuration); + ConfigureJsonSerializer(configuration); + ConfigureMvc(context.Services, configuration); + ConfigureCors(context.Services, configuration); + ConfigureSwagger(context.Services, configuration); + ConfigureDistributedLocking(context.Services, configuration); + ConfigureSecurity(context.Services, configuration, hostingEnvironment.IsDevelopment()); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/FodyWeavers.xml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/LINGYUN.Abp.MicroService.AIService.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/LINGYUN.Abp.MicroService.AIService.csproj new file mode 100644 index 000000000..dafa6b426 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/LINGYUN.Abp.MicroService.AIService.csproj @@ -0,0 +1,71 @@ + + + + net10.0 + enable + enable + LINGYUN.Abp.MicroService.AIService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Program.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Program.cs new file mode 100644 index 000000000..85b652951 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Program.cs @@ -0,0 +1,99 @@ +using LINGYUN.Abp.Identity.Session.AspNetCore; +using LINGYUN.Abp.MicroService.AIService; +using LINGYUN.Abp.Serilog.Enrichers.Application; +using Serilog; +using Volo.Abp.IO; +using Volo.Abp.Modularity.PlugIns; + +Log.Information("Starting AIService Host..."); + +try +{ + var builder = WebApplication.CreateBuilder(args); + builder.Host.AddAppSettingsSecretsJson() + .UseAutofac() + .ConfigureAppConfiguration((context, config) => + { + if (context.Configuration.GetValue("AgileConfig:IsEnabled", false)) + { + config.AddAgileConfig(new AgileConfig.Client.ConfigClient(context.Configuration)); + } + }) + .UseSerilog((context, provider, config) => + { + config.ReadFrom.Configuration(context.Configuration); + }); + + builder.AddServiceDefaults(); + + await builder.AddApplicationAsync(options => + { + var applicationName = Environment.GetEnvironmentVariable("APPLICATION_NAME") ?? "AIService"; + options.ApplicationName = applicationName; + AbpSerilogEnrichersConsts.ApplicationName = applicationName; + + var pluginFolder = Path.Combine(Directory.GetCurrentDirectory(), "Modules"); + DirectoryHelper.CreateIfNotExists(pluginFolder); + options.PlugInSources.AddFolder(pluginFolder, SearchOption.AllDirectories); + }); + + var app = builder.Build(); + + await app.InitializeApplicationAsync(); + + app.MapDefaultEndpoints(); + + app.UseForwardedHeaders(); + // 本地化 + app.UseMapRequestLocalization(); + // http调用链 + app.UseCorrelationId(); + // 文件系统 + app.MapAbpStaticAssets(); + // 路由 + app.UseRouting(); + // 跨域 + app.UseCors(); + // 认证 + app.UseAuthentication(); + app.UseJwtTokenMiddleware(); + // 多租户 + app.UseMultiTenancy(); + // 会话 + app.UseAbpSession(); + // jwt + app.UseDynamicClaims(); + // 授权 + app.UseAuthorization(); + // Swagger + app.UseSwagger(); + // Swagger可视化界面 + app.UseAbpSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Support AI Service API"); + + var configuration = app.Configuration; + options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]); + options.OAuthScopes(configuration["AuthServer:Audience"]); + }); + // 审计日志 + app.UseAuditing(); + app.UseAbpSerilogEnrichers(); + // 路由 + app.UseConfiguredEndpoints(); + + await app.RunAsync(); +} +catch (Exception ex) +{ + if (ex is HostAbortedException) + { + throw; + } + + Log.Fatal(ex, "Host terminated unexpectedly!"); +} +finally +{ + await Log.CloseAndFlushAsync(); +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Properties/launchSettings.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Properties/launchSettings.json new file mode 100644 index 000000000..312571616 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "LINGYUN.Abp.MicroService.AIService": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:49787;http://localhost:49788" + } + } +} \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/TenantHeaderParamter.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/TenantHeaderParamter.cs new file mode 100644 index 000000000..f72407569 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/TenantHeaderParamter.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.MicroService.AIService; + +public class TenantHeaderParamter : IOperationFilter +{ + private readonly AbpMultiTenancyOptions _multiTenancyOptions; + private readonly AbpAspNetCoreMultiTenancyOptions _aspNetCoreMultiTenancyOptions; + public TenantHeaderParamter( + IOptions multiTenancyOptions, + IOptions aspNetCoreMultiTenancyOptions) + { + _multiTenancyOptions = multiTenancyOptions.Value; + _aspNetCoreMultiTenancyOptions = aspNetCoreMultiTenancyOptions.Value; + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (_multiTenancyOptions.IsEnabled) + { + operation.Parameters = operation.Parameters ?? new List(); + operation.Parameters.Add(new OpenApiParameter + { + Name = _aspNetCoreMultiTenancyOptions.TenantKey, + In = ParameterLocation.Header, + Description = "Tenant Id in http header", + Required = false + }); + } + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.Development.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.Development.json new file mode 100644 index 000000000..c9a9d8327 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.Development.json @@ -0,0 +1,116 @@ +{ + "App": { + "ShowPii": true, + "CorsOrigins": [ "http://localhost:5666", "http://localhost:30000" ], + "RefreshClaimsUrl": "http://localhost:30015" + }, + "Auditing": { + "AllEntitiesSelector": true + }, + "DistributedCache": { + "HideErrors": true, + "KeyPrefix": "LINGYUN.Abp.Application", + "GlobalCacheEntryOptions": { + "SlidingExpiration": "30:00:00", + "AbsoluteExpirationRelativeToNow": "60:00:00" + } + }, + "ConnectionStrings": { + "Default": "Host=127.0.0.1;Database=abp;Username=postgres;Password=123456" + }, + "CAP": { + "EventBus": { + "DefaultGroupName": "AIService", + "Version": "v1", + "FailedRetryInterval": 300, + "FailedRetryCount": 10, + "CollectorCleaningInterval": 3600000 + }, + "PostgreSql": { + "TableNamePrefix": "admin", + "ConnectionString": "Host=127.0.0.1;Database=abp;Username=postgres;Password=123456" + }, + "RabbitMQ": { + "HostName": "localhost", + "Port": 5672, + "UserName": "admin", + "Password": "123456", + "ExchangeName": "LINGYUN.Abp.Application", + "VirtualHost": "/" + } + }, + "DistributedLock": { + "IsEnabled": true, + "Redis": { + "Configuration": "localhost,defaultDatabase=13" + } + }, + "Redis": { + "Configuration": "localhost,defaultDatabase=10", + "InstanceName": "LINGYUN.Abp.Application" + }, + "AuthServer": { + "Authority": "http://localhost:44385/", + "Audience": "admin-service", + "ValidAudiences": [ "lingyun-abp-application" ], + "MapInboundClaims": false, + "RequireHttpsMetadata": false, + "SwaggerClientId": "vue-oauth-client" + }, + "RemoteServices": { + "Platform": { + "BaseUrl": "http://localhost:30025", + "UseCurrentAccessToken": false + } + }, + "Logging": { + "Serilog": { + "Elasticsearch": { + "IndexFormat": "abp.dev.logging-{0:yyyy.MM.dd}" + } + } + }, + "AuditLogging": { + "Elasticsearch": { + "IndexPrefix": "abp.dev.auditing" + } + }, + "Elasticsearch": { + "NodeUris": "http://elasticsearch" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "System": "Warning", + "Microsoft": "Warning", + "DotNetCore": "Debug" + } + }, + "WriteTo": [ + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "Console", + "Args": { + "restrictedToMinimumLevel": "Debug", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "Elasticsearch", + "Args": { + "nodeUris": "http://elasticsearch", + "indexFormat": "abp.dev.logging-{0:yyyy.MM.dd}", + "autoRegisterTemplate": true, + "autoRegisterTemplateVersion": "ESv7" + } + } + ] + } + } + ] + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.json new file mode 100644 index 000000000..b8e4da13c --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AIService/appsettings.json @@ -0,0 +1,91 @@ +{ + "Clock": { + "Kind": "Local" + }, + "Forwarded": { + "ForwardedHeaders": "XForwardedFor,XForwardedProto" + }, + "StringEncryption": { + "DefaultPassPhrase": "s46c5q55nxpeS8Ra", + "InitVectorBytes": "s83ng0abvd02js84", + "DefaultSalt": "sf&5)s3#" + }, + "Json": { + "InputDateTimeFormats": [ + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-ddTHH:mm:ss" + ] + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "System": "Warning", + "Microsoft": "Warning", + "DotNetCore": "Information" + } + }, + "Enrich": [ "FromLogContext", "WithProcessId", "WithThreadId", "WithEnvironmentName", "WithMachineName", "WithApplicationName", "WithUniqueId" ], + "WriteTo": [ + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "Console", + "Args": { + "restrictedToMinimumLevel": "Debug", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Debug-.log", + "restrictedToMinimumLevel": "Debug", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Info-.log", + "restrictedToMinimumLevel": "Information", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Warn-.log", + "restrictedToMinimumLevel": "Warning", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Error-.log", + "restrictedToMinimumLevel": "Error", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/Fatal-.log", + "restrictedToMinimumLevel": "Fatal", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" + } + } + ] + } + } + ] + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json index 61baae0e6..34e4940a0 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.ApiGateway/yarp.json @@ -1,6 +1,21 @@ { "ReverseProxy": { "Routes": { + "ai-management-route": { + "ClusterId": "ai-management-cluster", + "Match": { + "Path": "/api/ai-management/{**everything}" + }, + "Transforms": [ + { + "HeaderPrefix": "X-Forwarded-", + "X-Forwarded": "Append" + }, + { + "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" + } + ] + }, "abp-route": { "ClusterId": "admin-service-cluster", "Match": { @@ -451,6 +466,16 @@ } }, "Clusters": { + "ai-management-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:30070", + "Metadata": { + "SwaggerEndpoint": "http://localhost:30070" + } + } + } + }, "auth-server-cluster": { "Destinations": { "destination1": { diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs index 48a47b899..dadaf8d82 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/AppHost.cs @@ -10,8 +10,17 @@ var redis = builder.AddRedis("redis") // Elasticsearch var elasticsearch = builder.AddElasticsearch("elasticsearch") .WithContainerName("elasticsearch") + .WithImageTag("8.17.3") .WithDataVolume("elasticsearch-dev") - .WithEnvironment("ES_JAVA_OPTS", "-Xms2g -Xmx2g"); + .WithEnvironment("ES_JAVA_OPTS", "-Xms2g -Xmx2g") + // see: https://www.funkysi1701.com/posts/2025/adding-elasticsearch-with-aspire/ + .WithEnvironment("xpack.security.enabled", "false"); + +// Kibana +builder.AddContainer("kibana", "kibana", "8.17.3") + .WithReference(elasticsearch) + .WithEndpoint(5601, 5601) + .WaitFor(elasticsearch); // Postgres var postgres = builder.AddPostgres("postgres") @@ -215,9 +224,21 @@ builder.AddProject("WorkflowS .WaitFor(rabbitmq) .WaitFor(taskService); +// AIService +AddDotNetProject< + Projects.LINGYUN_Abp_MicroService_AIService_DbMigrator, + Projects.LINGYUN_Abp_MicroService_AIService>( + builder: builder, + servicePrefix: "AI", + serviceSuffix: "Service", + migratorSuffix: "Migrator", + port: 30070, + portName: "ai", + waitProject: localizationService); + // ApiGateway var apigateway = builder.AddProject("ApiGateway") - .WithHttpEndpoint(port: 30000, name: "gateway") + // .WithHttpEndpoint(port: 30000, name: "gateway") .WithExternalHttpEndpoints() .WithReference(redis, "Redis") .WithReference(elasticsearch, "Elasticsearch") diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj index cccaa6637..807b9b7a0 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AppHost/LINGYUN.Abp.MicroService.AppHost.csproj @@ -17,6 +17,8 @@ + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj index 2a4b28ffb..ba3e066a1 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/LINGYUN.Abp.MicroService.TaskService.csproj @@ -55,6 +55,7 @@ + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs index 42e80af93..4933dbb3b 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.TaskService/TaskServiceModule.cs @@ -10,6 +10,7 @@ using LINGYUN.Abp.BackgroundTasks.Notifications; using LINGYUN.Abp.BackgroundTasks.Quartz; using LINGYUN.Abp.Claims.Mapping; using LINGYUN.Abp.Data.DbMigrator; +using LINGYUN.Abp.Elasticsearch.Jobs; using LINGYUN.Abp.Emailing.Platform; using LINGYUN.Abp.EventBus.CAP; using LINGYUN.Abp.ExceptionHandling.Emailing; @@ -54,6 +55,7 @@ namespace LINGYUN.Abp.MicroService.TaskService; typeof(AbpHttpClientIdentityModelWebModule), typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAspNetCoreMvcLocalizationModule), + typeof(AbpElasticsearchJobsModule), typeof(AbpBackgroundTasksJobsModule), typeof(AbpBackgroundTasksQuartzModule), typeof(AbpBackgroundTasksDistributedLockingModule), diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs new file mode 100644 index 000000000..95b93584f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentFactory.cs @@ -0,0 +1,110 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Localization; +using System; +using System.Threading.Tasks; +using Volo.Abp.AI; +using Volo.Abp.Authorization; +using Volo.Abp.DependencyInjection; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AI.Agent; +public class AgentFactory : IAgentFactory, IScopedDependency +{ + protected IServiceProvider ServiceProvider { get; } + protected IChatClientFactory ChatClientFactory { get; } + protected IStringLocalizerFactory StringLocalizerFactory { get; } + protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } + protected ISimpleStateCheckerManager StateCheckerManager { get; } + public AgentFactory( + IServiceProvider serviceProvider, + IChatClientFactory chatClientFactory, + IStringLocalizerFactory stringLocalizerFactory, + IWorkspaceDefinitionManager workspaceDefinitionManager, + ISimpleStateCheckerManager stateCheckerManager) + { + ServiceProvider = serviceProvider; + ChatClientFactory = chatClientFactory; + StringLocalizerFactory = stringLocalizerFactory; + WorkspaceDefinitionManager = workspaceDefinitionManager; + StateCheckerManager = stateCheckerManager; + } + + public async virtual Task CreateAsync() + { + var workspace = WorkspaceNameAttribute.GetWorkspaceName(); + + var chatClient = await ChatClientFactory.CreateAsync(); + + var workspaceDefine = await WorkspaceDefinitionManager.GetOrNullAsync(workspace); + + if (workspaceDefine != null) + { + await CheckWorkspaceStateAsync(workspaceDefine); + } + + return await CreateAgentAsync(chatClient, workspaceDefine); + } + + public async virtual Task CreateAsync(string workspace) + { + var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); + + await CheckWorkspaceStateAsync(workspaceDefine); + + var chatClient = await ChatClientFactory.CreateAsync(workspace); + + return await CreateAgentAsync(chatClient, workspaceDefine); + } + + protected async virtual Task CreateAgentAsync(IChatClient chatClient, WorkspaceDefinition? workspace) + { + string? description = null; + if (workspace?.Description != null) + { + description = workspace.Description.Localize(StringLocalizerFactory); + } + + var tools = await GetAgentToolsAsync(workspace); + + var clientAgentOptions = new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + Instructions = workspace?.Instructions, + Temperature = workspace?.Temperature, + MaxOutputTokens = workspace?.MaxOutputTokens, + PresencePenalty = workspace?.PresencePenalty, + FrequencyPenalty = workspace?.FrequencyPenalty, + Tools = tools, + }, + Name = workspace?.Name, + Description = description, + }; + + var aiAgent = chatClient.CreateAIAgent(clientAgentOptions) + .AsBuilder() + .UseLogging() + .UseOpenTelemetry() + .Build(ServiceProvider); + + return new WorkspaceAIAgent(aiAgent, workspace); + } + + protected virtual Task GetAgentToolsAsync(WorkspaceDefinition? workspace) + { + return Task.FromResult([]); + } + + protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) + { + if (!await StateCheckerManager.IsEnabledAsync(workspace)) + { + throw new AbpAuthorizationException( + $"Workspace is not enabled: {workspace.Name}!", + AbpAIErrorCodes.WorkspaceIsNotEnabled) + .WithData("Workspace", workspace.Name); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs new file mode 100644 index 000000000..78c9f0508 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/AgentService.cs @@ -0,0 +1,157 @@ +using LINGYUN.Abp.AI.Chats; +using LINGYUN.Abp.AI.Localization; +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AI.Tokens; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Localization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.Timing; +using AIChatMessage = Microsoft.Extensions.AI.ChatMessage; + +namespace LINGYUN.Abp.AI.Agent; +public class AgentService : IAgentService, IScopedDependency +{ + private readonly IClock _clock; + private readonly IGuidGenerator _guidGenerator; + private readonly IAgentFactory _agentFactory; + private readonly ITokenUsageStore _tokenUsageStore; + private readonly IChatMessageStore _chatMessageStore; + private readonly IConversationStore _conversationStore; + private readonly IStringLocalizer _localizerResource; + public AgentService( + IClock clock, + IGuidGenerator guidGenerator, + IAgentFactory agentFactory, + ITokenUsageStore tokenUsageStore, + IChatMessageStore chatMessageStore, + IConversationStore conversationStore, + IStringLocalizer localizerResource) + { + _clock = clock; + _guidGenerator = guidGenerator; + _agentFactory = agentFactory; + _tokenUsageStore = tokenUsageStore; + _chatMessageStore = chatMessageStore; + _conversationStore = conversationStore; + _localizerResource = localizerResource; + } + + public async virtual IAsyncEnumerable SendMessageAsync(Models.ChatMessage message) + { + var conversationId = await StoreConversation(message); + + message.WithConversationId(conversationId); + + var messages = await BuildChatMessages(message); + + var agent = await _agentFactory.CreateAsync(message.Workspace); + + var agentRunRes = agent.RunStreamingAsync(messages); + + var tokenUsageInfo = new TokenUsageInfo(message.Workspace, conversationId); + var agentMessageBuilder = new StringBuilder(); + + await foreach (var response in agentRunRes) + { + UpdateTokenUsageInfo(tokenUsageInfo, response); + agentMessageBuilder.Append(response.Text); + yield return response.Text; + } + + var messageId = await StoreChatMessage(message, agentMessageBuilder.ToString()); + + tokenUsageInfo.WithMessageId(messageId); + +#if DEBUG + Console.WriteLine(); + Console.WriteLine(tokenUsageInfo); +#endif + + await StoreTokenUsageInfo(tokenUsageInfo); + } + + protected virtual async Task> BuildChatMessages(Models.ChatMessage message) + { + var messages = new List(); + + if (message.ConversationId.HasValue) + { + var historyMessages = await _chatMessageStore.GetHistoryMessagesAsync(message.ConversationId.Value); + + // TODO: 应用摘要提示压缩 + foreach (var chatMessage in historyMessages) + { + messages.Add(new AIChatMessage(chatMessage.Role, chatMessage.GetMessagePrompt())); + } + } + + messages.Add(new AIChatMessage(ChatRole.User, message.GetMessagePrompt())); + + return messages; + } + + protected async virtual Task StoreChatMessage(Models.ChatMessage message, string agentMessage) + { + message.WithReply(agentMessage, _clock.Now); + + return await _chatMessageStore.SaveMessageAsync(message); + } + + protected async virtual Task StoreConversation(Models.ChatMessage message) + { + if (message.ConversationId.HasValue) + { + var conversation = await _conversationStore.FindAsync(message.ConversationId.Value); + if (conversation == null || conversation.ExpiredAt <= _clock.Now) + { + throw new BusinessException( + AbpAIErrorCodes.ConversationHasExpired, + "The conversation has expired. Please create a new one!"); + } + + conversation.UpdateAt = _clock.Now; + await _conversationStore.SaveAsync(conversation); + + return conversation.Id; + } + else + { + var conversation = new Conversation( + _guidGenerator.Create(), + _localizerResource["NewConversation"], + _clock.Now); + + await _conversationStore.SaveAsync(conversation); + + return conversation.Id; + } + } + + protected async virtual Task StoreTokenUsageInfo(TokenUsageInfo tokenUsageInfo) + { + await _tokenUsageStore.SaveTokenUsageAsync(tokenUsageInfo); + } + + private static void UpdateTokenUsageInfo(TokenUsageInfo tokenUsageInfo, AgentRunResponseUpdate response) + { + if (response.RawRepresentation != null && + response.RawRepresentation is ChatResponseUpdate update) + { + var usageContents = update.Contents.OfType(); + + tokenUsageInfo.InputTokenCount = usageContents.Max(x => x.Details.InputTokenCount); + tokenUsageInfo.OutputTokenCount = usageContents.Max(x => x.Details.OutputTokenCount); + tokenUsageInfo.TotalTokenCount = usageContents.Max(x => x.Details.TotalTokenCount); + tokenUsageInfo.ReasoningTokenCount = usageContents.Max(x => x.Details.ReasoningTokenCount); + tokenUsageInfo.CachedInputTokenCount = usageContents.Max(x => x.Details.CachedInputTokenCount); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs deleted file mode 100644 index 62dd55dca..000000000 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/ChatClientAgentFactory.cs +++ /dev/null @@ -1,80 +0,0 @@ -using LINGYUN.Abp.AI.Workspaces; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Localization; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using Volo.Abp.AI; -using Volo.Abp.DependencyInjection; - -namespace LINGYUN.Abp.AI.Agent; -public class ChatClientAgentFactory : IChatClientAgentFactory, ISingletonDependency -{ - private readonly static ConcurrentDictionary _chatClientAgentCache = new(); - - private readonly IChatClientFactory _chatClientFactory; - private readonly IStringLocalizerFactory _stringLocalizerFactory; - private readonly IWorkspaceDefinitionManager _workspaceDefinitionManager; - public ChatClientAgentFactory( - IChatClientFactory chatClientFactory, - IStringLocalizerFactory stringLocalizerFactory, - IWorkspaceDefinitionManager workspaceDefinitionManager) - { - _chatClientFactory = chatClientFactory; - _stringLocalizerFactory = stringLocalizerFactory; - _workspaceDefinitionManager = workspaceDefinitionManager; - } - - public async virtual Task CreateAsync() - { - var workspace = WorkspaceNameAttribute.GetWorkspaceName(); - if (_chatClientAgentCache.TryGetValue(workspace, out var chatClientAgent)) - { - return chatClientAgent; - } - - var chatClient = await _chatClientFactory.CreateAsync(); - - var workspaceDefine = await _workspaceDefinitionManager.GetOrNullAsync(workspace); - - string? description = null; - if (workspaceDefine?.Description != null) - { - description = workspaceDefine.Description.Localize(_stringLocalizerFactory); - } - - chatClientAgent = chatClient.CreateAIAgent( - instructions: workspaceDefine?.SystemPrompt, - name: workspaceDefine?.Name, - description: description); - - _chatClientAgentCache.TryAdd(workspace, chatClientAgent); - - return chatClientAgent; - } - - public async virtual Task CreateAsync(string workspace) - { - if (_chatClientAgentCache.TryGetValue(workspace, out var chatClientAgent)) - { - return chatClientAgent; - } - var workspaceDefine = await _workspaceDefinitionManager.GetAsync(workspace); - var chatClient = await _chatClientFactory.CreateAsync(workspace); - - string? description = null; - if (workspaceDefine.Description != null) - { - description = workspaceDefine.Description.Localize(_stringLocalizerFactory); - } - - chatClientAgent = chatClient.CreateAIAgent( - instructions: workspaceDefine.SystemPrompt, - name: workspaceDefine.Name, - description: description); - - _chatClientAgentCache.TryAdd(workspace, chatClientAgent); - - return chatClientAgent; - } -} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentFactory.cs similarity index 50% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentFactory.cs index 0d15511fd..b969e8f67 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IChatClientAgentFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentFactory.cs @@ -3,11 +3,11 @@ using Microsoft.Agents.AI; using System.Threading.Tasks; namespace LINGYUN.Abp.AI.Agent; -public interface IChatClientAgentFactory +public interface IAgentFactory { [NotNull] - Task CreateAsync(); + Task CreateAsync(); [NotNull] - Task CreateAsync(string workspace); + Task CreateAsync(string workspace); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs new file mode 100644 index 000000000..109f267d3 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/IAgentService.cs @@ -0,0 +1,8 @@ +using LINGYUN.Abp.AI.Models; +using System.Collections.Generic; + +namespace LINGYUN.Abp.AI.Agent; +public interface IAgentService +{ + IAsyncEnumerable SendMessageAsync(ChatMessage message); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs new file mode 100644 index 000000000..03998df61 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Agent/LINGYUN/Abp/AI/Agent/WorkspaceAIAgent.cs @@ -0,0 +1,59 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Agent; +public class WorkspaceAIAgent : AIAgent +{ + protected AIAgent InnerAgent { get; } + protected WorkspaceDefinition? Workspace { get; } + public WorkspaceAIAgent(AIAgent innerAgent, WorkspaceDefinition? workspace) + { + InnerAgent = innerAgent; + Workspace = workspace; + } + + public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) + { + return InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions); + } + + public override AgentThread GetNewThread() + { + return InnerAgent.GetNewThread(); + } + + protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerAgent.RunAsync(GetChatMessages(messages), thread, options, cancellationToken); + } + + protected override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerAgent.RunStreamingAsync(GetChatMessages(messages), thread, options, cancellationToken); + } + + protected virtual IEnumerable GetChatMessages(IEnumerable messages) + { + var unionMessages = new List(); + + if (Workspace != null) + { + if (!Workspace.SystemPrompt.IsNullOrWhiteSpace()) + { + unionMessages.Add(new ChatMessage(ChatRole.System, Workspace.SystemPrompt)); + } + if (!Workspace.Instructions.IsNullOrWhiteSpace()) + { + unionMessages.Add(new ChatMessage(ChatRole.System, Workspace.Instructions)); + } + } + return unionMessages.Union(messages); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj index 1c67a9757..b6e35c892 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN.Abp.AI.Core.csproj @@ -14,9 +14,16 @@ + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs index 8718805c2..9bd80e7b7 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAICoreModule.cs @@ -1,16 +1,23 @@ -using LINGYUN.Abp.AI.Localization; +using LINGYUN.Abp.AI.Internal; +using LINGYUN.Abp.AI.Localization; using LINGYUN.Abp.AI.Workspaces; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using Volo.Abp.AI; +using Volo.Abp.Guids; using Volo.Abp.Localization; +using Volo.Abp.Localization.ExceptionHandling; using Volo.Abp.Modularity; +using Volo.Abp.Timing; +using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.AI; [DependsOn( typeof(AbpAIModule), + typeof(AbpGuidsModule), + typeof(AbpTimingModule), typeof(AbpLocalizationModule))] public class AbpAICoreModule : AbpModule { @@ -21,14 +28,30 @@ public class AbpAICoreModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + Configure(options => { - options.Resources.Add(); + options.Resources + .Add() + .AddVirtualJson("/LINGYUN/Abp/AI/Localization/Resources"); }); Configure(options => { options.ChatClientProviders.Add(); + options.ChatClientProviders.Add(); + + options.KernelProviders.Add(); + options.KernelProviders.Add(); + }); + + Configure(options => + { + options.MapCodeNamespace(AbpAIErrorCodes.Namespace, typeof(AbpAIResource)); }); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs new file mode 100644 index 000000000..e5538e47e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/AbpAIErrorCodes.cs @@ -0,0 +1,13 @@ +namespace LINGYUN.Abp.AI; +public static class AbpAIErrorCodes +{ + public const string Namespace = "Abp.AI"; + /// + /// 工作区不可用: {Workspace}! + /// + public const string WorkspaceIsNotEnabled = Namespace + ":110001"; + /// + /// 对话已过期, 请重新创建会话! + /// + public const string ConversationHasExpired = Namespace + ":110101"; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs new file mode 100644 index 000000000..c15b7875f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProvider.cs @@ -0,0 +1,19 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.Extensions.AI; +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI; +public abstract class ChatClientProvider : IChatClientProvider, ITransientDependency +{ + public abstract string Name { get; } + + protected IServiceProvider ServiceProvider { get; } + protected ChatClientProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public abstract Task CreateAsync(WorkspaceDefinition workspace); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs new file mode 100644 index 000000000..724eb5a43 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IChatMessageStore.cs @@ -0,0 +1,12 @@ +using LINGYUN.Abp.AI.Models; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Chats; +public interface IChatMessageStore +{ + Task SaveMessageAsync(ChatMessage message); + + Task> GetHistoryMessagesAsync(Guid conversationId); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs new file mode 100644 index 000000000..e13119c9d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/IConversationStore.cs @@ -0,0 +1,13 @@ +using LINGYUN.Abp.AI.Models; +using System; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Chats; +public interface IConversationStore +{ + Task SaveAsync(Conversation conversation); + + Task FindAsync(Guid conversationId); + + Task CleanupAsync(); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs new file mode 100644 index 000000000..3519ea30c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryChatMessageStore.cs @@ -0,0 +1,52 @@ +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 InMemoryChatMessageStore : IChatMessageStore +{ + private static readonly ConcurrentDictionary> _userMessageCache = new ConcurrentDictionary>(); + + public Task> GetHistoryMessagesAsync(Guid conversationId) + { + var messages = new List(); + + foreach (var userMessages in _userMessageCache.Values) + { + messages.AddRange(userMessages.Where(x => x.ConversationId == conversationId)); + } + + return Task.FromResult>( + messages + .OrderByDescending(x => x.CreatedAt) + .Take(5) + .OrderBy(x => x.CreatedAt)); + } + + public Task SaveMessageAsync(ChatMessage message) + { + var messageId = message.Id; + if (!messageId.HasValue) + { + messageId = Guid.NewGuid(); + message.WithMessageId(messageId.Value); + } + if (_userMessageCache.ContainsKey(messageId.Value)) + { + _userMessageCache[messageId.Value].Add(message); + } + else + { + _userMessageCache[messageId.Value] = new List() { message }; + } + + return Task.FromResult(messageId.Value); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs new file mode 100644 index 000000000..d273e47eb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Chats/InMemoryConversationStore.cs @@ -0,0 +1,48 @@ +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Chats; + +[Dependency(ServiceLifetime.Singleton, TryRegister = true)] +public class InMemoryConversationStore : IConversationStore +{ + private static readonly ConcurrentDictionary _conversationCache = new ConcurrentDictionary(); + 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 FindAsync(Guid conversationId) + { + _conversationCache.TryGetValue(conversationId, out var conversation); + return Task.FromResult(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; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientFactory.cs similarity index 65% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientFactory.cs index bbdfa9c90..a369c05e0 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientFactory.cs @@ -1,25 +1,28 @@ using LINGYUN.Abp.AI.Workspaces; using Microsoft.Extensions.AI; using System; -using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AI; +using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; +using Volo.Abp.SimpleStateChecking; -namespace LINGYUN.Abp.AI; -public class ChatClientFactory : IChatClientFactory, ISingletonDependency +namespace LINGYUN.Abp.AI.Internal; +public class ChatClientFactory : IChatClientFactory, IScopedDependency { - private readonly static ConcurrentDictionary _chatClientCache = new(); + protected ISimpleStateCheckerManager StateCheckerManager { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected IChatClientProviderManager ChatClientProviderManager { get; } protected IServiceProvider ServiceProvider { get; } public ChatClientFactory( + ISimpleStateCheckerManager stateCheckerManager, IWorkspaceDefinitionManager workspaceDefinitionManager, IChatClientProviderManager chatClientProviderManager, IServiceProvider serviceProvider) { + StateCheckerManager = stateCheckerManager; WorkspaceDefinitionManager = workspaceDefinitionManager; ChatClientProviderManager = chatClientProviderManager; ServiceProvider = serviceProvider; @@ -28,10 +31,6 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency public async virtual Task CreateAsync() { var workspace = WorkspaceNameAttribute.GetWorkspaceName(); - if (_chatClientCache.TryGetValue(workspace, out var chatClient)) - { - return chatClient; - } var chatClientAccessorType = typeof(IChatClientAccessor<>).MakeGenericType(typeof(TWorkspace)); var chatClientAccessor = ServiceProvider.GetService(chatClientAccessorType); @@ -39,30 +38,19 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency chatClientAccessor is IChatClientAccessor accessor && accessor.ChatClient != null) { - chatClient = accessor.ChatClient; - _chatClientCache.TryAdd(workspace, chatClient); - } - else - { - chatClient = await CreateAsync(workspace); + return accessor.ChatClient; } - return chatClient; + + return await CreateAsync(workspace); } public async virtual Task CreateAsync(string workspace) { - if (_chatClientCache.TryGetValue(workspace, out var chatClient)) - { - return chatClient; - } - var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); - chatClient = await CreateChatClientAsync(workspaceDefine); - - _chatClientCache.TryAdd(workspace, chatClient); + await CheckWorkspaceStateAsync(workspaceDefine); - return chatClient; + return await CreateChatClientAsync(workspaceDefine); } protected async virtual Task CreateChatClientAsync(WorkspaceDefinition workspace) @@ -77,6 +65,17 @@ public class ChatClientFactory : IChatClientFactory, ISingletonDependency return await provider.CreateAsync(workspace); } - throw new AbpException($"The ChatClient provider implementation named {workspace.Provider} was not found"); + throw new AbpException($"The ChatClient provider implementation named {workspace.Provider} was not found!"); + } + + protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) + { + if (!await StateCheckerManager.IsEnabledAsync(workspace)) + { + throw new AbpAuthorizationException( + $"Workspace is not enabled: {workspace.Name}!", + AbpAIErrorCodes.WorkspaceIsNotEnabled) + .WithData("Workspace", workspace.Name); + } } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProviderManager.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientProviderManager.cs similarity index 97% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProviderManager.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientProviderManager.cs index 8d3cc80ec..09495b231 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/ChatClientProviderManager.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/ChatClientProviderManager.cs @@ -7,7 +7,7 @@ using System.Linq; using Volo.Abp; using Volo.Abp.DependencyInjection; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class ChatClientProviderManager : IChatClientProviderManager, ISingletonDependency { public List Providers => _lazyProviders.Value; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekChatClientProvider.cs new file mode 100644 index 000000000..bc0863f33 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekChatClientProvider.cs @@ -0,0 +1,15 @@ +using System; + +namespace LINGYUN.Abp.AI.Internal; +public class DeepSeekChatClientProvider : OpenAIChatClientProvider +{ + protected override string DefaultEndpoint => "https://api.deepseek.com/v1"; + + public new const string ProviderName = "DeepSeek"; + public override string Name => ProviderName; + public DeepSeekChatClientProvider( + IServiceProvider serviceProvider) + : base(serviceProvider) + { + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekKernelProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekKernelProvider.cs new file mode 100644 index 000000000..c27cb0e41 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/DeepSeekKernelProvider.cs @@ -0,0 +1,13 @@ +using System; + +namespace LINGYUN.Abp.AI.Internal; +public class DeepSeekKernelProvider : OpenAIKernelProvider +{ + protected override string DefaultEndpoint => "https://api.deepseek.com/v1"; + + public new const string ProviderName = "DeepSeek"; + public override string Name => ProviderName; + public DeepSeekKernelProvider(IServiceProvider serviceProvider) : base(serviceProvider) + { + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelFactory.cs similarity index 64% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelFactory.cs index 5709d9127..23c6aeda7 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelFactory.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelFactory.cs @@ -1,25 +1,28 @@ using LINGYUN.Abp.AI.Workspaces; using Microsoft.SemanticKernel; using System; -using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AI; +using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; +using Volo.Abp.SimpleStateChecking; -namespace LINGYUN.Abp.AI; -public class KernelFactory : IKernelFactory, ISingletonDependency +namespace LINGYUN.Abp.AI.Internal; +public class KernelFactory : IKernelFactory, IScopedDependency { - private readonly static ConcurrentDictionary _kernelCache = new(); + protected ISimpleStateCheckerManager StateCheckerManager { get; } protected IWorkspaceDefinitionManager WorkspaceDefinitionManager { get; } protected IKernelProviderManager KernelProviderManager { get; } protected IServiceProvider ServiceProvider { get; } public KernelFactory( + ISimpleStateCheckerManager stateCheckerManager, IWorkspaceDefinitionManager workspaceDefinitionManager, IKernelProviderManager kernelProviderManager, IServiceProvider serviceProvider) { + StateCheckerManager = stateCheckerManager; WorkspaceDefinitionManager = workspaceDefinitionManager; KernelProviderManager = kernelProviderManager; ServiceProvider = serviceProvider; @@ -28,10 +31,6 @@ public class KernelFactory : IKernelFactory, ISingletonDependency public async virtual Task CreateAsync() { var workspace = WorkspaceNameAttribute.GetWorkspaceName(); - if (_kernelCache.TryGetValue(workspace, out var kernel)) - { - return kernel; - } var kernelAccessorType = typeof(IKernelAccessor<>).MakeGenericType(typeof(TWorkspace)); var kernelAccessor = ServiceProvider.GetService(kernelAccessorType); @@ -39,30 +38,18 @@ public class KernelFactory : IKernelFactory, ISingletonDependency kernelAccessor is IKernelAccessor accessor && accessor.Kernel != null) { - kernel = accessor.Kernel; - _kernelCache.TryAdd(workspace, kernel); - } - else - { - kernel = await CreateAsync(workspace); + return accessor.Kernel; } - return kernel; + return await CreateAsync(workspace); } public async virtual Task CreateAsync(string workspace) { - if (_kernelCache.TryGetValue(workspace, out var kernel)) - { - return kernel; - } - var workspaceDefine = await WorkspaceDefinitionManager.GetAsync(workspace); - kernel = await CreateKernelAsync(workspaceDefine); - - _kernelCache.TryAdd(workspace, kernel); + await CheckWorkspaceStateAsync(workspaceDefine); - return kernel; + return await CreateKernelAsync(workspaceDefine); } protected async virtual Task CreateKernelAsync(WorkspaceDefinition workspace) @@ -77,6 +64,16 @@ public class KernelFactory : IKernelFactory, ISingletonDependency return await provider.CreateAsync(workspace); } - throw new AbpException($"The Kernel provider implementation named {workspace.Provider} was not found"); + throw new AbpException($"The Kernel provider implementation named {workspace.Provider} was not found!"); + } + protected async virtual Task CheckWorkspaceStateAsync(WorkspaceDefinition workspace) + { + if (!await StateCheckerManager.IsEnabledAsync(workspace)) + { + throw new AbpAuthorizationException( + $"Workspace is not enabled: {workspace.Name}!", + AbpAIErrorCodes.WorkspaceIsNotEnabled) + .WithData("Workspace", workspace.Name); + } } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProviderManager.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelProviderManager.cs similarity index 97% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProviderManager.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelProviderManager.cs index f2beb0025..ab4bd6221 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProviderManager.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/KernelProviderManager.cs @@ -6,7 +6,7 @@ using System.Linq; using Volo.Abp; using Volo.Abp.DependencyInjection; -namespace LINGYUN.Abp.AI; +namespace LINGYUN.Abp.AI.Internal; public class KernelProviderManager : IKernelProviderManager, ISingletonDependency { public List Providers => _lazyProviders.Value; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIChatClientProvider.cs similarity index 54% rename from aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs rename to aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIChatClientProvider.cs index f8d145334..6f2ab1234 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/OpenAIChatClientProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIChatClientProvider.cs @@ -5,17 +5,20 @@ using System; using System.ClientModel; using System.Threading.Tasks; using Volo.Abp; -using Volo.Abp.DependencyInjection; -namespace LINGYUN.Abp.AI; -public class OpenAIChatClientProvider : IChatClientProvider, ITransientDependency +namespace LINGYUN.Abp.AI.Internal; +public class OpenAIChatClientProvider : ChatClientProvider { - private const string DefaultEndpoint = "https://api.openai.com/v1"; - public const string ProviderName = "OpenAI"; + protected virtual string DefaultEndpoint => "https://api.openai.com/v1"; - public virtual string Name => ProviderName; + public const string ProviderName = "OpenAI"; + public override string Name => ProviderName; + public OpenAIChatClientProvider(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } - public virtual Task CreateAsync(WorkspaceDefinition workspace) + public override Task CreateAsync(WorkspaceDefinition workspace) { Check.NotNull(workspace, nameof(workspace)); Check.NotNullOrWhiteSpace(workspace.ApiKey, nameof(WorkspaceDefinition.ApiKey)); @@ -29,7 +32,13 @@ public class OpenAIChatClientProvider : IChatClientProvider, ITransientDependenc var chatClient = openAIClient .GetChatClient(workspace.ModelName) - .AsIChatClient(); + .AsIChatClient() + .AsBuilder() + .UseLogging() + .UseOpenTelemetry() + .UseFunctionInvocation() + .UseDistributedCache() + .Build(ServiceProvider); return Task.FromResult(chatClient); } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIKernelProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIKernelProvider.cs new file mode 100644 index 000000000..2eb16859e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Internal/OpenAIKernelProvider.cs @@ -0,0 +1,39 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.SemanticKernel; +using OpenAI; +using System; +using System.ClientModel; +using System.Threading.Tasks; +using Volo.Abp; + +namespace LINGYUN.Abp.AI.Internal; +public class OpenAIKernelProvider : KernelProvider +{ + protected virtual string DefaultEndpoint { get; set; } = "https://api.openai.com/v1"; + + public const string ProviderName = "OpenAI"; + public override string Name => ProviderName; + public OpenAIKernelProvider(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + public override Task CreateAsync(WorkspaceDefinition workspace) + { + Check.NotNull(workspace, nameof(workspace)); + Check.NotNullOrWhiteSpace(workspace.ApiKey, nameof(WorkspaceDefinition.ApiKey)); + + var openAIClient = new OpenAIClient( + new ApiKeyCredential(workspace.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(workspace.ApiBaseUrl ?? DefaultEndpoint), + }); + + var kernel = Kernel.CreateBuilder() + .AddOpenAIChatClient(workspace.ModelName, openAIClient) + .Build(); + + return Task.FromResult(kernel); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProvider.cs new file mode 100644 index 000000000..957f2a43c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/KernelProvider.cs @@ -0,0 +1,19 @@ +using LINGYUN.Abp.AI.Workspaces; +using Microsoft.SemanticKernel; +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI; +public abstract class KernelProvider : IKernelProvider, ITransientDependency +{ + public abstract string Name { get; } + + protected IServiceProvider ServiceProvider { get; } + protected KernelProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public abstract Task CreateAsync(WorkspaceDefinition workspace); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json new file mode 100644 index 000000000..5e631f34c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/en.json @@ -0,0 +1,8 @@ +{ + "culture": "en", + "texts": { + "Abp.AI:110001": "Workspace is not enabled: {Workspace}!", + "Abp.AI:110101": "The conversation has expired. Please create a new one!", + "NewConversation": "New Conversation" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..8681eedaf --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Localization/Resources/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "culture": "zh-Hans", + "texts": { + "Abp.AI:110001": "工作区不可用: {Workspace}!", + "Abp.AI:110101": "对话已过期, 请重新创建会话!", + "NewConversation": "新对话" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs new file mode 100644 index 000000000..143a537de --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/ChatMessage.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.AI; +using System; + +namespace LINGYUN.Abp.AI.Models; +public abstract class ChatMessage +{ + public string Workspace { get; } + + public Guid? Id { get; private set; } + + public Guid? ConversationId { get; private set; } + + public string? ReplyMessage { get; private set; } + + public DateTime? ReplyAt { get; private set; } + + public ChatRole Role { get; private set; } + + public DateTime CreatedAt { get; private set; } + protected ChatMessage( + string workspace, + ChatRole? role = null, + DateTime? createdAt = null) + { + Workspace = workspace; + Role = role ?? ChatRole.User; + CreatedAt = createdAt ?? DateTime.Now; + } + + public virtual ChatMessage WithMessageId(Guid id) + { + Id = id; + return this; + } + + public virtual ChatMessage WithConversationId(Guid conversationId) + { + ConversationId = conversationId; + return this; + } + + public virtual ChatMessage WithReply(string replyMessage, DateTime replyAt) + { + ReplyMessage = replyMessage; + ReplyAt = replyAt; + return this; + } + + public virtual string GetMessagePrompt() + { + return string.Empty; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs new file mode 100644 index 000000000..52943a7a1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/Conversation.cs @@ -0,0 +1,28 @@ +using System; + +namespace LINGYUN.Abp.AI.Models; +public class Conversation +{ + public Guid Id { get; private set; } + public string Name { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime? ExpiredAt { get; set; } + public DateTime? UpdateAt { get; set; } + public Conversation( + Guid id, + string name, + DateTime createdAt) + { + Id = id; + Name = name; + CreatedAt = createdAt; + UpdateAt = createdAt; + } + + public Conversation WithName(string name) + { + Name = name; + + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/MediaMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/MediaMessage.cs new file mode 100644 index 000000000..f851d303f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/MediaMessage.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Content; + +namespace LINGYUN.Abp.AI.Models; +public class MediaMessage +{ + public string Id { get; } + public IRemoteStreamContent Content { get; } + public MediaMessage(string id, IRemoteStreamContent content) + { + Id = id; + Content = content; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TextChatMessage.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TextChatMessage.cs new file mode 100644 index 000000000..20ea67e1c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TextChatMessage.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.AI; +using System; + +namespace LINGYUN.Abp.AI.Models; +public class TextChatMessage : ChatMessage +{ + /// + /// 消息内容 + /// + public string Content { get; } + public TextChatMessage( + string workspace, + string content, + ChatRole? role = null, + DateTime? createdAt = null) + : base(workspace, role, createdAt) + { + Content = content; + } + + public override string GetMessagePrompt() + { + return Content; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs new file mode 100644 index 000000000..f74b1867e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Models/TokenUsageInfo.cs @@ -0,0 +1,42 @@ +using System; +using System.Text; + +namespace LINGYUN.Abp.AI.Models; +public class TokenUsageInfo +{ + public string Workspace { get; } + public Guid? MessageId { get; private set; } + public Guid ConversationId { get; private set; } + public long? InputTokenCount { get; set; } + public long? OutputTokenCount { get; set; } + public long? TotalTokenCount { get; set; } + public long? CachedInputTokenCount { get; set; } + public long? ReasoningTokenCount { get; set; } + public TokenUsageInfo(string workspace, Guid conversationId) + { + Workspace = workspace; + ConversationId = conversationId; + } + public virtual TokenUsageInfo WithMessageId(Guid id) + { + MessageId = id; + return this; + } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine("---------------------- TokenUsage Begin ----------------------"); + sb.AppendLine($"====== Workspace - {Workspace}"); + sb.AppendLine($"====== MessageId - {MessageId}"); + sb.AppendLine($"====== ConversationId - {ConversationId}"); + sb.AppendLine($"====== InputTokenCount - {InputTokenCount}"); + sb.AppendLine($"====== OutputTokenCount - {OutputTokenCount}"); + sb.AppendLine($"====== TotalTokenCount - {TotalTokenCount}"); + sb.AppendLine($"====== ReasoningTokenCount - {ReasoningTokenCount}"); + sb.AppendLine("---------------------- TokenUsage End ----------------------"); + + return sb.ToString(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs new file mode 100644 index 000000000..dd44f988a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/ITokenUsageStore.cs @@ -0,0 +1,8 @@ +using LINGYUN.Abp.AI.Models; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Tokens; +public interface ITokenUsageStore +{ + Task SaveTokenUsageAsync(TokenUsageInfo tokenUsageInfo); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs new file mode 100644 index 000000000..9f492b8b2 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tokens/InMemoryTokenUsageStore.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Tokens; + +[Dependency(ServiceLifetime.Singleton, TryRegister = true)] +public class InMemoryTokenUsageStore : ITokenUsageStore +{ + private static readonly ConcurrentDictionary> _tokenUsageCache = new ConcurrentDictionary>(); + + public Task SaveTokenUsageAsync(TokenUsageInfo tokenUsageInfo) + { + if (_tokenUsageCache.TryGetValue(tokenUsageInfo.Workspace, out var tokenUsageInfos)) + { + tokenUsageInfos.Add(tokenUsageInfo); + } + else + { + _tokenUsageCache.TryAdd(tokenUsageInfo.Workspace, [tokenUsageInfo]); + } + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tools/GlobalFunctions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tools/GlobalFunctions.cs new file mode 100644 index 000000000..cc52e1fd7 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Tools/GlobalFunctions.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.AI; +using System; + +namespace LINGYUN.Abp.AI.Tools; +public static class GlobalFunctions +{ + public static AITool Now => AIFunctionFactory.Create( + () => DateTime.Now, + nameof(Now), + "Get now time"); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs index 56f37e127..2f3288616 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Core/LINGYUN/Abp/AI/Workspaces/WorkspaceDefinition.cs @@ -41,16 +41,84 @@ public class WorkspaceDefinition : IHasSimpleStateCheckers /// /// API 身份验证密钥 /// - public string? ApiKey { get; set; } + public string? ApiKey { get; private set; } /// /// 自定义端点 URL /// - public string? ApiBaseUrl { get; set; } + public string? ApiBaseUrl { get; private set; } /// /// 系统提示词 /// public string? SystemPrompt { get; set; } /// + /// 附加系统提示词 + /// + public string? Instructions { get; set; } + /// + /// 聊天回复时所依据的温度值, 为空时由模型提供者决定默认值 + /// + /// + /// 范围在 0 到 2 之间, 数值越高(比如 0.8)会使输出更加随机,而数值越低(比如 0.2)则会使输出更加集中且更具确定性 + /// + public float? Temperature { + get => _temperature; + set { + if (value.HasValue) + { + _temperature = Check.Range(value.Value, nameof(value), 0, 2); + } + else + { + _temperature = value; + } + } + } + private float? _temperature; + /// + /// 限制一次请求中模型生成 completion 的最大 token 数 + /// + public int? MaxOutputTokens { get; set; } + /// + /// 介于 -2.0 和 2.0 之间的数字 + /// + /// + /// 如果该值为正,那么新 token 会根据其在已有文本中的出现频率受到相应的惩罚,降低模型重复相同内容的可能性 + /// + public float? FrequencyPenalty { + get => _frequencyPenalty; + set { + if (value.HasValue) + { + _frequencyPenalty = Check.Range(value.Value, nameof(value), -2, 2); + } + else + { + _frequencyPenalty = value; + } + } + } + private float? _frequencyPenalty; + /// + /// 介于 -2.0 和 2.0 之间的数字 + /// + /// + /// 如果该值为正,那么新 token 会根据其是否已在已有文本中出现受到相应的惩罚,从而增加模型谈论新主题的可能性 + /// + public float? PresencePenalty { + get => _presencePenalty; + set { + if (value.HasValue) + { + _presencePenalty = Check.Range(value.Value, nameof(value), -2, 2); + } + else + { + _presencePenalty = value; + } + } + } + private float? _presencePenalty; + /// /// 启用/禁用工作区 /// public bool IsEnabled { get; set; } @@ -65,7 +133,13 @@ public class WorkspaceDefinition : IHasSimpleStateCheckers string provider, string modelName, ILocalizableString displayName, - ILocalizableString? description = null) + ILocalizableString? description = null, + string? systemPrompt = null, + string? instructions = null, + float? temperature = null, + int? maxOutputTokens = null, + float? frequencyPenalty = null, + float? presencePenalty = null) { Name = name; Provider = provider; @@ -73,6 +147,12 @@ public class WorkspaceDefinition : IHasSimpleStateCheckers _displayName = displayName; _displayName = displayName; Description = description; + SystemPrompt = systemPrompt; + Instructions = instructions; + Temperature = temperature; + MaxOutputTokens = maxOutputTokens; + FrequencyPenalty = frequencyPenalty; + PresencePenalty = presencePenalty; IsEnabled = true; Properties = new Dictionary(); diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj new file mode 100644 index 000000000..1d2169985 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN.Abp.AIManagement.Application.Contracts.csproj @@ -0,0 +1,26 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + LINGYUN.Abp.AIManagement.Application.Contracts + LINGYUN.Abp.AIManagement.Application.Contracts + false + false + false + enable + + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AIManagementRemoteServiceConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AIManagementRemoteServiceConsts.cs new file mode 100644 index 000000000..b68f59f17 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AIManagementRemoteServiceConsts.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Abp.AIManagement; +public static class AIManagementRemoteServiceConsts +{ + public const string RemoteServiceName = "AbpAIManagement"; + + public const string ModuleName = "ai-management"; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationContractsModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationContractsModule.cs new file mode 100644 index 000000000..289fb8ed2 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationContractsModule.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Application; +using Volo.Abp.Authorization; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementDomainSharedModule), + typeof(AbpDddApplicationContractsModule), + typeof(AbpAuthorizationAbstractionsModule))] +public class AbpAIManagementApplicationContractsModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ChatMessageDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ChatMessageDto.cs new file mode 100644 index 000000000..035fc1464 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ChatMessageDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public abstract class ChatMessageDto : ExtensibleAuditedEntityDto +{ + public string Workspace { get; set; } + + public DateTime CreatedAt { get; set; } + + public Guid? UserId { get; set; } + + public Guid? ConversationId { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationCreateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationCreateDto.cs new file mode 100644 index 000000000..8112305b2 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationCreateDto.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationCreateDto +{ + [DynamicStringLength(typeof(ConversationRecordConsts), nameof(ConversationRecordConsts.MaxNameLength))] + public string? Name { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationDto.cs new file mode 100644 index 000000000..8f5945c95 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationDto : AuditedEntityDto +{ + public string Name { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime ExpiredAt { get; set; } + + public DateTime? UpdateAt { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationGetListInput.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationGetListInput.cs new file mode 100644 index 000000000..d3b889f48 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationGetListInput.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationGetListInput : PagedAndSortedResultRequestDto +{ + public string? Filter { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationUpdateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationUpdateDto.cs new file mode 100644 index 000000000..4469eba6d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/ConversationUpdateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class ConversationUpdateDto +{ + [Required] + [DynamicStringLength(typeof(ConversationRecordConsts), nameof(ConversationRecordConsts.MaxNameLength))] + public string Name { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/SendTextChatMessageDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/SendTextChatMessageDto.cs new file mode 100644 index 000000000..ad0372260 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/SendTextChatMessageDto.cs @@ -0,0 +1,19 @@ +using LINGYUN.Abp.AIManagement.Workspaces; +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class SendTextChatMessageDto +{ + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxNameLength))] + public string Workspace { get; set; } + + [Required] + public Guid ConversationId { get; set; } + + [Required] + [DynamicStringLength(typeof(TextChatMessageRecordConsts), nameof(TextChatMessageRecordConsts.MaxContentLength))] + public string Content { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/TextChatMessageDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/TextChatMessageDto.cs new file mode 100644 index 000000000..1ef328229 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/Dtos/TextChatMessageDto.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LINGYUN.Abp.AIManagement.Chats.Dtos; +public class TextChatMessageDto +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IChatAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IChatAppService.cs new file mode 100644 index 000000000..dcfaa06ca --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IChatAppService.cs @@ -0,0 +1,9 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using System.Collections.Generic; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement.Chats; +public interface IChatAppService : IApplicationService +{ + IAsyncEnumerable SendMessageAsync(SendTextChatMessageDto input); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IConversationAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IConversationAppService.cs new file mode 100644 index 000000000..a59c397cb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Chats/IConversationAppService.cs @@ -0,0 +1,14 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using System; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement.Chats; +public interface IConversationAppService : + ICrudAppService< + ConversationDto, + Guid, + ConversationGetListInput, + ConversationCreateDto, + ConversationUpdateDto> +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateDto.cs new file mode 100644 index 000000000..9591c2773 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; +public class WorkspaceDefinitionRecordCreateDto : WorkspaceDefinitionRecordCreateOrUpdateDto +{ + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxNameLength))] + public string Name { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateOrUpdateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateOrUpdateDto.cs new file mode 100644 index 000000000..dcb866294 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordCreateOrUpdateDto.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.ObjectExtending; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; +public abstract class WorkspaceDefinitionRecordCreateOrUpdateDto : ExtensibleObject +{ + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxProviderLength))] + public string Provider { get; set; } + + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxModelNameLength))] + public string ModelName { get; set; } + + [Required] + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength))] + public string DisplayName { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxDescriptionLength))] + public string? Description { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxApiKeyLength))] + public string? ApiKey { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength))] + public string? ApiBaseUrl { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength))] + public string? SystemPrompt { get; set; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxInstructionsLength))] + 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; } + + [DynamicStringLength(typeof(WorkspaceDefinitionRecordConsts), nameof(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength))] + public string? StateCheckers { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordDto.cs new file mode 100644 index 000000000..f65dd0ee9 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordDto.cs @@ -0,0 +1,39 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Entities; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; + +[Serializable] +public class WorkspaceDefinitionRecordDto : ExtensibleAuditedEntityDto, IHasConcurrencyStamp +{ + public string Name { get; set; } + + public string Provider { get; set; } + + public string ModelName { get; set; } + + public string DisplayName { get; set; } + + public string? Description { get; set; } + + public string? ApiBaseUrl { get; 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; } + + public string ConcurrencyStamp { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordGetListInput.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordGetListInput.cs new file mode 100644 index 000000000..a4f99fb6c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordGetListInput.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; + +[Serializable] +public class WorkspaceDefinitionRecordGetListInput : PagedAndSortedResultRequestDto +{ + public string? Filter { get; set; } + + public string? Provider { get; set; } + + public string? ModelName { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordUpdateDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordUpdateDto.cs new file mode 100644 index 000000000..848ef7394 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/Dtos/WorkspaceDefinitionRecordUpdateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Domain.Entities; + +namespace LINGYUN.Abp.AIManagement.Workspaces.Dtos; +public class WorkspaceDefinitionRecordUpdateDto : WorkspaceDefinitionRecordCreateOrUpdateDto, IHasConcurrencyStamp +{ + [Required] + [StringLength(40)] + public string ConcurrencyStamp { get; set; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionAppService.cs new file mode 100644 index 000000000..6ebb52c95 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionAppService.cs @@ -0,0 +1,14 @@ +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using System; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public interface IWorkspaceDefinitionAppService : + ICrudAppService< + WorkspaceDefinitionRecordDto, + Guid, + WorkspaceDefinitionRecordGetListInput, + WorkspaceDefinitionRecordCreateDto, + WorkspaceDefinitionRecordUpdateDto> +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN.Abp.AIManagement.Application.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN.Abp.AIManagement.Application.csproj new file mode 100644 index 000000000..5706b07d9 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN.Abp.AIManagement.Application.csproj @@ -0,0 +1,26 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.Application + LINGYUN.Abp.AIManagement.Application + false + false + false + enable + + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AIManagementApplicationServiceBase.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AIManagementApplicationServiceBase.cs new file mode 100644 index 000000000..291b27256 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AIManagementApplicationServiceBase.cs @@ -0,0 +1,12 @@ +using LINGYUN.Abp.AIManagement.Localization; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.AIManagement; +public abstract class AIManagementApplicationServiceBase : ApplicationService +{ + protected AIManagementApplicationServiceBase() + { + LocalizationResource = typeof(AIManagementResource); + ObjectMapperContext = typeof(AbpAIManagementApplicationModule); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationMappers.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationMappers.cs new file mode 100644 index 000000000..adc71e599 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationMappers.cs @@ -0,0 +1,15 @@ +using LINGYUN.Abp.AIManagement.Workspaces; +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.ObjectExtending; + +namespace LINGYUN.Abp.AIManagement; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties(DefinitionChecks = MappingPropertyDefinitionChecks.None)] +public partial class WorkspaceDefinitionRecordToWorkspaceDefinitionRecordDtoMapper : MapperBase +{ + public override partial WorkspaceDefinitionRecordDto Map(WorkspaceDefinitionRecord source); + public override partial void Map(WorkspaceDefinitionRecord source, WorkspaceDefinitionRecordDto destination); +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationModule.cs new file mode 100644 index 000000000..4b5413042 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/AbpAIManagementApplicationModule.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Application; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementApplicationContractsModule), + typeof(AbpAIManagementDomainModule), + typeof(AbpDddApplicationModule))] +public class AbpAIManagementApplicationModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ChatAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ChatAppService.cs new file mode 100644 index 000000000..fe420a66a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ChatAppService.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.AI.Agent; +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using Microsoft.Extensions.AI; +using System.Collections.Generic; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ChatAppService : AIManagementApplicationServiceBase, IChatAppService +{ + private readonly IAgentService _agentService; + public ChatAppService(IAgentService agentService) + { + _agentService = agentService; + } + + public IAsyncEnumerable SendMessageAsync(SendTextChatMessageDto input) + { + var chatMessage = new TextChatMessage( + input.Workspace, + input.Content, + ChatRole.User, + Clock.Now); + + chatMessage.WithConversationId(input.ConversationId); + + return _agentService.SendMessageAsync(chatMessage); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ConversationAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ConversationAppService.cs new file mode 100644 index 000000000..203747602 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Chats/ConversationAppService.cs @@ -0,0 +1,82 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using LINGYUN.Abp.AIManagement.Localization; +using Microsoft.Extensions.Options; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ConversationAppService : + CrudAppService< + ConversationRecord, + ConversationDto, + Guid, + ConversationGetListInput, + ConversationCreateDto, + ConversationUpdateDto>, + IConversationAppService +{ + private readonly ConversationCleanupOptions _cleanupOptions; + public ConversationAppService( + IRepository repository, + IOptions cleanupOptions) + : base(repository) + { + _cleanupOptions = cleanupOptions.Value; + + LocalizationResource = typeof(AIManagementResource); + ObjectMapperContext = typeof(AbpAIManagementApplicationModule); + } + + protected async override Task> CreateFilteredQueryAsync(ConversationGetListInput input) + { + var queryable = await base.CreateFilteredQueryAsync(input); + + return queryable + .WhereIf(!input.Filter.IsNullOrWhiteSpace(), x => x.Name.Contains(input.Filter!)); + } + + protected override ConversationRecord MapToEntity(ConversationCreateDto createInput) + { + var createdAt = Clock.Now; + var expiredTime = createdAt.Add(_cleanupOptions.ExpiredTime); + var conversationName = createInput.Name ?? L["NewConversation"]; + return new ConversationRecord( + GuidGenerator.Create(), + conversationName, + createdAt, + expiredTime, + CurrentTenant.Id); + } + + protected override void MapToEntity(ConversationUpdateDto updateInput, ConversationRecord entity) + { + if (!string.Equals(entity.Name, updateInput.Name, StringComparison.InvariantCultureIgnoreCase)) + { + entity.SetName(updateInput.Name); + } + } + + protected override ConversationDto MapToGetOutputDto(ConversationRecord entity) + { + return new ConversationDto + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + ExpiredAt = entity.ExpiredAt, + CreationTime = entity.CreationTime, + CreatorId = entity.CreatorId, + LastModificationTime = entity.LastModificationTime, + LastModifierId = entity.LastModifierId, + Name = entity.Name, + UpdateAt = entity.UpdateAt, + }; + } + + protected override ConversationDto MapToGetListOutputDto(ConversationRecord entity) + { + return MapToGetOutputDto(entity); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionAppService.cs new file mode 100644 index 000000000..e249d025b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionAppService.cs @@ -0,0 +1,152 @@ +using LINGYUN.Abp.AIManagement.Localization; +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Data; +using Volo.Abp.Security.Encryption; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDefinitionAppService : + CrudAppService< + WorkspaceDefinitionRecord, + WorkspaceDefinitionRecordDto, + Guid, + WorkspaceDefinitionRecordGetListInput, + WorkspaceDefinitionRecordCreateDto, + WorkspaceDefinitionRecordUpdateDto>, + IWorkspaceDefinitionAppService +{ + protected IStringEncryptionService StringEncryptionService { get; } + protected IWorkspaceDefinitionRecordRepository WorkspaceDefinitionRecordRepository { get; } + public WorkspaceDefinitionAppService( + IStringEncryptionService stringEncryptionService, + IWorkspaceDefinitionRecordRepository repository) : base(repository) + { + StringEncryptionService = stringEncryptionService; + WorkspaceDefinitionRecordRepository = repository; + + LocalizationResource = typeof(AIManagementResource); + ObjectMapperContext = typeof(AbpAIManagementApplicationModule); + } + + protected async override Task> CreateFilteredQueryAsync(WorkspaceDefinitionRecordGetListInput input) + { + var queryable = await base.CreateFilteredQueryAsync(input); + + return queryable + .WhereIf(!input.Provider.IsNullOrWhiteSpace(), x => x.Provider == input.Provider) + .WhereIf(!input.ModelName.IsNullOrWhiteSpace(), x => x.ModelName == input.ModelName) + .WhereIf(!input.Filter.IsNullOrWhiteSpace(), x => x.Provider.Contains(input.Filter!) || + x.ModelName.Contains(input.Filter!) || x.DisplayName.Contains(input.Filter!) || + (!x.Description.IsNullOrWhiteSpace() && x.Description.Contains(input.Filter!)) || + (!x.SystemPrompt.IsNullOrWhiteSpace() && x.SystemPrompt.Contains(input.Filter!)) || + (!x.Instructions.IsNullOrWhiteSpace() && x.Instructions.Contains(input.Filter!))); + } + + protected async override Task MapToEntityAsync(WorkspaceDefinitionRecordCreateDto createInput) + { + if (await WorkspaceDefinitionRecordRepository.FindByNameAsync(createInput.Name) != null) + { + throw new WorkspaceAlreadyExistsException(createInput.Name); + } + + var record = new WorkspaceDefinitionRecord( + GuidGenerator.Create(), + createInput.Name, + createInput.Provider, + createInput.ModelName, + createInput.DisplayName, + createInput.Description, + createInput.SystemPrompt, + createInput.Instructions, + createInput.Temperature, + createInput.MaxOutputTokens, + createInput.FrequencyPenalty, + createInput.PresencePenalty, + createInput.StateCheckers); + + if (!createInput.ApiKey.IsNullOrWhiteSpace()) + { + var encryptApiKey = StringEncryptionService.Encrypt(createInput.ApiKey); + record.SetApiKey(encryptApiKey, createInput.ApiBaseUrl); + } + + return record; + } + + protected override void MapToEntity(WorkspaceDefinitionRecordUpdateDto updateInput, WorkspaceDefinitionRecord entity) + { + if (entity.DisplayName != updateInput.DisplayName) + { + entity.SetDisplayName(updateInput.DisplayName); + } + + if (entity.Description != updateInput.Description) + { + entity.Description = updateInput.Description; + } + + if (entity.Provider != updateInput.Provider || entity.ModelName != updateInput.ModelName) + { + entity.SetModel(updateInput.Provider, updateInput.ModelName); + } + + if (entity.SystemPrompt != updateInput.SystemPrompt) + { + entity.SystemPrompt = updateInput.SystemPrompt; + } + + if (entity.Instructions != updateInput.Instructions) + { + entity.Instructions = updateInput.Instructions; + } + + if (entity.IsEnabled != updateInput.IsEnabled) + { + entity.IsEnabled = updateInput.IsEnabled; + } + + if (entity.Temperature != updateInput.Temperature) + { + entity.Temperature = updateInput.Temperature; + } + + if (entity.MaxOutputTokens != updateInput.MaxOutputTokens) + { + entity.MaxOutputTokens = updateInput.MaxOutputTokens; + } + + if (entity.FrequencyPenalty != updateInput.FrequencyPenalty) + { + entity.FrequencyPenalty = updateInput.FrequencyPenalty; + } + + if (entity.PresencePenalty != updateInput.PresencePenalty) + { + entity.PresencePenalty = updateInput.PresencePenalty; + } + + if (entity.StateCheckers != updateInput.StateCheckers) + { + entity.StateCheckers = updateInput.StateCheckers; + } + + if (!updateInput.ApiKey.IsNullOrWhiteSpace()) + { + var encryptApiKey = StringEncryptionService.Encrypt(updateInput.ApiKey); + entity.SetApiKey(encryptApiKey, updateInput.ApiBaseUrl); + } + + if (!entity.HasSameExtraProperties(updateInput)) + { + entity.ExtraProperties.Clear(); + + foreach (var property in updateInput.ExtraProperties) + { + entity.ExtraProperties.Add(property.Key, property.Value); + } + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj new file mode 100644 index 000000000..9f7983f9b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN.Abp.AIManagement.Domain.Shared.csproj @@ -0,0 +1,26 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + LINGYUN.Abp.AIManagement.Domain.Shared + LINGYUN.Abp.AIManagement.Domain.Shared + false + false + false + enable + + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AIManagementErrorCodes.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AIManagementErrorCodes.cs new file mode 100644 index 000000000..ae295d8db --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AIManagementErrorCodes.cs @@ -0,0 +1,12 @@ +namespace LINGYUN.Abp.AIManagement; +public static class AIManagementErrorCodes +{ + public const string Namespace = "AIManagement"; + + public static class Workspace + { + public const string Prefix = Namespace + ":100"; + + public const string NameAlreadyExists = Prefix + "001"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..5c0168a94 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,25 @@ +using LINGYUN.Abp.AIManagement.Localization; +using Volo.Abp.Domain; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn(typeof(AbpDddDomainSharedModule))] +public class AbpAIManagementDomainSharedModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources.Add() + .AddVirtualJson("/LINGYUN/Abp/AIManagement/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs new file mode 100644 index 000000000..34283daae --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordConsts.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AIManagement.Chats; +public static class ChatMessageRecordConsts +{ + public static int MaxChatRoleLength { get; set; } = 20; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs new file mode 100644 index 000000000..30c3868cb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ConversationRecordConsts.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AIManagement.Chats; +public static class ConversationRecordConsts +{ + public static int MaxNameLength { get; set; } = 50; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordConsts.cs new file mode 100644 index 000000000..a603c0e87 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordConsts.cs @@ -0,0 +1,5 @@ +namespace LINGYUN.Abp.AIManagement.Chats; +public static class TextChatMessageRecordConsts +{ + public static int MaxContentLength { get; set; } = 1024; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/AIManagementResource.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/AIManagementResource.cs new file mode 100644 index 000000000..d31c83f9a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/AIManagementResource.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.AIManagement.Localization; + +[LocalizationResourceName("AIManagement")] +public class AIManagementResource +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json new file mode 100644 index 000000000..d29ec98c1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "DisplayName:MaxLatestHistoryMessagesToKeep": "Carry the recent conversation records", + "Description:MaxLatestHistoryMessagesToKeep": "When a user initiates a conversation with a large model, the upper limit of the user's recent conversation history that is carried along." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..6eb63499b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Localization/Resources/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "DisplayName:MaxLatestHistoryMessagesToKeep": "携带最近对话记录", + "Description:MaxLatestHistoryMessagesToKeep": "用户发起与大模型对话时,携带用户最近对话记录的上限." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceAlreadyExistsException.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceAlreadyExistsException.cs new file mode 100644 index 000000000..5e23fd8b3 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceAlreadyExistsException.cs @@ -0,0 +1,16 @@ +using Volo.Abp; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceAlreadyExistsException : BusinessException +{ + public string Workspace { get; } + public WorkspaceAlreadyExistsException(string workspace) + : base( + AIManagementErrorCodes.Workspace.NameAlreadyExists, + $"A Workspace named {workspace} already exists!") + { + Workspace = workspace; + + WithData(nameof(Workspace), workspace); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecordConsts.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecordConsts.cs new file mode 100644 index 000000000..f8a2e6bd1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecordConsts.cs @@ -0,0 +1,14 @@ +namespace LINGYUN.Abp.AIManagement.Workspaces; +public static class WorkspaceDefinitionRecordConsts +{ + public static int MaxNameLength { get; set; } = 64; + public static int MaxProviderLength { get; set; } = 20; + public static int MaxModelNameLength { get; set; } = 64; + public static int MaxDisplayNameLength { get; set; } = 128; + public static int MaxDescriptionLength { get; set; } = 128; + public static int MaxApiKeyLength { get; set; } = 64; + public static int MaxApiBaseUrlLength { get; set; } = 128; + public static int MaxSystemPromptLength { get; set; } = 512; + public static int MaxInstructionsLength { get; set; } = 512; + public static int MaxStateCheckersLength { get; set; } = 256; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..813624bf7 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN.Abp.AIManagement.Domain.csproj @@ -0,0 +1,30 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.Domain + LINGYUN.Abp.AIManagement.Domain + false + false + false + enable + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..72ab61ffd --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AIManagementOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace LINGYUN.Abp.AIManagement; +public class AIManagementOptions +{ + public bool IsDynamicWorkspaceStoreEnabled { get; set; } + public bool SaveStaticWorkspacesToDatabase { get; set; } + public AIManagementOptions() + { + + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs new file mode 100644 index 000000000..947baaa18 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDbProperties.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Data; + +namespace LINGYUN.Abp.AIManagement; +public static class AbpAIManagementDbProperties +{ + public static string DbTablePrefix { get; set; } = AbpCommonDbProperties.DbTablePrefix + "AI"; + + public static string? DbSchema { get; set; } = AbpCommonDbProperties.DbSchema; + + public const string ConnectionStringName = "AbpAIManagement"; +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs new file mode 100644 index 000000000..93cdb0261 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainMappers.cs @@ -0,0 +1,15 @@ +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AIManagement.Chats; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.ObjectExtending; + +namespace LINGYUN.Abp.AIManagement; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties(DefinitionChecks = MappingPropertyDefinitionChecks.None)] +public partial class TextChatMessageRecordToUserTextMessageMapper : MapperBase +{ + public override partial TextChatMessage Map(TextChatMessageRecord source); + public override partial void Map(TextChatMessageRecord source, TextChatMessage destination); +} 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 new file mode 100644 index 000000000..75ba502f1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/AbpAIManagementDomainModule.cs @@ -0,0 +1,69 @@ +using LINGYUN.Abp.AI.Agent; +using LINGYUN.Abp.AI.Localization; +using LINGYUN.Abp.AIManagement.Localization; +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.Localization; +using Volo.Abp.Mapperly; +using Volo.Abp.Modularity; +using Volo.Abp.Threading; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementDomainSharedModule), + typeof(AbpAIAgentModule), + typeof(AbpCachingModule), + typeof(AbpMapperlyModule), + 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; + }); + } + + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes(typeof(AbpAIResource)); + }); + } + + 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/Chats/ChatMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs new file mode 100644 index 000000000..805aaadd3 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecord.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.AI; +using System; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Chats; +public abstract class ChatMessageRecord : AuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; private set; } + + public string Workspace { get; private set; } + + public ChatRole Role { get; private set; } + + public DateTime CreatedAt { get; private set; } + + public Guid? UserId { get; private set; } + + public Guid? ConversationId { get; private set; } + + public string? ReplyMessage { get; private set; } + + public DateTime? ReplyAt { get; private set; } + + protected ChatMessageRecord() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public ChatMessageRecord( + Guid id, + string workspace, + ChatRole role, + DateTime createdAt, + Guid? tenantId = null) + : base(id) + { + Workspace = workspace; + Role = role; + CreatedAt = createdAt; + TenantId = tenantId; + } + + public virtual ChatMessageRecord SetUserId(Guid userId) + { + UserId = userId; + return this; + } + + public virtual ChatMessageRecord SetConversationId(Guid conversationId) + { + ConversationId = conversationId; + return this; + } + + public virtual ChatMessageRecord SetReply(string replyMessage, DateTime? replyAt) + { + ReplyMessage = replyMessage; + ReplyAt = replyAt; + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs new file mode 100644 index 000000000..724af7db4 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ChatMessageStore.cs @@ -0,0 +1,129 @@ +using LINGYUN.Abp.AI.Chats; +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AIManagement.Settings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.AIManagement.Chats; + +[Dependency(ReplaceServices = true)] +public class ChatMessageStore : IChatMessageStore, ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + private readonly IGuidGenerator _guidGenerator; + private readonly ISettingProvider _settingProvider; + private readonly IObjectMapper _objectMapper; + private readonly ITextChatMessageRecordRepository _messageRecordRepository; + + public ChatMessageStore( + ICurrentTenant currentTenant, + IGuidGenerator guidGenerator, + ISettingProvider settingProvider, + IObjectMapper objectMapper, + ITextChatMessageRecordRepository messageRecordRepository) + { + _currentTenant = currentTenant; + _guidGenerator = guidGenerator; + _settingProvider = settingProvider; + _objectMapper = objectMapper; + _messageRecordRepository = messageRecordRepository; + } + + public async virtual Task> GetHistoryMessagesAsync(Guid conversationId) + { + var maxLatestHistoryMessagesToKeep = await _settingProvider.GetAsync( + AIManagementSettingNames.ChatMessage.MaxLatestHistoryMessagesToKeep, 0); + if (maxLatestHistoryMessagesToKeep < 1) + { + return Array.Empty(); + } + + var userTextMessages = await _messageRecordRepository.GetHistoryMessagesAsync(conversationId, maxLatestHistoryMessagesToKeep); + + return userTextMessages.Select(msg => + { + var chatMessage = new TextChatMessage(msg.Workspace, msg.Content, msg.Role, msg.CreatedAt); + chatMessage.WithMessageId(msg.Id); + if (msg.ConversationId.HasValue) + { + chatMessage.WithConversationId(msg.ConversationId.Value); + } + if (!msg.ReplyMessage.IsNullOrWhiteSpace() && msg.ReplyAt.HasValue) + { + chatMessage.WithReply(msg.ReplyMessage, msg.ReplyAt.Value); + } + + return chatMessage; + }); + } + + public async virtual Task SaveMessageAsync(ChatMessage message) + { + var messageId = message.Id; + if (!messageId.HasValue) + { + messageId = _guidGenerator.Create(); + message.WithMessageId(messageId.Value); + } + + await StoreMessageAsync(messageId.Value, message); + + return messageId.Value; + } + + protected async virtual Task StoreMessageAsync(Guid messageId, ChatMessage message) + { + switch (message) + { + case TextChatMessage textMessage: + await StoreUserTextMessageAsync(messageId, textMessage); + break; + } + } + + protected async virtual Task StoreUserTextMessageAsync(Guid messageId, TextChatMessage textMessage) + { + var textMessageRecord = await _messageRecordRepository.FindAsync(messageId); + if (textMessageRecord == null) + { + textMessageRecord = new TextChatMessageRecord( + messageId, + textMessage.Workspace, + textMessage.Content, + textMessage.Role, + textMessage.CreatedAt, + _currentTenant.Id); + + UpdateUserMessageRecord(textMessageRecord, textMessage); + + await _messageRecordRepository.InsertAsync(textMessageRecord); + } + else + { + textMessageRecord.SetContent(textMessage.Content); + + UpdateUserMessageRecord(textMessageRecord, textMessage); + + await _messageRecordRepository.UpdateAsync(textMessageRecord); + } + } + + private static void UpdateUserMessageRecord(ChatMessageRecord messageRecord, ChatMessage message) + { + if (message.ConversationId.HasValue) + { + messageRecord.SetConversationId(message.ConversationId.Value); + } + if (!message.ReplyMessage.IsNullOrWhiteSpace()) + { + messageRecord.SetReply(message.ReplyMessage, message.ReplyAt); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs new file mode 100644 index 000000000..22f6cbe26 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationCleanupOptions.cs @@ -0,0 +1,15 @@ +using System; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ConversationCleanupOptions +{ + public bool IsCleanupEnabled { get; set; } + public TimeSpan ExpiredTime { get; set; } + public int CleanupPeriod { get; set; } + public ConversationCleanupOptions() + { + IsCleanupEnabled = true; + CleanupPeriod = 3_600_000; + ExpiredTime = TimeSpan.FromHours(2); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs new file mode 100644 index 000000000..10192ab8b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationRecord.cs @@ -0,0 +1,39 @@ +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class ConversationRecord : AuditedEntity, 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; + } + + public void SetName(string name) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), ConversationRecordConsts.MaxNameLength); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs new file mode 100644 index 000000000..3d7d2ed5e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationStore.cs @@ -0,0 +1,95 @@ +using LINGYUN.Abp.AI.Chats; +using LINGYUN.Abp.AI.Models; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Specifications; +using Volo.Abp.Timing; + +namespace LINGYUN.Abp.AIManagement.Chats; + +[Dependency(ReplaceServices = true)] +public class ConversationStore : IConversationStore, ITransientDependency +{ + private readonly IClock _clock; + private readonly ICurrentTenant _currentTenant; + private readonly ConversationCleanupOptions _cleanupOptions; + private readonly IConversationRecordRepository _conversationRecordRepository; + + public ConversationStore( + IClock clock, + ICurrentTenant currentTenant, + IOptions 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( + 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 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); + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs new file mode 100644 index 000000000..6ed7e0f1a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/IConversationRecordRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Specifications; + +namespace LINGYUN.Abp.AIManagement.Chats; +public interface IConversationRecordRepository : IBasicRepository +{ + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string? sorting = $"{nameof(ConversationRecord.CreatedAt)} DESC", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs new file mode 100644 index 000000000..de20f6e27 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ITextChatMessageRecordRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace LINGYUN.Abp.AIManagement.Chats; +public interface ITextChatMessageRecordRepository : IBasicRepository +{ + Task> GetHistoryMessagesAsync( + Guid conversationId, + int maxResultCount = 0, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecord.cs new file mode 100644 index 000000000..16efa2653 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecord.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.AI; +using System; +using Volo.Abp; + +namespace LINGYUN.Abp.AIManagement.Chats; +public class TextChatMessageRecord : ChatMessageRecord +{ + public string Content { get; private set; } + + public TextChatMessageRecord() + { + } + + public TextChatMessageRecord( + Guid id, + string workspace, + string content, + ChatRole role, + DateTime createdAt, + Guid? tenantId = null) + : base(id, workspace, role, createdAt, tenantId) + { + SetContent(content); + } + + public virtual TextChatMessageRecord SetContent(string content) + { + Content = Check.NotNullOrWhiteSpace(content, nameof(content), TextChatMessageRecordConsts.MaxContentLength); + return this; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs new file mode 100644 index 000000000..e36886420 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingDefinitionProvider.cs @@ -0,0 +1,22 @@ +using LINGYUN.Abp.AIManagement.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.AIManagement.Settings; +public class AIManagementSettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add( + new SettingDefinition( + AIManagementSettingNames.ChatMessage.MaxLatestHistoryMessagesToKeep, + defaultValue: "5", + displayName: L("DisplayName:MaxLatestHistoryMessagesToKeep"), + description: L("Description:MaxLatestHistoryMessagesToKeep"))); + } + + private static ILocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs new file mode 100644 index 000000000..5e87515f6 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Settings/AIManagementSettingNames.cs @@ -0,0 +1,10 @@ +namespace LINGYUN.Abp.AIManagement.Settings; +public static class AIManagementSettingNames +{ + public const string Prefix = "Abp.AIManagement"; + + public static class ChatMessage + { + public const string MaxLatestHistoryMessagesToKeep = Prefix + ".MaxLatestHistoryMessagesToKeep"; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/ITokenUsageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/ITokenUsageRecordRepository.cs new file mode 100644 index 000000000..11724dda4 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/ITokenUsageRecordRepository.cs @@ -0,0 +1,26 @@ +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.Tokens; +public interface ITokenUsageRecordRepository : IBasicRepository +{ + Task FindByMessageIdAsync( + Guid conversationId, + Guid? messageId, + CancellationToken cancellationToken = default); + + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(TokenUsageRecord.CreationTime)}", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs new file mode 100644 index 000000000..136747a39 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageRecord.cs @@ -0,0 +1,42 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Tokens; +public class TokenUsageRecord : AuditedEntity, IMultiTenant +{ + public Guid? TenantId { get; private set; } + public Guid? MessageId { get; private set; } + public Guid? ConversationId { get; private set; } + public long? InputTokenCount { get; set; } + public long? OutputTokenCount { get; set; } + public long? TotalTokenCount { get; set; } + public long? CachedInputTokenCount { get; set; } + public long? ReasoningTokenCount { get; set; } + protected TokenUsageRecord() + { + + } + + public TokenUsageRecord( + Guid id, + Guid? messageId = null, + Guid? conversationId = null, + long? inputTokenCount = null, + long? outputTokenCount = null, + long? totalTokenCount = null, + long? cachedInputTokenCount = null, + long? reasoningTokenCount = null, + Guid? tenantId = null) + : base(id) + { + MessageId = messageId; + ConversationId = conversationId; + InputTokenCount = inputTokenCount; + OutputTokenCount = outputTokenCount; + TotalTokenCount = totalTokenCount; + CachedInputTokenCount = cachedInputTokenCount; + ReasoningTokenCount = reasoningTokenCount; + TenantId = tenantId; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageStore.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageStore.cs new file mode 100644 index 000000000..026ba82cd --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Tokens/TokenUsageStore.cs @@ -0,0 +1,68 @@ +using LINGYUN.Abp.AI.Models; +using LINGYUN.Abp.AI.Tokens; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.AIManagement.Tokens; + +[Dependency(ReplaceServices = true)] +public class TokenUsageStore : ITokenUsageStore, ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + private readonly IGuidGenerator _guidGenerator; + private readonly ITokenUsageRecordRepository _tokenUsageRecordRepository; + + public TokenUsageStore( + ICurrentTenant currentTenant, + IGuidGenerator guidGenerator, + ITokenUsageRecordRepository tokenUsageRecordRepository) + { + _currentTenant = currentTenant; + _guidGenerator = guidGenerator; + _tokenUsageRecordRepository = tokenUsageRecordRepository; + } + + public async virtual Task SaveTokenUsageAsync(TokenUsageInfo tokenUsageInfo) + { + var tokenUsageRecord = await _tokenUsageRecordRepository.FindByMessageIdAsync( + tokenUsageInfo.ConversationId, + tokenUsageInfo.MessageId); + + if (tokenUsageRecord == null) + { + tokenUsageRecord = new TokenUsageRecord( + _guidGenerator.Create(), + tokenUsageInfo.MessageId, + tokenUsageInfo.ConversationId, + tokenUsageInfo.InputTokenCount, + tokenUsageInfo.OutputTokenCount, + tokenUsageInfo.TotalTokenCount, + tokenUsageInfo.CachedInputTokenCount, + tokenUsageInfo.ReasoningTokenCount, + _currentTenant.Id); + + await _tokenUsageRecordRepository.InsertAsync(tokenUsageRecord); + } + else + { + tokenUsageRecord.InputTokenCount ??= 0; + tokenUsageRecord.InputTokenCount += tokenUsageInfo.InputTokenCount; + + tokenUsageRecord.OutputTokenCount ??= 0; + tokenUsageRecord.OutputTokenCount += tokenUsageInfo.OutputTokenCount; + + tokenUsageRecord.CachedInputTokenCount ??= 0; + tokenUsageRecord.CachedInputTokenCount += tokenUsageInfo.CachedInputTokenCount; + + tokenUsageRecord.ReasoningTokenCount ??= 0; + tokenUsageRecord.ReasoningTokenCount += tokenUsageInfo.ReasoningTokenCount; + + tokenUsageRecord.TotalTokenCount ??= 0; + tokenUsageRecord.TotalTokenCount += tokenUsageInfo.TotalTokenCount; + + await _tokenUsageRecordRepository.UpdateAsync(tokenUsageRecord); + } + } +} 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..4550973af --- /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 IWorkspaceDefinitionRecordRepository WorkspaceDefinitionRecordRepository { 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( + IWorkspaceDefinitionRecordRepository workspaceRepository, + IWorkspaceDefinitionSerializer workspaceDefinitionSerializer, + IDynamicWorkspaceDefinitionStoreInMemoryCache storeCache, + IDistributedCache distributedCache, + IOptions cacheOptions, + IOptions aiManagementOptions, + IAbpDistributedLock distributedLock) + { + WorkspaceDefinitionRecordRepository = 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 WorkspaceDefinitionRecordRepository.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..3d7d0d142 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/DynamicWorkspaceDefinitionStoreInMemoryCache.cs @@ -0,0 +1,100 @@ +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.Security.Encryption; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class DynamicWorkspaceDefinitionStoreInMemoryCache : IDynamicWorkspaceDefinitionStoreInMemoryCache, ISingletonDependency +{ + public string CacheStamp { get; set; } + protected IDictionary WorkspaceDefinitions { get; } + protected IStringEncryptionService StringEncryptionService { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public SemaphoreSlim SyncSemaphore { get; } = new(1, 1); + + public DateTime? LastCheckTime { get; set; } + + public DynamicWorkspaceDefinitionStoreInMemoryCache( + IStringEncryptionService stringEncryptionService, + ISimpleStateCheckerSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + StringEncryptionService = stringEncryptionService; + 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()) + { + var decryptApiKey = StringEncryptionService.Decrypt(workspace.ApiKey); + workspaceDef.WithApiKey(decryptApiKey!); + } + 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..ec3c6b82b --- /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/IWorkspaceDefinitionRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.cs new file mode 100644 index 000000000..4c893dd04 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/IWorkspaceDefinitionRecordRepository.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 IWorkspaceDefinitionRecordRepository : IRepository +{ + Task FindByNameAsync( + string name, + CancellationToken cancellationToken = default); +} 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..f2e186875 --- /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/StaticWorkspaceSaver.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/StaticWorkspaceSaver.cs new file mode 100644 index 000000000..255d65e41 --- /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 IWorkspaceDefinitionRecordRepository 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, + IWorkspaceDefinitionRecordRepository 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(WorkspaceDefinitionRecord[] 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(WorkspaceDefinitionRecord[] 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/WorkspaceDefinitionRecord.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs new file mode 100644 index 000000000..e93500b17 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionRecord.cs @@ -0,0 +1,268 @@ +using System; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Auditing; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDefinitionRecord : 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; 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 WorkspaceDefinitionRecord() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public WorkspaceDefinitionRecord( + Guid id, + string name, + string provider, + string modelName, + string displayName, + string? description = 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), WorkspaceDefinitionRecordConsts.MaxNameLength); + Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), WorkspaceDefinitionRecordConsts.MaxProviderLength); + ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceDefinitionRecordConsts.MaxModelNameLength); + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceDefinitionRecordConsts.MaxDisplayNameLength); + Description = Check.Length(description, nameof(description), WorkspaceDefinitionRecordConsts.MaxDescriptionLength); + SystemPrompt = Check.Length(systemPrompt, nameof(systemPrompt), WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); + Instructions = Check.Length(instructions, nameof(instructions), WorkspaceDefinitionRecordConsts.MaxInstructionsLength); + StateCheckers = Check.Length(stateCheckers, nameof(stateCheckers), WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); + Temperature = temperature; + MaxOutputTokens = maxOutputTokens; + FrequencyPenalty = frequencyPenalty; + PresencePenalty = presencePenalty; + + IsEnabled = true; + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public void SetDisplayName(string displayName) + { + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), WorkspaceDefinitionRecordConsts.MaxDisplayNameLength); + } + + public void SetModel(string provider, string modelName) + { + Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), WorkspaceDefinitionRecordConsts.MaxProviderLength); + ModelName = Check.NotNullOrWhiteSpace(modelName, nameof(modelName), WorkspaceDefinitionRecordConsts.MaxModelNameLength); + } + + public void SetApiKey(string? apiKey = null, string? apiBaseUrl = null) + { + ApiKey = Check.Length(apiKey, nameof(apiKey), WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + ApiBaseUrl = Check.Length(apiBaseUrl, nameof(apiBaseUrl), WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength); + } + + public bool HasSameData(WorkspaceDefinitionRecord 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(WorkspaceDefinitionRecord 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..94096f797 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionSerializer.cs @@ -0,0 +1,81 @@ +using LINGYUN.Abp.AI.Workspaces; +using System; +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.Security.Encryption; +using Volo.Abp.SimpleStateChecking; + +namespace LINGYUN.Abp.AIManagement.Workspaces; +public class WorkspaceDefinitionSerializer : IWorkspaceDefinitionSerializer, ITransientDependency +{ + protected IGuidGenerator GuidGenerator { get; } + protected IStringEncryptionService StringEncryptionService { get; } + protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public WorkspaceDefinitionSerializer( + IGuidGenerator guidGenerator, + IStringEncryptionService stringEncryptionService, + ISimpleStateCheckerSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + GuidGenerator = guidGenerator; + StringEncryptionService = stringEncryptionService; + 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 WorkspaceDefinitionRecord( + GuidGenerator.Create(), + definition.Name, + definition.Provider, + definition.ModelName, + LocalizableStringSerializer.Serialize(definition.DisplayName)!, + definition.Description != null ? LocalizableStringSerializer.Serialize(definition.Description) : null, + definition.SystemPrompt, + definition.Instructions, + definition.Temperature, + definition.MaxOutputTokens, + definition.FrequencyPenalty, + definition.PresencePenalty, + SerializeStateCheckers(definition.StateCheckers)); + + if (!definition.ApiKey.IsNullOrWhiteSpace()) + { + var encryptApiKey = StringEncryptionService.Encrypt(definition.ApiKey); + workspace.SetApiKey(encryptApiKey, definition.ApiBaseUrl); + } + + 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..793814d3a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDynamicInitializer.cs @@ -0,0 +1,138 @@ +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; } + + public WorkspaceDynamicInitializer(IServiceProvider serviceProvider) + { + Logger = NullLogger.Instance; + + ServiceProvider = serviceProvider; + } + + public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) + { + var options = ServiceProvider.GetRequiredService>().Value; + + if (!options.SaveStaticWorkspacesToDatabase && !options.IsDynamicWorkspaceStoreEnabled) + { + 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 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; + } + + var staticWorkspaceSaver = 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 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; + } + + var dynamicWorkspaceDefinitionStore = ServiceProvider.GetRequiredService(); + + 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() + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN.Abp.AIManagement.EntityFrameworkCore.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN.Abp.AIManagement.EntityFrameworkCore.csproj new file mode 100644 index 000000000..32e7bafeb --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN.Abp.AIManagement.EntityFrameworkCore.csproj @@ -0,0 +1,25 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.EntityFrameworkCore + LINGYUN.Abp.AIManagement.EntityFrameworkCore + false + false + false + enable + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs new file mode 100644 index 000000000..fb3b6a9f2 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContext.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.Tokens; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; + +[ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] +public class AIManagementDbContext : AbpDbContext, IAIManagementDbContext +{ + public DbSet WorkspaceDefinitions { get; set; } + public DbSet TextChatMessageRecords { get; set; } + public DbSet ConversationRecords { get; set; } + public DbSet TokenUsageRecords { get; set; } + public AIManagementDbContext( + DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigureAIManagement(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs new file mode 100644 index 000000000..f3f5e9f9a --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AIManagementDbContextModelBuilderExtensions.cs @@ -0,0 +1,99 @@ +using JetBrains.Annotations; +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.EntityFrameworkCore.ValueConversions; +using LINGYUN.Abp.AIManagement.Tokens; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public static class AIManagementDbContextModelBuilderExtensions +{ + public static void ConfigureAIManagement( + [NotNull] this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "Conversations", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name) + .HasMaxLength(ConversationRecordConsts.MaxNameLength) + .IsRequired(); + }); + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "TextChatMessages", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Workspace) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength) + .IsRequired(); + b.Property(x => x.Role) + .HasMaxLength(ChatMessageRecordConsts.MaxChatRoleLength) + .HasConversion(new ChatRoleValueConverter()) + .IsRequired(); + b.Property(x => x.Content) + .HasMaxLength(TextChatMessageRecordConsts.MaxContentLength) + .IsRequired(); + + b.HasIndex(x => new { x.TenantId, x.ConversationId }); + + b.ApplyObjectExtensionMappings(); + }); + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "TokenUsages", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.HasIndex(x => new { x.TenantId, x.ConversationId }); + }); + + if (builder.IsHostDatabase()) + { + builder.Entity(b => + { + b.ToTable(AbpAIManagementDbProperties.DbTablePrefix + "WorkspaceDefinitions", AbpAIManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxNameLength) + .IsRequired(); + b.Property(x => x.Provider) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxProviderLength) + .IsRequired(); + b.Property(x => x.ModelName) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxModelNameLength) + .IsRequired(); + b.Property(x => x.DisplayName) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDisplayNameLength) + .IsRequired(); + b.Property(x => x.Description) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxDescriptionLength); + b.Property(x => x.ApiKey) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiKeyLength); + b.Property(x => x.ApiBaseUrl) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxApiBaseUrlLength); + b.Property(x => x.SystemPrompt) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxSystemPromptLength); + b.Property(x => x.Instructions) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxInstructionsLength); + b.Property(x => x.StateCheckers) + .HasMaxLength(WorkspaceDefinitionRecordConsts.MaxStateCheckersLength); + + b.HasIndex(x => new { x.Name }).IsUnique(); + + b.ApplyObjectExtensionMappings(); + }); + } + + builder.TryConfigureObjectExtensions(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs new file mode 100644 index 000000000..bb4cf3964 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/AbpAIManagementEntityFrameworkCoreModule.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.Tokens; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; + +[DependsOn( + typeof(AbpAIManagementDomainModule), + typeof(AbpEntityFrameworkCoreModule))] +public class AbpAIManagementEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + options.AddDefaultRepositories(); + + options.AddRepository(); + options.AddRepository(); + options.AddRepository(); + + options.AddRepository(); + }); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs new file mode 100644 index 000000000..d79ea9709 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreConversationRecordRepository.cs @@ -0,0 +1,44 @@ +using LINGYUN.Abp.AIManagement.Chats; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Specifications; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public class EfCoreConversationRecordRepository : EfCoreRepository, IConversationRecordRepository +{ + public EfCoreConversationRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .CountAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task> GetListAsync( + ISpecification 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)); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs new file mode 100644 index 000000000..d5d075bd4 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTextChatMessageRecordRepository.cs @@ -0,0 +1,32 @@ +using LINGYUN.Abp.AIManagement.Chats; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public class EfCoreTextChatMessageRecordRepository : EfCoreRepository, ITextChatMessageRecordRepository +{ + public EfCoreTextChatMessageRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task> GetHistoryMessagesAsync( + Guid conversationId, + int maxResultCount = 0, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(x => x.ConversationId == conversationId) + .OrderByDescending(x => x.CreationTime) + .Take(maxResultCount) + .OrderBy(x => x.CreationTime) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTokenUsageRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTokenUsageRecordRepository.cs new file mode 100644 index 000000000..2bb211cc6 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreTokenUsageRecordRepository.cs @@ -0,0 +1,54 @@ +using LINGYUN.Abp.AIManagement.Tokens; +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 EfCoreTokenUsageRecordRepository : EfCoreRepository, ITokenUsageRecordRepository +{ + public EfCoreTokenUsageRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task FindByMessageIdAsync( + Guid conversationId, + Guid? messageId, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(x => x.ConversationId == conversationId && x.MessageId == messageId) + .FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .CountAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(TokenUsageRecord.CreationTime)}", + int maxResultCount = 10, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .OrderBy(!sorting.IsNullOrWhiteSpace() ? sorting : $"{nameof(TokenUsageRecord.CreationTime)}") + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceDefinitionRecordRepository.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceDefinitionRecordRepository.cs new file mode 100644 index 000000000..7c0bcd3b5 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/EfCoreWorkspaceDefinitionRecordRepository.cs @@ -0,0 +1,26 @@ +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using System; +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; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; +public class EfCoreWorkspaceDefinitionRecordRepository : EfCoreRepository, IWorkspaceDefinitionRecordRepository +{ + public EfCoreWorkspaceDefinitionRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task FindByNameAsync(string name, CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(x => x.Name == name) + .FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); ; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs new file mode 100644 index 000000000..edc46235d --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/IAIManagementDbContext.cs @@ -0,0 +1,17 @@ +using LINGYUN.Abp.AIManagement.Chats; +using LINGYUN.Abp.AIManagement.Tokens; +using LINGYUN.Abp.AIManagement.Workspaces; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore; + +[ConnectionStringName(AbpAIManagementDbProperties.ConnectionStringName)] +public interface IAIManagementDbContext : IEfCoreDbContext +{ + DbSet WorkspaceDefinitions { get; } + DbSet TextChatMessageRecords { get; } + DbSet ConversationRecords { get; } + DbSet TokenUsageRecords { get; } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/ValueConversions/ChatRoleValueConverter.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/ValueConversions/ChatRoleValueConverter.cs new file mode 100644 index 000000000..c4a1faf2c --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.EntityFrameworkCore/LINGYUN/Abp/AIManagement/EntityFrameworkCore/ValueConversions/ChatRoleValueConverter.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.Extensions.AI; + +namespace LINGYUN.Abp.AIManagement.EntityFrameworkCore.ValueConversions; +public class ChatRoleValueConverter(ConverterMappingHints? mappingHints = null) : ValueConverter( + value => value.Value, + value => new ChatRole(value), + mappingHints + ) +{ +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN.Abp.AIManagement.HttpApi.Client.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN.Abp.AIManagement.HttpApi.Client.csproj new file mode 100644 index 000000000..dc145f2f1 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN.Abp.AIManagement.HttpApi.Client.csproj @@ -0,0 +1,25 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + LINGYUN.Abp.AIManagement.HttpApi.Client + LINGYUN.Abp.AIManagement.HttpApi.Client + false + false + false + enable + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs new file mode 100644 index 000000000..751b9bfaa --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi.Client/LINGYUN/Abp/AIManagement/AbpAIManagementDomainSharedModule.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +public class AbpAIManagementDomainSharedModule : AbpModule +{ + +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xml b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN.Abp.AIManagement.HttpApi.csproj b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN.Abp.AIManagement.HttpApi.csproj new file mode 100644 index 000000000..00f6afa11 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN.Abp.AIManagement.HttpApi.csproj @@ -0,0 +1,25 @@ + + + + + + + net10.0 + LINGYUN.Abp.AIManagement.HttpApi + LINGYUN.Abp.AIManagement.HttpApi + false + false + false + enable + + + + + + + + + + + + diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementHttpApiModule.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementHttpApiModule.cs new file mode 100644 index 000000000..7e119483f --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/AbpAIManagementHttpApiModule.cs @@ -0,0 +1,41 @@ +using LINGYUN.Abp.AIManagement.Localization; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.AIManagement; + +[DependsOn( + typeof(AbpAIManagementApplicationContractsModule), + typeof(AbpAspNetCoreMvcModule))] +public class AbpAIManagementHttpApiModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(options => + { + options.AddAssemblyResource(typeof(AIManagementResource), typeof(AbpAIManagementApplicationContractsModule).Assembly); + }); + + PreConfigure(mvcBuilder => + { + mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAIManagementHttpApiModule).Assembly); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes(typeof(AbpUiResource)); + }); + + context.Services.AddTransient(); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ChatController.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ChatController.cs new file mode 100644 index 000000000..811a29c56 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ChatController.cs @@ -0,0 +1,30 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Abp.AIManagement.Chats; + +[Controller] +[RemoteService(Name = AIManagementRemoteServiceConsts.RemoteServiceName)] +[Area(AIManagementRemoteServiceConsts.ModuleName)] +[Route($"api/{AIManagementRemoteServiceConsts.ModuleName}/chats")] +public class ChatController : AbpControllerBase, IChatAppService +{ + private readonly IChatAppService _service; + public ChatController(IChatAppService service) + { + _service = service; + } + + [HttpPost] + [ServiceFilter] + public async virtual IAsyncEnumerable SendMessageAsync(SendTextChatMessageDto input) + { + await foreach (var content in _service.SendMessageAsync(input)) + { + yield return content; + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ConversationController.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ConversationController.cs new file mode 100644 index 000000000..e90aced0b --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Chats/ConversationController.cs @@ -0,0 +1,52 @@ +using LINGYUN.Abp.AIManagement.Chats.Dtos; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Abp.AIManagement.Chats; + +[Controller] +[RemoteService(Name = AIManagementRemoteServiceConsts.RemoteServiceName)] +[Area(AIManagementRemoteServiceConsts.ModuleName)] +[Route($"api/{AIManagementRemoteServiceConsts.ModuleName}/chats/conversations")] +public class ConversationController : AbpControllerBase, IConversationAppService +{ + private readonly IConversationAppService _service; + public ConversationController(IConversationAppService service) + { + _service = service; + } + + [HttpPost] + public virtual Task CreateAsync(ConversationCreateDto input) + { + return _service.CreateAsync(input); + } + + [HttpDelete("{id}")] + public virtual Task DeleteAsync(Guid id) + { + return _service.DeleteAsync(id); + } + + [HttpGet("{id}")] + public virtual Task GetAsync(Guid id) + { + return _service.GetAsync(id); + } + + [HttpGet] + public virtual Task> GetListAsync(ConversationGetListInput input) + { + return _service.GetListAsync(input); + } + + [HttpPut("{id}")] + public virtual Task UpdateAsync(Guid id, ConversationUpdateDto input) + { + return _service.UpdateAsync(id, input); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionController.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionController.cs new file mode 100644 index 000000000..58444f420 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/LINGYUN/Abp/AIManagement/Workspaces/WorkspaceDefinitionController.cs @@ -0,0 +1,52 @@ +using LINGYUN.Abp.AIManagement.Workspaces.Dtos; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Abp.AIManagement.Workspaces; + +[Controller] +[RemoteService(Name = AIManagementRemoteServiceConsts.RemoteServiceName)] +[Area(AIManagementRemoteServiceConsts.ModuleName)] +[Route($"api/{AIManagementRemoteServiceConsts.ModuleName}/workspaces")] +public class WorkspaceDefinitionController : AbpControllerBase, IWorkspaceDefinitionAppService +{ + private readonly IWorkspaceDefinitionAppService _service; + public WorkspaceDefinitionController(IWorkspaceDefinitionAppService service) + { + _service = service; + } + + [HttpPost] + public virtual Task CreateAsync(WorkspaceDefinitionRecordCreateDto input) + { + return _service.CreateAsync(input); + } + + [HttpDelete("{id}")] + public virtual Task DeleteAsync(Guid id) + { + return _service.DeleteAsync(id); + } + + [HttpGet("{id}")] + public virtual Task GetAsync(Guid id) + { + return _service.GetAsync(id); + } + + [HttpGet] + public virtual Task> GetListAsync(WorkspaceDefinitionRecordGetListInput input) + { + return _service.GetListAsync(input); + } + + [HttpPut("{id}")] + public virtual Task UpdateAsync(Guid id, WorkspaceDefinitionRecordUpdateDto input) + { + return _service.UpdateAsync(id, input); + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResult.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResult.cs new file mode 100644 index 000000000..f44e8f911 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResult.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc; +public class SseAsyncEnumerableResult : IActionResult +{ + private readonly IAsyncEnumerable _asyncEnumerable; + public SseAsyncEnumerableResult(IAsyncEnumerable asyncEnumerable) + { + _asyncEnumerable = asyncEnumerable; + } + public async Task ExecuteResultAsync(ActionContext context) + { + var response = context.HttpContext.Response; + + response.Headers.Append("Content-Type", "text/event-stream"); + response.Headers.Append("Cache-Control", "no-cache"); + response.Headers.Append("Connection", "keep-alive"); + response.Headers.Append("X-Accel-Buffering", "no"); + + try + { + await foreach (var content in _asyncEnumerable) + { + if (!string.IsNullOrEmpty(content)) + { + await response.WriteAsync($"data: {content}\n\n"); + await response.Body.FlushAsync(); + } + } + + await response.WriteAsync("data: [DONE]\n\n"); + await response.Body.FlushAsync(); + } + catch (OperationCanceledException) + { + // ignore + } + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResultFilter.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResultFilter.cs new file mode 100644 index 000000000..55d20014e --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.HttpApi/Microsoft/AspNetCore/Mvc/SseAsyncEnumerableResultFilter.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc; +public class SseAsyncEnumerableResultFilter : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var executedContext = await next(); + + if (executedContext.Result is ObjectResult objectResult && + objectResult.Value is IAsyncEnumerable asyncEnumerable) + { + executedContext.Result = new SseAsyncEnumerableResult(asyncEnumerable); + } + } +}