From 3e2d7dda42378a80f00b2bd3fe3acb26a77196c1 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 23 Jan 2025 23:45:43 -0600 Subject: [PATCH] add global stats --- .../Infrastructures/Enums/CacheType.cs | 15 +-- .../Infrastructures/SharpCacheSettings.cs | 2 +- .../Repositories/IBotSharpRepository.cs | 5 +- .../Statistics/Enums/StatCategory.cs | 7 ++ .../Statistics/Model/Statistics.cs | 13 -- .../Statistics/Models/BotSharpStats.cs | 34 ++++++ .../Services/IBotSharpStatService.cs | 9 ++ .../Statistics/Settings/StatisticsSettings.cs | 11 +- .../BotSharp.Abstraction/Utilities/MathExt.cs | 5 + .../BotSharp.Core/Agents/AgentPlugin.cs | 1 + .../Conversations/Services/TokenStatistics.cs | 24 +++- .../Infrastructures/DistributedLocker.cs | 11 +- .../FileRepository/FileRepository.Stats.cs | 107 ++++++++-------- .../FileRepository/FileRepository.cs | 11 +- .../Services/BotSharpStatService.cs | 115 ++++++++++++++++++ src/Infrastructure/BotSharp.Core/Using.cs | 4 + .../BotSharpLoggerExtensions.cs | 1 + .../Hooks/GlobalStatsConversationHook.cs | 45 +++++++ .../Hooks/RateLimitConversationHook.cs | 3 + .../Hooks/ChatHubConversationHook.cs | 1 - .../Hooks/StatsConversationHook.cs | 4 - .../Collections/GlobalStatisticsDocument.cs | 9 ++ .../MongoDbContext.cs | 3 + .../Repository/MongoRepository.Stats.cs | 65 ++++++++-- 24 files changed, 398 insertions(+), 107 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatCategory.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Model/Statistics.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatService.cs create mode 100644 src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs create mode 100644 src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs create mode 100644 src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/CacheType.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/CacheType.cs index 53165851..97008972 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/CacheType.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/CacheType.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace BotSharp.Abstraction.Infrastructures.Enums; -namespace BotSharp.Abstraction.Infrastructures.Enums +public enum CacheType { - public enum CacheType - { - MemoryCache, - RedisCache - } + MemoryCache = 1, + RedisCache = 2 } diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheSettings.cs index 7f42f1ca..acb0bb52 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheSettings.cs @@ -3,6 +3,6 @@ namespace BotSharp.Abstraction.Infrastructures; public class SharpCacheSettings { public bool Enabled { get; set; } = true; - public CacheType CacheType { get; set; } = Enums.CacheType.MemoryCache; + public CacheType CacheType { get; set; } = CacheType.MemoryCache; public string Prefix { get; set; } = "cache"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index d0174467..9f818789 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -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.Models; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Translation.Models; using BotSharp.Abstraction.Users.Enums; @@ -119,7 +120,9 @@ public interface IBotSharpRepository : IHaveServiceProvider #endregion #region Statistics - void IncrementConversationCount(); + BotSharpStats? GetGlobalStats(string category, string group, DateTime recordDate) => throw new NotImplementedException(); + bool SaveGlobalStats(BotSharpStats body) => throw new NotImplementedException(); + #endregion #region Translation diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatCategory.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatCategory.cs new file mode 100644 index 00000000..f2a25599 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatCategory.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Statistics.Enums; + +public static class StatCategory +{ + public static string LlmCost = "llm-cost"; + public static string AgentCall = "agent-call"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Model/Statistics.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Model/Statistics.cs deleted file mode 100644 index 5f4f1e5c..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Model/Statistics.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace BotSharp.Abstraction.Statistics.Model -{ - public class Statistics - { - public string Id { get; set; } = string.Empty; - public int ConversationCount { get; set; } - public DateTime UpdatedDateTime { get; set; } - } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs new file mode 100644 index 00000000..fdbba50d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs @@ -0,0 +1,34 @@ +namespace BotSharp.Abstraction.Statistics.Models; + +public class BotSharpStats +{ + [JsonPropertyName("category")] + public string Category { get; set; } = null!; + + [JsonPropertyName("group")] + public string Group { get; set; } = null!; + + [JsonPropertyName("data")] + public IDictionary Data { get; set; } = new Dictionary(); + + private DateTime innerRecordDate; + + [JsonPropertyName("record_date")] + public DateTime RecordDate + { + get + { + return innerRecordDate; + } + set + { + var date = new DateTime(value.Year, value.Month, value.Day, value.Hour, 0, 0); + innerRecordDate = date; + } + } + + public override string ToString() + { + return $"{Category}-{Group}: {Data?.Count ?? 0} ({RecordDate})"; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatService.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatService.cs new file mode 100644 index 00000000..1fa8ba9e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatService.cs @@ -0,0 +1,9 @@ +using BotSharp.Abstraction.Statistics.Models; + +namespace BotSharp.Abstraction.Statistics.Services; + +public interface IBotSharpStatService +{ + bool UpdateLlmCost(BotSharpStats stats); + bool UpdateAgentCall(BotSharpStats stats); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Settings/StatisticsSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Settings/StatisticsSettings.cs index 2758dfc3..47fa2950 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Settings/StatisticsSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Settings/StatisticsSettings.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace BotSharp.Abstraction.Statistics.Settings; -namespace BotSharp.Abstraction.Statistics.Settings +public class StatisticsSettings { - public class StatisticsSettings - { - public string DataDir { get; set; } - } + public bool Enabled { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/MathExt.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/MathExt.cs index 4ea18179..a216ee21 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/MathExt.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/MathExt.cs @@ -11,4 +11,9 @@ public static class MathExt { return Math.Max(Math.Max(a, b), c); } + + public static decimal Round(decimal value, MidpointRounding rouding = MidpointRounding.AwayFromZero) + { + return Math.Round(value, rouding); + } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index b21772a1..ad29bed8 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -32,6 +32,7 @@ public class AgentPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(provider => { diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index 1383ff1a..59c95259 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -1,7 +1,6 @@ using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.MLTasks; using System.Diagnostics; -using System.Drawing; namespace BotSharp.Core.Conversations.Services; @@ -42,8 +41,10 @@ public class TokenStatistics : ITokenStatistics var settingsService = _services.GetRequiredService(); var settings = settingsService.GetSetting(stats.Provider, _model); - _promptCost += stats.PromptCount / 1000f * settings.PromptCost; - _completionCost += stats.CompletionCount / 1000f * settings.CompletionCost; + var deltaPromptCost = stats.PromptCount / 1000f * settings.PromptCost; + var deltaCompletionCost = stats.CompletionCount / 1000 * settings.CompletionCost; + _promptCost += deltaPromptCost; + _completionCost += deltaCompletionCost; // Accumulated Token var stat = _services.GetRequiredService(); @@ -56,6 +57,23 @@ public class TokenStatistics : ITokenStatistics var total_cost = float.Parse(stat.GetState("llm_total_cost", "0")); total_cost += Cost; stat.SetState("llm_total_cost", total_cost, isNeedVersion: false, source: StateSource.Application); + + + var globalStats = _services.GetRequiredService(); + var body = new BotSharpStats + { + Category = StatCategory.LlmCost, + Group = $"Provider: {stats.Provider} | Model: {stats.Model}", + Data = new Dictionary + { + { "prompt_token_count_total", stats.PromptCount }, + { "completion_token_count_total", stats.CompletionCount }, + { "prompt_cost_total", deltaPromptCost }, + { "completion_cost_total", deltaCompletionCost } + }, + RecordDate = DateTime.UtcNow + }; + globalStats.UpdateLlmCost(body); } public void PrintStatistics() diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs index 3571f435..0b1a9c1d 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs @@ -9,7 +9,9 @@ public class DistributedLocker : IDistributedLocker private readonly IServiceProvider _services; private readonly ILogger _logger; - public DistributedLocker(IServiceProvider services, ILogger logger) + public DistributedLocker( + IServiceProvider services, + ILogger logger) { _services = services; _logger = logger; @@ -46,6 +48,13 @@ public class DistributedLocker : IDistributedLocker var timeout = TimeSpan.FromSeconds(timeoutInSeconds); var redis = _services.GetRequiredService(); + if (redis == null) + { + _logger.LogWarning($"The Redis server is experiencing issues and is not functioning as expected."); + action(); + return false; + } + var @lock = new RedisDistributedLock(resource, redis.GetDatabase()); using (var handle = @lock.TryAcquire(timeout)) { diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs index b9c3b664..602c1fc4 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs @@ -1,57 +1,68 @@ -using BotSharp.Abstraction.Statistics.Model; using System.IO; -namespace BotSharp.Core.Repository -{ - public partial class FileRepository - { - public void IncrementConversationCount() - { - var statsFileDirectory = FindCurrentStatsDirectory(); - if (statsFileDirectory == null) - { - statsFileDirectory = CreateStatsFileDirectory(); - } - var fileName = GenerateStatsFileName(); - var statsFile = Path.Combine(statsFileDirectory, fileName); - if (!File.Exists(statsFile)) - { - File.WriteAllText(statsFile, JsonSerializer.Serialize(new Statistics() - { - Id = Guid.NewGuid().ToString(), - UpdatedDateTime = DateTime.UtcNow - }, _options)); - } - var json = File.ReadAllText(statsFile); - var stats = JsonSerializer.Deserialize(json, _options); - stats.ConversationCount += 1; - stats.UpdatedDateTime = DateTime.UtcNow; - File.WriteAllText(statsFile, JsonSerializer.Serialize(stats, _options)); - } - public string? CreateStatsFileDirectory() - { - var dir = GenerateStatsDirectoryName(); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - return dir; - } - private string? FindCurrentStatsDirectory() - { - var dir = GenerateStatsDirectoryName(); - if (!Directory.Exists(dir)) return null; +namespace BotSharp.Core.Repository; - return dir; - } - private string GenerateStatsDirectoryName() +public partial class FileRepository +{ + public BotSharpStats? GetGlobalStats(string category, string group, DateTime recordDate) + { + var baseDir = Path.Combine(_dbSettings.FileRepository, STATS_FOLDER); + var dir = Path.Combine(baseDir, category, recordDate.Year.ToString(), recordDate.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 text = File.ReadAllText(file); + var list = JsonSerializer.Deserialize>(text, _options); + var found = list?.FirstOrDefault(x => x.Category.IsEqualTo(category) + && x.Group.IsEqualTo(group) + && x.RecordDate == recordDate); + return found; + } + + public bool SaveGlobalStats(BotSharpStats body) + { + var baseDir = Path.Combine(_dbSettings.FileRepository, STATS_FOLDER); + var dir = Path.Combine(baseDir, body.Category, body.RecordDate.Year.ToString(), body.RecordDate.Month.ToString("D2")); + if (!Directory.Exists(dir)) { - return Path.Combine(_dbSettings.FileRepository, _statisticsSetting.DataDir, DateTime.UtcNow.Year.ToString(), DateTime.UtcNow.ToString("MM")); + Directory.CreateDirectory(dir); } - private string GenerateStatsFileName() + + var file = Path.Combine(dir, STATS_FILE); + if (!File.Exists(file)) { - var fileName = DateTime.UtcNow.ToString("MMdd"); - return $"{fileName}-{STATS_FILE}"; + var list = new List { body }; + File.WriteAllText(file, JsonSerializer.Serialize(list, _options)); } + else + { + var text = File.ReadAllText(file); + var list = JsonSerializer.Deserialize>(text, _options); + var found = list?.FirstOrDefault(x => x.Category.IsEqualTo(body.Category) + && x.Group.IsEqualTo(body.Group) + && x.RecordDate == body.RecordDate); + + if (found != null) + { + found.Category = body.Category; + found.Group = body.Group; + found.Data = body.Data; + found.RecordDate = body.RecordDate; + } + else if (list != null) + { + list.Add(body); + } + else if (list == null) + { + list = new List { body }; + } + + File.WriteAllText(file, JsonSerializer.Serialize(list, _options)); + } + + return true; } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index 6a718d65..a5a2ffef 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -1,11 +1,11 @@ using System.IO; +using System.Text.RegularExpressions; +using System.Text.Encodings.Web; using FunctionDef = BotSharp.Abstraction.Functions.Models.FunctionDef; using BotSharp.Abstraction.Users.Models; -using System.Text.Encodings.Web; using BotSharp.Abstraction.Plugins.Models; -using BotSharp.Abstraction.Statistics.Settings; using BotSharp.Abstraction.Tasks.Models; -using System.Text.RegularExpressions; + namespace BotSharp.Core.Repository; @@ -15,7 +15,6 @@ public partial class FileRepository : IBotSharpRepository private readonly BotSharpDatabaseSettings _dbSettings; private readonly AgentSettings _agentSettings; private readonly ConversationSetting _conversationSettings; - private readonly StatisticsSettings _statisticsSetting; private readonly ILogger _logger; private JsonSerializerOptions _options; @@ -53,6 +52,8 @@ public partial class FileRepository : IBotSharpRepository private const string EXECUTION_LOG_FILE = "execution.log"; private const string PLUGIN_CONFIG_FILE = "config.json"; + + private const string STATS_FOLDER = "stats"; private const string STATS_FILE = "stats.json"; private const string CRON_FILE = "cron.json"; @@ -62,14 +63,12 @@ public partial class FileRepository : IBotSharpRepository BotSharpDatabaseSettings dbSettings, AgentSettings agentSettings, ConversationSetting conversationSettings, - StatisticsSettings statisticsSettings, ILogger logger) { _services = services; _dbSettings = dbSettings; _agentSettings = agentSettings; _conversationSettings = conversationSettings; - _statisticsSetting = statisticsSettings; _logger = logger; _options = new JsonSerializerOptions diff --git a/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs new file mode 100644 index 00000000..3fcc8215 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs @@ -0,0 +1,115 @@ +using BotSharp.Abstraction.Infrastructures; +using BotSharp.Abstraction.Statistics.Settings; + +namespace BotSharp.Core.Statistics.Services; + +public class BotSharpStatService : IBotSharpStatService +{ + private readonly IServiceProvider _services; + private readonly ILogger _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 BotSharpStatService( + IServiceProvider services, + ILogger logger, + StatisticsSettings settings) + { + _services = services; + _logger = logger; + _settings = settings; + } + + public bool UpdateLlmCost(BotSharpStats stats) + { + try + { + if (!_settings.Enabled) return false; + + var db = _services.GetRequiredService(); + var locker = _services.GetRequiredService(); + + var res = locker.Lock(GLOBAL_LLM_COST, () => + { + var body = db.GetGlobalStats(stats.Category, stats.Group, stats.RecordDate); + if (body == null) + { + db.SaveGlobalStats(stats); + return; + } + + foreach (var item in stats.Data) + { + var value = item.Value; + if (body.Data.TryGetValue(item.Key, out var curValue) && curValue != null) + { + var str = curValue.ToString(); + if (long.TryParse(str, out var count)) + { + value = long.Parse(value?.ToString() ?? "0") + count; + } + else if (decimal.TryParse(str, out var num)) + { + value = decimal.Parse(value?.ToString() ?? "0") + num; + } + } + body.Data[item.Key] = value; + } + + 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(); + var locker = _services.GetRequiredService(); + + var res = locker.Lock(GLOBAL_AGENT_CALL, () => + { + var body = db.GetGlobalStats(stats.Category, stats.Group, stats.RecordDate); + if (body == null) + { + db.SaveGlobalStats(stats); + return; + } + + foreach (var item in stats.Data) + { + var value = item.Value; + if (body.Data.TryGetValue(item.Key, out var curValue) && curValue != null) + { + var str = curValue.ToString(); + if (long.TryParse(str, out var count)) + { + value = long.Parse(value?.ToString() ?? "0") + count; + } + } + body.Data[item.Key] = value; + } + + 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}"); + return false; + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index 0aaaf01a..8655afb1 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -36,10 +36,14 @@ global using BotSharp.Abstraction.Translation.Attributes; global using BotSharp.Abstraction.Messaging.Enums; global using BotSharp.Abstraction.Knowledges.Models; global using BotSharp.Abstraction.SideCar.Attributes; +global using BotSharp.Abstraction.Statistics.Models; +global using BotSharp.Abstraction.Statistics.Enums; +global using BotSharp.Abstraction.Statistics.Services; global using BotSharp.Core.Repository; global using BotSharp.Core.Routing; global using BotSharp.Core.Agents.Services; global using BotSharp.Core.Conversations.Services; global using BotSharp.Core.Infrastructures; global using BotSharp.Core.Users.Services; +global using BotSharp.Core.Statistics.Services; global using BotSharp.Abstraction.Infrastructures.Events; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs b/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs index dfc9c27d..3ea2cd82 100644 --- a/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs +++ b/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs @@ -14,6 +14,7 @@ public static class BotSharpLoggerExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs new file mode 100644 index 00000000..1b7c5c94 --- /dev/null +++ b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs @@ -0,0 +1,45 @@ +using BotSharp.Abstraction.Statistics.Enums; +using BotSharp.Abstraction.Statistics.Models; +using BotSharp.Abstraction.Statistics.Services; + +namespace BotSharp.Logger.Hooks; + +public class GlobalStatsConversationHook : ConversationHookBase +{ + private readonly IServiceProvider _services; + + public GlobalStatsConversationHook( + IServiceProvider services) + { + _services = services; + } + + public override async Task OnMessageReceived(RoleDialogModel message) + { + UpdateAgentCall(message); + } + + public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) + { + UpdateAgentCall(message); + } + + private void UpdateAgentCall(RoleDialogModel message) + { + // record agent call + var globalStats = _services.GetRequiredService(); + + var body = new BotSharpStats + { + Category = StatCategory.AgentCall, + Group = $"Agent: {message.CurrentAgentId}", + Data = new Dictionary + { + { "agent_id", message.CurrentAgentId }, + { "agent_call_count", 1 } + }, + RecordDate = DateTime.UtcNow + }; + globalStats.UpdateAgentCall(body); + } +} diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs index 9b71214d..82fb0411 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs @@ -1,6 +1,9 @@ using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Repositories.Filters; +using BotSharp.Abstraction.Statistics.Enums; +using BotSharp.Abstraction.Statistics.Models; +using BotSharp.Abstraction.Statistics.Services; using BotSharp.Abstraction.Users; namespace BotSharp.Logger.Hooks; diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 6b425b4b..83cb2746 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -55,7 +55,6 @@ public class ChatHubConversationHook : ConversationHookBase var sender = await userService.GetMyProfile(); // Update console conversation UI for CSR - var model = new ChatResponseModel() { ConversationId = conv.ConversationId, diff --git a/src/Plugins/BotSharp.Plugin.Dashboard/Hooks/StatsConversationHook.cs b/src/Plugins/BotSharp.Plugin.Dashboard/Hooks/StatsConversationHook.cs index 5c16a173..c590318d 100644 --- a/src/Plugins/BotSharp.Plugin.Dashboard/Hooks/StatsConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.Dashboard/Hooks/StatsConversationHook.cs @@ -1,6 +1,4 @@ using BotSharp.Abstraction.Conversations; -using BotSharp.Abstraction.Plugins.Models; -using BotSharp.Abstraction.Repositories; namespace BotSharp.Plugin.Dashboard.Hooks; @@ -14,7 +12,5 @@ public class StatsConversationHook : ConversationHookBase public override async Task OnConversationInitialized(Conversation conversation) { - var db = _services.GetRequiredService(); - db.IncrementConversationCount(); } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs new file mode 100644 index 00000000..67270daa --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Plugin.MongoStorage.Collections; + +public class GlobalStatisticsDocument : MongoBase +{ + public string Category { get; set; } + public string Group { get; set; } + public IDictionary Data { get; set; } = new Dictionary(); + public DateTime RecordDate { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index 82e9f0d4..050ebedf 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -169,4 +169,7 @@ public class MongoDbContext public IMongoCollection CrontabItems => Database.GetCollection($"{_collectionPrefix}_CronTabItems"); + public IMongoCollection GlobalStatistics + => Database.GetCollection($"{_collectionPrefix}_GlobalStatistics"); + } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs index eea27ee9..d71e493d 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs @@ -1,16 +1,61 @@ -using System; -using System.Collections.Generic; -using System.Text; +using BotSharp.Abstraction.Statistics.Models; -namespace BotSharp.Plugin.MongoStorage.Repository +namespace BotSharp.Plugin.MongoStorage.Repository; + +public partial class MongoRepository { - public partial class MongoRepository + public BotSharpStats? GetGlobalStats(string category, string group, DateTime recordDate) { - #region Statistics - public void IncrementConversationCount() + var date = BuildRecordDate(recordDate); + + var builder = Builders.Filter; + var filters = new List>() { - - } - #endregion + builder.Eq(x => x.Category, category), + builder.Eq(x => x.Group, group), + builder.Eq(x => x.RecordDate, date) + }; + + var filterDef = builder.And(filters); + var found = _dc.GlobalStatistics.Find(filterDef).FirstOrDefault(); + if (found == null) return null; + + return new BotSharpStats + { + Category = found.Category, + Group = found.Group, + Data = found.Data, + RecordDate = found.RecordDate, + }; } + + public bool SaveGlobalStats(BotSharpStats body) + { + var date = BuildRecordDate(body.RecordDate); + var builder = Builders.Filter; + var filters = new List>() + { + builder.Eq(x => x.Category, body.Category), + builder.Eq(x => x.Group, body.Group), + builder.Eq(x => x.RecordDate, date) + }; + + var filterDef = builder.And(filters); + var updateDef = Builders.Update + .SetOnInsert(x => x.Id, Guid.NewGuid().ToString()) + .Set(x => x.Category, body.Category) + .Set(x => x.Group, body.Group) + .Set(x => x.Data, body.Data) + .Set(x => x.RecordDate, date); + + _dc.GlobalStatistics.UpdateOne(filterDef, updateDef, _options); + return true; + } + + #region Private methods + private DateTime BuildRecordDate(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, 0, 0); + } + #endregion }