From 9b30898f06fb9fb3c4933cf5aa76f538b4ff73bb Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 5 Sep 2025 14:51:26 -0500 Subject: [PATCH] add chart data and code endpoints --- .../Chart/IBotSharpChartService.cs | 14 ++ .../Chart/Models/ChartCodeOptions.cs | 25 +++ .../Chart/Models/ChartCodeResult.cs | 7 + .../Chart/Models/ChartDataOptions.cs | 9 + .../Chart/Models/ChartDataResult.cs | 6 + .../Controllers/ConversationController.cs | 30 ++++ src/Infrastructure/BotSharp.OpenAPI/Using.cs | 1 + .../Request/ConversationChartDataRequest.cs | 17 ++ .../Response/ConversationChartDataResponse.cs | 40 +++++ .../util-chart-plot_instruction.liquid | 5 +- .../BotSharp.Plugin.SqlDriver.csproj | 2 +- .../LlmContext/ChartLlmContextOut.cs | 18 ++ .../Services/SqlChartService.cs | 155 ++++++++++++++++++ .../SqlDriverPlugin.cs | 1 + .../BotSharp.Plugin.SqlDriver/Using.cs | 2 + src/WebStarter/appsettings.json | 1 + 16 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeOptions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeResult.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataOptions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartDataResult.cs create mode 100644 src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationChartDataRequest.cs create mode 100644 src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs create mode 100644 src/Plugins/BotSharp.Plugin.SqlDriver/LlmContext/ChartLlmContextOut.cs create mode 100644 src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartService.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs new file mode 100644 index 000000000..1a433e6fc --- /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 000000000..beac046db --- /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 000000000..143ba218d --- /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 000000000..249ec080c --- /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 000000000..28292d5ae --- /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.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 45f53c0c2..2f127b100 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; @@ -530,6 +531,35 @@ public IActionResult DownloadMessageFile([FromRoute] string conversationId, [Fro } #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 602f44378..ea5f9f7cc 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Using.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Using.cs @@ -28,6 +28,7 @@ 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 000000000..19fcd14e4 --- /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/ConversationChartDataResponse.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs new file mode 100644 index 000000000..197c11746 --- /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/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 7a37bab96..7721493f3 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 @@ -7,23 +7,24 @@ You must strictly follow the "Hard Requirements", "Render Requirements", "Code R ***** Hard Requirements ***** ** Your output javascript code must be wrapped in one or multiple blocks with everything needed inside. -** You need to import ECharts.js exactly once to plot the charts. The script source is "https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.1/echarts.min.js". +** 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. * 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'. +** Initialize the chart with explicit non-zero width (at least 800px) and non-zero height (at least 500px). ***** 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 ensure the generated charts have visible height (at least 500px) and width (at least 800px). DO NOT generate charts with zero height or zero width. ** You must not apply any styles on any html element. diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj index 2e7b802df..bda50ed05 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 000000000..a1c73f059 --- /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 000000000..7e091bc3b --- /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 579697718..1dd27ad79 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs @@ -30,5 +30,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) 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 f0bf54803..94e9d49cf 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.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/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 8124a4a8a..8800c3800 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -549,6 +549,7 @@ "BotSharp.Plugin.EmailHandler", "BotSharp.Plugin.AudioHandler", "BotSharp.Plugin.ChartHandler", + "BotSharp.Plugin.SqlDriver", "BotSharp.Plugin.TencentCos" ] }