refine global stats

This commit is contained in:
Jicheng Lu 2025-01-27 23:10:37 -06:00
parent a8fa46eff5
commit 453d4bc38f
13 changed files with 119 additions and 113 deletions

View file

@ -1,7 +1,7 @@
namespace BotSharp.Abstraction.Statistics.Enums;
public static class StatCategory
public static class StatsCategory
{
public static string LlmCost = "llm-cost";
public static string AgentLlmCost = "agent-llm-cost";
public static string AgentCall = "agent-call";
}

View file

@ -0,0 +1,8 @@
namespace BotSharp.Abstraction.Statistics.Enums;
public enum StatsOperation
{
Add = 1,
Subtract = 2,
Reset = 3
}

View file

@ -9,7 +9,7 @@ public class BotSharpStats
public string Group { get; set; } = null!;
[JsonPropertyName("data")]
public IDictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
public IDictionary<string, double> Data { get; set; } = new Dictionary<string, double>();
private DateTime innerRecordTime;

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Statistics.Models;
public class BotSharpStatsInput
{
public string Category { get; set; }
public string Group { get; set; }
public List<StatsKeyValuePair> Data { get; set; } = [];
public DateTime RecordTime { get; set; }
}

View file

@ -0,0 +1,27 @@
using BotSharp.Abstraction.Statistics.Enums;
namespace BotSharp.Abstraction.Statistics.Models;
public class StatsKeyValuePair
{
public string Key { get; set; }
public double Value { get; set; }
public StatsOperation Operation { get; set; }
public StatsKeyValuePair()
{
}
public StatsKeyValuePair(string key, double value, StatsOperation operation = StatsOperation.Add)
{
Key = key;
Value = value;
Operation = operation;
}
public override string ToString()
{
return $"[{Key}]: {Value} ({Operation})";
}
}

View file

@ -4,6 +4,5 @@ namespace BotSharp.Abstraction.Statistics.Services;
public interface IBotSharpStatsService
{
bool UpdateLlmCost(BotSharpStats stats);
bool UpdateAgentCall(BotSharpStats stats);
bool UpdateStats(string resourceKey, BotSharpStatsInput input);
}

View file

@ -60,20 +60,19 @@ public class TokenStatistics : ITokenStatistics
var globalStats = _services.GetRequiredService<IBotSharpStatsService>();
var body = new BotSharpStats
var body = new BotSharpStatsInput
{
Category = StatCategory.LlmCost,
Group = $"Agent: {message.CurrentAgentId}",
Data = new Dictionary<string, object>
{
{ "prompt_token_count_total", stats.PromptCount },
{ "completion_token_count_total", stats.CompletionCount },
{ "prompt_cost_total", deltaPromptCost },
{ "completion_cost_total", deltaCompletionCost }
},
RecordTime = DateTime.UtcNow
Category = StatsCategory.AgentLlmCost,
Group = message.CurrentAgentId,
RecordTime = DateTime.UtcNow,
Data = [
new StatsKeyValuePair("prompt_token_count_total", stats.PromptCount),
new StatsKeyValuePair("completion_token_count_total", stats.CompletionCount),
new StatsKeyValuePair("prompt_cost_total", deltaPromptCost),
new StatsKeyValuePair("completion_cost_total", deltaCompletionCost)
]
};
globalStats.UpdateLlmCost(body);
globalStats.UpdateStats("global-llm-cost", body);
}
public void PrintStatistics()

View file

@ -13,11 +13,12 @@ public partial class FileRepository
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 == recordTime);
&& x.RecordTime == time);
return found;
}
@ -38,11 +39,12 @@ 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 == body.RecordTime);
&& x.RecordTime == time);
if (found != null)
{
@ -65,4 +67,12 @@ 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

@ -9,8 +9,6 @@ public class BotSharpStatsService : IBotSharpStatsService
private readonly ILogger<BotSharpStatsService> _logger;
private readonly StatisticsSettings _settings;
private const string GLOBAL_LLM_COST = "global-llm-cost";
private const string GLOBAL_AGENT_CALL = "global-agent-call";
private const int TIMEOUT_SECONDS = 5;
public BotSharpStatsService(
@ -23,109 +21,71 @@ public class BotSharpStatsService : IBotSharpStatsService
_settings = settings;
}
public bool UpdateLlmCost(BotSharpStats stats)
public bool UpdateStats(string resourceKey, BotSharpStatsInput input)
{
try
{
if (!_settings.Enabled) return false;
var db = _services.GetRequiredService<IBotSharpRepository>();
var locker = _services.GetRequiredService<IDistributedLocker>();
var res = locker.Lock(GLOBAL_LLM_COST, () =>
if (!_settings.Enabled
|| string.IsNullOrEmpty(resourceKey)
|| input == null
|| string.IsNullOrEmpty(input.Category)
|| string.IsNullOrEmpty(input.Group))
{
var body = db.GetGlobalStats(stats.Category, stats.Group, stats.RecordTime);
return false;
}
var locker = _services.GetRequiredService<IDistributedLocker>();
var res = locker.Lock(resourceKey, () =>
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var body = db.GetGlobalStats(input.Category, input.Group, input.RecordTime);
if (body == null)
{
var stats = new BotSharpStats
{
Category = input.Category,
Group = input.Group,
RecordTime = input.RecordTime,
Data = input.Data.ToDictionary(x => x.Key, x => x.Value)
};
db.SaveGlobalStats(stats);
return;
}
foreach (var item in stats.Data)
foreach (var item in input.Data)
{
var curValue = item.Value;
if (body.Data.TryGetValue(item.Key, out var preValue))
{
var preValStr = preValue?.ToString();
var curValStr = curValue?.ToString();
try
switch (item.Operation)
{
if (int.TryParse(preValStr, out var count))
{
curValue = int.Parse(curValStr ?? "0") + count;
}
else if (double.TryParse(preValStr, out var num))
{
curValue = double.Parse(curValStr ?? "0") + num;
}
}
catch
{
continue;
case StatsOperation.Add:
preValue += curValue;
break;
case StatsOperation.Subtract:
preValue -= curValue;
break;
case StatsOperation.Reset:
preValue = 0;
break;
}
body.Data[item.Key] = preValue;
}
body.Data[item.Key] = curValue;
}
db.SaveGlobalStats(body);
}, TIMEOUT_SECONDS);
return res;
}
catch (Exception ex)
{
_logger.LogError($"Error when updating global llm cost stats {stats}. {ex.Message}\r\n{ex.InnerException}");
return false;
}
}
public bool UpdateAgentCall(BotSharpStats stats)
{
try
{
if (!_settings.Enabled) return false;
var db = _services.GetRequiredService<IBotSharpRepository>();
var locker = _services.GetRequiredService<IDistributedLocker>();
var res = locker.Lock(GLOBAL_AGENT_CALL, () =>
{
var body = db.GetGlobalStats(stats.Category, stats.Group, stats.RecordTime);
if (body == null)
{
db.SaveGlobalStats(stats);
return;
}
foreach (var item in stats.Data)
{
var curValue = item.Value;
if (body.Data.TryGetValue(item.Key, out var preValue))
else
{
var preValStr = preValue?.ToString();
var curValStr = curValue?.ToString();
try
{
if (int.TryParse(preValStr, out var count))
{
curValue = int.Parse(curValStr ?? "0") + count;
}
}
catch
{
continue;
}
body.Data[item.Key] = curValue;
}
body.Data[item.Key] = curValue;
}
db.SaveGlobalStats(body);
}, TIMEOUT_SECONDS);
return res;
}
catch (Exception ex)
{
_logger.LogError($"Error when updating global agent call stats {stats}. {ex.Message}\r\n{ex.InnerException}");
_logger.LogError($"Error when updating global stats {input.Category}-{input.Group}. {ex.Message}\r\n{ex.InnerException}");
return false;
}
}

View file

@ -29,17 +29,15 @@ public class GlobalStatsConversationHook : ConversationHookBase
// record agent call
var globalStats = _services.GetRequiredService<IBotSharpStatsService>();
var body = new BotSharpStats
var body = new BotSharpStatsInput
{
Category = StatCategory.AgentCall,
Group = $"Agent: {message.CurrentAgentId}",
Data = new Dictionary<string, object>
{
{ "agent_id", message.CurrentAgentId },
{ "agent_call_count", 1 }
},
Category = StatsCategory.AgentCall,
Group = message.CurrentAgentId,
Data = [
new StatsKeyValuePair("agent_call_count", 1)
],
RecordTime = DateTime.UtcNow
};
globalStats.UpdateAgentCall(body);
globalStats.UpdateStats("global-agent-call", body);
}
}

View file

@ -253,9 +253,7 @@ public class ChatCompletionProvider : IChatCompletion
else if (message.Role == AgentRole.User)
{
var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content;
var textPart = ChatMessageContentPart.CreateTextPart(text);
var contentParts = new List<ChatMessageContentPart> { textPart };
messages.Add(new UserChatMessage(contentParts));
messages.Add(new UserChatMessage(text));
}
else if (message.Role == AgentRole.Assistant)
{

View file

@ -6,7 +6,6 @@ global using System.Linq;
global using System.Text.Json;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using DeepSeek.Core;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Agents.Models;
global using BotSharp.Abstraction.MLTasks;
@ -15,5 +14,4 @@ global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Conversations;
global using BotSharp.Abstraction.Loggers;
global using BotSharp.Abstraction.Functions.Models;
global using BotSharp.Abstraction.Utilities;
global using BotSharp.Plugin.DeepSeekAI.Models;
global using BotSharp.Abstraction.Utilities;

View file

@ -4,6 +4,6 @@ public class GlobalStatisticsDocument : MongoBase
{
public string Category { get; set; }
public string Group { get; set; }
public IDictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
public IDictionary<string, double> Data { get; set; } = new Dictionary<string, double>();
public DateTime RecordTime { get; set; }
}