diff --git a/BACKEND/Codex.CQRS/Commands/SendMessageCommand.cs b/BACKEND/Codex.CQRS/Commands/SendMessageCommand.cs index 04e79f2..5f896a9 100644 --- a/BACKEND/Codex.CQRS/Commands/SendMessageCommand.cs +++ b/BACKEND/Codex.CQRS/Commands/SendMessageCommand.cs @@ -200,6 +200,8 @@ public class SendMessageCommandHandler : ICommandHandler m.ConversationId == conversation.Id && m.IsInActiveWindow) + .OrderByDescending(m => m.MessageIndex) + .Take(agent.ConversationWindowSize) .OrderBy(m => m.MessageIndex) .ToListAsync(cancellationToken); diff --git a/BACKEND/Codex.Dal/Codex.Dal.csproj b/BACKEND/Codex.Dal/Codex.Dal.csproj index a38b3de..fecc951 100644 --- a/BACKEND/Codex.Dal/Codex.Dal.csproj +++ b/BACKEND/Codex.Dal/Codex.Dal.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/BACKEND/Codex.Dal/CodexDbContext.cs b/BACKEND/Codex.Dal/CodexDbContext.cs index 3cce9d7..172f324 100644 --- a/BACKEND/Codex.Dal/CodexDbContext.cs +++ b/BACKEND/Codex.Dal/CodexDbContext.cs @@ -60,6 +60,7 @@ public class CodexDbContext : DbContext // Indexes entity.HasIndex(a => new { a.Status, a.IsDeleted }); entity.HasIndex(a => a.Type); + entity.HasIndex(a => a.Name); // Performance: name searches // Relationships entity.HasMany(a => a.Tools) @@ -125,6 +126,7 @@ public class CodexDbContext : DbContext entity.HasIndex(e => e.ConversationId); entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.CompletedAt); // Performance: time-based queries // Relationships entity.HasOne(e => e.Conversation) @@ -156,6 +158,7 @@ public class CodexDbContext : DbContext // Indexes entity.HasIndex(c => new { c.IsActive, c.LastMessageAt }) .IsDescending(false, true); // IsActive ASC, LastMessageAt DESC + entity.HasIndex(c => c.Title); // Performance: title searches // Relationships entity.HasMany(c => c.Messages) @@ -178,10 +181,10 @@ public class CodexDbContext : DbContext // Composite index for efficient conversation window queries entity.HasIndex(m => new { m.ConversationId, m.IsInActiveWindow, m.MessageIndex }); - // Index for ordering messages - entity.HasIndex(m => new { m.ConversationId, m.MessageIndex }); - // Index for role filtering entity.HasIndex(m => m.Role); + + // Performance: time-based queries + entity.HasIndex(m => m.CreatedAt); } } diff --git a/BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.Designer.cs b/BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.Designer.cs new file mode 100644 index 0000000..11d2ac8 --- /dev/null +++ b/BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.Designer.cs @@ -0,0 +1,384 @@ +// +using System; +using System.Text.Json; +using Codex.Dal; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Codex.Dal.Migrations +{ + [DbContext(typeof(CodexDbContext))] + [Migration("20251027032413_AddPerformanceIndexes")] + partial class AddPerformanceIndexes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Codex.Dal.Entities.Agent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyEncrypted") + .HasColumnType("text"); + + b.Property("ConversationWindowSize") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("EnableMemory") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("MaxTokens") + .HasColumnType("integer"); + + b.Property("ModelEndpoint") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ModelProvider") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProviderType") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SystemPrompt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Temperature") + .HasColumnType("double precision"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("Type"); + + b.HasIndex("Status", "IsDeleted"); + + b.ToTable("Agents"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.AgentExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentId") + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("ExecutionTimeMs") + .HasColumnType("bigint"); + + b.Property("Input") + .HasColumnType("text"); + + b.Property("InputTokens") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("OutputTokens") + .HasColumnType("integer"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("ToolCallResults") + .HasColumnType("text"); + + b.Property("ToolCalls") + .HasColumnType("text"); + + b.Property("TotalTokens") + .HasColumnType("integer"); + + b.Property("UserPrompt") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CompletedAt"); + + b.HasIndex("ConversationId"); + + b.HasIndex("Status"); + + b.HasIndex("AgentId", "StartedAt") + .IsDescending(false, true); + + b.ToTable("AgentExecutions"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.AgentTool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentId") + .HasColumnType("uuid"); + + b.Property("ApiBaseUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ApiKeyEncrypted") + .HasColumnType("text"); + + b.Property("Configuration") + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("McpAuthTokenEncrypted") + .HasColumnType("text"); + + b.Property("McpServerUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ToolName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Type"); + + b.HasIndex("AgentId", "IsEnabled"); + + b.ToTable("AgentTools"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastMessageAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageCount") + .HasColumnType("integer"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Summary") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("Title"); + + b.HasIndex("IsActive", "LastMessageAt") + .IsDescending(false, true); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.ConversationMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutionId") + .HasColumnType("uuid"); + + b.Property("IsInActiveWindow") + .HasColumnType("boolean"); + + b.Property("MessageIndex") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("ToolCalls") + .HasColumnType("text"); + + b.Property("ToolResults") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExecutionId"); + + b.HasIndex("Role"); + + b.HasIndex("ConversationId", "IsInActiveWindow", "MessageIndex"); + + b.ToTable("ConversationMessages"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.AgentExecution", b => + { + b.HasOne("Codex.Dal.Entities.Agent", "Agent") + .WithMany("Executions") + .HasForeignKey("AgentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Codex.Dal.Entities.Conversation", "Conversation") + .WithMany("Executions") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Agent"); + + b.Navigation("Conversation"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.AgentTool", b => + { + b.HasOne("Codex.Dal.Entities.Agent", "Agent") + .WithMany("Tools") + .HasForeignKey("AgentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Agent"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.ConversationMessage", b => + { + b.HasOne("Codex.Dal.Entities.Conversation", "Conversation") + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Codex.Dal.Entities.AgentExecution", "Execution") + .WithMany("Messages") + .HasForeignKey("ExecutionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Conversation"); + + b.Navigation("Execution"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.Agent", b => + { + b.Navigation("Executions"); + + b.Navigation("Tools"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.AgentExecution", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Codex.Dal.Entities.Conversation", b => + { + b.Navigation("Executions"); + + b.Navigation("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.cs b/BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.cs new file mode 100644 index 0000000..0717472 --- /dev/null +++ b/BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Codex.Dal.Migrations +{ + /// + public partial class AddPerformanceIndexes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ConversationMessages_ConversationId_MessageIndex", + table: "ConversationMessages"); + + migrationBuilder.CreateIndex( + name: "IX_Conversations_Title", + table: "Conversations", + column: "Title"); + + migrationBuilder.CreateIndex( + name: "IX_ConversationMessages_CreatedAt", + table: "ConversationMessages", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Agents_Name", + table: "Agents", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_AgentExecutions_CompletedAt", + table: "AgentExecutions", + column: "CompletedAt"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Conversations_Title", + table: "Conversations"); + + migrationBuilder.DropIndex( + name: "IX_ConversationMessages_CreatedAt", + table: "ConversationMessages"); + + migrationBuilder.DropIndex( + name: "IX_Agents_Name", + table: "Agents"); + + migrationBuilder.DropIndex( + name: "IX_AgentExecutions_CompletedAt", + table: "AgentExecutions"); + + migrationBuilder.CreateIndex( + name: "IX_ConversationMessages_ConversationId_MessageIndex", + table: "ConversationMessages", + columns: new[] { "ConversationId", "MessageIndex" }); + } + } +} diff --git a/BACKEND/Codex.Dal/Migrations/CodexDbContextModelSnapshot.cs b/BACKEND/Codex.Dal/Migrations/CodexDbContextModelSnapshot.cs index 65a9f3c..dfaf093 100644 --- a/BACKEND/Codex.Dal/Migrations/CodexDbContextModelSnapshot.cs +++ b/BACKEND/Codex.Dal/Migrations/CodexDbContextModelSnapshot.cs @@ -92,6 +92,8 @@ namespace Codex.Dal.Migrations b.HasKey("Id"); + b.HasIndex("Name"); + b.HasIndex("Type"); b.HasIndex("Status", "IsDeleted"); @@ -160,6 +162,8 @@ namespace Codex.Dal.Migrations b.HasKey("Id"); + b.HasIndex("CompletedAt"); + b.HasIndex("ConversationId"); b.HasIndex("Status"); @@ -248,6 +252,8 @@ namespace Codex.Dal.Migrations b.HasKey("Id"); + b.HasIndex("Title"); + b.HasIndex("IsActive", "LastMessageAt") .IsDescending(false, true); @@ -293,12 +299,12 @@ namespace Codex.Dal.Migrations b.HasKey("Id"); + b.HasIndex("CreatedAt"); + b.HasIndex("ExecutionId"); b.HasIndex("Role"); - b.HasIndex("ConversationId", "MessageIndex"); - b.HasIndex("ConversationId", "IsInActiveWindow", "MessageIndex"); b.ToTable("ConversationMessages");