diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs new file mode 100644 index 00000000..1a433e6f --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs @@ -0,0 +1,14 @@ +using BotSharp.Abstraction.Chart.Models; + +namespace BotSharp.Abstraction.Chart; + +public interface IBotSharpChartService +{ + public string Provider { get; } + + Task GetConversationChartData(string conversationId, string messageId, ChartDataOptions options) + => throw new NotImplementedException(); + + Task GetConversationChartCode(string conversationId, string messageId, ChartCodeOptions options) + => throw new NotImplementedException(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeOptions.cs new file mode 100644 index 00000000..beac046d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeOptions.cs @@ -0,0 +1,25 @@ +namespace BotSharp.Abstraction.Chart.Models; + +public class ChartCodeOptions +{ + public string? AgentId { get; set; } + public string? TemplateName { get; set; } + public string Text { get; set; } = string.Empty; + + /// + /// Conversation state that can be used to fetch chart data + /// + public string? TargetStateName { get; set; } + + public ChartLlmOptions? Llm { get; set; } + public List>? States { get; set; } + +} + +public class ChartLlmOptions +{ + public string? Provider { get; set; } + public string? Model { get; set; } + public int? MaxOutputTokens { get; set; } + public string? ReasoningEffortLevel { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeResult.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeResult.cs new file mode 100644 index 00000000..143ba218 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeResult.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Chart.Models; + +public class ChartCodeResult +{ + public string Code { get; set; } + public string Language { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataOptions.cs new file mode 100644 index 00000000..249ec080 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataOptions.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Chart.Models; + +public class ChartDataOptions +{ + /// + /// Conversation state that can be used to fetch chart data + /// + public string? TargetStateName { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataResult.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataResult.cs new file mode 100644 index 00000000..28292d5a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataResult.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Chart.Models; + +public class ChartDataResult +{ + public object Data { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs index 9e4fcb41..e6873807 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Dtos/ChatResponseDto.cs @@ -36,12 +36,19 @@ public class ChatResponseDto : InstructResult [JsonPropertyName("indication")] public string? Indication { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message_label")] + public string? MessageLabel { get; set; } + [JsonPropertyName("has_message_files")] public bool HasMessageFiles { get; set; } [JsonPropertyName("is_streaming")] public bool IsStreaming { get; set; } + [JsonPropertyName("is_append")] + public bool IsAppend { get; set; } + [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs index 5ffdf187..675c0885 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs @@ -98,6 +98,10 @@ public class DialogMetaData [JsonPropertyName("message_type")] public string MessageType { get; set; } = default!; + [JsonPropertyName("message_label")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? MessageLabel { get; set; } + [JsonPropertyName("function_name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? FunctionName { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index f4e75425..ed9d68a5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -19,6 +19,11 @@ public class RoleDialogModel : ITrackableMessage /// public string MessageType { get; set; } = MessageTypeName.Plain; + /// + /// The message label + /// + public string? MessageLabel { get; set; } + /// /// user, system, assistant, function /// @@ -127,6 +132,13 @@ public class RoleDialogModel : ITrackableMessage [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public bool IsStreaming { get; set; } + /// + /// Additional messages that can be sent sequentially and save to db + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public ChatMessageWrapper? AdditionalMessageWrapper { get; set; } + + public RoleDialogModel() { } @@ -160,6 +172,7 @@ public class RoleDialogModel : ITrackableMessage CurrentAgentId = source.CurrentAgentId, MessageId = source.MessageId, MessageType = source.MessageType, + MessageLabel = source.MessageLabel, FunctionArgs = source.FunctionArgs, FunctionName = source.FunctionName, ToolCallId = source.ToolCallId, @@ -171,7 +184,26 @@ public class RoleDialogModel : ITrackableMessage Instruction = source.Instruction, Data = source.Data, IsStreaming = source.IsStreaming, - Annotations = source.Annotations + Annotations = source.Annotations, + AdditionalMessageWrapper = source.AdditionalMessageWrapper }; } } + +public class ChatMessageWrapper +{ + /// + /// Messages sending interval in milliseconds + /// + public int SendingInterval { get; set; } + + /// + /// Whether the Messages are saved to db + /// + public bool SaveToDb { get; set; } + + /// + /// Messages to send or save + /// + public List? Messages { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs index 4bca1240..d820aa6b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Crontab/Settings/CrontabSettings.cs @@ -4,9 +4,16 @@ public class CrontabSettings { public CrontabBaseSetting EventSubscriber { get; set; } = new(); public CrontabBaseSetting Watcher { get; set; } = new(); + public string LockName { get; set; } = "CrontabWatcher:locker"; + public DebugSetting Debug { get; set; } = new(); } public class CrontabBaseSetting { public bool Enabled { get; set; } = true; } + +public class DebugSetting +{ + public string AllowRuleTrigger { get; set; } = ""; +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs index 7bb621f4..d5073520 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs @@ -10,11 +10,15 @@ public class CrontabWatcher : BackgroundService { private readonly ILogger _logger; private readonly IServiceProvider _services; + private readonly CrontabSettings _cronSettings; + private string DIST_KEY; - public CrontabWatcher(IServiceProvider services, ILogger logger) + public CrontabWatcher(IServiceProvider services, ILogger logger, CrontabSettings cronSettings) { _logger = logger; _services = services; + _cronSettings = cronSettings; + DIST_KEY = _cronSettings.LockName; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -29,7 +33,7 @@ public class CrontabWatcher : BackgroundService { var delay = Task.Delay(1000, stoppingToken); - await locker.LockAsync("CrontabWatcher:locker", async () => + await locker.LockAsync(DIST_KEY, async () => { await RunCronChecker(scope.ServiceProvider); }); @@ -89,7 +93,10 @@ public class CrontabWatcher : BackgroundService _logger.LogInformation($"The current time matches the cron expression {item}"); #if DEBUG - await HandleCrontabEvent(item); + if (item.Title == settings.Debug.AllowRuleTrigger) + { + await HandleCrontabEvent(item); + } #else if (publisher != null) { diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 1155dc38..9eb5d1f8 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging.Models.RichContent; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index eeceeb09..26020da3 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -31,63 +31,21 @@ public class ConversationStorage : IConversationStorage foreach ( var dialog in dialogs) { - if (dialog.Role == AgentRole.Function) + var innerList = new List { dialog }; + if (dialog.AdditionalMessageWrapper != null + && dialog.AdditionalMessageWrapper.SaveToDb + && dialog.AdditionalMessageWrapper.Messages?.Count > 0) { - var meta = new DialogMetaData - { - Role = dialog.Role, - AgentId = dialog.CurrentAgentId, - MessageId = dialog.MessageId, - MessageType = dialog.MessageType, - FunctionName = dialog.FunctionName, - FunctionArgs = dialog.FunctionArgs, - ToolCallId = dialog.ToolCallId, - CreatedTime = dialog.CreatedAt - }; - - var content = dialog.Content.RemoveNewLine(); - if (string.IsNullOrEmpty(content)) - { - continue; - } - dialogElements.Add(new DialogElement - { - MetaData = meta, - Content = dialog.Content, - SecondaryContent = dialog.SecondaryContent, - Payload = dialog.Payload - }); + innerList.AddRange(dialog.AdditionalMessageWrapper.Messages); } - else + + foreach (var item in innerList) { - var meta = new DialogMetaData + var element = BuildDialogElement(item); + if (element != null) { - Role = dialog.Role, - AgentId = dialog.CurrentAgentId, - MessageId = dialog.MessageId, - MessageType = dialog.MessageType, - SenderId = dialog.SenderId, - FunctionName = dialog.FunctionName, - CreatedTime = dialog.CreatedAt - }; - - var content = dialog.Content.RemoveNewLine(); - if (string.IsNullOrEmpty(content)) - { - continue; + dialogElements.Add(element); } - - var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _options.JsonSerializerOptions) : null; - var secondaryRichContent = dialog.SecondaryRichContent != null ? JsonSerializer.Serialize(dialog.SecondaryRichContent, _options.JsonSerializerOptions) : null; - dialogElements.Add(new DialogElement - { - MetaData = meta, - Content = dialog.Content, - SecondaryContent = dialog.SecondaryContent, - RichContent = richContent, - SecondaryRichContent = secondaryRichContent, - Payload = dialog.Payload - }); } } @@ -108,11 +66,7 @@ public class ConversationStorage : IConversationStorage var secondaryContent = dialog.SecondaryContent; var payload = string.IsNullOrEmpty(dialog.Payload) ? null : dialog.Payload; var role = meta.Role; - var currentAgentId = meta.AgentId; - var messageId = meta.MessageId; - var messageType = meta.MessageType; - var senderId = role == AgentRole.Function ? currentAgentId : meta.SenderId; - var createdAt = meta.CreatedTime; + var senderId = role == AgentRole.Function ? meta?.AgentId : meta?.SenderId; var richContent = !string.IsNullOrEmpty(dialog.RichContent) ? JsonSerializer.Deserialize>(dialog.RichContent, _options.JsonSerializerOptions) : null; var secondaryRichContent = !string.IsNullOrEmpty(dialog.SecondaryRichContent) ? @@ -120,14 +74,15 @@ public class ConversationStorage : IConversationStorage var record = new RoleDialogModel(role, content) { - CurrentAgentId = currentAgentId, - MessageId = messageId, - MessageType = messageType, - CreatedAt = createdAt, + CurrentAgentId = meta?.AgentId ?? string.Empty, + MessageId = meta?.MessageId ?? string.Empty, + MessageType = meta?.MessageType ?? string.Empty, + MessageLabel = meta?.MessageLabel, + CreatedAt = meta?.CreatedTime ?? default, SenderId = senderId, - FunctionName = meta.FunctionName, - FunctionArgs = meta.FunctionArgs, - ToolCallId = meta.ToolCallId, + FunctionName = meta?.FunctionName, + FunctionArgs = meta?.FunctionArgs, + ToolCallId = meta?.ToolCallId, RichContent = richContent, SecondaryContent = secondaryContent, SecondaryRichContent = secondaryRichContent, @@ -148,4 +103,69 @@ public class ConversationStorage : IConversationStorage return results; } + + private DialogElement? BuildDialogElement(RoleDialogModel dialog) + { + DialogElement? element = null; + + if (dialog.Role == AgentRole.Function) + { + var meta = new DialogMetaData + { + Role = dialog.Role, + AgentId = dialog.CurrentAgentId, + MessageId = dialog.MessageId, + MessageType = dialog.MessageType, + MessageLabel = dialog.MessageLabel, + FunctionName = dialog.FunctionName, + FunctionArgs = dialog.FunctionArgs, + ToolCallId = dialog.ToolCallId, + CreatedTime = dialog.CreatedAt + }; + + var content = dialog.Content.RemoveNewLine(); + if (!string.IsNullOrEmpty(content)) + { + element = new DialogElement + { + MetaData = meta, + Content = dialog.Content, + SecondaryContent = dialog.SecondaryContent, + Payload = dialog.Payload + }; + } + } + else + { + var meta = new DialogMetaData + { + Role = dialog.Role, + AgentId = dialog.CurrentAgentId, + MessageId = dialog.MessageId, + MessageType = dialog.MessageType, + MessageLabel = dialog.MessageLabel, + SenderId = dialog.SenderId, + FunctionName = dialog.FunctionName, + CreatedTime = dialog.CreatedAt + }; + + var content = dialog.Content.RemoveNewLine(); + if (!string.IsNullOrEmpty(content)) + { + var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _options.JsonSerializerOptions) : null; + var secondaryRichContent = dialog.SecondaryRichContent != null ? JsonSerializer.Serialize(dialog.SecondaryRichContent, _options.JsonSerializerOptions) : null; + element = new DialogElement + { + MetaData = meta, + Content = dialog.Content, + SecondaryContent = dialog.SecondaryContent, + RichContent = richContent, + SecondaryRichContent = secondaryRichContent, + Payload = dialog.Payload + }; + } + } + + return element; + } } diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs index 45841749..a62744a5 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs @@ -31,11 +31,17 @@ public partial class LocalFileStorageService public BinaryData GetSpeechFile(string conversationId, string fileName) { + if (string.IsNullOrWhiteSpace(conversationId) || string.IsNullOrWhiteSpace(fileName)) + { + return BinaryData.Empty; + } + var path = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, TEXT_TO_SPEECH_FOLDER, fileName); if (!File.Exists(path)) { return BinaryData.Empty; } + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read); return BinaryData.FromStream(fs); } diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs index 9aac4df3..81e668c1 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs @@ -19,16 +19,30 @@ public partial class LocalFileStorageService foreach (var messageId in messageIds) { + if (string.IsNullOrWhiteSpace(messageId)) + { + continue; + } + var dir = Path.Combine(pathPrefix, messageId, FileSourceType.User); - if (!ExistDirectory(dir)) continue; + if (!ExistDirectory(dir)) + { + continue; + } foreach (var subDir in Directory.GetDirectories(dir)) { var file = Directory.GetFiles(subDir).FirstOrDefault(); - if (file == null) continue; + if (file == null) + { + continue; + } var screenshots = await GetScreenshots(file, subDir, messageId, source); - if (screenshots.IsNullOrEmpty()) continue; + if (screenshots.IsNullOrEmpty()) + { + continue; + } files.AddRange(screenshots); } @@ -41,10 +55,18 @@ public partial class LocalFileStorageService string source, IEnumerable? contentTypes = null) { var files = new List(); - if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return files; + if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) + { + return files; + } foreach (var messageId in messageIds) { + if (string.IsNullOrWhiteSpace(messageId)) + { + continue; + } + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId, source); if (!ExistDirectory(dir)) { @@ -85,6 +107,14 @@ public partial class LocalFileStorageService public string GetMessageFile(string conversationId, string messageId, string source, string index, string fileName) { + if (string.IsNullOrWhiteSpace(conversationId) + || string.IsNullOrWhiteSpace(messageId) + || string.IsNullOrWhiteSpace(source) + || string.IsNullOrWhiteSpace(index)) + { + return string.Empty; + } + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId, source, index); if (!ExistDirectory(dir)) { @@ -98,10 +128,18 @@ public partial class LocalFileStorageService public IEnumerable GetMessagesWithFile(string conversationId, IEnumerable messageIds) { var foundMsgs = new List(); - if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return foundMsgs; + if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) + { + return foundMsgs; + } foreach (var messageId in messageIds) { + if (string.IsNullOrWhiteSpace(messageId)) + { + continue; + } + var prefix = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId); var userDir = Path.Combine(prefix, FileSourceType.User); if (ExistDirectory(userDir)) @@ -121,10 +159,19 @@ public partial class LocalFileStorageService public bool SaveMessageFiles(string conversationId, string messageId, string source, List files) { - if (files.IsNullOrEmpty()) return false; + if (string.IsNullOrWhiteSpace(conversationId) + || string.IsNullOrWhiteSpace(messageId) + || string.IsNullOrWhiteSpace(source) + || files.IsNullOrEmpty()) + { + return false; + } var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true); - if (!ExistDirectory(dir)) return false; + if (!ExistDirectory(dir)) + { + return false; + } for (int i = 0; i < files.Count; i++) { @@ -164,7 +211,10 @@ public partial class LocalFileStorageService public bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null) { - if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false; + if (string.IsNullOrEmpty(conversationId) || messageIds == null) + { + return false; + } if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId)) { @@ -192,7 +242,10 @@ public partial class LocalFileStorageService foreach (var messageId in messageIds) { var dir = GetConversationFileDirectory(conversationId, messageId); - if (!ExistDirectory(dir)) continue; + if (!ExistDirectory(dir)) + { + continue; + } DeleteDirectory(dir); Thread.Sleep(100); @@ -203,12 +256,18 @@ public partial class LocalFileStorageService public bool DeleteConversationFiles(IEnumerable conversationIds) { - if (conversationIds.IsNullOrEmpty()) return false; + if (conversationIds.IsNullOrEmpty()) + { + return false; + } foreach (var conversationId in conversationIds) { var convDir = GetConversationDirectory(conversationId); - if (!ExistDirectory(convDir)) continue; + if (!ExistDirectory(convDir)) + { + continue; + } DeleteDirectory(convDir); } @@ -241,7 +300,10 @@ public partial class LocalFileStorageService private IEnumerable GetMessageIds(IEnumerable dialogs, int? offset = null) { - if (dialogs.IsNullOrEmpty()) return Enumerable.Empty(); + if (dialogs.IsNullOrEmpty()) + { + return Enumerable.Empty(); + } if (offset.HasValue && offset < 1) { @@ -264,13 +326,17 @@ public partial class LocalFileStorageService private async Task> ConvertPdfToImages(string pdfLoc, string imageLoc) { var converters = _services.GetServices(); - if (converters.IsNullOrEmpty()) return Enumerable.Empty(); + if (converters.IsNullOrEmpty()) + { + return Enumerable.Empty(); + } var converter = GetPdf2ImageConverter(); if (converter == null) { return Enumerable.Empty(); } + return await converter.ConvertPdfToImages(pdfLoc, imageLoc); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 6acb58b2..5b378a24 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -59,6 +59,8 @@ public partial class RoutingService message.Indication = response.Indication; message.CurrentAgentId = agent.Id; message.IsStreaming = response.IsStreaming; + message.MessageLabel = response.MessageLabel; + message.AdditionalMessageWrapper = null; await InvokeFunction(message, dialogs, options); } @@ -74,6 +76,8 @@ public partial class RoutingService message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content); message.CurrentAgentId = agent.Id; message.IsStreaming = response.IsStreaming; + message.MessageLabel = response.MessageLabel; + message.AdditionalMessageWrapper = null; dialogs.Add(message); Context.SetDialogs(dialogs); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index dea0947d..f23f150c 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -65,6 +65,8 @@ public partial class RoutingService message.StopCompletion = clonedMessage.StopCompletion; message.RichContent = clonedMessage.RichContent; message.Data = clonedMessage.Data; + message.MessageLabel = clonedMessage.MessageLabel; + message.AdditionalMessageWrapper = clonedMessage.AdditionalMessageWrapper; } catch (JsonException ex) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 45f53c0c..7f12eec2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Chart; using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Files.Enums; using BotSharp.Abstraction.Files.Utilities; @@ -108,6 +109,7 @@ public class ConversationController : ControllerBase { ConversationId = conversationId, MessageId = message.MessageId, + MessageLabel = message.MessageLabel, CreatedAt = message.CreatedAt, Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Data = message.Data, @@ -123,6 +125,7 @@ public class ConversationController : ControllerBase { ConversationId = conversationId, MessageId = message.MessageId, + MessageLabel = message.MessageLabel, CreatedAt = message.CreatedAt, Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Function = message.FunctionName, @@ -368,9 +371,11 @@ public class ConversationController : ControllerBase { response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; response.Function = msg.FunctionName; + response.MessageLabel = msg.MessageLabel; response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; response.Instruction = msg.Instruction; response.Data = msg.Data; + response.AdditionalMessageWrapper = ChatResponseWrapper.From(msg.AdditionalMessageWrapper, conversationId, inputMsg.MessageId); }); var state = _services.GetRequiredService(); @@ -423,11 +428,13 @@ public class ConversationController : ControllerBase async msg => { response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.MessageLabel = msg.MessageLabel; response.Function = msg.FunctionName; response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; response.Instruction = msg.Instruction; response.Data = msg.Data; response.States = state.GetStates(); + response.AdditionalMessageWrapper = ChatResponseWrapper.From(msg.AdditionalMessageWrapper, conversationId, inputMsg.MessageId); await OnChunkReceived(Response, response); }); @@ -530,6 +537,35 @@ public class ConversationController : ControllerBase } #endregion + #region Chart + [AllowAnonymous] + [HttpGet("/conversation/{conversationId}/message/{messageId}/user/chart/data")] + public async Task GetConversationChartData( + [FromRoute] string conversationId, + [FromRoute] string messageId, + [FromQuery] ConversationChartDataRequest request) + { + var chart = _services.GetServices().FirstOrDefault(x => x.Provider == request?.ChartProvider); + if (chart == null) return null; + + var result = await chart.GetConversationChartData(conversationId, messageId, request); + return ConversationChartDataResponse.From(result); + } + + [HttpPost("/conversation/{conversationId}/message/{messageId}/user/chart/code")] + public async Task GetConversationChartCode( + [FromRoute] string conversationId, + [FromRoute] string messageId, + [FromBody] ConversationChartCodeRequest request) + { + var chart = _services.GetServices().FirstOrDefault(x => x.Provider == request?.ChartProvider); + if (chart == null) return null; + + var result = await chart.GetConversationChartCode(conversationId, messageId, request); + return ConversationChartCodeResponse.From(result); + } + #endregion + #region Dashboard [HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")] public async Task PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Using.cs b/src/Infrastructure/BotSharp.OpenAPI/Using.cs index 602f4437..ea5f9f7c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Using.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Using.cs @@ -28,6 +28,7 @@ global using BotSharp.Abstraction.Files.Models; global using BotSharp.Abstraction.Files; global using BotSharp.Abstraction.VectorStorage.Enums; global using BotSharp.Abstraction.Knowledges.Models; +global using BotSharp.Abstraction.Chart.Models; global using BotSharp.OpenAPI.ViewModels.Conversations; global using BotSharp.OpenAPI.ViewModels.Users; global using BotSharp.OpenAPI.ViewModels.Agents; diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationChartDataRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationChartDataRequest.cs new file mode 100644 index 00000000..19fcd14e --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationChartDataRequest.cs @@ -0,0 +1,17 @@ +namespace BotSharp.OpenAPI.ViewModels.Conversations; + +public class ConversationChartDataRequest : ChartDataOptions +{ + /// + /// Chart service provider + /// + public string ChartProvider { get; set; } = "Botsharp"; +} + +public class ConversationChartCodeRequest : ChartCodeOptions +{ + /// + /// Chart service provider + /// + public string ChartProvider { get; set; } = "Botsharp"; +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs index 4d149200..37db3084 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs @@ -1,7 +1,46 @@ using BotSharp.Abstraction.Conversations.Dtos; +using System.Text.Json.Serialization; namespace BotSharp.OpenAPI.ViewModels.Conversations; public class ChatResponseModel : ChatResponseDto { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("additional_message_wrapper")] + public ChatResponseWrapper? AdditionalMessageWrapper { get; set; } } + +public class ChatResponseWrapper +{ + [JsonPropertyName("sending_interval")] + public int SendingInterval { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("messages")] + public List? Messages { get; set; } + + public static ChatResponseWrapper? From(ChatMessageWrapper? wrapper, string conversationId, string? messageId = null) + { + if (wrapper == null) + { + return null; + } + + return new ChatResponseWrapper + { + SendingInterval = wrapper.SendingInterval, + Messages = wrapper?.Messages?.Select(x => new ChatResponseModel + { + ConversationId = conversationId, + MessageId = messageId ?? x.MessageId, + Text = !string.IsNullOrEmpty(x.SecondaryContent) ? x.SecondaryContent : x.Content, + MessageLabel = x.MessageLabel, + Function = x.FunctionName, + RichContent = x.SecondaryRichContent ?? x.RichContent, + Instruction = x.Instruction, + Data = x.Data, + IsAppend = true + })?.ToList() + }; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs new file mode 100644 index 00000000..197c1174 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs @@ -0,0 +1,40 @@ +namespace BotSharp.OpenAPI.ViewModels.Conversations; + +public class ConversationChartDataResponse +{ + public object Data { get; set; } + + public static ConversationChartDataResponse? From(ChartDataResult? result) + { + if (result == null) + { + return null; + } + + return new() + { + Data = result.Data + }; + } +} + + +public class ConversationChartCodeResponse +{ + public string Code { get; set; } + public string Language { get; set; } + + public static ConversationChartCodeResponse? From(ChartCodeResult? result) + { + if (result == null) + { + return null; + } + + return new() + { + Code = result.Code, + Language = result.Language + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs index 8e810683..d1fc5943 100644 --- a/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Messaging.Models.RichContent.Template; +using BotSharp.Abstraction.Routing; namespace BotSharp.Plugin.ChartHandler.Functions; @@ -6,6 +7,7 @@ public class PlotChartFn : IFunctionCallback { private readonly IServiceProvider _services; private readonly ILogger _logger; + private readonly ChartHandlerSettings _settings; public string Name => "util-chart-plot_chart"; public string Indication => "Plotting chart"; @@ -13,16 +15,19 @@ public class PlotChartFn : IFunctionCallback public PlotChartFn( IServiceProvider services, - ILogger logger) + ILogger logger, + ChartHandlerSettings settings) { _services = services; _logger = logger; + _settings = settings; } public async Task Execute(RoleDialogModel message) { var agentService = _services.GetRequiredService(); var convService = _services.GetRequiredService(); + var routingCtx = _services.GetRequiredService(); var args = JsonSerializer.Deserialize(message.FunctionArgs); @@ -33,10 +38,7 @@ public class PlotChartFn : IFunctionCallback Id = agent.Id, Name = agent.Name, Instruction = inst, - LlmConfig = new AgentLlmConfig - { - MaxOutputTokens = 8192 - }, + LlmConfig = GetLlmConfig(), TemplateDict = new Dictionary { { "plotting_requirement", args?.PlottingRequirement ?? string.Empty }, @@ -44,14 +46,21 @@ public class PlotChartFn : IFunctionCallback } }; - var response = await GetChatCompletion(innerAgent, [ - new RoleDialogModel(AgentRole.User, "Please follow the instruction to generate the javascript code.") - { - CurrentAgentId = message.CurrentAgentId, - MessageId = message.MessageId - } - ]); + var dialogs = routingCtx.GetDialogs(); + if (dialogs.IsNullOrEmpty()) + { + dialogs = convService.GetDialogHistory(); + } + var messageLimit = _settings.ChartPlot?.MessageLimit > 0 ? _settings.ChartPlot.MessageLimit.Value : 50; + dialogs = dialogs.TakeLast(messageLimit).ToList(); + dialogs.Add(new RoleDialogModel(AgentRole.User, "Please follow the instruction and chat context to generate valid javascript code.") + { + CurrentAgentId = message.CurrentAgentId, + MessageId = message.MessageId + }); + + var response = await GetChatCompletion(innerAgent, dialogs); var obj = response.JsonContent(); message.Content = obj?.GreetingMessage ?? "Here is the chart you ask for:"; message.RichContent = new RichContent @@ -63,6 +72,29 @@ public class PlotChartFn : IFunctionCallback Language = "javascript" } }; + + if (!string.IsNullOrEmpty(obj?.ReportSummary)) + { + message.AdditionalMessageWrapper = new() + { + SendingInterval = 1500, + SaveToDb = true, + Messages = new List + { + new(AgentRole.Assistant, obj.ReportSummary) + { + MessageId = message.MessageId, + MessageLabel = "chart_report_summary", + Indication = "Summarizing", + CurrentAgentId = message.CurrentAgentId, + FunctionName = message.FunctionName, + FunctionArgs = message.FunctionArgs, + CreatedAt = DateTime.UtcNow + } + } + }; + } + message.StopCompletion = true; return true; } @@ -111,14 +143,29 @@ public class PlotChartFn : IFunctionCallback var model = "gpt-5"; var state = _services.GetRequiredService(); - var settings = _services.GetRequiredService(); provider = state.GetState("chart_plot_llm_provider") - .IfNullOrEmptyAs(settings.ChartPlot?.LlmProvider) + .IfNullOrEmptyAs(_settings.ChartPlot?.LlmProvider) .IfNullOrEmptyAs(provider); model = state.GetState("chart_plot_llm_model") - .IfNullOrEmptyAs(settings.ChartPlot?.LlmModel) + .IfNullOrEmptyAs(_settings.ChartPlot?.LlmModel) .IfNullOrEmptyAs(model); return (provider, model); } + + private AgentLlmConfig GetLlmConfig() + { + var maxOutputTokens = _settings?.ChartPlot?.MaxOutputTokens ?? 8192; + var reasoningEffortLevel = _settings?.ChartPlot?.ReasoningEffortLevel ?? "minimal"; + + var state = _services.GetRequiredService(); + maxOutputTokens = int.TryParse(state.GetState("chart_plot_max_output_tokens"), out var tokens) ? tokens : maxOutputTokens; + reasoningEffortLevel = state.GetState("chart_plot_reasoning_effort_level").IfNullOrEmptyAs(reasoningEffortLevel); + + return new AgentLlmConfig + { + MaxOutputTokens = maxOutputTokens, + ReasoningEffortLevel = reasoningEffortLevel + }; + } } diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextOut.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextOut.cs index 7109f72d..863ad44e 100644 --- a/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextOut.cs +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/LlmContext/LlmContextOut.cs @@ -8,6 +8,10 @@ public class LlmContextOut [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? GreetingMessage { get; set; } + [JsonPropertyName("report_summary")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ReportSummary { get; set; } + [JsonPropertyName("js_code")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? JsCode { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/Settings/ChartHandlerSettings.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/Settings/ChartHandlerSettings.cs index 61b0a6be..1218b397 100644 --- a/src/Plugins/BotSharp.Plugin.ChartHandler/Settings/ChartHandlerSettings.cs +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/Settings/ChartHandlerSettings.cs @@ -7,6 +7,9 @@ public class ChartHandlerSettings public class ChartPlotSetting { - public string LlmProvider { get; set; } - public string LlmModel { get; set; } + public string? LlmProvider { get; set; } + public string? LlmModel { get; set; } + public int? MaxOutputTokens { get; set; } + public string? ReasoningEffortLevel { get; set; } + public int? MessageLimit { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_instruction.liquid b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_instruction.liquid index 0d631b51..af0a04a9 100644 --- a/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_instruction.liquid +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-chart-plot_instruction.liquid @@ -1,31 +1,46 @@ Please take a look at "Plotting Requirement" and generate a javascript code that can be used to render the charts on an html element. +You must strictly follow the "Hard Requirements", "Render Requirements", "Code Requirements" and "Response Format" below. === Plotting Requirement === {{ plotting_requirement }} + ***** Hard Requirements ***** ** Your output javascript code must be wrapped in one or multiple blocks with everything needed inside. -** You need to import ECharts.js to plot the charts. The script source is "https://cdnjs.cloudflare.com/ajax/libs/echarts/6.0.0/echarts.min.js". -** You need to add the MODE bar for each chart you plot. -** You must render the charts under the div html element with id {{ chart_element_id }}. -** Add a custom mode bar button named "Fullscreen" that toggles the chart container in and out of fullscreen using the Fullscreen API. Requirements for this button: - * Always call the Fullscreen API on the chart container div itself (document.getElementById("{{ chart_element_id }}")), not on the document or on Plotly’s SVG. +** You need to import ECharts.js exactly once to plot the charts. The script source is "https://cdnjs.cloudflare.com/ajax/libs/echarts/6.0.0/echarts.min.js". +** You must add an ECharts Toolbox bar at the top left corner of the chart. +** ALWAYS add a "Full screen" button right next to the Toolbox bar. +** The "Full screen" button can toggle the chart container in and out of fullscreen using the Fullscreen API. Requirements for this button: + * Always call the Fullscreen API on the chart container div itself (document.getElementById("{{ chart_element_id }}")), not on the document. + * When entering fullscreen, the chart container must have widhth: 100vw and height: 100vh. * Use el.requestFullscreen() with fallbacks to el.webkitRequestFullscreen || el.msRequestFullscreen. * Exit fullscreen with document.exitFullscreen() and vendor fallbacks. * Listen for fullscreenchange, webkitfullscreenchange, and msfullscreenchange to keep the button working across repeated clicks and ESC exits. * Ensure the chart fully expands and scales to the entire screen when fullscreen is active. - * Provide a simple inline SVG path icon for the button (no external assets). - * Use Plotly.newPlot(container, data, layout, {displayModeBar:true, modeBarButtonsToAdd:[fullscreenBtn]}); - * fullscreenBtn must be a fully-formed object {name, title, icon, click}. + * fullscreenBtn must be a fully-formed object {show: true, name, title, icon: 'path://M3 3 H9 V5 H5 V9 H3 Z M15 3 H21 V9 H19 V5 H15 Z M3 15 H5 V19 H9 V21 H3 Z M19 15 H21 V21 H15 V19 H19 Z', onclick}. + * When using "chart.setOption" to define the fullscreen button, DO NOT use "graphic". Include the fullscreenBtn object in toolbox.feature with name 'myFullscreen'. +** You must initialize the chart with explicit non-zero width (at least 800px) and non-zero height (at least 500px). +** When generating code for multiple charts, arrange them in a clear layout and make sure they do not overlap. +** Only using the dataset provided in the context, NEVER generate or assume any additional dataset on your own. + +***** Render Requirements ***** +** You must render the charts under the div html element with id {{ chart_element_id }}. ** You must not create any new html element. ** You must not apply any styles on any html element. -** Keep code compact (few tokens), but fix all errors before returning. -** Do not generate charts with zero height and zero width. + + +***** Code Requirements ***** +** You must strictly follow the valid javascript and ECharts.js syntax. +** You must ensure no syntax/runtime error before returning the response. +** You must ensure the method is applied on variable with correct type, such as never applying "toFixed" to a non-number variable. Use explicit conversion if necessary. +** You must ensure the math operations are valid, such as avoid dividing by zero. ** Please ensure the code can be executed and the charts are rendered correctly. -*** Response Format *** + +***** Response Format ***** You must output the response in the following JSON format: { "greeting_message": "A short polite message that informs user that the charts have been generated.", - "js_code": "The javascript code that can generate the charts as requested." + "js_code": "The javascript code that can generate the charts as requested.", + "report_summary": "Generate an insightful summary report in markdown format based on the data. You can summarize using one or multiple titles and list the key findings under each title. Bold all key findings and use level-4 headings or smaller (####, #####, etc.). DO NOT make everything in one line." } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 3831ecd7..9aa7e5b6 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -5,6 +5,7 @@ using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.SideCar; using BotSharp.Abstraction.Users.Dtos; using Microsoft.AspNetCore.SignalR; +using System; using System.Runtime.CompilerServices; namespace BotSharp.Plugin.ChatHub.Hooks; @@ -94,33 +95,80 @@ public class ChatHubConversationHook : ConversationHookBase var conv = _services.GetRequiredService(); var state = _services.GetRequiredService(); + + var sender = new UserDto + { + FirstName = "AI", + LastName = "Assistant", + Role = AgentRole.Assistant + }; + var data = new ChatResponseDto() { ConversationId = conv.ConversationId, MessageId = message.MessageId, + MessageLabel = message.MessageLabel, Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Function = message.FunctionName, RichContent = message.SecondaryRichContent ?? message.RichContent, Data = message.Data, States = state.GetStates(), IsStreaming = message.IsStreaming, - Sender = new() - { - FirstName = "AI", - LastName = "Assistant", - Role = AgentRole.Assistant - } + Sender = sender }; - // Send typing-off to client + // Send type-off to client var action = new ConversationSenderActionModel { ConversationId = conv.ConversationId, SenderAction = SenderActionEnum.TypingOff }; await SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action); - await SendEvent(ChatEvent.OnMessageReceivedFromAssistant, conv.ConversationId, data); + + var wrapper = message.AdditionalMessageWrapper; + if (wrapper?.SendingInterval > 0 && wrapper?.Messages?.Count > 0) + { + action.SenderAction = SenderActionEnum.TypingOn; + await SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action); + + foreach (var item in wrapper.Messages) + { + if (!string.IsNullOrWhiteSpace(item.Indication)) + { + data = new ChatResponseDto + { + ConversationId = conv.ConversationId, + MessageId = item.MessageId, + MessageLabel = item.MessageLabel, + Indication = item.Indication, + Sender = sender + }; + await SendEvent(ChatEvent.OnIndicationReceived, conv.ConversationId, data); + } + + await Task.Delay(wrapper.SendingInterval); + + data = new ChatResponseDto + { + ConversationId = conv.ConversationId, + MessageId = item.MessageId, + MessageLabel = item.MessageLabel, + Text = !string.IsNullOrEmpty(item.SecondaryContent) ? item.SecondaryContent : item.Content, + Function = item.FunctionName, + RichContent = item.SecondaryRichContent ?? item.RichContent, + Data = item.Data, + States = state.GetStates(), + IsAppend = true, + Sender = sender + }; + await SendEvent(ChatEvent.OnMessageReceivedFromAssistant, conv.ConversationId, data); + } + + action.SenderAction = SenderActionEnum.TypingOff; + await SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action); + } + await base.OnResponseGenerated(message); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs index b67fc53c..7dd7ece7 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs @@ -46,6 +46,7 @@ public class DialogMetaDataMongoElement public string AgentId { get; set; } = default!; public string MessageId { get; set; } = default!; public string MessageType { get; set; } = default!; + public string? MessageLabel { get; set; } public string? FunctionName { get; set; } public string? FunctionArgs { get; set; } public string? ToolCallId { get; set; } @@ -60,6 +61,7 @@ public class DialogMetaDataMongoElement AgentId = meta.AgentId, MessageId = meta.MessageId, MessageType = meta.MessageType, + MessageLabel = meta.MessageLabel, FunctionName = meta.FunctionName, FunctionArgs = meta.FunctionArgs, ToolCallId = meta.ToolCallId, @@ -76,6 +78,7 @@ public class DialogMetaDataMongoElement AgentId = meta.AgentId, MessageId = meta.MessageId, MessageType = meta.MessageType, + MessageLabel = meta.MessageLabel, FunctionName = meta.FunctionName, FunctionArgs = meta.FunctionArgs, ToolCallId = meta.ToolCallId, diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj index 2e7b802d..bda50ed0 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/LlmContext/ChartLlmContextOut.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/LlmContext/ChartLlmContextOut.cs new file mode 100644 index 00000000..a1c73f05 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/LlmContext/ChartLlmContextOut.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.SqlDriver.LlmContext; + +internal class ChartLlmContextOut +{ + [JsonPropertyName("greeting_message")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? GreetingMessage { get; set; } + + [JsonPropertyName("report_summary")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ReportSummary { get; set; } + + [JsonPropertyName("js_code")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? JsCode { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartService.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartService.cs new file mode 100644 index 00000000..7e091bc3 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartService.cs @@ -0,0 +1,155 @@ +using BotSharp.Abstraction.Options; +using BotSharp.Abstraction.Repositories; +using BotSharp.Core.Infrastructures; +using BotSharp.Plugin.SqlDriver.LlmContext; + +namespace BotSharp.Plugin.SqlDriver.Services; + +public class SqlChartService : IBotSharpChartService +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly BotSharpOptions _botSharpOptions; + + public SqlChartService( + IServiceProvider services, + ILogger logger, + BotSharpOptions botSharpOptions) + { + _services = services; + _logger = logger; + _botSharpOptions = botSharpOptions; + } + + public string Provider => "sql_driver"; + + public async Task GetConversationChartData(string conversationId, string messageId, ChartDataOptions options) + { + if (string.IsNullOrWhiteSpace(conversationId)) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(options?.TargetStateName)) + { + var db = _services.GetRequiredService(); + var states = db.GetConversationStates(conversationId); + var value = states?.GetValueOrDefault(options?.TargetStateName)?.Values?.LastOrDefault()?.Data; + + // To do + //return new ChartDataResult(); + } + + // Dummy data for testing + var data = new + { + categories = new string[] { "A", "B", "C", "D", "E" }, + values = new int[] { 42, 67, 29, 85, 53 } + }; + + return new ChartDataResult { Data = data }; + } + + public async Task GetConversationChartCode(string conversationId, string messageId, ChartCodeOptions options) + { + if (string.IsNullOrWhiteSpace(conversationId)) + { + return null; + } + + var agentService = _services.GetRequiredService(); + + var agentId = options.AgentId.IfNullOrEmptyAs(BuiltInAgentId.UtilityAssistant); + var templateName = options.TemplateName.IfNullOrEmptyAs("util-chart-plot_instruction"); + var inst = GetChartCodeInstruction(agentId, templateName); + + var agent = await agentService.GetAgent(agentId); + agent = new Agent + { + Id = agent.Id, + Name = agent.Name, + Instruction = inst, + LlmConfig = new AgentLlmConfig + { + MaxOutputTokens = options.Llm?.MaxOutputTokens ?? 8192, + ReasoningEffortLevel = options.Llm?.ReasoningEffortLevel + }, + TemplateDict = BuildChartStates(options) + }; + + var dialogs = new List + { + new RoleDialogModel + { + Role = AgentRole.User, + MessageId = messageId, + Content = options.Text.IfNullOrEmptyAs("Please follow the instruction to generate response.") + } + }; + var response = await GetChatCompletion(agent, dialogs, options); + var obj = response.JsonContent(); + + return new ChartCodeResult + { + Code = obj?.JsCode, + Language = "javascript" + }; + } + + + private Dictionary BuildChartStates(ChartCodeOptions options) + { + var states = new Dictionary(); + + if (!options.States.IsNullOrEmpty()) + { + foreach (var item in options.States) + { + if (item.Value == null) + { + continue; + } + states[item.Key] = item.Value; + } + } + return states; + } + + private string GetChartCodeInstruction(string agentId, string templateName) + { + var db = _services.GetRequiredService(); + var templateContent = db.GetAgentTemplate(agentId, templateName); + return templateContent; + } + + private async Task GetChatCompletion(Agent agent, List dialogs, ChartCodeOptions options) + { + try + { + var (provider, model) = GetLlmProviderModel(options); + var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, model: model); + var response = await completion.GetChatCompletions(agent, dialogs); + return response.Content; + } + catch (Exception ex) + { + var error = $"Error when generating chart code. {ex.Message}"; + _logger.LogWarning(ex, error); + return error; + } + } + + private (string, string) GetLlmProviderModel(ChartCodeOptions options) + { + var provider = "openai"; + var model = "gpt-5"; + + if (options?.Llm != null) + { + provider = options.Llm.Provider.IfNullOrEmptyAs(provider); + model = options.Llm.Model.IfNullOrEmptyAs(model); + } + + return (provider, model); + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs index 57969771..1dd27ad7 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs @@ -30,5 +30,6 @@ public class SqlDriverPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs index f0bf5480..94e9d49c 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs @@ -25,6 +25,8 @@ global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Instructs; global using BotSharp.Abstraction.Instructs.Models; global using BotSharp.Abstraction.Routing; +global using BotSharp.Abstraction.Chart; +global using BotSharp.Abstraction.Chart.Models; global using BotSharp.Plugin.SqlDriver.Models; global using BotSharp.Plugin.SqlDriver.Hooks; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs index 72bef238..1d96c7f3 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs @@ -7,8 +7,9 @@ namespace BotSharp.Plugin.SqlDriver.UtilFunctions; public class SqlSelect : IFunctionCallback { - public string Name => "util-db-sql_select"; private readonly IServiceProvider _services; + public string Name => "util-db-sql_select"; + public string Indication => "Extracting data"; public SqlSelect(IServiceProvider services) { diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index e0f8842d..992883a5 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -320,7 +320,9 @@ "ChartHandler": { "ChartPlot": { "LlmProvider": "openai", - "LlmModel": "gpt-5" + "LlmModel": "gpt-5", + "MaxOutputTokens": 8192, + "ReasoningEffortLevel": "minimal" } }, @@ -579,6 +581,7 @@ "BotSharp.Plugin.EmailHandler", "BotSharp.Plugin.AudioHandler", "BotSharp.Plugin.ChartHandler", + "BotSharp.Plugin.SqlDriver", "BotSharp.Plugin.TencentCos" ] }