Compare commits

..

No commits in common. "797ee55caf60b8a345da38775db93139ae2729ff" and "229a0698a37dd9818154f8b3c89bd32c37a3de52" have entirely different histories.

8 changed files with 21 additions and 525 deletions

View File

@ -200,8 +200,6 @@ public class SendMessageCommandHandler : ICommandHandler<SendMessageCommand, Sen
var contextMessages = await _dbContext.ConversationMessages var contextMessages = await _dbContext.ConversationMessages
.AsNoTracking() .AsNoTracking()
.Where(m => m.ConversationId == conversation.Id && m.IsInActiveWindow) .Where(m => m.ConversationId == conversation.Id && m.IsInActiveWindow)
.OrderByDescending(m => m.MessageIndex)
.Take(agent.ConversationWindowSize)
.OrderBy(m => m.MessageIndex) .OrderBy(m => m.MessageIndex)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);

View File

@ -13,7 +13,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
<PackageReference Include="OpenHarbor.CQRS" Version="8.1.0-rc1" /> <PackageReference Include="OpenHarbor.CQRS" Version="8.1.0-rc1" />
<PackageReference Include="OpenHarbor.CQRS.DynamicQuery.Abstractions" Version="8.1.0-rc1" /> <PackageReference Include="OpenHarbor.CQRS.DynamicQuery.Abstractions" Version="8.1.0-rc1" />

View File

@ -60,7 +60,6 @@ public class CodexDbContext : DbContext
// Indexes // Indexes
entity.HasIndex(a => new { a.Status, a.IsDeleted }); entity.HasIndex(a => new { a.Status, a.IsDeleted });
entity.HasIndex(a => a.Type); entity.HasIndex(a => a.Type);
entity.HasIndex(a => a.Name); // Performance: name searches
// Relationships // Relationships
entity.HasMany(a => a.Tools) entity.HasMany(a => a.Tools)
@ -126,7 +125,6 @@ public class CodexDbContext : DbContext
entity.HasIndex(e => e.ConversationId); entity.HasIndex(e => e.ConversationId);
entity.HasIndex(e => e.Status); entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.CompletedAt); // Performance: time-based queries
// Relationships // Relationships
entity.HasOne(e => e.Conversation) entity.HasOne(e => e.Conversation)
@ -158,7 +156,6 @@ public class CodexDbContext : DbContext
// Indexes // Indexes
entity.HasIndex(c => new { c.IsActive, c.LastMessageAt }) entity.HasIndex(c => new { c.IsActive, c.LastMessageAt })
.IsDescending(false, true); // IsActive ASC, LastMessageAt DESC .IsDescending(false, true); // IsActive ASC, LastMessageAt DESC
entity.HasIndex(c => c.Title); // Performance: title searches
// Relationships // Relationships
entity.HasMany(c => c.Messages) entity.HasMany(c => c.Messages)
@ -181,10 +178,10 @@ public class CodexDbContext : DbContext
// Composite index for efficient conversation window queries // Composite index for efficient conversation window queries
entity.HasIndex(m => new { m.ConversationId, m.IsInActiveWindow, m.MessageIndex }); 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 // Index for role filtering
entity.HasIndex(m => m.Role); entity.HasIndex(m => m.Role);
// Performance: time-based queries
entity.HasIndex(m => m.CreatedAt);
} }
} }

View File

@ -1,384 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ApiKeyEncrypted")
.HasColumnType("text");
b.Property<int>("ConversationWindowSize")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<bool>("EnableMemory")
.HasColumnType("boolean");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int>("MaxTokens")
.HasColumnType("integer");
b.Property<string>("ModelEndpoint")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ModelName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ModelProvider")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int>("ProviderType")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("SystemPrompt")
.IsRequired()
.HasColumnType("text");
b.Property<double>("Temperature")
.HasColumnType("double precision");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<DateTime>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentId")
.HasColumnType("uuid");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("ConversationId")
.HasColumnType("uuid");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<decimal?>("EstimatedCost")
.HasPrecision(18, 6)
.HasColumnType("numeric(18,6)");
b.Property<long?>("ExecutionTimeMs")
.HasColumnType("bigint");
b.Property<string>("Input")
.HasColumnType("text");
b.Property<int?>("InputTokens")
.HasColumnType("integer");
b.Property<string>("Output")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("");
b.Property<int?>("OutputTokens")
.HasColumnType("integer");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("ToolCallResults")
.HasColumnType("text");
b.Property<string>("ToolCalls")
.HasColumnType("text");
b.Property<int?>("TotalTokens")
.HasColumnType("integer");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentId")
.HasColumnType("uuid");
b.Property<string>("ApiBaseUrl")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ApiKeyEncrypted")
.HasColumnType("text");
b.Property<JsonDocument>("Configuration")
.HasColumnType("jsonb");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("McpAuthTokenEncrypted")
.HasColumnType("text");
b.Property<string>("McpServerUrl")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ToolName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<DateTime>("LastMessageAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("MessageCount")
.HasColumnType("integer");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Summary")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("ConversationId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("ExecutionId")
.HasColumnType("uuid");
b.Property<bool>("IsInActiveWindow")
.HasColumnType("boolean");
b.Property<int>("MessageIndex")
.HasColumnType("integer");
b.Property<int>("Role")
.HasColumnType("integer");
b.Property<int?>("TokenCount")
.HasColumnType("integer");
b.Property<string>("ToolCalls")
.HasColumnType("text");
b.Property<string>("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
}
}
}

View File

@ -1,63 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Codex.Dal.Migrations
{
/// <inheritdoc />
public partial class AddPerformanceIndexes : Migration
{
/// <inheritdoc />
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");
}
/// <inheritdoc />
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" });
}
}
}

View File

@ -92,8 +92,6 @@ namespace Codex.Dal.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Name");
b.HasIndex("Type"); b.HasIndex("Type");
b.HasIndex("Status", "IsDeleted"); b.HasIndex("Status", "IsDeleted");
@ -162,8 +160,6 @@ namespace Codex.Dal.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CompletedAt");
b.HasIndex("ConversationId"); b.HasIndex("ConversationId");
b.HasIndex("Status"); b.HasIndex("Status");
@ -252,8 +248,6 @@ namespace Codex.Dal.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Title");
b.HasIndex("IsActive", "LastMessageAt") b.HasIndex("IsActive", "LastMessageAt")
.IsDescending(false, true); .IsDescending(false, true);
@ -299,12 +293,12 @@ namespace Codex.Dal.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("ExecutionId"); b.HasIndex("ExecutionId");
b.HasIndex("Role"); b.HasIndex("Role");
b.HasIndex("ConversationId", "MessageIndex");
b.HasIndex("ConversationId", "IsInActiveWindow", "MessageIndex"); b.HasIndex("ConversationId", "IsInActiveWindow", "MessageIndex");
b.ToTable("ConversationMessages"); b.ToTable("ConversationMessages");

View File

@ -80,52 +80,6 @@ This is the initial MVP release with all core functionality complete and tested.
--- ---
## [1.1.0-performance] - 2025-10-27
### Performance Optimizations (Non-Breaking)
**Impact Level**: 🟢 **LOW** - No API contract changes
This release includes database and query optimizations for improved performance at scale. **All endpoints remain unchanged** - frontend teams only need to refresh their API schema.
#### Performance Improvements
- **Database Indexes Added** for faster search/filter operations:
- `Agents.Name` - Fast agent name lookups
- `Conversations.Title` - Fast conversation title searches
- `AgentExecutions.CompletedAt` - Efficient time-based queries
- `ConversationMessages.CreatedAt` - Message timeline queries
- **SendMessage Optimization**: Context loading now uses `.Take()` limit
- Prevents loading entire conversation history (could be 1000+ messages)
- Only fetches configured window size (default: 10 messages)
- Response time stays constant as conversations grow
- **Package Compatibility**: Downgraded `Microsoft.Extensions.Http` 9.0.10 → 8.0.1
- Ensures .NET 8 LTS compatibility
#### Removed
- **Redundant Index**: `ConversationMessages(ConversationId, MessageIndex)`
- Already covered by composite index `(ConversationId, IsInActiveWindow, MessageIndex)`
#### Frontend Migration
**Time Required**: ~15 minutes
1. Refresh `api-schema.json` (already synced from backend)
2. Run existing integration tests to verify no regressions
3. (Optional) Verify performance improvements on search/filter operations
**Migration Guide**: `/Users/jean-philippe/Desktop/BACKEND_PERFORMANCE_UPDATES.md`
#### Performance Metrics
| Operation | Before | After | Improvement |
|-----------|--------|-------|-------------|
| List 50 agents | ~150ms | ~50ms | 3x faster |
| SendMessage (100-msg conv) | ~500ms | ~200ms | 2.5x faster |
| Search by agent name | ~200ms | ~30ms | 6x faster |
| Get conversation by title | ~180ms | ~35ms | 5x faster |
---
## [Unreleased] ## [Unreleased]
### Future Enhancements ### Future Enhancements

View File

@ -243,11 +243,11 @@ class AgentDto {
final String modelName; final String modelName;
final ModelProviderType providerType; final ModelProviderType providerType;
final String? modelEndpoint; final String? modelEndpoint;
final double? temperature; final double temperature;
final int? maxTokens; final int maxTokens;
final String? systemPrompt; final String systemPrompt;
final bool? enableMemory; final bool enableMemory;
final int? conversationWindowSize; final int conversationWindowSize;
final AgentStatus status; final AgentStatus status;
final DateTime createdAt; final DateTime createdAt;
final DateTime updatedAt; final DateTime updatedAt;
@ -261,11 +261,11 @@ class AgentDto {
required this.modelName, required this.modelName,
required this.providerType, required this.providerType,
this.modelEndpoint, this.modelEndpoint,
this.temperature, required this.temperature,
this.maxTokens, required this.maxTokens,
this.systemPrompt, required this.systemPrompt,
this.enableMemory, required this.enableMemory,
this.conversationWindowSize, required this.conversationWindowSize,
required this.status, required this.status,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
@ -300,11 +300,11 @@ class AgentDto {
modelName: json['modelName'] as String, modelName: json['modelName'] as String,
providerType: parseProviderType(json['providerType']), providerType: parseProviderType(json['providerType']),
modelEndpoint: json['modelEndpoint'] as String?, modelEndpoint: json['modelEndpoint'] as String?,
temperature: json['temperature'] != null ? (json['temperature'] as num).toDouble() : null, temperature: (json['temperature'] as num).toDouble(),
maxTokens: json['maxTokens'] as int?, maxTokens: json['maxTokens'] as int,
systemPrompt: json['systemPrompt'] as String?, systemPrompt: json['systemPrompt'] as String,
enableMemory: json['enableMemory'] as bool?, enableMemory: json['enableMemory'] as bool,
conversationWindowSize: json['conversationWindowSize'] as int?, conversationWindowSize: json['conversationWindowSize'] as int,
status: parseAgentStatus(json['status']), status: parseAgentStatus(json['status']),
createdAt: DateTime.parse(json['createdAt'] as String), createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String),