Skip to content

Commit a050c97

Browse files
committed
feat(backend/Infrastructure): implement conversation message storage
Added PostgreSQL-backed storage for conversation messages and metadata. Implemented the `PostgresChatMessageStore` for managing chat messages and `ConversationThreadMetadataConfiguration` for thread metadata. Modified files (6): - ConversationThreadMetadataConfiguration.cs: Configured EF for thread metadata - PostgresChatMessageStore.cs: Implemented message storage logic - 20251126173145_AddConversationMessages.Designer.cs: Generated migration for messages - 20251126173145_AddConversationMessages.cs: Created migration for message storage - AgentStreamingService.cs: Integrated message streaming service - ConversationService.cs: Managed conversation threads and messages This enhances the chat functionality with persistent storage and retrieval.
1 parent 158fbd2 commit a050c97

11 files changed

Lines changed: 1028 additions & 409 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
3+
using TaskAgent.Domain.Entities;
4+
5+
namespace TaskAgent.Infrastructure.Data.Configurations;
6+
7+
/// <summary>
8+
/// Entity Framework configuration for ConversationThreadMetadata entity
9+
/// </summary>
10+
public class ConversationThreadMetadataConfiguration : IEntityTypeConfiguration<ConversationThreadMetadata>
11+
{
12+
public void Configure(EntityTypeBuilder<ConversationThreadMetadata> builder)
13+
{
14+
// Table name
15+
builder.ToTable("ConversationThreads");
16+
17+
// Primary key
18+
builder.HasKey(t => t.ThreadId);
19+
20+
// Properties
21+
builder.Property(t => t.ThreadId)
22+
.HasMaxLength(50)
23+
.IsRequired();
24+
25+
builder.Property(t => t.Title)
26+
.HasMaxLength(200);
27+
28+
builder.Property(t => t.Preview)
29+
.HasMaxLength(500);
30+
31+
builder.Property(t => t.MessageCount)
32+
.IsRequired();
33+
34+
builder.Property(t => t.SerializedState)
35+
.HasColumnType("text"); // PostgreSQL text type for large JSON
36+
37+
builder.Property(t => t.CreatedAt)
38+
.IsRequired()
39+
.HasColumnType("timestamptz");
40+
41+
builder.Property(t => t.UpdatedAt)
42+
.IsRequired()
43+
.HasColumnType("timestamptz");
44+
45+
builder.Property(t => t.IsActive)
46+
.IsRequired();
47+
48+
// Indexes
49+
builder.HasIndex(t => t.IsActive)
50+
.HasDatabaseName("IX_ConversationThreads_IsActive");
51+
52+
builder.HasIndex(t => t.UpdatedAt)
53+
.HasDatabaseName("IX_ConversationThreads_UpdatedAt");
54+
55+
builder.HasIndex(t => new { t.IsActive, t.UpdatedAt })
56+
.HasDatabaseName("IX_ConversationThreads_IsActive_UpdatedAt");
57+
}
58+
}
Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,67 @@
11
using Microsoft.EntityFrameworkCore;
2-
using TaskAgent.Domain.Constants;
32
using TaskAgent.Domain.Entities;
3+
using TaskAgent.Infrastructure.Data.Configurations;
44

55
namespace TaskAgent.Infrastructure.Data;
66

77
/// <summary>
8-
/// Database context for Conversation Threads stored in PostgreSQL
9-
/// Stores complete serialized AgentThread as JSON blob
8+
/// PostgreSQL database context for conversation messages and thread metadata.
109
/// </summary>
10+
/// <remarks>
11+
/// Follows Microsoft Agent Framework <c>ChatMessageStore</c> pattern for message persistence.
12+
/// </remarks>
1113
public class ConversationDbContext : DbContext
1214
{
1315
public ConversationDbContext(DbContextOptions<ConversationDbContext> options)
1416
: base(options) { }
1517

16-
public DbSet<ConversationThread> ConversationThreads => Set<ConversationThread>();
18+
public DbSet<ConversationMessage> ConversationMessages => Set<ConversationMessage>();
19+
public DbSet<ConversationThreadMetadata> ConversationThreads => Set<ConversationThreadMetadata>();
1720

1821
protected override void OnModelCreating(ModelBuilder modelBuilder)
1922
{
2023
base.OnModelCreating(modelBuilder);
2124

22-
modelBuilder.Entity<ConversationThread>(entity =>
25+
modelBuilder.Entity<ConversationMessage>(entity =>
2326
{
2427
// Table name
25-
entity.ToTable("ConversationThreads");
28+
entity.ToTable("ConversationMessages");
2629

2730
// Primary key
28-
entity.HasKey(e => e.ThreadId);
31+
entity.HasKey(e => e.MessageId);
2932

3033
// Properties configuration
3134
entity
32-
.Property(e => e.ThreadId)
35+
.Property(e => e.MessageId)
3336
.IsRequired()
34-
.HasMaxLength(100)
35-
.HasColumnType("varchar(100)");
37+
.HasMaxLength(50)
38+
.HasColumnType("varchar(50)");
3639

3740
entity
38-
.Property(e => e.SerializedThread)
41+
.Property(e => e.ThreadId)
3942
.IsRequired()
40-
.HasColumnType("json");
41-
42-
entity.Property(e => e.CreatedAt).IsRequired().HasColumnType("timestamptz");
43-
44-
entity.Property(e => e.UpdatedAt).IsRequired().HasColumnType("timestamptz");
45-
46-
entity.Property(e => e.MessageCount).IsRequired().HasDefaultValue(0);
47-
48-
entity.Property(e => e.IsActive).IsRequired().HasDefaultValue(true);
43+
.HasMaxLength(50)
44+
.HasColumnType("varchar(50)");
4945

5046
entity
51-
.Property(e => e.Title)
52-
.HasMaxLength(ConversationThreadConstants.MAX_TITLE_LENGTH);
47+
.Property(e => e.Role)
48+
.IsRequired()
49+
.HasMaxLength(20)
50+
.HasColumnType("varchar(20)");
5351

54-
entity
55-
.Property(e => e.Preview)
56-
.HasMaxLength(ConversationThreadConstants.MAX_PREVIEW_LENGTH);
52+
entity.Property(e => e.Content).IsRequired().HasColumnType("json");
53+
54+
entity.Property(e => e.Timestamp).IsRequired().HasColumnType("timestamptz");
5755

5856
// Indexes for better query performance
5957
entity
60-
.HasIndex(e => e.UpdatedAt)
61-
.HasDatabaseName("IX_ConversationThreads_UpdatedAt")
62-
.IsDescending();
58+
.HasIndex(e => new { e.ThreadId, e.Timestamp })
59+
.HasDatabaseName("IX_ConversationMessages_ThreadId_Timestamp");
6360

64-
entity.HasIndex(e => e.IsActive).HasDatabaseName("IX_ConversationThreads_IsActive");
65-
66-
entity
67-
.HasIndex(e => e.CreatedAt)
68-
.HasDatabaseName("IX_ConversationThreads_CreatedAt")
69-
.IsDescending();
61+
entity.HasIndex(e => e.ThreadId).HasDatabaseName("IX_ConversationMessages_ThreadId");
7062
});
63+
64+
// Apply ConversationThreadMetadata configuration
65+
modelBuilder.ApplyConfiguration(new ConversationThreadMetadataConfiguration());
7166
}
7267
}

src/backend/services/TaskAgent/src/TaskAgent.Infrastructure/InfrastructureServiceExtensions.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ IConfiguration configuration
3636
// Register repositories
3737
services.AddScoped<ITaskRepository, TaskRepository>();
3838

39-
// PostgreSQL DbContext for Conversations
39+
// PostgreSQL DbContext for Conversations (ChatMessageStore pattern)
4040
string? conversationsConnectionString = configuration.GetConnectionString(
4141
"ConversationsConnection"
4242
);
@@ -51,11 +51,9 @@ IConfiguration configuration
5151
options.UseNpgsql(conversationsConnectionString)
5252
);
5353

54-
// Register repositories
55-
services.AddScoped<ITaskRepository, TaskRepository>();
56-
57-
// PostgreSQL-backed thread persistence (production-ready)
58-
services.AddScoped<IThreadPersistenceService, PostgresThreadPersistenceService>();
54+
// Register application services (implementations in Infrastructure)
55+
services.AddScoped<IAgentStreamingService, AgentStreamingService>();
56+
services.AddScoped<IConversationService, ConversationService>();
5957

6058
RegisterContentSafety(services, configuration);
6159

0 commit comments

Comments
 (0)