diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ITokenStatistics.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ITokenStatistics.cs index 3355e6d3..8e2f8f22 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ITokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ITokenStatistics.cs @@ -7,6 +7,6 @@ public interface ITokenStatistics float Cost { get; } void StartTimer(); void StopTimer(); - void AddToken(TokenStatsModel stats); + void AddToken(TokenStatsModel stats, RoleDialogModel message); void PrintStatistics(); } \ No newline at end of file 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 c631f2d1..b435c7c2 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; @@ -121,7 +122,9 @@ public interface IBotSharpRepository : IHaveServiceProvider #endregion #region Statistics - void IncrementConversationCount(); + BotSharpStats? GetGlobalStats(string category, string group, DateTime recordTime) => throw new NotImplementedException(); + bool SaveGlobalStats(BotSharpStats body) => throw new NotImplementedException(); + #endregion #region Translation diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs index f4bb238d..c10ffd8d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs @@ -4,7 +4,6 @@ using Rougamo.Context; using Microsoft.Extensions.DependencyInjection; using BotSharp.Abstraction.Shared; - namespace BotSharp.Abstraction.SideCar.Attributes; [AttributeUsage(AttributeTargets.Method, Inherited = true)] @@ -34,7 +33,7 @@ public class SideCarAttribute : AsyncMoAttribute if (typeof(Task).IsAssignableFrom(retType)) { var syncResultType = retType.IsConstructedGenericType ? retType.GenericTypeArguments[0] : typeof(void); - (isHandled, value) = CallAsyncMethod(sidecar, sidecarMethod, syncResultType, methodArgs); + (isHandled, value) = await CallAsyncMethod(sidecar, sidecarMethod, syncResultType, methodArgs); } else { @@ -67,9 +66,10 @@ public class SideCarAttribute : AsyncMoAttribute return (sidecar, sidecarMethod); } - private (bool, object?) CallAsyncMethod(IConversationSideCar instance, MethodInfo method, Type retType, object[] args) + private async Task<(bool, object?)> CallAsyncMethod(IConversationSideCar instance, MethodInfo method, Type retType, object[] args) { object? value = null; + object? res = null; var isHandled = false; var enabled = instance != null && instance.IsEnabled() && method != null; @@ -81,12 +81,20 @@ public class SideCarAttribute : AsyncMoAttribute isHandled = true; if (retType == typeof(void)) { - value = GetMethod(nameof(CallAsync)).Invoke(this, [instance, method, args]); + res = GetMethod(nameof(CallAsync)).Invoke(this, [instance, method, args]); } else { - var task = GetMethod(nameof(CallGenericAsync)).MakeGenericMethod(retType).Invoke(this, [instance, method, args]); - value = task?.GetType().GetProperty("Result")?.GetValue(task); + res = GetMethod(nameof(CallGenericAsync)).MakeGenericMethod(retType).Invoke(this, [instance, method, args]); + } + + if (res != null && res is Task task) + { + await task; + if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + value = task?.GetType()?.GetProperty("Result")?.GetValue(task); + } } return (isHandled, value); 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..36165def --- /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 innerRecordTime; + + [JsonPropertyName("record_time")] + public DateTime RecordTime + { + get + { + return innerRecordTime; + } + set + { + var date = new DateTime(value.Year, value.Month, value.Day, value.Hour, 0, 0); + innerRecordTime = DateTime.SpecifyKind(date, DateTimeKind.Utc); + } + } + + public override string ToString() + { + return $"{Category}-{Group}: {Data?.Count ?? 0} ({RecordTime})"; + } +} \ 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..a2b7d5be 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; @@ -33,7 +32,7 @@ public class TokenStatistics : ITokenStatistics _logger = logger; } - public void AddToken(TokenStatsModel stats) + public void AddToken(TokenStatsModel stats, RoleDialogModel message) { _model = stats.Model; _promptTokenCount += stats.PromptCount; @@ -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 = $"Agent: {message.CurrentAgentId}", + Data = new Dictionary + { + { "prompt_token_count_total", stats.PromptCount }, + { "completion_token_count_total", stats.CompletionCount }, + { "prompt_cost_total", deltaPromptCost }, + { "completion_cost_total", deltaCompletionCost } + }, + RecordTime = 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.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index d38b2a89..1b665608 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -73,7 +73,7 @@ namespace BotSharp.Core.Repository break; } - _agents = []; + ResetInnerAgents(); } #region Update Agent Fields @@ -541,7 +541,7 @@ namespace BotSharp.Core.Repository } } - ResetLocalAgents(); + ResetInnerAgents(); } public void BulkInsertUserAgents(List userAgents) @@ -574,7 +574,7 @@ namespace BotSharp.Core.Repository Thread.Sleep(50); } - ResetLocalAgents(); + ResetInnerAgents(); } public bool DeleteAgents() @@ -629,7 +629,7 @@ namespace BotSharp.Core.Repository // Delete agent folder Directory.Delete(agentDir, true); - ResetLocalAgents(); + ResetInnerAgents(); return true; } catch @@ -638,7 +638,7 @@ namespace BotSharp.Core.Repository } } - private void ResetLocalAgents() + private void ResetInnerAgents() { _agents = []; _userAgents = []; diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs index b9c3b664..f4d3f40a 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 recordTime) + { + var baseDir = Path.Combine(_dbSettings.FileRepository, STATS_FOLDER); + var dir = Path.Combine(baseDir, category, recordTime.Year.ToString(), recordTime.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.RecordTime == recordTime); + 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")); + 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.RecordTime == body.RecordTime); + + if (found != null) + { + found.Category = body.Category; + found.Group = body.Group; + found.Data = body.Data; + found.RecordTime = body.RecordTime; + } + 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..d39b94bb --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs @@ -0,0 +1,132 @@ +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.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)) + { + var preValStr = preValue?.ToString(); + var curValStr = curValue?.ToString(); + try + { + 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; + } + } + + 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(); + var locker = _services.GetRequiredService(); + + 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)) + { + 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; + } + + 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..79ba4115 --- /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 } + }, + RecordTime = 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/Infrastructure/BotSharp.Logger/Hooks/TokenStatsConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/TokenStatsConversationHook.cs index f9bfb6e5..c365eaaf 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/TokenStatsConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/TokenStatsConversationHook.cs @@ -18,7 +18,7 @@ public class TokenStatsConversationHook : IContentGeneratingHook public async Task AfterGenerated(RoleDialogModel message, TokenStatsModel tokenStats) { _tokenStatistics.StopTimer(); - _tokenStatistics.AddToken(tokenStats); + _tokenStatistics.AddToken(tokenStats, message); await Task.CompletedTask; } } 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..e90bf4f5 --- /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 RecordTime { 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..44e667ca 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs @@ -1,16 +1,62 @@ -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 recordTime) { - #region Statistics - public void IncrementConversationCount() + var time = BuildRecordTime(recordTime); + + 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.RecordTime, time) + }; + + 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, + RecordTime = found.RecordTime + }; } + + public bool SaveGlobalStats(BotSharpStats body) + { + var time = BuildRecordTime(body.RecordTime); + 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.RecordTime, time) + }; + + 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.RecordTime, time); + + _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 } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index 9d96bf3c..5ef088e3 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -147,37 +147,20 @@ public class TwilioVoiceController : TwilioController } else { - // keep waiting for user response - if (request.Attempts > 3) + if (request.Attempts > _settings.MaxGatherAttempts) { - var instruction = new ConversationalVoiceResponse - { - SpeechPaths = new List(), - CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}", - ActionOnEmptyResult = true - }; - - // prompt user to speak clearly - if (request.SeqNum == 0) - { - instruction.SpeechPaths.Add("twilio/welcome.mp3"); - } - else - { - var lastRepy = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum - 1); - instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{lastRepy.SpeechFileName}"); - } await HookEmitter.Emit(_services, async hook => { - await hook.OnWaitingUserResponse(request, instruction); + await hook.OnAgentHangUp(request); }, new HookEmitOption { OnlyOnce = true }); - response = twilio.ReturnInstructions(instruction); + response = twilio.HangUp(null); } + // keep waiting for user response else { var instruction = new ConversationalVoiceResponse diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs b/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs index bbf4bab3..4c65481f 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs @@ -11,4 +11,5 @@ public class TwilioSetting public string CallbackHost { get; set; } public string AgentId { get; set; } public string CsrAgentNumber { get; set; } + public int MaxGatherAttempts { get; set; } = 4; } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index c600215a..c5ea7db1 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -173,7 +173,13 @@ }, "Statistics": { - "DataDir": "stats" + "Enabled": false + }, + + "SharpCache": { + "Enabled": true, + "CacheType": 1, + "Prefix": "botsharp" }, "LlamaSharp": {