This commit is contained in:
Haiping Chen 2025-01-29 06:16:48 -06:00
commit 15a30f2670
19 changed files with 198 additions and 90 deletions

View file

@ -61,4 +61,6 @@ public interface IConversationService
Task<Conversation> GetConversationRecordOrCreateNew(string agentId);
bool IsConversationMode();
void SaveStates();
}

View file

@ -3,6 +3,7 @@ using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Roles.Models;
using BotSharp.Abstraction.Shared;
using BotSharp.Abstraction.Statistics.Enums;
using BotSharp.Abstraction.Statistics.Models;
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Translation.Models;
@ -120,8 +121,10 @@ public interface IBotSharpRepository : IHaveServiceProvider
#endregion
#region Statistics
BotSharpStats? GetGlobalStats(string category, string group, DateTime recordTime) => throw new NotImplementedException();
bool SaveGlobalStats(BotSharpStats body) => throw new NotImplementedException();
BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime, StatsInterval interval)
=> throw new NotImplementedException();
bool SaveGlobalStats(BotSharpStats body)
=> throw new NotImplementedException();
#endregion

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Statistics.Enums;
public enum StatsInterval
{
Hour = 1,
Day = 2,
Week = 3,
Month = 4
}

View file

@ -1,34 +1,80 @@
using BotSharp.Abstraction.Statistics.Enums;
namespace BotSharp.Abstraction.Statistics.Models;
public class BotSharpStats
{
[JsonPropertyName("category")]
public string Category { get; set; } = null!;
[JsonPropertyName("metric")]
public string Metric { get; set; } = null!;
[JsonPropertyName("group")]
public string Group { get; set; } = null!;
[JsonPropertyName("dimension")]
public string Dimension { get; set; } = null!;
[JsonPropertyName("data")]
public IDictionary<string, double> Data { get; set; } = new Dictionary<string, double>();
private DateTime innerRecordTime;
[JsonPropertyName("record_time")]
public DateTime RecordTime
public DateTime RecordTime { get; set; } = DateTime.UtcNow;
[JsonIgnore]
public StatsInterval IntervalType { get; set; }
[JsonPropertyName("interval")]
public string Interval
{
get
{
return innerRecordTime;
}
return IntervalType.ToString();
}
set
{
var date = new DateTime(value.Year, value.Month, value.Day, value.Hour, 0, 0);
innerRecordTime = DateTime.SpecifyKind(date, DateTimeKind.Utc);
if (Enum.TryParse(value, out StatsInterval type))
{
IntervalType = type;
}
}
}
[JsonPropertyName("start_time")]
public DateTime StartTime { get; set; }
[JsonPropertyName("end_time")]
public DateTime EndTime { get; set; }
public override string ToString()
{
return $"{Category}-{Group}: {Data?.Count ?? 0} ({RecordTime})";
return $"{Metric}-{Dimension} ({Interval}): {Data?.Count ?? 0}";
}
public static (DateTime, DateTime) BuildTimeInterval(DateTime recordTime, StatsInterval interval)
{
DateTime startTime = recordTime;
DateTime endTime = DateTime.UtcNow;
switch (interval)
{
case StatsInterval.Hour:
startTime = new DateTime(recordTime.Year, recordTime.Month, recordTime.Day, recordTime.Hour, 0, 0);
endTime = startTime.AddHours(1);
break;
case StatsInterval.Week:
var dayOfWeek = startTime.DayOfWeek;
var firstDayOfWeek = startTime.AddDays(-(int)dayOfWeek);
startTime = new DateTime(firstDayOfWeek.Year, firstDayOfWeek.Month, firstDayOfWeek.Day, 0, 0, 0);
endTime = startTime.AddDays(7);
break;
case StatsInterval.Month:
startTime = new DateTime(recordTime.Year, recordTime.Month, 1);
endTime = startTime.AddMonths(1);
break;
default:
startTime = new DateTime(recordTime.Year, recordTime.Month, recordTime.Day, 0, 0, 0);
endTime = startTime.AddDays(1);
break;
}
startTime = DateTime.SpecifyKind(startTime, DateTimeKind.Utc);
endTime = DateTime.SpecifyKind(endTime, DateTimeKind.Utc);
return (startTime, endTime);
}
}

View file

@ -1,9 +1,12 @@
using BotSharp.Abstraction.Statistics.Enums;
namespace BotSharp.Abstraction.Statistics.Models;
public class BotSharpStatsInput
{
public string Category { get; set; }
public string Group { get; set; }
public string Metric { get; set; }
public string Dimension { get; set; }
public List<StatsKeyValuePair> Data { get; set; } = [];
public DateTime RecordTime { get; set; }
public DateTime RecordTime { get; set; } = DateTime.UtcNow;
public StatsInterval IntervalType { get; set; } = StatsInterval.Day;
}

View file

@ -1,7 +1,6 @@
using BotSharp.Abstraction.Infrastructures.Events;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
namespace BotSharp.Core.Crontab.Services;
@ -22,6 +21,7 @@ public class CrontabEventSubscription : BackgroundService
using (var scope = _services.CreateScope())
{
var publisher = scope.ServiceProvider.GetRequiredService<IEventPublisher>();
var subscriber = scope.ServiceProvider.GetRequiredService<IEventSubscriber>();
var cron = scope.ServiceProvider.GetRequiredService<ICrontabService>();
var crons = await cron.GetCrontable();
@ -29,15 +29,20 @@ public class CrontabEventSubscription : BackgroundService
{
_ = Task.Run(async () =>
{
// Clean unhandled messages
await publisher.RemoveAsync($"Crontab:{item.Title}", count: 100);
await subscriber.SubscribeAsync($"Crontab:{item.Title}",
"Crontab",
port: 0,
priorityEnabled: false, async (sender, args) =>
priorityEnabled: false,
async (sender, args) =>
{
var scope = _services.CreateScope();
cron = scope.ServiceProvider.GetRequiredService<ICrontabService>();
await cron.ScheduledTimeArrived(item);
}, stoppingToken: stoppingToken);
},
stoppingToken: stoppingToken);
});
}
}

View file

@ -1,6 +1,8 @@
using BotSharp.Abstraction.Models;
namespace BotSharp.Core.Rules.Engines;
public interface IRuleEngine
{
Task Triggered(IRuleTrigger trigger, string data);
Task Triggered(IRuleTrigger trigger, string data, List<MessageState>? states = null);
}

View file

@ -18,7 +18,7 @@ public class RuleEngine : IRuleEngine
_logger = logger;
}
public async Task Triggered(IRuleTrigger trigger, string data)
public async Task Triggered(IRuleTrigger trigger, string data, List<MessageState>? states = null)
{
// Pull all user defined rules
var agentService = _services.GetRequiredService<IAgentService>();
@ -36,10 +36,11 @@ public class RuleEngine : IRuleEngine
// Trigger the agents
var instructService = _services.GetRequiredService<IInstructService>();
var convService = _services.GetRequiredService<IConversationService>();
foreach (var agent in preFilteredAgents)
{
var convService = _services.GetRequiredService<IConversationService>();
var conv = await convService.NewConversation(new Conversation
{
Channel = trigger.Channel,
@ -49,18 +50,25 @@ public class RuleEngine : IRuleEngine
var message = new RoleDialogModel(AgentRole.User, data);
var states = new List<MessageState>
var allStates = new List<MessageState>
{
new("channel", trigger.Channel),
new("channel_id", trigger.EntityId)
new("channel", trigger.Channel)
};
convService.SetConversationId(conv.Id, states);
if (states != null)
{
allStates.AddRange(states);
}
convService.SetConversationId(conv.Id, allStates);
await convService.SendMessage(agent.Id,
message,
null,
msg => Task.CompletedTask);
convService.SaveStates();
/*foreach (var rule in agent.Rules)
{
var userSay = $"===Input data with Before and After values===\r\n{data}\r\n\r\n===Trigger Criteria===\r\n{rule.Criteria}\r\n\r\nJust output 1 or 0 without explanation: ";

View file

@ -1,6 +1,6 @@
namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService : IConversationService
public partial class ConversationService
{
public async Task<bool> TruncateConversation(string conversationId, string messageId, string? newMessageId = null)
{

View file

@ -2,7 +2,7 @@ using BotSharp.Abstraction.Infrastructures.Enums;
namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService : IConversationService
public partial class ConversationService
{
public async Task UpdateBreakpoint(bool resetStates = false, string? reason = null, params string[] excludedStates)
{

View file

@ -216,4 +216,9 @@ public partial class ConversationService : IConversationService
var agent = db.GetAgent(routingCtx.EntryAgentId, basicsOnly: true);
return agent?.MaxMessageCount;
}
public void SaveStates()
{
_state.Save();
}
}

View file

@ -211,11 +211,11 @@ public class ConversationStateService : IConversationStateService, IDisposable
{
if (_conversationId == null)
{
Reset();
return;
}
var states = new List<StateKeyValue>();
foreach (var pair in _curStates)
{
var key = pair.Key;
@ -244,6 +244,7 @@ public class ConversationStateService : IConversationStateService, IDisposable
}
_db.UpdateConversationStates(_conversationId, states);
Reset();
_logger.LogInformation($"Saved states of conversation {_conversationId}");
}
@ -421,4 +422,10 @@ public class ConversationStateService : IConversationStateService, IDisposable
{
_curStates.Clear();
}
private void Reset()
{
_curStates.Clear();
_historyStates.Clear();
}
}

View file

@ -62,9 +62,10 @@ public class TokenStatistics : ITokenStatistics
var globalStats = _services.GetRequiredService<IBotSharpStatsService>();
var body = new BotSharpStatsInput
{
Category = StatsCategory.AgentLlmCost,
Group = message.CurrentAgentId,
Metric = StatsCategory.AgentLlmCost,
Dimension = message.CurrentAgentId,
RecordTime = DateTime.UtcNow,
IntervalType = StatsInterval.Day,
Data = [
new StatsKeyValuePair("prompt_token_count_total", stats.PromptCount),
new StatsKeyValuePair("completion_token_count_total", stats.CompletionCount),

View file

@ -182,7 +182,10 @@ public class RedisPublisher : IEventPublisher
var db = _redis.GetDatabase();
var entries = await db.StreamRangeAsync(channel, "-", "+", count: count, messageOrder: Order.Ascending);
var deletedCount = await db.StreamDeleteAsync(channel, entries.Select(x => x.Id).ToArray());
_logger.LogWarning($"Deleted {deletedCount} messages from Redis stream {channel}");
if (entries.Length > 0)
{
var deletedCount = await db.StreamDeleteAsync(channel, entries.Select(x => x.Id).ToArray());
_logger.LogWarning($"Deleted {deletedCount} messages from Redis stream {channel}");
}
}
}

View file

@ -4,28 +4,34 @@ namespace BotSharp.Core.Repository;
public partial class FileRepository
{
public BotSharpStats? GetGlobalStats(string category, string group, DateTime recordTime)
public BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime, StatsInterval interval)
{
var baseDir = Path.Combine(_dbSettings.FileRepository, STATS_FOLDER);
var dir = Path.Combine(baseDir, category, recordTime.Year.ToString(), recordTime.Month.ToString("D2"));
var (startTime, endTime) = BotSharpStats.BuildTimeInterval(recordTime, interval);
var dir = Path.Combine(baseDir, metric, startTime.Year.ToString(), startTime.Month.ToString("D2"));
if (!Directory.Exists(dir)) return null;
var file = Directory.GetFiles(dir).FirstOrDefault(x => Path.GetFileName(x) == STATS_FILE);
if (file == null) return null;
var time = BuildRecordTime(recordTime);
var text = File.ReadAllText(file);
var list = JsonSerializer.Deserialize<List<BotSharpStats>>(text, _options);
var found = list?.FirstOrDefault(x => x.Category.IsEqualTo(category)
&& x.Group.IsEqualTo(group)
&& x.RecordTime == time);
var found = list?.FirstOrDefault(x => x.Metric.IsEqualTo(metric)
&& x.Dimension.IsEqualTo(dimension)
&& x.StartTime == startTime
&& x.EndTime == endTime);
return found;
}
public bool SaveGlobalStats(BotSharpStats body)
{
var baseDir = Path.Combine(_dbSettings.FileRepository, STATS_FOLDER);
var dir = Path.Combine(baseDir, body.Category, body.RecordTime.Year.ToString(), body.RecordTime.Month.ToString("D2"));
var (startTime, endTime) = BotSharpStats.BuildTimeInterval(body.RecordTime, body.IntervalType);
body.StartTime = startTime;
body.EndTime = endTime;
var dir = Path.Combine(baseDir, body.Metric, startTime.Year.ToString(), startTime.Month.ToString("D2"));
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
@ -39,19 +45,22 @@ public partial class FileRepository
}
else
{
var time = BuildRecordTime(body.RecordTime);
var text = File.ReadAllText(file);
var list = JsonSerializer.Deserialize<List<BotSharpStats>>(text, _options);
var found = list?.FirstOrDefault(x => x.Category.IsEqualTo(body.Category)
&& x.Group.IsEqualTo(body.Group)
&& x.RecordTime == time);
var found = list?.FirstOrDefault(x => x.Metric.IsEqualTo(body.Metric)
&& x.Dimension.IsEqualTo(body.Dimension)
&& x.StartTime == startTime
&& x.EndTime == endTime);
if (found != null)
{
found.Category = body.Category;
found.Group = body.Group;
found.Metric = body.Metric;
found.Dimension = body.Dimension;
found.Data = body.Data;
found.RecordTime = body.RecordTime;
found.StartTime = body.StartTime;
found.EndTime = body.EndTime;
found.Interval = body.Interval;
}
else if (list != null)
{
@ -67,12 +76,4 @@ public partial class FileRepository
return true;
}
#region Private methods
private DateTime BuildRecordTime(DateTime date)
{
var recordDate = new DateTime(date.Year, date.Month, date.Day, date.Hour, 0, 0);
return DateTime.SpecifyKind(recordDate, DateTimeKind.Utc);
}
#endregion
}

View file

@ -29,8 +29,9 @@ public class BotSharpStatsService : IBotSharpStatsService
if (!_settings.Enabled
|| string.IsNullOrEmpty(resourceKey)
|| input == null
|| string.IsNullOrEmpty(input.Category)
|| string.IsNullOrEmpty(input.Group))
|| string.IsNullOrEmpty(input.Metric)
|| string.IsNullOrEmpty(input.Dimension)
|| input.Data.IsNullOrEmpty())
{
return false;
}
@ -39,14 +40,15 @@ public class BotSharpStatsService : IBotSharpStatsService
var res = locker.Lock(resourceKey, () =>
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var body = db.GetGlobalStats(input.Category, input.Group, input.RecordTime);
var body = db.GetGlobalStats(input.Metric, input.Dimension, input.RecordTime, input.IntervalType);
if (body == null)
{
var stats = new BotSharpStats
{
Category = input.Category,
Group = input.Group,
Metric = input.Metric,
Dimension = input.Dimension,
RecordTime = input.RecordTime,
IntervalType = input.IntervalType,
Data = input.Data.ToDictionary(x => x.Key, x => x.Value)
};
db.SaveGlobalStats(stats);
@ -85,7 +87,7 @@ public class BotSharpStatsService : IBotSharpStatsService
}
catch (Exception ex)
{
_logger.LogError($"Error when updating global stats {input.Category}-{input.Group}. {ex.Message}\r\n{ex.InnerException}");
_logger.LogError($"Error when updating global stats {input.Metric}-{input.Dimension}. {ex.Message}\r\n{ex.InnerException}");
return false;
}
}

View file

@ -31,12 +31,13 @@ public class GlobalStatsConversationHook : ConversationHookBase
var body = new BotSharpStatsInput
{
Category = StatsCategory.AgentCall,
Group = message.CurrentAgentId,
Metric = StatsCategory.AgentCall,
Dimension = message.CurrentAgentId,
RecordTime = DateTime.UtcNow,
IntervalType = StatsInterval.Day,
Data = [
new StatsKeyValuePair("agent_call_count", 1)
],
RecordTime = DateTime.UtcNow
]
};
globalStats.UpdateStats("global-agent-call", body);
}

View file

@ -1,9 +1,14 @@
using BotSharp.Abstraction.Statistics.Enums;
namespace BotSharp.Plugin.MongoStorage.Collections;
public class GlobalStatisticsDocument : MongoBase
{
public string Category { get; set; }
public string Group { get; set; }
public string Metric { get; set; }
public string Dimension { get; set; }
public IDictionary<string, double> Data { get; set; } = new Dictionary<string, double>();
public DateTime RecordTime { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public string Interval { get; set; }
}

View file

@ -1,19 +1,21 @@
using BotSharp.Abstraction.Statistics.Enums;
using BotSharp.Abstraction.Statistics.Models;
namespace BotSharp.Plugin.MongoStorage.Repository;
public partial class MongoRepository
{
public BotSharpStats? GetGlobalStats(string category, string group, DateTime recordTime)
public BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime, StatsInterval interval)
{
var time = BuildRecordTime(recordTime);
var (startTime, endTime) = BotSharpStats.BuildTimeInterval(recordTime, interval);
var builder = Builders<GlobalStatisticsDocument>.Filter;
var filters = new List<FilterDefinition<GlobalStatisticsDocument>>()
{
builder.Eq(x => x.Category, category),
builder.Eq(x => x.Group, group),
builder.Eq(x => x.RecordTime, time)
builder.Eq(x => x.Metric, metric),
builder.Eq(x => x.Dimension, dimension),
builder.Eq(x => x.StartTime, startTime),
builder.Eq(x => x.EndTime, endTime)
};
var filterDef = builder.And(filters);
@ -22,41 +24,44 @@ public partial class MongoRepository
return new BotSharpStats
{
Category = found.Category,
Group = found.Group,
Metric = found.Metric,
Dimension = found.Dimension,
Data = found.Data,
RecordTime = found.RecordTime
RecordTime = found.RecordTime,
StartTime = startTime,
EndTime = endTime,
Interval = interval.ToString()
};
}
public bool SaveGlobalStats(BotSharpStats body)
{
var time = BuildRecordTime(body.RecordTime);
var (startTime, endTime) = BotSharpStats.BuildTimeInterval(body.RecordTime, body.IntervalType);
body.RecordTime = DateTime.SpecifyKind(body.RecordTime, DateTimeKind.Utc);
body.StartTime = startTime;
body.EndTime = endTime;
var builder = Builders<GlobalStatisticsDocument>.Filter;
var filters = new List<FilterDefinition<GlobalStatisticsDocument>>()
{
builder.Eq(x => x.Category, body.Category),
builder.Eq(x => x.Group, body.Group),
builder.Eq(x => x.RecordTime, time)
builder.Eq(x => x.Metric, body.Metric),
builder.Eq(x => x.Dimension, body.Dimension),
builder.Eq(x => x.StartTime, startTime),
builder.Eq(x => x.EndTime, endTime)
};
var filterDef = builder.And(filters);
var updateDef = Builders<GlobalStatisticsDocument>.Update
.SetOnInsert(x => x.Id, Guid.NewGuid().ToString())
.Set(x => x.Category, body.Category)
.Set(x => x.Group, body.Group)
.Set(x => x.Metric, body.Metric)
.Set(x => x.Dimension, body.Dimension)
.Set(x => x.Data, body.Data)
.Set(x => x.RecordTime, time);
.Set(x => x.StartTime, body.StartTime)
.Set(x => x.EndTime, body.EndTime)
.Set(x => x.Interval, body.Interval)
.Set(x => x.RecordTime, body.RecordTime);
_dc.GlobalStatistics.UpdateOne(filterDef, updateDef, _options);
return true;
}
#region Private methods
private DateTime BuildRecordTime(DateTime date)
{
var recordDate = new DateTime(date.Year, date.Month, date.Day, date.Hour, 0, 0);
return DateTime.SpecifyKind(recordDate, DateTimeKind.Utc);
}
#endregion
}