Merge pull request #1149 from iceljc/features/add-chart-data

add chart data and code endpoints
This commit is contained in:
iceljc 2025-09-05 15:00:34 -05:00 committed by GitHub
commit 7fda8d2338
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 330 additions and 3 deletions

View file

@ -0,0 +1,14 @@
using BotSharp.Abstraction.Chart.Models;
namespace BotSharp.Abstraction.Chart;
public interface IBotSharpChartService
{
public string Provider { get; }
Task<ChartDataResult?> GetConversationChartData(string conversationId, string messageId, ChartDataOptions options)
=> throw new NotImplementedException();
Task<ChartCodeResult?> GetConversationChartCode(string conversationId, string messageId, ChartCodeOptions options)
=> throw new NotImplementedException();
}

View file

@ -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;
/// <summary>
/// Conversation state that can be used to fetch chart data
/// </summary>
public string? TargetStateName { get; set; }
public ChartLlmOptions? Llm { get; set; }
public List<KeyValue<object>>? 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; }
}

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Chart.Models;
public class ChartCodeResult
{
public string Code { get; set; }
public string Language { get; set; }
}

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Chart.Models;
public class ChartDataOptions
{
/// <summary>
/// Conversation state that can be used to fetch chart data
/// </summary>
public string? TargetStateName { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Chart.Models;
public class ChartDataResult
{
public object Data { get; set; }
}

View file

@ -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 class ConversationController : ControllerBase
}
#endregion
#region Chart
[AllowAnonymous]
[HttpGet("/conversation/{conversationId}/message/{messageId}/user/chart/data")]
public async Task<ConversationChartDataResponse?> GetConversationChartData(
[FromRoute] string conversationId,
[FromRoute] string messageId,
[FromQuery] ConversationChartDataRequest request)
{
var chart = _services.GetServices<IBotSharpChartService>().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<ConversationChartCodeResponse?> GetConversationChartCode(
[FromRoute] string conversationId,
[FromRoute] string messageId,
[FromBody] ConversationChartCodeRequest request)
{
var chart = _services.GetServices<IBotSharpChartService>().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<bool> PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId)

View file

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

View file

@ -0,0 +1,17 @@
namespace BotSharp.OpenAPI.ViewModels.Conversations;
public class ConversationChartDataRequest : ChartDataOptions
{
/// <summary>
/// Chart service provider
/// </summary>
public string ChartProvider { get; set; } = "Botsharp";
}
public class ConversationChartCodeRequest : ChartCodeOptions
{
/// <summary>
/// Chart service provider
/// </summary>
public string ChartProvider { get; set; } = "Botsharp";
}

View file

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

View file

@ -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 <script>...</script> 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.

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>

View file

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

View file

@ -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<SqlChartService> _logger;
private readonly BotSharpOptions _botSharpOptions;
public SqlChartService(
IServiceProvider services,
ILogger<SqlChartService> logger,
BotSharpOptions botSharpOptions)
{
_services = services;
_logger = logger;
_botSharpOptions = botSharpOptions;
}
public string Provider => "sql_driver";
public async Task<ChartDataResult?> GetConversationChartData(string conversationId, string messageId, ChartDataOptions options)
{
if (string.IsNullOrWhiteSpace(conversationId))
{
return null;
}
if (!string.IsNullOrWhiteSpace(options?.TargetStateName))
{
var db = _services.GetRequiredService<IBotSharpRepository>();
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<ChartCodeResult?> GetConversationChartCode(string conversationId, string messageId, ChartCodeOptions options)
{
if (string.IsNullOrWhiteSpace(conversationId))
{
return null;
}
var agentService = _services.GetRequiredService<IAgentService>();
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<RoleDialogModel>
{
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<ChartLlmContextOut>();
return new ChartCodeResult
{
Code = obj?.JsCode,
Language = "javascript"
};
}
private Dictionary<string, object> BuildChartStates(ChartCodeOptions options)
{
var states = new Dictionary<string, object>();
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<IBotSharpRepository>();
var templateContent = db.GetAgentTemplate(agentId, templateName);
return templateContent;
}
private async Task<string> GetChatCompletion(Agent agent, List<RoleDialogModel> 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);
}
}

View file

@ -30,5 +30,6 @@ public class SqlDriverPlugin : IBotSharpPlugin
services.AddScoped<IConversationHook, SqlDriverConversationHook>();
services.AddScoped<IAgentUtilityHook, SqlUtilityHook>();
services.AddScoped<ICrontabHook, SqlDriverCrontabHook>();
services.AddScoped<IBotSharpChartService, SqlChartService>();
}
}

View file

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

View file

@ -549,6 +549,7 @@
"BotSharp.Plugin.EmailHandler",
"BotSharp.Plugin.AudioHandler",
"BotSharp.Plugin.ChartHandler",
"BotSharp.Plugin.SqlDriver",
"BotSharp.Plugin.TencentCos"
]
}