Merge branch 'SciSharp:master' into master
This commit is contained in:
commit
7113eafb33
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, object> Data { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
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})";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public class AgentPlugin : IBotSharpPlugin
|
|||
services.AddScoped<ILlmProviderService, LlmProviderService>();
|
||||
services.AddScoped<IAgentService, AgentService>();
|
||||
services.AddScoped<IAgentHook, BasicAgentHook>();
|
||||
services.AddScoped<IBotSharpStatService, BotSharpStatService>();
|
||||
|
||||
services.AddScoped(provider =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<ILlmProviderService>();
|
||||
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<IConversationStateService>();
|
||||
|
|
@ -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<IBotSharpStatService>();
|
||||
var body = new BotSharpStats
|
||||
{
|
||||
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
|
||||
};
|
||||
globalStats.UpdateLlmCost(body);
|
||||
}
|
||||
|
||||
public void PrintStatistics()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ public class DistributedLocker : IDistributedLocker
|
|||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public DistributedLocker(IServiceProvider services, ILogger<DistributedLocker> logger)
|
||||
public DistributedLocker(
|
||||
IServiceProvider services,
|
||||
ILogger<DistributedLocker> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
|
|
@ -46,6 +48,13 @@ public class DistributedLocker : IDistributedLocker
|
|||
var timeout = TimeSpan.FromSeconds(timeoutInSeconds);
|
||||
|
||||
var redis = _services.GetRequiredService<IConnectionMultiplexer>();
|
||||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<UserAgent> 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 = [];
|
||||
|
|
|
|||
|
|
@ -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<Statistics>(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<List<BotSharpStats>>(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<BotSharpStats> { body };
|
||||
File.WriteAllText(file, JsonSerializer.Serialize(list, _options));
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
|
||||
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<BotSharpStats> { body };
|
||||
}
|
||||
|
||||
File.WriteAllText(file, JsonSerializer.Serialize(list, _options));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FileRepository> _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<FileRepository> logger)
|
||||
{
|
||||
_services = services;
|
||||
_dbSettings = dbSettings;
|
||||
_agentSettings = agentSettings;
|
||||
_conversationSettings = conversationSettings;
|
||||
_statisticsSetting = statisticsSettings;
|
||||
_logger = logger;
|
||||
|
||||
_options = new JsonSerializerOptions
|
||||
|
|
|
|||
|
|
@ -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<BotSharpStatService> _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<BotSharpStatService> logger,
|
||||
StatisticsSettings settings)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public bool UpdateLlmCost(BotSharpStats stats)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_settings.Enabled) return false;
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var locker = _services.GetRequiredService<IDistributedLocker>();
|
||||
|
||||
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<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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -14,6 +14,7 @@ public static class BotSharpLoggerExtensions
|
|||
services.AddScoped<IContentGeneratingHook, TokenStatsConversationHook>();
|
||||
services.AddScoped<IContentGeneratingHook, VerboseLogHook>();
|
||||
services.AddScoped<IConversationHook, RateLimitConversationHook>();
|
||||
services.AddScoped<IConversationHook, GlobalStatsConversationHook>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IBotSharpStatService>();
|
||||
|
||||
var body = new BotSharpStats
|
||||
{
|
||||
Category = StatCategory.AgentCall,
|
||||
Group = $"Agent: {message.CurrentAgentId}",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "agent_id", message.CurrentAgentId },
|
||||
{ "agent_call_count", 1 }
|
||||
},
|
||||
RecordTime = DateTime.UtcNow
|
||||
};
|
||||
globalStats.UpdateAgentCall(body);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<IBotSharpRepository>();
|
||||
db.IncrementConversationCount();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, object> Data { get; set; } = new Dictionary<string, object>();
|
||||
public DateTime RecordTime { get; set; }
|
||||
}
|
||||
|
|
@ -169,4 +169,7 @@ public class MongoDbContext
|
|||
public IMongoCollection<CrontabItemDocument> CrontabItems
|
||||
=> Database.GetCollection<CrontabItemDocument>($"{_collectionPrefix}_CronTabItems");
|
||||
|
||||
public IMongoCollection<GlobalStatisticsDocument> GlobalStatistics
|
||||
=> Database.GetCollection<GlobalStatisticsDocument>($"{_collectionPrefix}_GlobalStatistics");
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GlobalStatisticsDocument>.Filter;
|
||||
var filters = new List<FilterDefinition<GlobalStatisticsDocument>>()
|
||||
{
|
||||
|
||||
}
|
||||
#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<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)
|
||||
};
|
||||
|
||||
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.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>(),
|
||||
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<ITwilioSessionHook>(_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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,7 +173,13 @@
|
|||
},
|
||||
|
||||
"Statistics": {
|
||||
"DataDir": "stats"
|
||||
"Enabled": false
|
||||
},
|
||||
|
||||
"SharpCache": {
|
||||
"Enabled": true,
|
||||
"CacheType": 1,
|
||||
"Prefix": "botsharp"
|
||||
},
|
||||
|
||||
"LlamaSharp": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue