From e0fffe191c0c8ea92e107c349a950926c1eb9a28 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 4 Mar 2025 17:25:35 -0600 Subject: [PATCH 1/4] add latest state --- .../Instructs/IInstructHook.cs | 1 + .../Instructs/InstructHookBase.cs | 5 + .../Instructs/Models/InstructLogFilter.cs | 16 ++ .../Instructs/Models/InstructResponseModel.cs | 8 + .../Loggers/Models/InstructionLogModel.cs | 24 ++ .../BotSharp.Abstraction/Models/KeyValue.cs | 14 ++ .../Repositories/IBotSharpRepository.cs | 9 + .../Utilities/StringExtensions.cs | 16 ++ .../FileRepository.Conversation.cs | 211 +++++++++++++++--- .../FileRepository/FileRepository.Log.cs | 35 +++ .../FileRepository/FileRepository.cs | 3 +- .../BotSharpLoggerExtensions.cs | 1 + .../Hooks/InstructionLogHook.cs | 96 ++++++++ src/Infrastructure/BotSharp.Logger/Using.cs | 1 + .../Controllers/InstructModeController.cs | 12 + .../Collections/ConversationDocument.cs | 1 + .../Collections/InstructionLogBetaDocument.cs | 38 ++++ .../MongoDbContext.cs | 2 + .../MongoRepository.Conversation.cs | 89 ++++++-- .../Repository/MongoRepository.Log.cs | 100 +++++++++ .../Repository/MongoRepository.cs | 6 +- 21 files changed, 636 insertions(+), 52 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs create mode 100644 src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs create mode 100644 src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs index 8eaee99be..c9fa1cd72 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs @@ -7,4 +7,5 @@ public interface IInstructHook string SelfId { get; } Task BeforeCompletion(Agent agent, RoleDialogModel message); Task AfterCompletion(Agent agent, InstructResult result); + Task OnResponseGenerated(InstructResponseModel response); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs index b59e46409..f19cca5cc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs @@ -15,4 +15,9 @@ public virtual async Task AfterCompletion(Agent agent, InstructResult result) { return; } + + public virtual async Task OnResponseGenerated(InstructResponseModel response) + { + return; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs new file mode 100644 index 000000000..d8cafcee5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace BotSharp.Abstraction.Instructs.Models; + +public class InstructLogFilter : Pagination +{ + public List? AgentIds { get; set; } + public List? Providers { get; set; } + public List? Models { get; set; } + public List? States { get; set; } + + public static InstructLogFilter Empty() + { + return new InstructLogFilter(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs new file mode 100644 index 000000000..e2ee794df --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Instructs.Models; + +public class InstructResponseModel +{ + public string? AgentId { get; set; } + public string Provider { get; set; } + public string Model { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs new file mode 100644 index 000000000..f77b08fe3 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs @@ -0,0 +1,24 @@ +namespace BotSharp.Abstraction.Loggers.Models; + +public class InstructionLogModel +{ + [JsonPropertyName("agent_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? AgentId { get; set; } + + [JsonPropertyName("provider")] + public string Provider { get; set; } = default!; + + [JsonPropertyName("model")] + public string Model { get; set; } = default!; + + [JsonPropertyName("user_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? UserId { get; set; } + + [JsonPropertyName("states")] + public Dictionary States { get; set; } = []; + + [JsonPropertyName("created_time")] + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/KeyValue.cs b/src/Infrastructure/BotSharp.Abstraction/Models/KeyValue.cs index 812864893..b1dfa7348 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/KeyValue.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/KeyValue.cs @@ -13,3 +13,17 @@ public override string ToString() return $"Key: {Key}, Value: {Value}"; } } + +public class KeyValue +{ + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("value")] + public T? Value { get; set; } + + public override string ToString() + { + return $"Key: {Key}, Value: {Value}"; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 235c449b4..6f8de07d2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; @@ -171,6 +172,14 @@ List GetConversationStateLogs(string conversationId) => throw new NotImplementedException(); #endregion + #region Instruction Log + bool SaveInstructionLogs(IEnumerable logs) + => throw new NotImplementedException(); + + PagedItems GetInstructionLogs(InstructLogFilter filter) + => throw new NotImplementedException(); + #endregion + #region Statistics BotSharpStats? GetGlobalStats(string metric, string dimension, string dimRefVal, DateTime recordTime, StatsInterval interval) => throw new NotImplementedException(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs index 65f7b78e7..af222d0dd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs @@ -92,4 +92,20 @@ public static string JsonArrayContent(this string text) return JsonSerializer.Deserialize(text, options); } + + public static bool IsPrimitiveValue(this string value) + { + return int.TryParse(value, out _) || + long.TryParse(value, out _) || + double.TryParse(value, out _) || + float.TryParse(value, out _) || + bool.TryParse(value, out _) || + char.TryParse(value, out _) || + byte.TryParse(value, out _) || + sbyte.TryParse(value, out _) || + short.TryParse(value, out _) || + ushort.TryParse(value, out _) || + uint.TryParse(value, out _) || + ulong.TryParse(value, out _); + } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 7bd1dd151..5769320bc 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -33,13 +33,19 @@ public void CreateNewConversation(Conversation conversation) var stateFile = Path.Combine(dir, STATE_FILE); if (!File.Exists(stateFile)) { - File.WriteAllText(stateFile, JsonSerializer.Serialize(new List(), _options)); + File.WriteAllText(stateFile, "[]"); + } + + var latestStateFile = Path.Combine(dir, CONV_LATEST_STATE_FILE); + if (!File.Exists(latestStateFile)) + { + File.WriteAllText(latestStateFile, "{}"); } var breakpointFile = Path.Combine(dir, BREAKPOINT_FILE); if (!File.Exists(breakpointFile)) { - File.WriteAllText(breakpointFile, JsonSerializer.Serialize(new List(), _options)); + File.WriteAllText(breakpointFile, "[]"); } } @@ -300,14 +306,21 @@ public void UpdateConversationStates(string conversationId, List if (states.IsNullOrEmpty()) return; var convDir = FindConversationDirectory(conversationId); - if (!string.IsNullOrEmpty(convDir)) + if (string.IsNullOrEmpty(convDir)) return; + + var stateFile = Path.Combine(convDir, STATE_FILE); + if (File.Exists(stateFile)) { - var stateFile = Path.Combine(convDir, STATE_FILE); - if (File.Exists(stateFile)) - { - var stateStr = JsonSerializer.Serialize(states, _options); - File.WriteAllText(stateFile, stateStr); - } + var stateStr = JsonSerializer.Serialize(states, _options); + File.WriteAllText(stateFile, stateStr); + } + + var latestStateFile = Path.Combine(convDir, CONV_LATEST_STATE_FILE); + if (File.Exists(latestStateFile)) + { + var latestStates = BuildLatestStates(states); + var stateStr = JsonSerializer.Serialize(latestStates, _options); + File.WriteAllText(latestStateFile, stateStr); } } @@ -427,25 +440,57 @@ public PagedItems GetConversations(ConversationFilter filter) } // Check states - if (filter != null && !filter.States.IsNullOrEmpty()) + if (matched && filter != null && !filter.States.IsNullOrEmpty()) { - var stateFile = Path.Combine(d, STATE_FILE); - var convStates = CollectConversationStates(stateFile); - foreach (var pair in filter.States) - { - if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) continue; + var latestStateFile = Path.Combine(d, CONV_LATEST_STATE_FILE); + var convStates = CollectConversationLatestStates(latestStateFile); - var foundState = convStates.FirstOrDefault(x => x.Key.IsEqualTo(pair.Key)); - if (foundState == null) - { - matched = false; - break; - } - - if (!string.IsNullOrWhiteSpace(pair.Value)) + if (convStates.IsNullOrEmpty()) + { + matched = false; + } + else + { + foreach (var pair in filter.States) { - var curValue = foundState.Values.LastOrDefault()?.Data; - matched = matched && pair.Value.IsEqualTo(curValue); + if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) continue; + + var components = pair.Key.Split(".").ToList(); + var primaryKey = components[0]; + if (convStates.TryGetValue(primaryKey, out var doc)) + { + var elem = doc.RootElement.GetProperty("data"); + if (components.Count < 2) + { + if (!string.IsNullOrWhiteSpace(pair.Value)) + { + if (elem.ValueKind == JsonValueKind.Array) + { + matched = elem.EnumerateArray().Select(x => x.ToString()).Contains(pair.Value); + } + else if (elem.ValueKind == JsonValueKind.String) + { + matched = elem.GetString() == pair.Value; + } + else + { + matched = elem.GetRawText() == pair.Value; + } + } + } + else + { + var paths = components.Where((_, idx) => idx > 0); + var found = FindState(elem, paths, pair.Value); + matched = found != null; + } + } + else + { + matched = false; + } + + if (!matched) break; } } } @@ -575,8 +620,9 @@ public List TruncateConversation(string conversationId, string messageId // Handle truncated states var refTime = dialogs.ElementAt(foundIdx).MetaData.CreatedTime; var stateDir = Path.Combine(convDir, STATE_FILE); + var latestStateDir = Path.Combine(convDir, CONV_LATEST_STATE_FILE); var states = CollectConversationStates(stateDir); - isSaved = HandleTruncatedStates(stateDir, states, messageId, refTime); + isSaved = HandleTruncatedStates(stateDir, latestStateDir, states, messageId, refTime); // Handle truncated breakpoints var breakpointDir = Path.Combine(convDir, BREAKPOINT_FILE); @@ -703,7 +749,7 @@ private bool HandleTruncatedDialogs(string convDir, string dialogDir, List states, string refMsgId, DateTime refTime) + private bool HandleTruncatedStates(string stateDir, string latestStateDir, List states, string refMsgId, DateTime refTime) { var truncatedStates = new List(); foreach (var state in states) @@ -724,6 +770,10 @@ private bool HandleTruncatedStates(string stateDir, List states, } var isSaved = SaveTruncatedStates(stateDir, truncatedStates); + if (isSaved) + { + SaveTruncatedLatestStates(latestStateDir, truncatedStates); + } return isSaved; } @@ -794,6 +844,17 @@ private bool SaveTruncatedStates(string stateDir, List states) return true; } + private bool SaveTruncatedLatestStates(string latestStateDir, List states) + { + if (string.IsNullOrEmpty(latestStateDir) || states == null) return false; + if (!File.Exists(latestStateDir)) File.Create(latestStateDir); + + var latestStates = BuildLatestStates(states); + var stateStr = JsonSerializer.Serialize(latestStates, _options); + File.WriteAllText(latestStateDir, stateStr); + return true; + } + private bool SaveTruncatedBreakpoints(string breakpointDir, List breakpoints) { if (string.IsNullOrEmpty(breakpointDir) || breakpoints == null) return false; @@ -804,22 +865,98 @@ private bool SaveTruncatedBreakpoints(string breakpointDir, List CollectConversationLatestStates(string latestStateDir) { - if (string.IsNullOrEmpty(text)) return text; + if (string.IsNullOrEmpty(latestStateDir) || !File.Exists(latestStateDir)) return []; - var bytes = Encoding.UTF8.GetBytes(text); - var encoded = Convert.ToBase64String(bytes); - return encoded; + var str = File.ReadAllText(latestStateDir); + var states = JsonSerializer.Deserialize>(str, _options); + return states ?? []; } - private string? DecodeText(string? text) + private Dictionary BuildLatestStates(List states) { - if (string.IsNullOrEmpty(text)) return text; + var endNodes = new Dictionary(); + foreach (var pair in states) + { + var value = pair.Values?.LastOrDefault(); + if (value == null || !value.Active) continue; + + try + { + var jsonStr = JsonSerializer.Serialize(new { Data = JsonDocument.Parse(value.Data) }, _options); + var json = JsonDocument.Parse(jsonStr); + endNodes[pair.Key] = json; + } + catch + { + var str = JsonSerializer.Serialize(new { Data = value.Data }, _options); + var json = JsonDocument.Parse(str); + endNodes[pair.Key] = json; + } + } + + return endNodes; + } + + private JsonElement? FindState(JsonElement? root, IEnumerable paths, string? targetValue) + { + JsonElement? elem = null; + + if (root == null || paths.IsNullOrEmpty()) + { + return elem; + } + + elem = root; + for (int i = 0; i < paths.Count(); i++) + { + var field = paths.ElementAt(i); + if (elem.Value.ValueKind == JsonValueKind.Array) + { + if (elem.Value.EnumerateArray().IsNullOrEmpty()) + { + elem = null; + break; + } + else + { + foreach (var item in elem.Value.EnumerateArray()) + { + var subPaths = paths.Where((_, idx) => idx >= i); + elem = FindState(item, subPaths, targetValue); + if (elem != null) + { + return elem; + } + } + } + } + else if (elem.Value.TryGetProperty(field, out var prop)) + { + elem = prop; + } + } + + if (elem != null && !string.IsNullOrWhiteSpace(targetValue)) + { + if (elem.Value.ValueKind == JsonValueKind.Array) + { + var isInArray = elem.Value.EnumerateArray().Select(x => x.ToString()).Contains(targetValue); + return isInArray ? elem : null; + } + else if ((elem.Value.ValueKind == JsonValueKind.String && elem.Value.GetString() == targetValue) + || (elem.Value.ValueKind != JsonValueKind.String && elem.Value.GetRawText() == targetValue)) + { + return elem; + } + else + { + return null; + } + } - var decoded = Convert.FromBase64String(text); - var origin = Encoding.UTF8.GetString(decoded); - return origin; + return elem; } #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs index f82cb6faa..9f7ca3055 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Loggers.Models; using System.IO; @@ -122,6 +123,40 @@ public List GetConversationStateLogs(string conversat } #endregion + #region Instruction Log + public bool SaveInstructionLogs(IEnumerable logs) + { + if (logs.IsNullOrEmpty()) return false; + + var baseDir = Path.Combine(_dbSettings.FileRepository, INSTRUCTION_LOG_FOLDER); + if (!Directory.Exists(baseDir)) + { + Directory.CreateDirectory(baseDir); + } + + foreach (var log in logs) + { + var file = Path.Combine(baseDir, $"{Guid.NewGuid()}.log"); + var text = JsonSerializer.Serialize(log, _options); + File.WriteAllText(file, text); + } + return true; + } + + public PagedItems GetInstructionLogs(InstructLogFilter filter) + { + if (filter == null) + { + filter = InstructLogFilter.Empty(); + } + + return new PagedItems + { + + }; + } + #endregion + #region Private methods private int GetNextLogIndex(string logDir, string id) { diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index 78e90e4bc..f26e8bc04 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -6,7 +6,6 @@ using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Tasks.Models; - namespace BotSharp.Core.Repository; public partial class FileRepository : IBotSharpRepository @@ -34,6 +33,7 @@ public partial class FileRepository : IBotSharpRepository private const string DIALOG_FILE = "dialogs.json"; private const string STATE_FILE = "state.json"; private const string BREAKPOINT_FILE = "breakpoint.json"; + private const string CONV_LATEST_STATE_FILE = "latest-state.json"; private const string TRANSLATION_MEMORY_FILE = "memory.json"; private const string USERS_FOLDER = "users"; @@ -54,6 +54,7 @@ public partial class FileRepository : IBotSharpRepository private const string STATS_FILE = "stats.json"; private const string CRON_FILE = "cron.json"; + private const string INSTRUCTION_LOG_FOLDER = "instruction-logs"; public FileRepository( IServiceProvider services, diff --git a/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs b/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs index 6ccd70acd..0404ceb46 100644 --- a/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs +++ b/src/Infrastructure/BotSharp.Logger/BotSharpLoggerExtensions.cs @@ -15,6 +15,7 @@ public static IServiceCollection AddBotSharpLogger(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs new file mode 100644 index 000000000..03e1f8ff9 --- /dev/null +++ b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs @@ -0,0 +1,96 @@ +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Loggers.Models; +using BotSharp.Abstraction.Options; +using BotSharp.Abstraction.Users; +using System.Text.Json; + +namespace BotSharp.Logger.Hooks; + +public class InstructionLogHook : InstructHookBase +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly BotSharpOptions _options; + private readonly IUserIdentity _user; + + public InstructionLogHook( + IServiceProvider services, + ILogger logger, + IUserIdentity user, + BotSharpOptions options) + { + _services = services; + _logger = logger; + _user = user; + _options = options; + } + + public override async Task OnResponseGenerated(InstructResponseModel response) + { + if (response == null) return; + + var db = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + + var user = db.GetUserById(_user.Id); + db.SaveInstructionLogs(new List + { + new InstructionLogModel + { + AgentId = response.AgentId, + Provider = response.Provider, + Model = response.Model, + States = state.GetStates(), + UserId = user?.Id + } + }); + return; + } + + private Dictionary CollectStates() + { + var res = new Dictionary(); + var state = _services.GetRequiredService(); + var curStates = state.GetStates(); + + curStates["test"] = JsonSerializer.Serialize(new + { + Number = "789", + Dummy = new + { + Id = 123, + Name = "name", + Score = 12.123 + }, + Items = new List + { + new + { + Name = "image", + Label = "before-service", + Attribute = new + { + Location = "Chicago", + Time = "afternoon" + } + }, + new + { + Name = "pdf", + Label = "after-service", + Attribute = new + { + Location = "New York", + Time = "morning" + } + }, + }, + Lists = new List + { + "abc", + "bcd" + } + }, _options.JsonSerializerOptions); + return curStates; + } +} diff --git a/src/Infrastructure/BotSharp.Logger/Using.cs b/src/Infrastructure/BotSharp.Logger/Using.cs index 5115e776a..9c975007f 100644 --- a/src/Infrastructure/BotSharp.Logger/Using.cs +++ b/src/Infrastructure/BotSharp.Logger/Using.cs @@ -12,4 +12,5 @@ global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Repositories; global using BotSharp.Abstraction.Conversations.Settings; +global using BotSharp.Abstraction.Instructs; global using BotSharp.Logger.Hooks; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs index dbce9598c..450542236 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs @@ -75,6 +75,18 @@ public async Task ChatCompletion([FromBody] IncomingInstructRequest inpu { new RoleDialogModel(AgentRole.User, input.Text) }); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = input.AgentId, + Provider = input.Provider, + Model = input.Model + }); + } + return message.Content; } #endregion diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs index ace9472d7..8fb852390 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs @@ -14,4 +14,5 @@ public class ConversationDocument : MongoBase public List Tags { get; set; } = []; public DateTime CreatedTime { get; set; } public DateTime UpdatedTime { get; set; } + public Dictionary LatestStates { get; set; } = new(); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs new file mode 100644 index 000000000..615e93af8 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs @@ -0,0 +1,38 @@ +using BotSharp.Abstraction.Loggers.Models; +using System.Text.Json; + +namespace BotSharp.Plugin.MongoStorage.Collections; + +public class InstructionLogBetaDocument : MongoBase +{ + public string? AgentId { get; set; } + public string Provider { get; set; } = default!; + public string Model { get; set; } = default!; + public string? UserId { get; set; } + public Dictionary States { get; set; } = new(); + public DateTime CreatedTime { get; set; } + + public static InstructionLogBetaDocument ToMongoModel(InstructionLogModel log) + { + return new InstructionLogBetaDocument + { + AgentId = log.AgentId, + Provider = log.Provider, + Model = log.Model, + UserId = log.UserId, + CreatedTime = log.CreatedTime + }; + } + + public static InstructionLogModel ToDomainModel(InstructionLogBetaDocument log) + { + return new InstructionLogModel + { + AgentId = log.AgentId, + Provider = log.Provider, + Model = log.Model, + UserId = log.UserId, + CreatedTime = log.CreatedTime + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index 4aa82b2e6..9adc04b97 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -193,4 +193,6 @@ public IMongoCollection CrontabItems public IMongoCollection GlobalStatistics => GetCollectionOrCreate("GlobalStatistics"); + public IMongoCollection InstructionLogs + => GetCollectionOrCreate("InstructionLogsBeta"); } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index a78db60c2..1c0eeb2f3 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Repositories.Filters; +using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -23,7 +24,8 @@ public void CreateNewConversation(Conversation conversation) Status = conversation.Status, Tags = conversation.Tags ?? new(), CreatedTime = utcNow, - UpdatedTime = utcNow + UpdatedTime = utcNow, + LatestStates = [] }; var dialogDoc = new ConversationDialogDocument @@ -72,8 +74,9 @@ public bool DeleteConversations(IEnumerable conversationIds) var cronDeleted = _dc.CrontabItems.DeleteMany(conbTabItems); var convDeleted = _dc.Conversations.DeleteMany(filterConv); - return convDeleted.DeletedCount > 0 || dialogDeleted.DeletedCount > 0 || statesDeleted.DeletedCount > 0 || promptLogDeleted.DeletedCount > 0 - || contentLogDeleted.DeletedCount > 0 || stateLogDeleted.DeletedCount > 0 || convDeleted.DeletedCount > 0; + return convDeleted.DeletedCount > 0 || dialogDeleted.DeletedCount > 0 || statesDeleted.DeletedCount > 0 + || promptLogDeleted.DeletedCount > 0 || contentLogDeleted.DeletedCount > 0 + || stateLogDeleted.DeletedCount > 0 || convDeleted.DeletedCount > 0; } [SideCar] @@ -266,6 +269,14 @@ public void UpdateConversationStates(string conversationId, List .Set(x => x.UpdatedTime, DateTime.UtcNow); _dc.ConversationStates.UpdateOne(filterStates, updateStates); + + // Update latest states + var endNodes = BuildLatestStates(saveStates); + var filter = Builders.Filter.Eq(x => x.Id, conversationId); + var update = Builders.Update.Set(x => x.LatestStates, endNodes) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + + _dc.Conversations.UpdateOne(filter, update); } public void UpdateConversationStatus(string conversationId, string status) @@ -371,21 +382,47 @@ public PagedItems GetConversations(ConversationFilter filter) } // Filter states - var stateFilters = new List>(); if (filter != null && string.IsNullOrEmpty(filter.Id) && !filter.States.IsNullOrEmpty()) { foreach (var pair in filter.States) { - var elementFilters = new List> { Builders.Filter.Eq(x => x.Key, pair.Key) }; - if (!string.IsNullOrEmpty(pair.Value)) + if (string.IsNullOrWhiteSpace(pair.Key)) continue; + + // Format key + var keys = pair.Key.Split(".").ToList(); + keys.Insert(1, "data"); + keys.Insert(0, "LatestStates"); + var formattedKey = string.Join(".", keys); + + if (string.IsNullOrWhiteSpace(pair.Value)) + { + convFilters.Add(convBuilder.Exists(formattedKey)); + } + else if (bool.TryParse(pair.Value, out var boolValue)) + { + convFilters.Add(convBuilder.Eq(formattedKey, boolValue)); + } + else if (int.TryParse(pair.Value, out var intValue)) + { + convFilters.Add(convBuilder.Eq(formattedKey, intValue)); + } + else if (decimal.TryParse(pair.Value, out var decimalValue)) + { + convFilters.Add(convBuilder.Eq(formattedKey, decimalValue)); + } + else if (float.TryParse(pair.Value, out var floatValue)) + { + convFilters.Add(convBuilder.Eq(formattedKey, floatValue)); + } + else if (double.TryParse(pair.Value, out var doubleValue)) { - elementFilters.Add(Builders.Filter.Eq("Values.Data", pair.Value)); + convFilters.Add(convBuilder.Eq(formattedKey, doubleValue)); + } + else + { + convFilters.Add(convBuilder.Eq(formattedKey, pair.Value)); } - stateFilters.Add(Builders.Filter.ElemMatch(x => x.States, Builders.Filter.And(elementFilters))); } - - var targetConvIds = _dc.ConversationStates.Find(Builders.Filter.And(stateFilters)).ToEnumerable().Select(x => x.ConversationId).Distinct().ToList(); - convFilters.Add(convBuilder.In(x => x.Id, targetConvIds)); } // Sort and paginate @@ -527,6 +564,7 @@ public List TruncateConversation(string conversationId, string messageId var stateFilter = Builders.Filter.Eq(x => x.ConversationId, conversationId); var foundStates = _dc.ConversationStates.Find(stateFilter).FirstOrDefault(); + var endNodes = new Dictionary(); if (foundStates != null) { // Truncate states @@ -550,6 +588,7 @@ public List TruncateConversation(string conversationId, string messageId truncatedStates.Add(state); } foundStates.States = truncatedStates; + endNodes = BuildLatestStates(truncatedStates); } // Truncate breakpoints @@ -573,6 +612,7 @@ public List TruncateConversation(string conversationId, string messageId // Update conversation var convFilter = Builders.Filter.Eq(x => x.Id, conversationId); var updateConv = Builders.Update.Set(x => x.UpdatedTime, DateTime.UtcNow) + .Set(x => x.LatestStates, endNodes) .Set(x => x.DialogCount, truncatedDialogs.Count); _dc.Conversations.UpdateOne(convFilter, updateConv); @@ -621,8 +661,6 @@ public List GetConversationStateSearchKeys(int messageLowerLimit = 2, in return keys; } - - private string ConvertSnakeCaseToPascalCase(string snakeCase) { string[] words = snakeCase.Split('_'); @@ -640,4 +678,29 @@ private string ConvertSnakeCaseToPascalCase(string snakeCase) return pascalCase.ToString(); } + + private Dictionary BuildLatestStates(List states) + { + var endNodes = new Dictionary(); + foreach (var pair in states) + { + var value = pair.Values?.LastOrDefault(); + if (value == null || !value.Active) continue; + + try + { + var jsonStr = JsonSerializer.Serialize(new { Data = JsonDocument.Parse(value.Data) }, _botSharpOptions.JsonSerializerOptions); + var json = BsonDocument.Parse(jsonStr); + endNodes[pair.Key] = json; + } + catch + { + var str = JsonSerializer.Serialize(new { Data = value.Data }, _botSharpOptions.JsonSerializerOptions); + var json = BsonDocument.Parse(str); + endNodes[pair.Key] = json; + } + } + + return endNodes; + } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs index e4c64527d..c90960dde 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs @@ -1,4 +1,6 @@ +using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Loggers.Models; +using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -110,4 +112,102 @@ public List GetConversationStateLogs(string conversat return logs; } #endregion + + #region Instruction Log + public bool SaveInstructionLogs(IEnumerable logs) + { + if (logs.IsNullOrEmpty()) return false; + + var docs = new List(); + foreach (var log in logs) + { + var doc = InstructionLogBetaDocument.ToMongoModel(log); + foreach (var pair in log.States) + { + try + { + var jsonStr = JsonSerializer.Serialize(new { Data = JsonDocument.Parse(pair.Value) }, _botSharpOptions.JsonSerializerOptions); + var json = BsonDocument.Parse(jsonStr); + doc.States[pair.Key] = json; + } + catch + { + var jsonStr = JsonSerializer.Serialize(new { Data = pair.Value }, _botSharpOptions.JsonSerializerOptions); + var json = BsonDocument.Parse(jsonStr); + doc.States[pair.Key] = json; + } + } + docs.Add(doc); + } + + _dc.InstructionLogs.InsertMany(docs); + return true; + } + + public PagedItems GetInstructionLogs(InstructLogFilter filter) + { + if (filter == null) + { + filter = InstructLogFilter.Empty(); + } + + var builder = Builders.Filter; + var filters = new List>() { builder.Empty }; + + // Filter logs + if (!filter.AgentIds.IsNullOrEmpty()) + { + filters.Add(builder.In(x => x.AgentId, filter.AgentIds)); + } + if (!filter.Providers.IsNullOrEmpty()) + { + filters.Add(builder.In(x => x.Provider, filter.Providers)); + } + if (!filter.Models.IsNullOrEmpty()) + { + filters.Add(builder.In(x => x.Model, filter.Models)); + } + + if (!filter.States.IsNullOrEmpty()) + { + foreach (var pair in filter.States) + { + if (string.IsNullOrWhiteSpace(pair.Key)) continue; + + // Format key + var keys = pair.Key.Split(".").ToList(); + keys.Insert(1, "data"); + keys.Insert(0, "States"); + var formattedKey = string.Join(".", keys); + + if (pair.Value == null) + { + filters.Add(builder.Exists(formattedKey)); + } + else + { + filters.Add(builder.Eq(formattedKey, pair.Value)); + } + } + } + + var filterDef = builder.And(filters); + var sortDef = Builders.Sort.Descending(x => x.CreatedTime); + var docs = _dc.InstructionLogs.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); + var count = _dc.InstructionLogs.CountDocuments(filterDef); + + var logs = docs.Select(x => + { + var log = InstructionLogBetaDocument.ToDomainModel(x); + log.States = x.States.ToDictionary(x => x.Key, x => x.Value.GetElement("data").Value.ToString() ?? string.Empty); + return log; + }).ToList(); + + return new PagedItems + { + Items = logs, + Count = (int)count + }; + } + #endregion } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs index 258c18838..c91a291a0 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Options; using Microsoft.Extensions.Logging; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -7,16 +8,19 @@ public partial class MongoRepository : IBotSharpRepository private readonly MongoDbContext _dc; private readonly IServiceProvider _services; private readonly ILogger _logger; + private readonly BotSharpOptions _botSharpOptions; private UpdateOptions _options; public MongoRepository( MongoDbContext dc, IServiceProvider services, - ILogger logger) + ILogger logger, + BotSharpOptions botSharpOptions) { _dc = dc; _services = services; _logger = logger; + _botSharpOptions = botSharpOptions; _options = new UpdateOptions { IsUpsert = true, From 263ec44517a6030db533320c7934adc1685d4a85 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 4 Mar 2025 23:25:12 -0600 Subject: [PATCH 2/4] refine --- .../FileRepository.Conversation.cs | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 5769320bc..08a534d48 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -464,9 +464,15 @@ public PagedItems GetConversations(ConversationFilter filter) { if (!string.IsNullOrWhiteSpace(pair.Value)) { - if (elem.ValueKind == JsonValueKind.Array) + if (elem.ValueKind == JsonValueKind.Null) { - matched = elem.EnumerateArray().Select(x => x.ToString()).Contains(pair.Value); + matched = false; + } + else if (elem.ValueKind == JsonValueKind.Array) + { + matched = elem.EnumerateArray().Where(x => x.ValueKind != JsonValueKind.Null) + .Select(x => x.ToString()) + .Any(x => x == pair.Value); } else if (elem.ValueKind == JsonValueKind.String) { @@ -901,25 +907,21 @@ private Dictionary BuildLatestStates(List s private JsonElement? FindState(JsonElement? root, IEnumerable paths, string? targetValue) { - JsonElement? elem = null; + var elem = root; - if (root == null || paths.IsNullOrEmpty()) + if (elem == null || paths.IsNullOrEmpty()) { - return elem; + return null; } - elem = root; for (int i = 0; i < paths.Count(); i++) { + if (elem == null) return null; + var field = paths.ElementAt(i); if (elem.Value.ValueKind == JsonValueKind.Array) { - if (elem.Value.EnumerateArray().IsNullOrEmpty()) - { - elem = null; - break; - } - else + if (!elem.Value.EnumerateArray().IsNullOrEmpty()) { foreach (var item in elem.Value.EnumerateArray()) { @@ -931,18 +933,32 @@ private Dictionary BuildLatestStates(List s } } } + else + { + return null; + } } - else if (elem.Value.TryGetProperty(field, out var prop)) + else if (elem.Value.ValueKind == JsonValueKind.Object && elem.Value.TryGetProperty(field, out var prop)) { elem = prop; } + else + { + return null; + } } if (elem != null && !string.IsNullOrWhiteSpace(targetValue)) { - if (elem.Value.ValueKind == JsonValueKind.Array) + if (elem.Value.ValueKind == JsonValueKind.Null) + { + return null; + } + else if (elem.Value.ValueKind == JsonValueKind.Array) { - var isInArray = elem.Value.EnumerateArray().Select(x => x.ToString()).Contains(targetValue); + var isInArray = elem.Value.EnumerateArray().Where(x => x.ValueKind != JsonValueKind.Null) + .Select(x => x.ToString()) + .Any(x => x == targetValue); return isInArray ? elem : null; } else if ((elem.Value.ValueKind == JsonValueKind.String && elem.Value.GetString() == targetValue) From 366e1339c6190616b435bdbdd508a651fc672d13 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 5 Mar 2025 17:22:46 -0600 Subject: [PATCH 3/4] refactor instruction --- .../BotSharp.Abstraction.csproj | 2 +- .../Conversations/Models/RoleDialogModel.cs | 3 +- .../Instructs/Models/InstructLogFilter.cs | 2 +- .../Instructs/Models/InstructResponseModel.cs | 8 +- .../Loggers/Models/InstructionLogModel.cs | 28 ++++- .../MLTasks/IAudioCompletion.cs | 2 + .../MLTasks/IChatCompletion.cs | 2 + .../MLTasks/IImageCompletion.cs | 2 + .../MLTasks/IRealTimeCompletion.cs | 1 - .../MLTasks/ITextCompletion.cs | 1 + .../MLTasks/ITextEmbedding.cs | 7 +- .../Instruct/FileInstructService.Image.cs | 112 +++++++++++++++++- .../Instruct/FileInstructService.Pdf.cs | 24 +++- .../Instructs/InsturctionPlugin.cs | 11 +- .../Services/InstructService.Execute.cs | 20 ++++ .../FileRepository/FileRepository.Log.cs | 85 ++++++++++++- .../Hooks/InstructionLogHook.cs | 60 ++-------- .../BotSharp.OpenAPI/BotSharp.OpenAPI.csproj | 2 +- .../Controllers/InstructModeController.cs | 42 ++++++- .../Controllers/LoggerController.cs | 16 +++ .../Instructs/InstructMessageModel.cs | 1 + .../Providers/ChatCompletionProvider.cs | 9 +- .../Provider/NativeWhisperProvider.cs | 4 + .../Audio/AudioCompletionProvider.cs | 2 + .../Providers/Chat/ChatCompletionProvider.cs | 26 +++- .../Embedding/TextEmbeddingProvider.cs | 2 + .../Image/ImageCompletionProvider.cs | 1 + .../Providers/Text/TextCompletionProvider.cs | 1 + .../Providers/Chat/ChatCompletionProvider.cs | 24 +++- .../Providers/Text/TextCompletionProvider.cs | 1 + .../Chat/GeminiChatCompletionProvider.cs | 9 +- .../Chat/PalmChatCompletionProvider.cs | 8 +- .../Text/GeminiTextCompletionProvider.cs | 2 +- .../Text/PalmTextCompletionProvider.cs | 1 + .../Providers/ChatCompletionProvider.cs | 2 + .../Providers/ChatCompletionProvider.cs | 8 +- .../Providers/TextCompletionProvider.cs | 1 + .../Providers/TextEmbeddingProvider.cs | 3 +- .../Providers/ChatCompletionProvider.cs | 2 + .../Providers/TextCompletionProvider.cs | 2 + .../Providers/fastTextEmbeddingProvider.cs | 2 + .../Providers/ChatCompletionProvider.cs | 10 +- ...osoftExtensionsAIChatCompletionProvider.cs | 7 +- ...osoftExtensionsAITextCompletionProvider.cs | 1 + ...rosoftExtensionsAITextEmbeddingProvider.cs | 1 + .../Collections/InstructionLogBetaDocument.cs | 14 ++- .../MongoDbContext.cs | 2 +- .../Repository/MongoRepository.Log.cs | 39 +++--- .../Audio/AudioCompletionProvider.cs | 2 + .../Providers/Chat/ChatCompletionProvider.cs | 24 +++- .../Embedding/TextEmbeddingProvider.cs | 1 + .../Image/ImageCompletionProvider.cs | 1 + .../Providers/Text/TextCompletionProvider.cs | 1 + .../SemanticKernelChatCompletionProvider.cs | 4 +- .../SemanticKernelTextCompletionProvider.cs | 1 + .../SemanticKernelTextEmbeddingProvider.cs | 2 + .../Providers/ChatCompletionProvider.cs | 22 +++- 57 files changed, 537 insertions(+), 136 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index f189d3239..2266e9750 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index 585112a2a..6f96eec51 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -108,7 +108,8 @@ public class RoleDialogModel : ITrackableMessage [JsonPropertyName("generated_images")] public List GeneratedImages { get; set; } = new List(); - + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string RenderedInstruction { get; set; } = string.Empty; private RoleDialogModel() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs index d8cafcee5..9d1540521 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructLogFilter.cs @@ -7,7 +7,7 @@ public class InstructLogFilter : Pagination public List? AgentIds { get; set; } public List? Providers { get; set; } public List? Models { get; set; } - public List? States { get; set; } + public List? TemplateNames { get; set; } public static InstructLogFilter Empty() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs index e2ee794df..62752bf45 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructResponseModel.cs @@ -3,6 +3,10 @@ namespace BotSharp.Abstraction.Instructs.Models; public class InstructResponseModel { public string? AgentId { get; set; } - public string Provider { get; set; } - public string Model { get; set; } + public string Provider { get; set; } = default!; + public string Model { get; set; } = default!; + public string? TemplateName { get; set; } + public string UserMessage { get; set; } = default!; + public string? SystemInstruction { get; set; } + public string CompletionText { get; set; } = default!; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs index f77b08fe3..844d79424 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/InstructionLogModel.cs @@ -1,24 +1,50 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Loggers.Models; public class InstructionLogModel { + [JsonPropertyName("id")] + public string Id { get; set; } = default!; + [JsonPropertyName("agent_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? AgentId { get; set; } + [JsonPropertyName("agent_name")] + [JsonIgnore] + public string? AgentName { get; set; } + [JsonPropertyName("provider")] public string Provider { get; set; } = default!; [JsonPropertyName("model")] public string Model { get; set; } = default!; + [JsonPropertyName("template_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TemplateName { get; set; } + + [JsonPropertyName("user_message")] + public string UserMessage { get; set; } = string.Empty; + + [JsonPropertyName("system_instruction")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SystemInstruction { get; set; } + + [JsonPropertyName("completion_text")] + public string CompletionText { get; set; } = string.Empty; + [JsonPropertyName("user_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? UserId { get; set; } - [JsonPropertyName("states")] + [JsonIgnore] public Dictionary States { get; set; } = []; + [JsonPropertyName("states")] + public Dictionary InnerStates { get; set; } = []; + [JsonPropertyName("created_time")] public DateTime CreatedTime { get; set; } = DateTime.UtcNow; } diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IAudioCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IAudioCompletion.cs index 54ed2d7d3..85fc84f2a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IAudioCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IAudioCompletion.cs @@ -6,6 +6,8 @@ public interface IAudioCompletion { string Provider { get; } + string Model { get; } + Task GenerateTextFromAudioAsync(Stream audio, string audioFileName, string? text = null); Task GenerateAudioFromTextAsync(string text); diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs index 306e72e36..7cf52fe47 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs @@ -7,6 +7,8 @@ public interface IChatCompletion /// string Provider { get; } + string Model { get; } + /// /// Set model name, one provider can consume different model or version(s) /// diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IImageCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IImageCompletion.cs index 323fe643b..2b74a337c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IImageCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IImageCompletion.cs @@ -9,6 +9,8 @@ public interface IImageCompletion /// string Provider { get; } + string Model { get; } + /// /// Set model name, one provider can consume different model or version(s) /// diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs index be6f88212..c5103c188 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs @@ -6,7 +6,6 @@ public interface IRealTimeCompletion { string Provider { get; } string Model { get; } - void SetModelName(string model); Task Connect(RealtimeHubConnection conn, diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextCompletion.cs index 63a15940e..8fc549f72 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextCompletion.cs @@ -6,6 +6,7 @@ public interface ITextCompletion /// The LLM provider like Microsoft Azure, OpenAI, ClaudAI /// string Provider { get; } + string Model { get; } /// /// Set model name, one provider can consume different model or version(s) diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextEmbedding.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextEmbedding.cs index 79b3aff8c..a96dfec9e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextEmbedding.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ITextEmbedding.cs @@ -6,9 +6,14 @@ public interface ITextEmbedding /// The Embedding provider like Microsoft Azure, OpenAI, ClaudAI /// string Provider { get; } + string Model { get; } + + void SetModelName(string model); + + Task GetVectorAsync(string text); Task> GetVectorsAsync(List texts); - void SetModelName(string model); + void SetDimension(int dimension); int GetDimension(); } diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs index 4de657c38..2976ab475 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Instructs; using System.IO; namespace BotSharp.Core.Files.Services; @@ -6,10 +8,11 @@ public partial class FileInstructService { public async Task ReadImages(string? provider, string? model, string text, IEnumerable images, string? agentId = null) { + var innerAgentId = agentId ?? Guid.Empty.ToString(); var completion = CompletionProvider.GetChatCompletion(_services, provider: provider ?? "openai", model: model ?? "gpt-4o", multiModal: true); var message = await completion.GetChatCompletions(new Agent() { - Id = agentId ?? Guid.Empty.ToString(), + Id = innerAgentId, }, new List { new RoleDialogModel(AgentRole.User, text) @@ -17,16 +20,55 @@ public async Task ReadImages(string? provider, string? model, string tex Files = images?.Select(x => new BotSharpFile { FileUrl = x.FileUrl, FileData = x.FileData }).ToList() ?? new List() } }); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = innerAgentId, + Provider = completion.Provider, + Model = completion.Model, + UserMessage = text, + CompletionText = message.Content + }); + } + return message.Content; } public async Task GenerateImage(string? provider, string? model, string text, string? agentId = null) { + var innerAgentId = agentId ?? Guid.Empty.ToString(); var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-3"); var message = await completion.GetImageGeneration(new Agent() { - Id = agentId ?? Guid.Empty.ToString(), + Id = innerAgentId, }, new RoleDialogModel(AgentRole.User, text)); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = innerAgentId, + Provider = completion.Provider, + Model = completion.Model, + UserMessage = text, + CompletionText = message.Content + }); + } + return message; } @@ -37,6 +79,7 @@ public async Task VaryImage(string? provider, string? model, In throw new ArgumentException($"Cannot find image url or data!"); } + var innerAgentId = agentId ?? Guid.Empty.ToString(); var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2"); var bytes = await DownloadFile(image); using var stream = new MemoryStream(); @@ -46,10 +89,29 @@ public async Task VaryImage(string? provider, string? model, In var fileName = $"{image.FileName ?? "image"}.{image.FileExtension ?? "png"}"; var message = await completion.GetImageVariation(new Agent() { - Id = agentId ?? Guid.Empty.ToString() + Id = innerAgentId }, new RoleDialogModel(AgentRole.User, string.Empty), stream, fileName); stream.Close(); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = innerAgentId, + Provider = completion.Provider, + Model = completion.Model, + UserMessage = string.Empty, + CompletionText = message.Content + }); + } + return message; } @@ -60,6 +122,7 @@ public async Task EditImage(string? provider, string? model, st throw new ArgumentException($"Cannot find image url or data!"); } + var innerAgentId = agentId ?? Guid.Empty.ToString(); var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2"); var bytes = await DownloadFile(image); using var stream = new MemoryStream(); @@ -69,10 +132,29 @@ public async Task EditImage(string? provider, string? model, st var fileName = $"{image.FileName ?? "image"}.{image.FileExtension ?? "png"}"; var message = await completion.GetImageEdits(new Agent() { - Id = agentId ?? Guid.Empty.ToString() + Id = innerAgentId }, new RoleDialogModel(AgentRole.User, text), stream, fileName); stream.Close(); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = innerAgentId, + Provider = completion.Provider, + Model = completion.Model, + UserMessage = text, + CompletionText = message.Content + }); + } + return message; } @@ -84,6 +166,7 @@ public async Task EditImage(string? provider, string? model, st throw new ArgumentException($"Cannot find image/mask url or data"); } + var innerAgentId = agentId ?? Guid.Empty.ToString(); var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2"); var imageBytes = await DownloadFile(image); var maskBytes = await DownloadFile(mask); @@ -100,11 +183,30 @@ public async Task EditImage(string? provider, string? model, st var maskName = $"{mask.FileName ?? "mask"}.{mask.FileExtension ?? "png"}"; var message = await completion.GetImageEdits(new Agent() { - Id = agentId ?? Guid.Empty.ToString() + Id = innerAgentId }, new RoleDialogModel(AgentRole.User, text), imageStream, imageName, maskStream, maskName); imageStream.Close(); maskStream.Close(); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = innerAgentId, + Provider = completion.Provider, + Model = completion.Model, + UserMessage = text, + CompletionText = message.Content + }); + } + return message; } diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs index e1362455a..9f2132a1e 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs @@ -1,4 +1,6 @@ using BotSharp.Abstraction.Files.Converters; +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Instructs; namespace BotSharp.Core.Files.Services; @@ -23,11 +25,12 @@ public async Task ReadPdf(string? provider, string? model, string? model var images = await ConvertPdfToImages(pdfFiles); if (images.IsNullOrEmpty()) return content; + var innerAgentId = agentId ?? Guid.Empty.ToString(); var completion = CompletionProvider.GetChatCompletion(_services, provider: provider ?? "openai", model: model, modelId: modelId ?? "gpt-4", multiModal: true); var message = await completion.GetChatCompletions(new Agent() { - Id = agentId ?? Guid.Empty.ToString(), + Id = innerAgentId, }, new List { new RoleDialogModel(AgentRole.User, prompt) @@ -35,6 +38,25 @@ public async Task ReadPdf(string? provider, string? model, string? model Files = images.Select(x => new BotSharpFile { FileStorageUrl = x }).ToList() } }); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = innerAgentId, + Provider = completion.Provider, + Model = completion.Model, + UserMessage = prompt, + CompletionText = message.Content + }); + } + return message.Content; } catch (Exception ex) diff --git a/src/Infrastructure/BotSharp.Core/Instructs/InsturctionPlugin.cs b/src/Infrastructure/BotSharp.Core/Instructs/InsturctionPlugin.cs index 7b4596ffc..663c710ad 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/InsturctionPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/InsturctionPlugin.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Plugins.Models; +using BotSharp.Abstraction.Users.Enums; using Microsoft.Extensions.Configuration; namespace BotSharp.Core.Instructs; @@ -17,7 +18,15 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) public bool AttachMenu(List menu) { var section = menu.First(x => x.Label == "Apps"); - menu.Add(new PluginMenuDef("Instruction", link: "page/instruction", icon: "bx bx-book-content", weight: section.Weight + 5)); + menu.Add(new PluginMenuDef("Instruction", icon: "bx bx-book-content", weight: section.Weight + 5) + { + SubMenu = new List + { + new PluginMenuDef("Instruction", link: "page/instruction"), + new PluginMenuDef("Log", link: "page/instruction/log") { Roles = [UserRole.Root, UserRole.Admin] } + } + }); + return true; } } diff --git a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs index 1d9dd1d07..36f5b5233 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs @@ -43,6 +43,9 @@ public async Task Execute(string agentId, RoleDialogModel messag } } + var provider = string.Empty; + var model = string.Empty; + // Render prompt var prompt = string.IsNullOrEmpty(templateName) ? agentService.RenderedInstruction(agent) : @@ -57,11 +60,18 @@ public async Task Execute(string agentId, RoleDialogModel messag }; if (completer is ITextCompletion textCompleter) { + instruction = null; + provider = textCompleter.Provider; + model = textCompleter.Model; + var result = await textCompleter.GetCompletion(prompt, agentId, message.MessageId); response.Text = result; } else if (completer is IChatCompletion chatCompleter) { + provider = chatCompleter.Provider; + model = chatCompleter.Model; + if (instruction == "#TEMPLATE#") { instruction = prompt; @@ -93,6 +103,16 @@ public async Task Execute(string agentId, RoleDialogModel messag } await hook.AfterCompletion(agent, response); + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = agentId, + Provider = provider, + Model = model, + TemplateName = templateName, + UserMessage = prompt, + SystemInstruction = instruction, + CompletionText = response.Text + }); } return response; diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs index 9f7ca3055..ea1f27b5d 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs @@ -136,7 +136,8 @@ public bool SaveInstructionLogs(IEnumerable logs) foreach (var log in logs) { - var file = Path.Combine(baseDir, $"{Guid.NewGuid()}.log"); + var file = Path.Combine(baseDir, $"{Guid.NewGuid()}.json"); + log.InnerStates = BuildLogStates(log.States); var text = JsonSerializer.Serialize(log, _options); File.WriteAllText(file, text); } @@ -150,9 +151,67 @@ public PagedItems GetInstructionLogs(InstructLogFilter filt filter = InstructLogFilter.Empty(); } - return new PagedItems + var baseDir = Path.Combine(_dbSettings.FileRepository, INSTRUCTION_LOG_FOLDER); + if (!Directory.Exists(baseDir)) + { + return new(); + } + + var logs = new List(); + var files = Directory.GetFiles(baseDir); + foreach (var file in files) + { + var json = File.ReadAllText(file); + var log = JsonSerializer.Deserialize(json, _options); + if (log == null) continue; + + var matched = true; + if (!filter.AgentIds.IsNullOrEmpty()) + { + matched = matched && filter.AgentIds.Contains(log.AgentId); + } + if (!filter.Providers.IsNullOrEmpty()) + { + matched = matched && filter.Providers.Contains(log.Provider); + } + if (!filter.Models.IsNullOrEmpty()) + { + matched = matched && filter.Models.Contains(log.Model); + } + if (!filter.TemplateNames.IsNullOrEmpty()) + { + matched = matched && filter.TemplateNames.Contains(log.TemplateName); + } + + if (!matched) continue; + + log.Id = Path.GetFileNameWithoutExtension(file); + logs.Add(log); + } + + var records = logs.OrderByDescending(x => x.CreatedTime).Skip(filter.Offset).Take(filter.Size); + var agentIds = records.Where(x => !string.IsNullOrEmpty(x.AgentId)).Select(x => x.AgentId).ToList(); + var agents = GetAgents(new AgentFilter { + AgentIds = agentIds + }); + records = records.Select(x => + { + var states = x.InnerStates.ToDictionary(p => p.Key, p => + { + var data = p.Value.RootElement.GetProperty("data"); + return data.ValueKind != JsonValueKind.Null ? data.ToString() : null; + }); + x.AgentName = !string.IsNullOrEmpty(x.AgentId) ? agents.FirstOrDefault(a => a.Id == x.AgentId)?.Name : null; + x.States = states ?? []; + return x; + }).ToList(); + + return new PagedItems + { + Items = records, + Count = logs.Count() }; } #endregion @@ -176,6 +235,28 @@ private int GetNextLogIndex(string logDir, string id) return logIndexes.IsNullOrEmpty() ? 0 : logIndexes.Max() + 1; } + + private Dictionary BuildLogStates(Dictionary states) + { + var dic = new Dictionary(); + foreach (var pair in states) + { + try + { + var jsonStr = JsonSerializer.Serialize(new { Data = JsonDocument.Parse(pair.Value) }, _options); + var json = JsonDocument.Parse(jsonStr); + dic[pair.Key] = json; + } + catch + { + var str = JsonSerializer.Serialize(new { Data = pair.Value }, _options); + var json = JsonDocument.Parse(str); + dic[pair.Key] = json; + } + } + + return dic; + } #endregion } } diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs index 03e1f8ff9..265b631ae 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs @@ -1,8 +1,6 @@ using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Loggers.Models; -using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Users; -using System.Text.Json; namespace BotSharp.Logger.Hooks; @@ -10,19 +8,18 @@ public class InstructionLogHook : InstructHookBase { private readonly IServiceProvider _services; private readonly ILogger _logger; - private readonly BotSharpOptions _options; private readonly IUserIdentity _user; + public override string SelfId => string.Empty; + public InstructionLogHook( IServiceProvider services, ILogger logger, - IUserIdentity user, - BotSharpOptions options) + IUserIdentity user) { _services = services; _logger = logger; _user = user; - _options = options; } public override async Task OnResponseGenerated(InstructResponseModel response) @@ -40,57 +37,14 @@ public override async Task OnResponseGenerated(InstructResponseModel response) AgentId = response.AgentId, Provider = response.Provider, Model = response.Model, + TemplateName = response.TemplateName, + UserMessage = response.UserMessage, + SystemInstruction = response.SystemInstruction, + CompletionText = response.CompletionText, States = state.GetStates(), UserId = user?.Id } }); return; } - - private Dictionary CollectStates() - { - var res = new Dictionary(); - var state = _services.GetRequiredService(); - var curStates = state.GetStates(); - - curStates["test"] = JsonSerializer.Serialize(new - { - Number = "789", - Dummy = new - { - Id = 123, - Name = "name", - Score = 12.123 - }, - Items = new List - { - new - { - Name = "image", - Label = "before-service", - Attribute = new - { - Location = "Chicago", - Time = "afternoon" - } - }, - new - { - Name = "pdf", - Label = "after-service", - Attribute = new - { - Location = "New York", - Time = "morning" - } - }, - }, - Lists = new List - { - "abc", - "bcd" - } - }, _options.JsonSerializerOptions); - return curStates; - } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj index 58157b2dc..38ec30dba 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs index 450542236..6a1c37055 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs @@ -52,8 +52,29 @@ public async Task TextCompletion([FromBody] IncomingInstructRequest inpu .SetState("model", input.Model, source: StateSource.External) .SetState("model_id", input.ModelId, source: StateSource.External); + var agentId = input.AgentId ?? Guid.Empty.ToString(); var textCompletion = CompletionProvider.GetTextCompletion(_services); - return await textCompletion.GetCompletion(input.Text, input.AgentId ?? Guid.Empty.ToString(), Guid.NewGuid().ToString()); + var response = await textCompletion.GetCompletion(input.Text, agentId, Guid.NewGuid().ToString()); + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = agentId, + Provider = textCompletion.Provider, + Model = textCompletion.Model, + TemplateName = input.Template, + UserMessage = input.Text, + CompletionText = response + }); + } + return response; } #region Chat @@ -66,10 +87,11 @@ public async Task ChatCompletion([FromBody] IncomingInstructRequest inpu .SetState("model", input.Model, source: StateSource.External) .SetState("model_id", input.ModelId, source: StateSource.External); + var agentId = input.AgentId ?? Guid.Empty.ToString(); var completion = CompletionProvider.GetChatCompletion(_services); var message = await completion.GetChatCompletions(new Agent() { - Id = input.AgentId ?? Guid.Empty.ToString(), + Id = agentId, Instruction = input.Instruction }, new List { @@ -79,14 +101,22 @@ public async Task ChatCompletion([FromBody] IncomingInstructRequest inpu var hooks = _services.GetServices(); foreach (var hook in hooks) { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + await hook.OnResponseGenerated(new InstructResponseModel { - AgentId = input.AgentId, - Provider = input.Provider, - Model = input.Model + AgentId = agentId, + Provider = completion.Provider, + Model = completion.Model, + TemplateName = input.Template, + UserMessage = input.Text, + SystemInstruction = message.RenderedInstruction, + CompletionText = message.Content }); } - return message.Content; } #endregion diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs index de07a4821..e7ba7fd70 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs @@ -1,4 +1,8 @@ +using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Loggers.Models; +using BotSharp.Abstraction.Repositories; +using BotSharp.OpenAPI.ViewModels.Logs; +using EntityFrameworkCore.BootKit; using Microsoft.AspNetCore.Hosting; namespace BotSharp.OpenAPI.Controllers; @@ -45,4 +49,16 @@ public async Task> GetConversationStateLogs([Fro var conversationService = _services.GetRequiredService(); return await conversationService.GetConversationStateLogs(conversationId); } + + [HttpGet("/logger/instruction/log")] + public async Task> GetInstructionLogs([FromQuery] InstructLogFilter request) + { + var db = _services.GetRequiredService(); + var logs = db.GetInstructionLogs(request); + return new PagedItems + { + Items = logs.Items.Select(x => InstructionLogViewModel.From(x)), + Count = logs.Count + }; + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs index 3b0303fd1..1ad3aeadc 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs @@ -15,4 +15,5 @@ public class IncomingInstructRequest : IncomingMessageModel { public string? AgentId { get; set; } public string? Instruction { get; set; } + public string? Template { get; set; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs index b71306cf5..178bc67b7 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs @@ -9,10 +9,12 @@ namespace BotSharp.Plugin.AnthropicAI.Providers; public class ChatCompletionProvider : IChatCompletion { public string Provider => "anthropic"; + public string Model => _model; protected readonly AnthropicSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + private List renderedInstructions = []; protected string _model; @@ -57,6 +59,7 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List conversations, LlmModelSetting settings) { var instruction = ""; + renderedInstructions = []; var agentService = _services.GetRequiredService(); if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty()) { - instruction += agentService.RenderedInstruction(agent); + var text = agentService.RenderedInstruction(agent); + instruction += text; + renderedInstructions.Add(text); } /*var routing = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs b/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs index 1947ebc4c..00d1da58d 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs @@ -15,6 +15,8 @@ public class NativeWhisperProvider : IAudioCompletion private readonly ILogger _logger; public string Provider => "native-whisper"; + public string Model => _model; + private string _model; public NativeWhisperProvider( BotSharpDatabaseSettings dbSettings, @@ -56,11 +58,13 @@ public void SetModelName(string model) { if (Enum.TryParse(model, true, out GgmlType ggmlType)) { + _model = model; LoadWhisperModel(ggmlType); } else { _logger.LogWarning($"Unsupported model type: {model}. Use Tiny model instead!"); + _model = "Tiny"; LoadWhisperModel(GgmlType.Tiny); } } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Audio/AudioCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Audio/AudioCompletionProvider.cs index 8cc75ad8e..2948703e7 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Audio/AudioCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Audio/AudioCompletionProvider.cs @@ -5,6 +5,8 @@ public partial class AudioCompletionProvider : IAudioCompletion private readonly IServiceProvider _services; public string Provider => "openai"; + public string Model => _model; + private string _model; public AudioCompletionProvider(IServiceProvider service) diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index 3fef7c0cd..e49d91ce7 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -9,10 +9,12 @@ public class ChatCompletionProvider : IChatCompletion protected readonly AzureOpenAiSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + private List renderedInstructions = []; protected string _model; public virtual string Provider => "azure-openai"; + public string Model => _model; public ChatCompletionProvider( AzureOpenAiSettings settings, @@ -58,7 +60,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, var msg = new RoleDialogModel(AgentRole.Assistant, text) { - CurrentAgentId = agent.Id + CurrentAgentId = agent.Id, + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; // After chat completion hook @@ -163,7 +170,8 @@ public async Task GetChatCompletionsAsync(Agent agent, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, ToolCallId = toolCall?.Id, FunctionName = toolCall?.FunctionName, - FunctionArgs = toolCall?.FunctionArguments?.ToString() + FunctionArgs = toolCall?.FunctionArguments?.ToString(), + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; // Somethings LLM will generate a function name with agent name. @@ -199,7 +207,10 @@ public async Task GetChatCompletionsStreamingAsync(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List(); var settings = settingsService.GetSetting(Provider, _model); var allowMultiModal = settings != null && settings.MultiModal; + renderedInstructions = []; var messages = new List(); @@ -251,6 +266,7 @@ public async Task GetChatCompletionsStreamingAsync(Agent agent, List "azure-openai"; + public string Model => _model; public TextEmbeddingProvider( AzureOpenAiSettings settings, @@ -49,6 +50,7 @@ public void SetModelName(string model) _model = model; } + public void SetDimension(int dimension) { _dimension = dimension > 0 ? dimension : DEFAULT_DIMENSION; diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Image/ImageCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Image/ImageCompletionProvider.cs index 967f3e082..1b435d618 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Image/ImageCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Image/ImageCompletionProvider.cs @@ -14,6 +14,7 @@ public partial class ImageCompletionProvider : IImageCompletion protected string _model; public virtual string Provider => "azure-openai"; + public string Model => _model; public ImageCompletionProvider( AzureOpenAiSettings settings, diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Text/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Text/TextCompletionProvider.cs index 4953e114a..2b6f3dfb1 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Text/TextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Text/TextCompletionProvider.cs @@ -22,6 +22,7 @@ public class TextCompletionProvider : ITextCompletion }; public virtual string Provider => "azure-openai"; + public string Model => _model; public TextCompletionProvider( AzureOpenAiSettings settings, diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index ef4d0c8aa..4c21dd035 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -9,9 +9,11 @@ public class ChatCompletionProvider : IChatCompletion { protected readonly IServiceProvider _services; protected readonly ILogger _logger; + private List renderedInstructions = []; protected string _model; public virtual string Provider => "deepseek-ai"; + public string Model => _model; public ChatCompletionProvider( IServiceProvider services, @@ -51,7 +53,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List GetChatCompletionsAsync(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List(); var settings = settingsService.GetSetting(Provider, _model); var allowMultiModal = settings != null && settings.MultiModal; + renderedInstructions = []; var messages = new List(); @@ -226,6 +239,7 @@ public void SetModelName(string model) if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty()) { var text = agentService.RenderedInstruction(agent); + renderedInstructions.Add(text); messages.Add(new SystemChatMessage(text)); } diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs index a1a889782..27198238a 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs @@ -10,6 +10,7 @@ public class TextCompletionProvider : ITextCompletion protected string _model; public string Provider => "deepseek-ai"; + public string Model => _model; public TextCompletionProvider( IServiceProvider services, diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs index 9567ae578..174efcf23 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs @@ -2,7 +2,6 @@ using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Loggers; -using Google.Protobuf.WellKnownTypes; using Microsoft.Extensions.Logging; using Mscc.GenerativeAI; @@ -12,10 +11,12 @@ public class GeminiChatCompletionProvider : IChatCompletion { private readonly IServiceProvider _services; private readonly ILogger _logger; + private List renderedInstructions = []; private string _model; public string Provider => "google-ai"; + public string Model => _model; public GeminiChatCompletionProvider( IServiceProvider services, @@ -53,7 +54,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List(); var googleSettings = _services.GetRequiredService(); + renderedInstructions = []; // Add settings aiModel.UseGoogleSearch = googleSettings.Gemini.UseGoogleSearch; @@ -117,6 +121,7 @@ public void SetModelName(string model) Role = AgentRole.User }); + renderedInstructions.Add(instruction); systemPrompts.Add(instruction); } diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/PalmChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/PalmChatCompletionProvider.cs index c62aebb81..4fe6ad179 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/PalmChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/PalmChatCompletionProvider.cs @@ -13,10 +13,12 @@ public class PalmChatCompletionProvider : IChatCompletion { private readonly IServiceProvider _services; private readonly ILogger _logger; + private List renderedInstructions = []; private string _model; public string Provider => "google-palm"; + public string Model => _model; public PalmChatCompletionProvider( IServiceProvider services, @@ -61,7 +63,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List "google-ai"; + public string Model => _model; public GeminiTextCompletionProvider( IServiceProvider services, @@ -74,7 +75,6 @@ public void SetModelName(string model) _model = model; } - private void PrepareOptions(GenerativeModel aiModel) { var settings = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/PalmTextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/PalmTextCompletionProvider.cs index ebaf6c5f9..7df18bb73 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/PalmTextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/PalmTextCompletionProvider.cs @@ -14,6 +14,7 @@ public class PalmTextCompletionProvider : ITextCompletion private string _model; public string Provider => "google-palm"; + public string Model => _model; public PalmTextCompletionProvider( IServiceProvider services, diff --git a/src/Plugins/BotSharp.Plugin.HuggingFace/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.HuggingFace/Providers/ChatCompletionProvider.cs index 88b37dbb7..460677ba5 100644 --- a/src/Plugins/BotSharp.Plugin.HuggingFace/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.HuggingFace/Providers/ChatCompletionProvider.cs @@ -10,10 +10,12 @@ namespace BotSharp.Plugin.HuggingFace.Providers; public class ChatCompletionProvider : IChatCompletion { public string Provider => "huggingface"; + public string Model => _model; private readonly IServiceProvider _services; private readonly HuggingFaceSettings _settings; private readonly ILogger _logger; + private List renderedInstructions = []; private string _model; public ChatCompletionProvider(IServiceProvider services, diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs index 32f7220b8..45f41e95d 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/ChatCompletionProvider.cs @@ -8,6 +8,7 @@ public class ChatCompletionProvider : IChatCompletion private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly LlamaSharpSettings _settings; + private List renderedInstructions = []; private string _model; public ChatCompletionProvider(IServiceProvider services, @@ -20,6 +21,7 @@ public ChatCompletionProvider(IServiceProvider services, } public string Provider => "llama-sharp"; + public string Model => _model; public async Task GetChatCompletions(Agent agent, List conversations) { @@ -64,7 +66,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, var msg = new RoleDialogModel(AgentRole.Assistant, totalResponse) { - CurrentAgentId = agent.Id + CurrentAgentId = agent.Id, + RenderedInstruction = agent.Instruction }; // Text response received diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextCompletionProvider.cs index 2a98becc6..e911a8ff0 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextCompletionProvider.cs @@ -10,6 +10,7 @@ public class TextCompletionProvider : ITextCompletion private readonly ITokenStatistics _tokenStatistics; private string _model; public string Provider => "llama-sharp"; + public string Model => _model; public TextCompletionProvider(IServiceProvider services, ILogger logger, diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs index 1ec461ae1..5c58f470b 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs @@ -1,6 +1,4 @@ using System.IO; -using System.Xml.Linq; -using static System.Net.Mime.MediaTypeNames; namespace BotSharp.Plugin.LLamaSharp.Providers; @@ -14,6 +12,7 @@ public class TextEmbeddingProvider : ITextEmbedding protected int _dimension = DEFAULT_DIMENSION; public string Provider => "llama-sharp"; + public string Model => string.Empty; public TextEmbeddingProvider(IServiceProvider services, LlamaSharpSettings settings) { diff --git a/src/Plugins/BotSharp.Plugin.LangChain/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.LangChain/Providers/ChatCompletionProvider.cs index 61f661197..aa6b1175f 100644 --- a/src/Plugins/BotSharp.Plugin.LangChain/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LangChain/Providers/ChatCompletionProvider.cs @@ -20,6 +20,8 @@ public class ChatCompletionProvider(VertexAIConfiguration config, IServiceProvider services) : IChatCompletion { public string Provider => "vertexai"; + public string Model => _model; + private readonly VertexAIConfiguration _config = config; private readonly ChatSettings? _settings = settings; private readonly IServiceProvider _services = services; diff --git a/src/Plugins/BotSharp.Plugin.LangChain/Providers/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.LangChain/Providers/TextCompletionProvider.cs index 461864527..70f0cbdcd 100644 --- a/src/Plugins/BotSharp.Plugin.LangChain/Providers/TextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LangChain/Providers/TextCompletionProvider.cs @@ -19,6 +19,8 @@ public class TextCompletionProvider(VertexAIConfiguration config, IServiceProvider services) : ITextCompletion { public string Provider => "vertexai"; + public string Model => _model; + private readonly VertexAIConfiguration _config = config; private readonly ChatSettings? _settings = settings; private readonly IServiceProvider _services = services; diff --git a/src/Plugins/BotSharp.Plugin.MetaAI/Providers/fastTextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.MetaAI/Providers/fastTextEmbeddingProvider.cs index 7d9c61411..07b5b76e2 100644 --- a/src/Plugins/BotSharp.Plugin.MetaAI/Providers/fastTextEmbeddingProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MetaAI/Providers/fastTextEmbeddingProvider.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Models; using BotSharp.Plugin.MetaAI.Settings; using FastText.NetWrapper; using Microsoft.Extensions.DependencyInjection; @@ -17,6 +18,7 @@ public class fastTextEmbeddingProvider : ITextEmbedding private int _dimension; public string Provider => "meta-ai"; + public string Model => string.Empty; public fastTextEmbeddingProvider(IServiceProvider services) { diff --git a/src/Plugins/BotSharp.Plugin.MetaGLM/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MetaGLM/Providers/ChatCompletionProvider.cs index f99293f4d..b5b19671f 100644 --- a/src/Plugins/BotSharp.Plugin.MetaGLM/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MetaGLM/Providers/ChatCompletionProvider.cs @@ -5,11 +5,13 @@ namespace BotSharp.Plugin.MetaGLM.Providers; public class ChatCompletionProvider : IChatCompletion { public string Provider => "metaglm"; + public string Model => _model; private readonly MetaGLMSettings _settings; private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly MetaGLMClient metaGLMClient; + private List renderedInstructions = []; private string _model; public ChatCompletionProvider(IServiceProvider services, @@ -49,7 +51,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List conversations, var agentService = _services.GetRequiredService(); List messages = new List(); List toolcalls = new List(); + renderedInstructions = []; if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty()) { var instruction = agentService.RenderedInstruction(agent); + renderedInstructions.Add(instruction); messages.Add(new MessageItem("system", instruction)); } diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs index 8619ecadf..37d3877c8 100644 --- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs @@ -27,6 +27,7 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio private readonly IChatClient _client; private readonly ILogger _logger; private readonly IServiceProvider _services; + private List renderedInstructions = []; private string? _model; /// @@ -45,6 +46,7 @@ public MicrosoftExtensionsAIChatCompletionProvider( /// public string Provider => "microsoft.extensions.ai"; + public string Model => _model; /// public void SetModelName(string model) => _model = model; @@ -54,6 +56,7 @@ public async Task GetChatCompletions(Agent agent, List().ToArray(); + renderedInstructions = []; await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, conversations))); // Configure options @@ -82,6 +85,7 @@ public async Task GetChatCompletions(Agent agent, List().RenderedInstruction(agent) is string instruction && instruction.Length > 0) { + renderedInstructions.Add(instruction); messages.Add(new(ChatRole.System, instruction)); } @@ -143,7 +147,8 @@ public async Task GetChatCompletions(Agent agent, List())) { - CurrentAgentId = agent.Id + CurrentAgentId = agent.Id, + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; if (completion.Message.Contents.OfType().FirstOrDefault() is { } fcc) diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs index 94d266780..ed0e94d92 100644 --- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs @@ -24,6 +24,7 @@ public sealed class MicrosoftExtensionsAITextCompletionProvider : ITextCompletio /// public string Provider => "microsoft-extensions-ai"; + public string Model => _model; /// /// Creates an instance of the class. diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs index 958c46944..1758468aa 100644 --- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextEmbeddingProvider.cs @@ -23,6 +23,7 @@ public MicrosoftExtensionsAITextEmbeddingProvider(IEmbeddingGenerator public string Provider => "microsoft-extensions-ai"; + public string Model => _model; /// public async Task GetVectorAsync(string text) => diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs index 615e93af8..4f70ad607 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/InstructionLogBetaDocument.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Loggers.Models; -using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Collections; @@ -8,6 +7,10 @@ public class InstructionLogBetaDocument : MongoBase public string? AgentId { get; set; } public string Provider { get; set; } = default!; public string Model { get; set; } = default!; + public string? TemplateName { get; set; } + public string UserMessage { get; set; } = default!; + public string? SystemInstruction { get; set; } + public string CompletionText { get; set; } = default!; public string? UserId { get; set; } public Dictionary States { get; set; } = new(); public DateTime CreatedTime { get; set; } @@ -19,6 +22,10 @@ public static InstructionLogBetaDocument ToMongoModel(InstructionLogModel log) AgentId = log.AgentId, Provider = log.Provider, Model = log.Model, + TemplateName = log.TemplateName, + UserMessage = log.UserMessage, + SystemInstruction = log.SystemInstruction, + CompletionText = log.CompletionText, UserId = log.UserId, CreatedTime = log.CreatedTime }; @@ -28,9 +35,14 @@ public static InstructionLogModel ToDomainModel(InstructionLogBetaDocument log) { return new InstructionLogModel { + Id = log.Id, AgentId = log.AgentId, Provider = log.Provider, Model = log.Model, + TemplateName = log.TemplateName, + UserMessage = log.UserMessage, + SystemInstruction = log.SystemInstruction, + CompletionText = log.CompletionText, UserId = log.UserId, CreatedTime = log.CreatedTime }; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index 9adc04b97..277f9e255 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -194,5 +194,5 @@ public IMongoCollection GlobalStatistics => GetCollectionOrCreate("GlobalStatistics"); public IMongoCollection InstructionLogs - => GetCollectionOrCreate("InstructionLogsBeta"); + => GetCollectionOrCreate("InstructionLogs"); } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs index c90960dde..05d5a6a91 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Loggers.Models; +using BotSharp.Abstraction.Repositories.Filters; using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -167,28 +168,9 @@ public PagedItems GetInstructionLogs(InstructLogFilter filt { filters.Add(builder.In(x => x.Model, filter.Models)); } - - if (!filter.States.IsNullOrEmpty()) + if (!filter.TemplateNames.IsNullOrEmpty()) { - foreach (var pair in filter.States) - { - if (string.IsNullOrWhiteSpace(pair.Key)) continue; - - // Format key - var keys = pair.Key.Split(".").ToList(); - keys.Insert(1, "data"); - keys.Insert(0, "States"); - var formattedKey = string.Join(".", keys); - - if (pair.Value == null) - { - filters.Add(builder.Exists(formattedKey)); - } - else - { - filters.Add(builder.Eq(formattedKey, pair.Value)); - } - } + filters.Add(builder.In(x => x.TemplateName, filter.TemplateNames)); } var filterDef = builder.And(filters); @@ -196,10 +178,23 @@ public PagedItems GetInstructionLogs(InstructLogFilter filt var docs = _dc.InstructionLogs.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); var count = _dc.InstructionLogs.CountDocuments(filterDef); + var agentIds = docs.Where(x => !string.IsNullOrEmpty(x.AgentId)).Select(x => x.AgentId).ToList(); + var agents = GetAgents(new AgentFilter + { + AgentIds = agentIds + }); + var logs = docs.Select(x => { var log = InstructionLogBetaDocument.ToDomainModel(x); - log.States = x.States.ToDictionary(x => x.Key, x => x.Value.GetElement("data").Value.ToString() ?? string.Empty); + log.AgentName = !string.IsNullOrEmpty(x.AgentId) ? agents.FirstOrDefault(a => a.Id == x.AgentId)?.Name : null; + log.States = x.States.ToDictionary(p => p.Key, p => + { + var jsonStr = p.Value.ToJson(); + var jsonDoc = JsonDocument.Parse(jsonStr); + var data = jsonDoc.RootElement.GetProperty("data"); + return data.ValueKind != JsonValueKind.Null ? data.ToString() : null; + }); return log; }).ToList(); diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Audio/AudioCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Audio/AudioCompletionProvider.cs index 3311fd43f..338affd98 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Audio/AudioCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Audio/AudioCompletionProvider.cs @@ -7,6 +7,8 @@ public partial class AudioCompletionProvider : IAudioCompletion private readonly IServiceProvider _services; public string Provider => "openai"; + public string Model => _model; + private string _model; public AudioCompletionProvider(IServiceProvider service) diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index 15eb04c57..031fab9c1 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -10,8 +10,10 @@ public class ChatCompletionProvider : IChatCompletion protected readonly ILogger _logger; protected string _model; + private List renderedInstructions = []; public virtual string Provider => "openai"; + public string Model => _model; public ChatCompletionProvider( OpenAiSettings settings, @@ -53,7 +55,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, var msg = new RoleDialogModel(AgentRole.Assistant, text) { - CurrentAgentId = agent.Id + CurrentAgentId = agent.Id, + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; // After chat completion hook @@ -139,7 +144,8 @@ public async Task GetChatCompletionsAsync(Agent agent, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, ToolCallId = toolCall?.Id, FunctionName = toolCall?.FunctionName, - FunctionArgs = toolCall?.FunctionArguments?.ToString() + FunctionArgs = toolCall?.FunctionArguments?.ToString(), + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; // Somethings LLM will generate a function name with agent name. @@ -175,7 +181,10 @@ public async Task GetChatCompletionsStreamingAsync(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List(); var settings = settingsService.GetSetting(Provider, _model); var allowMultiModal = settings != null && settings.MultiModal; + renderedInstructions = []; var messages = new List(); @@ -227,6 +240,7 @@ public async Task GetChatCompletionsStreamingAsync(Agent agent, List "openai"; + public string Model => _model; public TextEmbeddingProvider( OpenAiSettings settings, diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs index 0e45e7892..7cfda0f59 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.cs @@ -14,6 +14,7 @@ public partial class ImageCompletionProvider : IImageCompletion protected string _model; public virtual string Provider => "openai"; + public string Model => _model; public ImageCompletionProvider( OpenAiSettings settings, diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs index 097a32b7e..91b398484 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs @@ -15,6 +15,7 @@ public class TextCompletionProvider : ITextCompletion protected string _model; public virtual string Provider => "openai"; + public string Model => _model; public TextCompletionProvider( OpenAiSettings settings, diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelChatCompletionProvider.cs index 156f238c5..3f742777d 100644 --- a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelChatCompletionProvider.cs @@ -26,6 +26,7 @@ public class SemanticKernelChatCompletionProvider : IChatCompletion /// public string Provider => "semantic-kernel"; + public string Model => _model; /// /// Create a new instance of @@ -74,7 +75,8 @@ public async Task GetChatCompletions(Agent agent, List public string Provider => "semantic-kernel"; + public string Model => _model; /// /// Create a new instance of diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelTextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelTextEmbeddingProvider.cs index 5486d9729..649150909 100644 --- a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelTextEmbeddingProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelTextEmbeddingProvider.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Models; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel.Embeddings; using System.Collections.Generic; @@ -33,6 +34,7 @@ public SemanticKernelTextEmbeddingProvider(ITextEmbeddingGenerationService embed protected int _dimension; public string Provider => "semantic-kernel"; + public string Model => string.Empty; /// public async Task GetVectorAsync(string text) diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs index 5edf4708c..d5e64e7d4 100644 --- a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs @@ -7,10 +7,12 @@ namespace BotSharp.Plugin.SparkDesk.Providers; public class ChatCompletionProvider : IChatCompletion { public string Provider => "sparkdesk"; + public string Model => _model; private readonly SparkDeskSettings _settings; private readonly IServiceProvider _services; private readonly ILogger _logger; + private List renderedInstructions = []; private string _model; public ChatCompletionProvider(IServiceProvider services, @@ -42,7 +44,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List GetChatCompletionsAsync(Agent agent, List(); var agentService = _services.GetRequiredService(); var messages = new List(); + renderedInstructions = []; if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty()) { var instruction = agentService.RenderedInstruction(agent); + renderedInstructions.Add(instruction); messages.Add(ChatMessage.FromSystem(instruction)); } if (!string.IsNullOrEmpty(agent.Knowledges)) From f60bcda89699c5dcb1b8e0cad11a2d07e53de1e6 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 5 Mar 2025 17:32:02 -0600 Subject: [PATCH 4/4] relocate --- .../Controllers/LoggerController.cs | 3 +- .../Instructs/InstructionLogViewModel.cs | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs index e7ba7fd70..2c46b5b4d 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs @@ -1,8 +1,7 @@ using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Repositories; -using BotSharp.OpenAPI.ViewModels.Logs; -using EntityFrameworkCore.BootKit; +using BotSharp.OpenAPI.ViewModels.Instructs; using Microsoft.AspNetCore.Hosting; namespace BotSharp.OpenAPI.Controllers; diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs new file mode 100644 index 000000000..88fc27e2a --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs @@ -0,0 +1,67 @@ +using BotSharp.Abstraction.Loggers.Models; +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Instructs; + +public class InstructionLogViewModel +{ + [JsonPropertyName("id")] + public string Id { get; set; } = default!; + + [JsonPropertyName("agent_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? AgentId { get; set; } + + [JsonPropertyName("agent_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? AgentName { get; set; } + + [JsonPropertyName("provider")] + public string Provider { get; set; } = default!; + + [JsonPropertyName("model")] + public string Model { get; set; } = default!; + + [JsonPropertyName("template_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TemplateName { get; set; } + + [JsonPropertyName("user_message")] + public string UserMessage { get; set; } = string.Empty; + + [JsonPropertyName("system_instruction")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SystemInstruction { get; set; } + + [JsonPropertyName("completion_text")] + public string CompletionText { get; set; } = string.Empty; + + [JsonPropertyName("user_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? UserId { get; set; } + + [JsonPropertyName("states")] + public Dictionary States { get; set; } = []; + + [JsonPropertyName("created_time")] + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + public static InstructionLogViewModel From(InstructionLogModel log) + { + return new InstructionLogViewModel + { + Id = log.Id, + AgentId = log.AgentId, + AgentName = log.AgentName, + Provider = log.Provider, + Model = log.Model, + TemplateName = log.TemplateName, + UserMessage = log.UserMessage, + SystemInstruction = log.SystemInstruction, + CompletionText = log.CompletionText, + UserId = log.UserId, + States = log.States, + CreatedTime = log.CreatedTime + }; + } +}