Merge branch 'SciSharp:master' into master

This commit is contained in:
hchen2020 2025-01-24 16:04:01 -06:00 committed by GitHub
commit 24a6031de5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 431 additions and 116 deletions

View file

@ -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();
}

View file

@ -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
}

View file

@ -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";
}

View file

@ -3,6 +3,7 @@ using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Roles.Models;
using BotSharp.Abstraction.Shared;
using BotSharp.Abstraction.Statistics.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 recordTime) => throw new NotImplementedException();
bool SaveGlobalStats(BotSharpStats body) => throw new NotImplementedException();
#endregion
#region Translation

View file

@ -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";
}

View file

@ -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; }
}
}

View file

@ -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})";
}
}

View file

@ -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);
}

View file

@ -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; }
}

View file

@ -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);
}
}

View file

@ -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 =>
{

View file

@ -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()

View file

@ -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))
{

View file

@ -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 = [];

View file

@ -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;
}
}

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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();
}
}

View file

@ -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; }
}

View file

@ -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");
}

View file

@ -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
}

View file

@ -173,7 +173,13 @@
},
"Statistics": {
"DataDir": "stats"
"Enabled": false
},
"SharpCache": {
"Enabled": true,
"CacheType": 1,
"Prefix": "botsharp"
},
"LlamaSharp": {