Skip to content

Commit d83238a

Browse files
committed
feat(Infrastructure): add in-memory thread persistence service
Implemented IThreadPersistenceService with in-memory storage for AI Agent threads. This allows for saving, retrieving, and deleting serialized thread data. Modified files (3): - IThreadPersistenceService.cs: Added interface definition - InMemoryThreadPersistenceService.cs: Implemented in-memory logic - DependencyInjection.cs: Registered in-memory service for DI
1 parent 8bf9315 commit d83238a

5 files changed

Lines changed: 151 additions & 21 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace TaskAgent.Application.Interfaces;
2+
3+
/// <summary>
4+
/// Service for persisting and retrieving AI Agent threads
5+
/// </summary>
6+
public interface IThreadPersistenceService
7+
{
8+
/// <summary>
9+
/// Saves a serialized thread to storage
10+
/// </summary>
11+
/// <param name="threadId">Unique identifier for the thread</param>
12+
/// <param name="serializedThread">Serialized thread data</param>
13+
Task SaveThreadAsync(string threadId, string serializedThread);
14+
15+
/// <summary>
16+
/// Retrieves a serialized thread from storage
17+
/// </summary>
18+
/// <param name="threadId">Unique identifier for the thread</param>
19+
/// <returns>Serialized thread data, or null if not found</returns>
20+
Task<string?> GetThreadAsync(string threadId);
21+
22+
/// <summary>
23+
/// Deletes a thread from storage
24+
/// </summary>
25+
/// <param name="threadId">Unique identifier for the thread</param>
26+
Task DeleteThreadAsync(string threadId);
27+
}

TaskAgent.Infrastructure/DependencyInjection.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ IConfiguration configuration
2525

2626
services.AddScoped<ITaskRepository, TaskRepository>();
2727

28+
// For production: replace with Redis/SQL implementation and use Scoped/Transient
29+
services.AddSingleton<IThreadPersistenceService, InMemoryThreadPersistenceService>();
30+
2831
RegisterContentSafety(services, configuration);
2932

3033
return services;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Collections.Concurrent;
2+
using TaskAgent.Application.Interfaces;
3+
4+
namespace TaskAgent.Infrastructure.Services;
5+
6+
/// <summary>
7+
/// In-memory implementation of thread persistence
8+
/// Simple implementation for single-server scenarios
9+
/// </summary>
10+
public class InMemoryThreadPersistenceService : IThreadPersistenceService
11+
{
12+
private readonly ConcurrentDictionary<string, string> _threads = new();
13+
14+
public Task SaveThreadAsync(string threadId, string serializedThread)
15+
{
16+
if (string.IsNullOrWhiteSpace(threadId))
17+
{
18+
throw new ArgumentException("Thread ID cannot be empty", nameof(threadId));
19+
}
20+
21+
if (string.IsNullOrWhiteSpace(serializedThread))
22+
{
23+
throw new ArgumentException(
24+
"Serialized thread cannot be empty",
25+
nameof(serializedThread)
26+
);
27+
}
28+
29+
_threads[threadId] = serializedThread;
30+
31+
return Task.CompletedTask;
32+
}
33+
34+
public Task<string?> GetThreadAsync(string threadId)
35+
{
36+
if (string.IsNullOrWhiteSpace(threadId))
37+
{
38+
return Task.FromResult<string?>(null);
39+
}
40+
41+
_threads.TryGetValue(threadId, out string? serializedThread);
42+
43+
return Task.FromResult(serializedThread);
44+
}
45+
46+
public Task DeleteThreadAsync(string threadId)
47+
{
48+
if (string.IsNullOrWhiteSpace(threadId))
49+
{
50+
return Task.CompletedTask;
51+
}
52+
53+
_threads.TryRemove(threadId, out _);
54+
55+
return Task.CompletedTask;
56+
}
57+
}

TaskAgent.WebApp/DependencyInjection.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ private static void RegisterTaskAgent(IServiceCollection services, IConfiguratio
5050
{
5151
ITaskRepository taskRepository = sp.GetRequiredService<ITaskRepository>();
5252
ILogger<TaskAgentService> logger = sp.GetRequiredService<ILogger<TaskAgentService>>();
53+
IThreadPersistenceService threadPersistence = sp.GetRequiredService<IThreadPersistenceService>();
5354
AIAgent agent = TaskAgentService.CreateAgent(client, modelDeployment, taskRepository);
54-
return new TaskAgentService(agent, logger);
55+
return new TaskAgentService(agent, logger, threadPersistence);
5556
});
5657
}
5758
}

TaskAgent.WebApp/Services/TaskAgentService.cs

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,30 @@
33
using Microsoft.Extensions.AI;
44
using OpenAI;
55
using OpenAI.Chat;
6+
using System.Text.Json;
67
using TaskAgent.Application.Functions;
78
using TaskAgent.Application.Interfaces;
89

910
namespace TaskAgent.WebApp.Services;
1011

1112
/// <summary>
12-
/// Service for managing AI Agent lifecycle and interactions
13+
/// Service for managing AI Agent lifecycle and interactions.
14+
/// Uses thread persistence to maintain conversation context across requests.
1315
/// </summary>
1416
public class TaskAgentService : ITaskAgentService
1517
{
1618
private readonly AIAgent _agent;
17-
private readonly Dictionary<string, object> _threads = new();
1819
private readonly ILogger<TaskAgentService> _logger;
20+
private readonly IThreadPersistenceService _threadPersistence;
1921

20-
public TaskAgentService(AIAgent agent, ILogger<TaskAgentService> logger)
22+
public TaskAgentService(
23+
AIAgent agent,
24+
ILogger<TaskAgentService> logger,
25+
IThreadPersistenceService threadPersistence)
2126
{
2227
_agent = agent ?? throw new ArgumentNullException(nameof(agent));
2328
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
29+
_threadPersistence = threadPersistence ?? throw new ArgumentNullException(nameof(threadPersistence));
2430
}
2531

2632
/// <summary>
@@ -194,44 +200,80 @@ When showing task details (single task):
194200
);
195201
}
196202

197-
public async Task<string> SendMessageAsync(string message, string threadId)
203+
public async Task<(string response, string threadId)> SendMessageAsync(
204+
string message,
205+
string? threadId = null
206+
)
198207
{
199208
if (string.IsNullOrWhiteSpace(message))
200209
{
201210
throw new ArgumentException("Message cannot be empty", nameof(message));
202211
}
203212

204-
if (string.IsNullOrWhiteSpace(threadId))
213+
// For Chat Completion agents, conversation history is IN THE THREAD OBJECT
214+
// We serialize/deserialize it to maintain context across stateless HTTP requests
215+
AgentThread thread;
216+
string activeThreadId;
217+
218+
if (!string.IsNullOrWhiteSpace(threadId))
205219
{
206-
throw new ArgumentException("Thread ID cannot be empty", nameof(threadId));
220+
// Try to restore previous conversation
221+
string? serializedThread = await _threadPersistence.GetThreadAsync(threadId);
222+
223+
if (serializedThread != null)
224+
{
225+
// Deserialize existing thread to continue conversation
226+
JsonElement threadJson = JsonSerializer.Deserialize<JsonElement>(serializedThread);
227+
thread = _agent.DeserializeThread(threadJson);
228+
activeThreadId = threadId;
229+
}
230+
else
231+
{
232+
// Thread not found - create new one but keep the threadId
233+
thread = _agent.GetNewThread();
234+
activeThreadId = threadId;
235+
_logger.LogWarning("ThreadId {ThreadId} not found in storage, creating new thread", threadId);
236+
}
207237
}
208-
209-
// Get existing thread or use the one passed in
210-
if (!_threads.TryGetValue(threadId, out object? thread))
238+
else
211239
{
212-
throw new ArgumentException(
213-
$"Thread ID '{threadId}' not found. Create a thread first using GetNewThreadId()",
214-
nameof(threadId)
215-
);
240+
// First message - create new thread
241+
thread = _agent.GetNewThread();
242+
activeThreadId = Guid.NewGuid().ToString();
216243
}
217244

218245
try
219246
{
247+
// Run the agent with the thread
220248
dynamic? response = await _agent.RunAsync(message, (dynamic)thread);
221-
string? responseText = response.Text;
222-
return responseText ?? "I'm sorry, I couldn't process that request.";
249+
string? responseText = response?.Text;
250+
251+
// Serialize and persist the updated thread for next request
252+
JsonElement serializedThread = thread.Serialize();
253+
string serializedJson = JsonSerializer.Serialize(
254+
serializedThread,
255+
JsonSerializerOptions.Web
256+
);
257+
await _threadPersistence.SaveThreadAsync(activeThreadId, serializedJson);
258+
259+
return (
260+
responseText ?? "I'm sorry, I couldn't process that request.",
261+
activeThreadId
262+
);
223263
}
224264
catch (Exception ex)
225265
{
226-
_logger.LogError(ex, "Error in agent execution");
227-
return $"An error occurred while processing your request: {ex.Message}";
266+
_logger.LogError(ex, "Error in agent execution for thread {ThreadId}", activeThreadId);
267+
return (
268+
$"An error occurred while processing your request: {ex.Message}",
269+
activeThreadId
270+
);
228271
}
229272
}
230273

231274
public string GetNewThreadId()
232275
{
233-
string threadId = Guid.NewGuid().ToString();
234-
_threads[threadId] = _agent.GetNewThread();
235-
return threadId;
276+
// Simply return a new GUID - client manages thread persistence
277+
return Guid.NewGuid().ToString();
236278
}
237279
}

0 commit comments

Comments
 (0)