Skip to content

Commit b52cbf5

Browse files
authored
Merge pull request #1354 from yileicn/master
add Conversation Log Cleanup
2 parents 09e1512 + e10e588 commit b52cbf5

12 files changed

Lines changed: 245 additions & 79 deletions

File tree

src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/ConversationSetting.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class CleanConversationSetting
2323
public int BatchSize { get; set; }
2424
public int MessageLimit { get; set; }
2525
public int BufferHours { get; set; }
26+
public int LogRetentionDays { get; set; }
27+
public int LogBatchSize { get; set; } = 2000;
2628
public IEnumerable<string> ExcludeAgentIds { get; set; } = new List<string>();
2729
}
2830

src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ Task<DateTimePagination<ConversationStateLogModel>> GetConversationStateLogs(str
198198
=> throw new NotImplementedException();
199199
#endregion
200200

201+
#region Log Cleanup
202+
Task<int> DeleteOldConversationLogs(int retentionDays, int batchSize)
203+
=> throw new NotImplementedException();
204+
#endregion
205+
201206
#region Instruction Log
202207
Task<bool> SaveInstructionLogs(IEnumerable<InstructionLogModel> logs)
203208
=> throw new NotImplementedException();

src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ public async Task<DateTimePagination<ConversationStateLogModel>> GetConversation
178178
}
179179
#endregion
180180

181+
#region Log Cleanup
182+
public async Task<int> DeleteOldConversationLogs(int retentionDays, int batchSize = 2000)
183+
{
184+
// For file repository, we might not need to implement this fully or we can just return 0
185+
// as it's typically used for local dev. Implementing it would require iterating all conversations.
186+
return await Task.FromResult(0);
187+
}
188+
#endregion
189+
181190
#region Instruction Log
182191
public async Task<bool> SaveInstructionLogs(IEnumerable<InstructionLogModel> logs)
183192
{

src/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs

Lines changed: 0 additions & 77 deletions
This file was deleted.

src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using BotSharp.Abstraction.Messaging.JsonConverters;
22
using BotSharp.Core.MCP.Settings;
33
using BotSharp.Core.Users.Services;
4-
using BotSharp.OpenAPI.BackgroundServices;
4+
using BotSharp.OpenAPI.Hooks;
5+
using BotSharp.OpenAPI.RuleTriggers;
56
using Microsoft.AspNetCore.Authentication;
67
using Microsoft.AspNetCore.Authentication.Cookies;
78
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -14,6 +15,10 @@
1415
using Microsoft.Net.Http.Headers;
1516
using Microsoft.OpenApi;
1617
using System.Text.Json.Serialization;
18+
using BotSharp.Abstraction.Crontab;
19+
using BotSharp.Abstraction.Rules;
20+
21+
1722
#if NET8_0
1823
using Microsoft.OpenApi.Models;
1924
#endif
@@ -36,7 +41,8 @@ public static IServiceCollection AddBotSharpOpenAPI(this IServiceCollection serv
3641
bool enableValidation)
3742
{
3843
services.AddScoped<IUserIdentity, UserIdentity>();
39-
services.AddHostedService<ConversationTimeoutService>();
44+
45+
services.AddCrontabServices();
4046

4147
// Add bearer authentication
4248
var schema = "MIXED_SCHEME";
@@ -223,6 +229,18 @@ private static async Task OnTicketReceivedContext(TicketReceivedContext context)
223229
}
224230
}
225231

232+
public static IServiceCollection AddCrontabServices(this IServiceCollection services)
233+
{
234+
services.AddScoped<ICrontabHook, IdleConversationCleanupCrontabHook>();
235+
services.AddScoped<ICrontabSource, IdleConversationCleanupRuleTrigger>();
236+
services.AddScoped<IRuleTrigger, IdleConversationCleanupRuleTrigger>();
237+
238+
services.AddScoped<ICrontabHook, ConversationLogCleanupCrontabHook>();
239+
services.AddScoped<ICrontabSource, ConversationLogCleanupRuleTrigger>();
240+
services.AddScoped<IRuleTrigger, ConversationLogCleanupRuleTrigger>();
241+
return services;
242+
}
243+
226244
/// <summary>
227245
/// Use Swagger/OpenAPI
228246
/// </summary>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using BotSharp.Abstraction.Conversations.Settings;
2+
using BotSharp.Abstraction.Crontab;
3+
using BotSharp.Abstraction.Crontab.Models;
4+
using BotSharp.Abstraction.Repositories;
5+
using BotSharp.OpenAPI.RuleTriggers;
6+
using Microsoft.Extensions.Hosting;
7+
using Microsoft.Extensions.Logging;
8+
using System;
9+
using System.Threading.Tasks;
10+
11+
namespace BotSharp.OpenAPI.Hooks
12+
{
13+
public class ConversationLogCleanupCrontabHook : ICrontabHook
14+
{
15+
private readonly ConversationSetting _settings;
16+
private readonly IBotSharpRepository _db;
17+
private readonly ILogger<ConversationLogCleanupCrontabHook> _logger;
18+
private readonly IHostApplicationLifetime _appLifetime;
19+
20+
public string[]? Triggers => new[] { nameof(ConversationLogCleanupRuleTrigger) };
21+
22+
public ConversationLogCleanupCrontabHook(
23+
ConversationSetting settings,
24+
IBotSharpRepository db,
25+
ILogger<ConversationLogCleanupCrontabHook> logger,
26+
IHostApplicationLifetime appLifetime)
27+
{
28+
_settings = settings;
29+
_db = db;
30+
_logger = logger;
31+
_appLifetime = appLifetime;
32+
}
33+
34+
public async Task OnCronTriggered(CrontabItem item)
35+
{
36+
var cleanSetting = _settings.CleanSetting;
37+
38+
if (cleanSetting == null || !cleanSetting.Enable || cleanSetting.LogRetentionDays <= 0) return;
39+
40+
int totalDeleted = 0;
41+
var cancellationToken = _appLifetime.ApplicationStopping;
42+
43+
while (!cancellationToken.IsCancellationRequested)
44+
{
45+
var deletedCount = await _db.DeleteOldConversationLogs(cleanSetting.LogRetentionDays, cleanSetting.LogBatchSize);
46+
if (deletedCount == 0) break;
47+
48+
totalDeleted += deletedCount;
49+
_logger.LogInformation($"Cleaned {deletedCount} conversation logs older than {cleanSetting.LogRetentionDays} days in this batch.");
50+
51+
try
52+
{
53+
// Sleep slightly to yield database resources, will throw TaskCanceledException on shutdown
54+
await Task.Delay(1000, cancellationToken);
55+
}
56+
catch (OperationCanceledException)
57+
{
58+
_logger.LogWarning("Conversation log cleanup was interrupted due to application shutdown.");
59+
break;
60+
}
61+
}
62+
63+
if (totalDeleted > 0)
64+
{
65+
_logger.LogInformation($"Successfully cleaned a total of {totalDeleted} conversation logs older than {cleanSetting.LogRetentionDays} days.");
66+
}
67+
}
68+
}
69+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using BotSharp.Abstraction.Conversations;
2+
using BotSharp.Abstraction.Conversations.Settings;
3+
using BotSharp.Abstraction.Crontab;
4+
using BotSharp.Abstraction.Crontab.Models;
5+
using BotSharp.OpenAPI.RuleTriggers;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace BotSharp.OpenAPI.Hooks
9+
{
10+
public class IdleConversationCleanupCrontabHook : ICrontabHook
11+
{
12+
private readonly ConversationSetting _settings;
13+
private readonly IConversationService _conversationService;
14+
private readonly ILogger<IdleConversationCleanupCrontabHook> _logger;
15+
16+
public string[]? Triggers => new[] { nameof(IdleConversationCleanupRuleTrigger) };
17+
18+
public IdleConversationCleanupCrontabHook(
19+
ConversationSetting settings,
20+
IConversationService conversationService,
21+
ILogger<IdleConversationCleanupCrontabHook> logger)
22+
{
23+
_settings = settings;
24+
_conversationService = conversationService;
25+
_logger = logger;
26+
}
27+
28+
public async Task OnCronTriggered(CrontabItem item)
29+
{
30+
var cleanSetting = _settings.CleanSetting;
31+
32+
if (cleanSetting == null || !cleanSetting.Enable) return;
33+
34+
try
35+
{
36+
var batchSize = cleanSetting.BatchSize;
37+
var limit = cleanSetting.MessageLimit;
38+
var bufferHours = cleanSetting.BufferHours;
39+
var excludeAgentIds = cleanSetting.ExcludeAgentIds ?? new List<string>();
40+
var conversationIds = await _conversationService.GetIdleConversations(batchSize, limit, bufferHours, excludeAgentIds);
41+
42+
if (!conversationIds.IsNullOrEmpty())
43+
{
44+
await _conversationService.DeleteConversations(conversationIds);
45+
}
46+
}
47+
catch (Exception ex)
48+
{
49+
_logger.LogError(ex, "Error occurred closing conversations.");
50+
}
51+
}
52+
}
53+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using BotSharp.Abstraction.Conversations.Enums;
2+
using BotSharp.Abstraction.Crontab;
3+
using BotSharp.Abstraction.Crontab.Models;
4+
using BotSharp.Abstraction.Rules;
5+
6+
namespace BotSharp.OpenAPI.RuleTriggers
7+
{
8+
public class ConversationLogCleanupRuleTrigger : IRuleTrigger, ICrontabSource
9+
{
10+
public string Channel => ConversationChannel.Crontab;
11+
public string Name => nameof(ConversationLogCleanupRuleTrigger);
12+
public string EntityType { get; set; } = string.Empty;
13+
public string EntityId { get; set; } = string.Empty;
14+
15+
public CrontabItem GetCrontabItem()
16+
{
17+
return new CrontabItem
18+
{
19+
Title = nameof(ConversationLogCleanupRuleTrigger),
20+
Description = "Clean up old conversation logs daily",
21+
Cron = "0 6 * * *", // Run at 6:00 AM UTC (Midnight Chicago Standard Time)
22+
TriggerType = CronTabItemTriggerType.MessageQueue
23+
};
24+
}
25+
}
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using BotSharp.Abstraction.Conversations.Enums;
2+
using BotSharp.Abstraction.Crontab;
3+
using BotSharp.Abstraction.Crontab.Models;
4+
using BotSharp.Abstraction.Rules;
5+
6+
namespace BotSharp.OpenAPI.RuleTriggers
7+
{
8+
public class IdleConversationCleanupRuleTrigger : IRuleTrigger, ICrontabSource
9+
{
10+
public string Channel => ConversationChannel.Crontab;
11+
public string Name => nameof(IdleConversationCleanupRuleTrigger);
12+
public string EntityType { get; set; } = string.Empty;
13+
public string EntityId { get; set; } = string.Empty;
14+
15+
public CrontabItem GetCrontabItem()
16+
{
17+
return new CrontabItem
18+
{
19+
Title = nameof(IdleConversationCleanupRuleTrigger),
20+
Description = "Clean up idle conversations hourly",
21+
Cron = "0 * * * *",
22+
TriggerType = CronTabItemTriggerType.MessageQueue
23+
};
24+
}
25+
}
26+
}

src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,37 @@ public async Task<DateTimePagination<ConversationStateLogModel>> GetConversation
139139
}
140140
#endregion
141141

142+
#region Log Cleanup
143+
public async Task<int> DeleteOldConversationLogs(int retentionDays, int batchSize)
144+
{
145+
if (retentionDays <= 0) return 0;
146+
var threshold = DateTime.UtcNow.AddDays(-retentionDays);
147+
148+
var contentLogFilter = Builders<ConversationContentLogDocument>.Filter.Lt(x => x.CreatedTime, threshold);
149+
var stateLogFilter = Builders<ConversationStateLogDocument>.Filter.Lt(x => x.CreatedTime, threshold);
150+
151+
var contentDocsToDelete = await _dc.ContentLogs.Find(contentLogFilter).Limit(batchSize).Project(x => x.Id).ToListAsync();
152+
long contentDeletedCount = 0;
153+
if (contentDocsToDelete.Any())
154+
{
155+
var deleteFilter = Builders<ConversationContentLogDocument>.Filter.In(x => x.Id, contentDocsToDelete);
156+
var contentDeleted = await _dc.ContentLogs.DeleteManyAsync(deleteFilter);
157+
contentDeletedCount = contentDeleted.DeletedCount;
158+
}
159+
160+
var stateDocsToDelete = await _dc.StateLogs.Find(stateLogFilter).Limit(batchSize).Project(x => x.Id).ToListAsync();
161+
long stateDeletedCount = 0;
162+
if (stateDocsToDelete.Any())
163+
{
164+
var deleteFilter = Builders<ConversationStateLogDocument>.Filter.In(x => x.Id, stateDocsToDelete);
165+
var stateDeleted = await _dc.StateLogs.DeleteManyAsync(deleteFilter);
166+
stateDeletedCount = stateDeleted.DeletedCount;
167+
}
168+
169+
return (int)(contentDeletedCount + stateDeletedCount);
170+
}
171+
#endregion
172+
142173
#region Instruction Log
143174
public async Task<bool> SaveInstructionLogs(IEnumerable<InstructionLogModel> logs)
144175
{

0 commit comments

Comments
 (0)