Merge branch 'master' of https://github.com/SciSharp/BotSharp into features/refine-model-settings

This commit is contained in:
Jicheng Lu 2025-09-11 09:55:52 -05:00
commit e12808e36e
34 changed files with 770 additions and 122 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

@ -36,12 +36,19 @@ public class ChatResponseDto : InstructResult
[JsonPropertyName("indication")] [JsonPropertyName("indication")]
public string? Indication { get; set; } public string? Indication { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("message_label")]
public string? MessageLabel { get; set; }
[JsonPropertyName("has_message_files")] [JsonPropertyName("has_message_files")]
public bool HasMessageFiles { get; set; } public bool HasMessageFiles { get; set; }
[JsonPropertyName("is_streaming")] [JsonPropertyName("is_streaming")]
public bool IsStreaming { get; set; } public bool IsStreaming { get; set; }
[JsonPropertyName("is_append")]
public bool IsAppend { get; set; }
[JsonPropertyName("created_at")] [JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
} }

View file

@ -98,6 +98,10 @@ public class DialogMetaData
[JsonPropertyName("message_type")] [JsonPropertyName("message_type")]
public string MessageType { get; set; } = default!; public string MessageType { get; set; } = default!;
[JsonPropertyName("message_label")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MessageLabel { get; set; }
[JsonPropertyName("function_name")] [JsonPropertyName("function_name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FunctionName { get; set; } public string? FunctionName { get; set; }

View file

@ -19,6 +19,11 @@ public class RoleDialogModel : ITrackableMessage
/// </summary> /// </summary>
public string MessageType { get; set; } = MessageTypeName.Plain; public string MessageType { get; set; } = MessageTypeName.Plain;
/// <summary>
/// The message label
/// </summary>
public string? MessageLabel { get; set; }
/// <summary> /// <summary>
/// user, system, assistant, function /// user, system, assistant, function
/// </summary> /// </summary>
@ -127,6 +132,13 @@ public class RoleDialogModel : ITrackableMessage
[JsonIgnore(Condition = JsonIgnoreCondition.Always)] [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public bool IsStreaming { get; set; } public bool IsStreaming { get; set; }
/// <summary>
/// Additional messages that can be sent sequentially and save to db
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public ChatMessageWrapper? AdditionalMessageWrapper { get; set; }
public RoleDialogModel() public RoleDialogModel()
{ {
} }
@ -160,6 +172,7 @@ public class RoleDialogModel : ITrackableMessage
CurrentAgentId = source.CurrentAgentId, CurrentAgentId = source.CurrentAgentId,
MessageId = source.MessageId, MessageId = source.MessageId,
MessageType = source.MessageType, MessageType = source.MessageType,
MessageLabel = source.MessageLabel,
FunctionArgs = source.FunctionArgs, FunctionArgs = source.FunctionArgs,
FunctionName = source.FunctionName, FunctionName = source.FunctionName,
ToolCallId = source.ToolCallId, ToolCallId = source.ToolCallId,
@ -171,7 +184,26 @@ public class RoleDialogModel : ITrackableMessage
Instruction = source.Instruction, Instruction = source.Instruction,
Data = source.Data, Data = source.Data,
IsStreaming = source.IsStreaming, IsStreaming = source.IsStreaming,
Annotations = source.Annotations Annotations = source.Annotations,
AdditionalMessageWrapper = source.AdditionalMessageWrapper
}; };
} }
} }
public class ChatMessageWrapper
{
/// <summary>
/// Messages sending interval in milliseconds
/// </summary>
public int SendingInterval { get; set; }
/// <summary>
/// Whether the Messages are saved to db
/// </summary>
public bool SaveToDb { get; set; }
/// <summary>
/// Messages to send or save
/// </summary>
public List<RoleDialogModel>? Messages { get; set; }
}

View file

@ -4,9 +4,16 @@ public class CrontabSettings
{ {
public CrontabBaseSetting EventSubscriber { get; set; } = new(); public CrontabBaseSetting EventSubscriber { get; set; } = new();
public CrontabBaseSetting Watcher { 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 class CrontabBaseSetting
{ {
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
} }
public class DebugSetting
{
public string AllowRuleTrigger { get; set; } = "";
}

View file

@ -10,11 +10,15 @@ public class CrontabWatcher : BackgroundService
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private readonly CrontabSettings _cronSettings;
private string DIST_KEY;
public CrontabWatcher(IServiceProvider services, ILogger<CrontabWatcher> logger) public CrontabWatcher(IServiceProvider services, ILogger<CrontabWatcher> logger, CrontabSettings cronSettings)
{ {
_logger = logger; _logger = logger;
_services = services; _services = services;
_cronSettings = cronSettings;
DIST_KEY = _cronSettings.LockName;
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@ -29,7 +33,7 @@ public class CrontabWatcher : BackgroundService
{ {
var delay = Task.Delay(1000, stoppingToken); var delay = Task.Delay(1000, stoppingToken);
await locker.LockAsync("CrontabWatcher:locker", async () => await locker.LockAsync(DIST_KEY, async () =>
{ {
await RunCronChecker(scope.ServiceProvider); await RunCronChecker(scope.ServiceProvider);
}); });
@ -89,7 +93,10 @@ public class CrontabWatcher : BackgroundService
_logger.LogInformation($"The current time matches the cron expression {item}"); _logger.LogInformation($"The current time matches the cron expression {item}");
#if DEBUG #if DEBUG
await HandleCrontabEvent(item); if (item.Title == settings.Debug.AllowRuleTrigger)
{
await HandleCrontabEvent(item);
}
#else #else
if (publisher != null) if (publisher != null)
{ {

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Hooks;
using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Models.RichContent; using BotSharp.Abstraction.Messaging.Models.RichContent;

View file

@ -31,63 +31,21 @@ public class ConversationStorage : IConversationStorage
foreach ( var dialog in dialogs) foreach ( var dialog in dialogs)
{ {
if (dialog.Role == AgentRole.Function) var innerList = new List<RoleDialogModel> { dialog };
if (dialog.AdditionalMessageWrapper != null
&& dialog.AdditionalMessageWrapper.SaveToDb
&& dialog.AdditionalMessageWrapper.Messages?.Count > 0)
{ {
var meta = new DialogMetaData innerList.AddRange(dialog.AdditionalMessageWrapper.Messages);
{
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
});
} }
else
foreach (var item in innerList)
{ {
var meta = new DialogMetaData var element = BuildDialogElement(item);
if (element != null)
{ {
Role = dialog.Role, dialogElements.Add(element);
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;
} }
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 secondaryContent = dialog.SecondaryContent;
var payload = string.IsNullOrEmpty(dialog.Payload) ? null : dialog.Payload; var payload = string.IsNullOrEmpty(dialog.Payload) ? null : dialog.Payload;
var role = meta.Role; var role = meta.Role;
var currentAgentId = meta.AgentId; var senderId = role == AgentRole.Function ? meta?.AgentId : meta?.SenderId;
var messageId = meta.MessageId;
var messageType = meta.MessageType;
var senderId = role == AgentRole.Function ? currentAgentId : meta.SenderId;
var createdAt = meta.CreatedTime;
var richContent = !string.IsNullOrEmpty(dialog.RichContent) ? var richContent = !string.IsNullOrEmpty(dialog.RichContent) ?
JsonSerializer.Deserialize<RichContent<IRichMessage>>(dialog.RichContent, _options.JsonSerializerOptions) : null; JsonSerializer.Deserialize<RichContent<IRichMessage>>(dialog.RichContent, _options.JsonSerializerOptions) : null;
var secondaryRichContent = !string.IsNullOrEmpty(dialog.SecondaryRichContent) ? var secondaryRichContent = !string.IsNullOrEmpty(dialog.SecondaryRichContent) ?
@ -120,14 +74,15 @@ public class ConversationStorage : IConversationStorage
var record = new RoleDialogModel(role, content) var record = new RoleDialogModel(role, content)
{ {
CurrentAgentId = currentAgentId, CurrentAgentId = meta?.AgentId ?? string.Empty,
MessageId = messageId, MessageId = meta?.MessageId ?? string.Empty,
MessageType = messageType, MessageType = meta?.MessageType ?? string.Empty,
CreatedAt = createdAt, MessageLabel = meta?.MessageLabel,
CreatedAt = meta?.CreatedTime ?? default,
SenderId = senderId, SenderId = senderId,
FunctionName = meta.FunctionName, FunctionName = meta?.FunctionName,
FunctionArgs = meta.FunctionArgs, FunctionArgs = meta?.FunctionArgs,
ToolCallId = meta.ToolCallId, ToolCallId = meta?.ToolCallId,
RichContent = richContent, RichContent = richContent,
SecondaryContent = secondaryContent, SecondaryContent = secondaryContent,
SecondaryRichContent = secondaryRichContent, SecondaryRichContent = secondaryRichContent,
@ -148,4 +103,69 @@ public class ConversationStorage : IConversationStorage
return results; 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;
}
} }

View file

@ -31,11 +31,17 @@ public partial class LocalFileStorageService
public BinaryData GetSpeechFile(string conversationId, string fileName) 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); var path = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, TEXT_TO_SPEECH_FOLDER, fileName);
if (!File.Exists(path)) if (!File.Exists(path))
{ {
return BinaryData.Empty; return BinaryData.Empty;
} }
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read); using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
return BinaryData.FromStream(fs); return BinaryData.FromStream(fs);
} }

View file

@ -19,16 +19,30 @@ public partial class LocalFileStorageService
foreach (var messageId in messageIds) foreach (var messageId in messageIds)
{ {
if (string.IsNullOrWhiteSpace(messageId))
{
continue;
}
var dir = Path.Combine(pathPrefix, messageId, FileSourceType.User); var dir = Path.Combine(pathPrefix, messageId, FileSourceType.User);
if (!ExistDirectory(dir)) continue; if (!ExistDirectory(dir))
{
continue;
}
foreach (var subDir in Directory.GetDirectories(dir)) foreach (var subDir in Directory.GetDirectories(dir))
{ {
var file = Directory.GetFiles(subDir).FirstOrDefault(); var file = Directory.GetFiles(subDir).FirstOrDefault();
if (file == null) continue; if (file == null)
{
continue;
}
var screenshots = await GetScreenshots(file, subDir, messageId, source); var screenshots = await GetScreenshots(file, subDir, messageId, source);
if (screenshots.IsNullOrEmpty()) continue; if (screenshots.IsNullOrEmpty())
{
continue;
}
files.AddRange(screenshots); files.AddRange(screenshots);
} }
@ -41,10 +55,18 @@ public partial class LocalFileStorageService
string source, IEnumerable<string>? contentTypes = null) string source, IEnumerable<string>? contentTypes = null)
{ {
var files = new List<MessageFileModel>(); var files = new List<MessageFileModel>();
if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return files; if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty())
{
return files;
}
foreach (var messageId in messageIds) foreach (var messageId in messageIds)
{ {
if (string.IsNullOrWhiteSpace(messageId))
{
continue;
}
var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId, source); var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId, source);
if (!ExistDirectory(dir)) if (!ExistDirectory(dir))
{ {
@ -85,6 +107,14 @@ public partial class LocalFileStorageService
public string GetMessageFile(string conversationId, string messageId, string source, string index, string fileName) 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); var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId, source, index);
if (!ExistDirectory(dir)) if (!ExistDirectory(dir))
{ {
@ -98,10 +128,18 @@ public partial class LocalFileStorageService
public IEnumerable<MessageFileModel> GetMessagesWithFile(string conversationId, IEnumerable<string> messageIds) public IEnumerable<MessageFileModel> GetMessagesWithFile(string conversationId, IEnumerable<string> messageIds)
{ {
var foundMsgs = new List<MessageFileModel>(); var foundMsgs = new List<MessageFileModel>();
if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return foundMsgs; if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty())
{
return foundMsgs;
}
foreach (var messageId in messageIds) foreach (var messageId in messageIds)
{ {
if (string.IsNullOrWhiteSpace(messageId))
{
continue;
}
var prefix = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId); var prefix = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId);
var userDir = Path.Combine(prefix, FileSourceType.User); var userDir = Path.Combine(prefix, FileSourceType.User);
if (ExistDirectory(userDir)) if (ExistDirectory(userDir))
@ -121,10 +159,19 @@ public partial class LocalFileStorageService
public bool SaveMessageFiles(string conversationId, string messageId, string source, List<FileDataModel> files) public bool SaveMessageFiles(string conversationId, string messageId, string source, List<FileDataModel> 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); 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++) for (int i = 0; i < files.Count; i++)
{ {
@ -164,7 +211,10 @@ public partial class LocalFileStorageService
public bool DeleteMessageFiles(string conversationId, IEnumerable<string> messageIds, string targetMessageId, string? newMessageId = null) public bool DeleteMessageFiles(string conversationId, IEnumerable<string> 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)) if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId))
{ {
@ -192,7 +242,10 @@ public partial class LocalFileStorageService
foreach (var messageId in messageIds) foreach (var messageId in messageIds)
{ {
var dir = GetConversationFileDirectory(conversationId, messageId); var dir = GetConversationFileDirectory(conversationId, messageId);
if (!ExistDirectory(dir)) continue; if (!ExistDirectory(dir))
{
continue;
}
DeleteDirectory(dir); DeleteDirectory(dir);
Thread.Sleep(100); Thread.Sleep(100);
@ -203,12 +256,18 @@ public partial class LocalFileStorageService
public bool DeleteConversationFiles(IEnumerable<string> conversationIds) public bool DeleteConversationFiles(IEnumerable<string> conversationIds)
{ {
if (conversationIds.IsNullOrEmpty()) return false; if (conversationIds.IsNullOrEmpty())
{
return false;
}
foreach (var conversationId in conversationIds) foreach (var conversationId in conversationIds)
{ {
var convDir = GetConversationDirectory(conversationId); var convDir = GetConversationDirectory(conversationId);
if (!ExistDirectory(convDir)) continue; if (!ExistDirectory(convDir))
{
continue;
}
DeleteDirectory(convDir); DeleteDirectory(convDir);
} }
@ -241,7 +300,10 @@ public partial class LocalFileStorageService
private IEnumerable<string> GetMessageIds(IEnumerable<RoleDialogModel> dialogs, int? offset = null) private IEnumerable<string> GetMessageIds(IEnumerable<RoleDialogModel> dialogs, int? offset = null)
{ {
if (dialogs.IsNullOrEmpty()) return Enumerable.Empty<string>(); if (dialogs.IsNullOrEmpty())
{
return Enumerable.Empty<string>();
}
if (offset.HasValue && offset < 1) if (offset.HasValue && offset < 1)
{ {
@ -264,13 +326,17 @@ public partial class LocalFileStorageService
private async Task<IEnumerable<string>> ConvertPdfToImages(string pdfLoc, string imageLoc) private async Task<IEnumerable<string>> ConvertPdfToImages(string pdfLoc, string imageLoc)
{ {
var converters = _services.GetServices<IPdf2ImageConverter>(); var converters = _services.GetServices<IPdf2ImageConverter>();
if (converters.IsNullOrEmpty()) return Enumerable.Empty<string>(); if (converters.IsNullOrEmpty())
{
return Enumerable.Empty<string>();
}
var converter = GetPdf2ImageConverter(); var converter = GetPdf2ImageConverter();
if (converter == null) if (converter == null)
{ {
return Enumerable.Empty<string>(); return Enumerable.Empty<string>();
} }
return await converter.ConvertPdfToImages(pdfLoc, imageLoc); return await converter.ConvertPdfToImages(pdfLoc, imageLoc);
} }

View file

@ -59,6 +59,8 @@ public partial class RoutingService
message.Indication = response.Indication; message.Indication = response.Indication;
message.CurrentAgentId = agent.Id; message.CurrentAgentId = agent.Id;
message.IsStreaming = response.IsStreaming; message.IsStreaming = response.IsStreaming;
message.MessageLabel = response.MessageLabel;
message.AdditionalMessageWrapper = null;
await InvokeFunction(message, dialogs, options); await InvokeFunction(message, dialogs, options);
} }
@ -74,6 +76,8 @@ public partial class RoutingService
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content); message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content);
message.CurrentAgentId = agent.Id; message.CurrentAgentId = agent.Id;
message.IsStreaming = response.IsStreaming; message.IsStreaming = response.IsStreaming;
message.MessageLabel = response.MessageLabel;
message.AdditionalMessageWrapper = null;
dialogs.Add(message); dialogs.Add(message);
Context.SetDialogs(dialogs); Context.SetDialogs(dialogs);
} }

View file

@ -65,6 +65,8 @@ public partial class RoutingService
message.StopCompletion = clonedMessage.StopCompletion; message.StopCompletion = clonedMessage.StopCompletion;
message.RichContent = clonedMessage.RichContent; message.RichContent = clonedMessage.RichContent;
message.Data = clonedMessage.Data; message.Data = clonedMessage.Data;
message.MessageLabel = clonedMessage.MessageLabel;
message.AdditionalMessageWrapper = clonedMessage.AdditionalMessageWrapper;
} }
catch (JsonException ex) catch (JsonException ex)
{ {

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Chart;
using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Files.Constants;
using BotSharp.Abstraction.Files.Enums; using BotSharp.Abstraction.Files.Enums;
using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Files.Utilities;
@ -108,6 +109,7 @@ public class ConversationController : ControllerBase
{ {
ConversationId = conversationId, ConversationId = conversationId,
MessageId = message.MessageId, MessageId = message.MessageId,
MessageLabel = message.MessageLabel,
CreatedAt = message.CreatedAt, CreatedAt = message.CreatedAt,
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Data = message.Data, Data = message.Data,
@ -123,6 +125,7 @@ public class ConversationController : ControllerBase
{ {
ConversationId = conversationId, ConversationId = conversationId,
MessageId = message.MessageId, MessageId = message.MessageId,
MessageLabel = message.MessageLabel,
CreatedAt = message.CreatedAt, CreatedAt = message.CreatedAt,
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Function = message.FunctionName, Function = message.FunctionName,
@ -368,9 +371,11 @@ public class ConversationController : ControllerBase
{ {
response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content;
response.Function = msg.FunctionName; response.Function = msg.FunctionName;
response.MessageLabel = msg.MessageLabel;
response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; response.RichContent = msg.SecondaryRichContent ?? msg.RichContent;
response.Instruction = msg.Instruction; response.Instruction = msg.Instruction;
response.Data = msg.Data; response.Data = msg.Data;
response.AdditionalMessageWrapper = ChatResponseWrapper.From(msg.AdditionalMessageWrapper, conversationId, inputMsg.MessageId);
}); });
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
@ -423,11 +428,13 @@ public class ConversationController : ControllerBase
async msg => async msg =>
{ {
response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content;
response.MessageLabel = msg.MessageLabel;
response.Function = msg.FunctionName; response.Function = msg.FunctionName;
response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; response.RichContent = msg.SecondaryRichContent ?? msg.RichContent;
response.Instruction = msg.Instruction; response.Instruction = msg.Instruction;
response.Data = msg.Data; response.Data = msg.Data;
response.States = state.GetStates(); response.States = state.GetStates();
response.AdditionalMessageWrapper = ChatResponseWrapper.From(msg.AdditionalMessageWrapper, conversationId, inputMsg.MessageId);
await OnChunkReceived(Response, response); await OnChunkReceived(Response, response);
}); });
@ -530,6 +537,35 @@ public class ConversationController : ControllerBase
} }
#endregion #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 #region Dashboard
[HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")] [HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")]
public async Task<bool> PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId) 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.Files;
global using BotSharp.Abstraction.VectorStorage.Enums; global using BotSharp.Abstraction.VectorStorage.Enums;
global using BotSharp.Abstraction.Knowledges.Models; global using BotSharp.Abstraction.Knowledges.Models;
global using BotSharp.Abstraction.Chart.Models;
global using BotSharp.OpenAPI.ViewModels.Conversations; global using BotSharp.OpenAPI.ViewModels.Conversations;
global using BotSharp.OpenAPI.ViewModels.Users; global using BotSharp.OpenAPI.ViewModels.Users;
global using BotSharp.OpenAPI.ViewModels.Agents; 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

@ -1,7 +1,46 @@
using BotSharp.Abstraction.Conversations.Dtos; using BotSharp.Abstraction.Conversations.Dtos;
using System.Text.Json.Serialization;
namespace BotSharp.OpenAPI.ViewModels.Conversations; namespace BotSharp.OpenAPI.ViewModels.Conversations;
public class ChatResponseModel : ChatResponseDto 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<ChatResponseModel>? 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()
};
}
}

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

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Messaging.Models.RichContent.Template; using BotSharp.Abstraction.Messaging.Models.RichContent.Template;
using BotSharp.Abstraction.Routing;
namespace BotSharp.Plugin.ChartHandler.Functions; namespace BotSharp.Plugin.ChartHandler.Functions;
@ -6,6 +7,7 @@ public class PlotChartFn : IFunctionCallback
{ {
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private readonly ILogger<PlotChartFn> _logger; private readonly ILogger<PlotChartFn> _logger;
private readonly ChartHandlerSettings _settings;
public string Name => "util-chart-plot_chart"; public string Name => "util-chart-plot_chart";
public string Indication => "Plotting chart"; public string Indication => "Plotting chart";
@ -13,16 +15,19 @@ public class PlotChartFn : IFunctionCallback
public PlotChartFn( public PlotChartFn(
IServiceProvider services, IServiceProvider services,
ILogger<PlotChartFn> logger) ILogger<PlotChartFn> logger,
ChartHandlerSettings settings)
{ {
_services = services; _services = services;
_logger = logger; _logger = logger;
_settings = settings;
} }
public async Task<bool> Execute(RoleDialogModel message) public async Task<bool> Execute(RoleDialogModel message)
{ {
var agentService = _services.GetRequiredService<IAgentService>(); var agentService = _services.GetRequiredService<IAgentService>();
var convService = _services.GetRequiredService<IConversationService>(); var convService = _services.GetRequiredService<IConversationService>();
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs); var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs);
@ -33,10 +38,7 @@ public class PlotChartFn : IFunctionCallback
Id = agent.Id, Id = agent.Id,
Name = agent.Name, Name = agent.Name,
Instruction = inst, Instruction = inst,
LlmConfig = new AgentLlmConfig LlmConfig = GetLlmConfig(),
{
MaxOutputTokens = 8192
},
TemplateDict = new Dictionary<string, object> TemplateDict = new Dictionary<string, object>
{ {
{ "plotting_requirement", args?.PlottingRequirement ?? string.Empty }, { "plotting_requirement", args?.PlottingRequirement ?? string.Empty },
@ -44,14 +46,21 @@ public class PlotChartFn : IFunctionCallback
} }
}; };
var response = await GetChatCompletion(innerAgent, [ var dialogs = routingCtx.GetDialogs();
new RoleDialogModel(AgentRole.User, "Please follow the instruction to generate the javascript code.") if (dialogs.IsNullOrEmpty())
{ {
CurrentAgentId = message.CurrentAgentId, dialogs = convService.GetDialogHistory();
MessageId = message.MessageId }
}
]);
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<LlmContextOut>(); var obj = response.JsonContent<LlmContextOut>();
message.Content = obj?.GreetingMessage ?? "Here is the chart you ask for:"; message.Content = obj?.GreetingMessage ?? "Here is the chart you ask for:";
message.RichContent = new RichContent<IRichMessage> message.RichContent = new RichContent<IRichMessage>
@ -63,6 +72,29 @@ public class PlotChartFn : IFunctionCallback
Language = "javascript" Language = "javascript"
} }
}; };
if (!string.IsNullOrEmpty(obj?.ReportSummary))
{
message.AdditionalMessageWrapper = new()
{
SendingInterval = 1500,
SaveToDb = true,
Messages = new List<RoleDialogModel>
{
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; message.StopCompletion = true;
return true; return true;
} }
@ -111,14 +143,29 @@ public class PlotChartFn : IFunctionCallback
var model = "gpt-5"; var model = "gpt-5";
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
var settings = _services.GetRequiredService<ChartHandlerSettings>();
provider = state.GetState("chart_plot_llm_provider") provider = state.GetState("chart_plot_llm_provider")
.IfNullOrEmptyAs(settings.ChartPlot?.LlmProvider) .IfNullOrEmptyAs(_settings.ChartPlot?.LlmProvider)
.IfNullOrEmptyAs(provider); .IfNullOrEmptyAs(provider);
model = state.GetState("chart_plot_llm_model") model = state.GetState("chart_plot_llm_model")
.IfNullOrEmptyAs(settings.ChartPlot?.LlmModel) .IfNullOrEmptyAs(_settings.ChartPlot?.LlmModel)
.IfNullOrEmptyAs(model); .IfNullOrEmptyAs(model);
return (provider, model); return (provider, model);
} }
private AgentLlmConfig GetLlmConfig()
{
var maxOutputTokens = _settings?.ChartPlot?.MaxOutputTokens ?? 8192;
var reasoningEffortLevel = _settings?.ChartPlot?.ReasoningEffortLevel ?? "minimal";
var state = _services.GetRequiredService<IConversationStateService>();
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
};
}
} }

View file

@ -8,6 +8,10 @@ public class LlmContextOut
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? GreetingMessage { get; set; } public string? GreetingMessage { get; set; }
[JsonPropertyName("report_summary")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ReportSummary { get; set; }
[JsonPropertyName("js_code")] [JsonPropertyName("js_code")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? JsCode { get; set; } public string? JsCode { get; set; }

View file

@ -7,6 +7,9 @@ public class ChartHandlerSettings
public class ChartPlotSetting public class ChartPlotSetting
{ {
public string LlmProvider { get; set; } public string? LlmProvider { get; set; }
public string LlmModel { get; set; } public string? LlmModel { get; set; }
public int? MaxOutputTokens { get; set; }
public string? ReasoningEffortLevel { get; set; }
public int? MessageLimit { get; set; }
} }

View file

@ -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. 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 ===
{{ plotting_requirement }} {{ plotting_requirement }}
***** Hard Requirements ***** ***** Hard Requirements *****
** Your output javascript code must be wrapped in one or multiple <script>...</script> blocks with everything needed inside. ** Your output javascript code must be wrapped in one or multiple <script>...</script> 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 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 need to add the MODE bar for each chart you plot. ** You must add an ECharts Toolbox bar at the top left corner of the chart.
** You must render the charts under the div html element with id {{ chart_element_id }}. ** ALWAYS add a "Full screen" button right next to the Toolbox bar.
** 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: ** 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 or on Plotlys SVG. * 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. * Use el.requestFullscreen() with fallbacks to el.webkitRequestFullscreen || el.msRequestFullscreen.
* Exit fullscreen with document.exitFullscreen() and vendor fallbacks. * 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. * 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. * 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). * 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}.
* Use Plotly.newPlot(container, data, layout, {displayModeBar:true, modeBarButtonsToAdd:[fullscreenBtn]}); * When using "chart.setOption" to define the fullscreen button, DO NOT use "graphic". Include the fullscreenBtn object in toolbox.feature with name 'myFullscreen'.
* fullscreenBtn must be a fully-formed object {name, title, icon, click}. ** 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 create any new html element.
** You must not apply any styles on any 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. ** 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: 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.", "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."
} }

View file

@ -5,6 +5,7 @@ using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.SideCar; using BotSharp.Abstraction.SideCar;
using BotSharp.Abstraction.Users.Dtos; using BotSharp.Abstraction.Users.Dtos;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace BotSharp.Plugin.ChatHub.Hooks; namespace BotSharp.Plugin.ChatHub.Hooks;
@ -94,33 +95,80 @@ public class ChatHubConversationHook : ConversationHookBase
var conv = _services.GetRequiredService<IConversationService>(); var conv = _services.GetRequiredService<IConversationService>();
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
var sender = new UserDto
{
FirstName = "AI",
LastName = "Assistant",
Role = AgentRole.Assistant
};
var data = new ChatResponseDto() var data = new ChatResponseDto()
{ {
ConversationId = conv.ConversationId, ConversationId = conv.ConversationId,
MessageId = message.MessageId, MessageId = message.MessageId,
MessageLabel = message.MessageLabel,
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Function = message.FunctionName, Function = message.FunctionName,
RichContent = message.SecondaryRichContent ?? message.RichContent, RichContent = message.SecondaryRichContent ?? message.RichContent,
Data = message.Data, Data = message.Data,
States = state.GetStates(), States = state.GetStates(),
IsStreaming = message.IsStreaming, IsStreaming = message.IsStreaming,
Sender = new() Sender = sender
{
FirstName = "AI",
LastName = "Assistant",
Role = AgentRole.Assistant
}
}; };
// Send typing-off to client // Send type-off to client
var action = new ConversationSenderActionModel var action = new ConversationSenderActionModel
{ {
ConversationId = conv.ConversationId, ConversationId = conv.ConversationId,
SenderAction = SenderActionEnum.TypingOff SenderAction = SenderActionEnum.TypingOff
}; };
await SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action); await SendEvent(ChatEvent.OnSenderActionGenerated, conv.ConversationId, action);
await SendEvent(ChatEvent.OnMessageReceivedFromAssistant, conv.ConversationId, data); 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); await base.OnResponseGenerated(message);
} }

View file

@ -46,6 +46,7 @@ public class DialogMetaDataMongoElement
public string AgentId { get; set; } = default!; public string AgentId { get; set; } = default!;
public string MessageId { get; set; } = default!; public string MessageId { get; set; } = default!;
public string MessageType { get; set; } = default!; public string MessageType { get; set; } = default!;
public string? MessageLabel { get; set; }
public string? FunctionName { get; set; } public string? FunctionName { get; set; }
public string? FunctionArgs { get; set; } public string? FunctionArgs { get; set; }
public string? ToolCallId { get; set; } public string? ToolCallId { get; set; }
@ -60,6 +61,7 @@ public class DialogMetaDataMongoElement
AgentId = meta.AgentId, AgentId = meta.AgentId,
MessageId = meta.MessageId, MessageId = meta.MessageId,
MessageType = meta.MessageType, MessageType = meta.MessageType,
MessageLabel = meta.MessageLabel,
FunctionName = meta.FunctionName, FunctionName = meta.FunctionName,
FunctionArgs = meta.FunctionArgs, FunctionArgs = meta.FunctionArgs,
ToolCallId = meta.ToolCallId, ToolCallId = meta.ToolCallId,
@ -76,6 +78,7 @@ public class DialogMetaDataMongoElement
AgentId = meta.AgentId, AgentId = meta.AgentId,
MessageId = meta.MessageId, MessageId = meta.MessageId,
MessageType = meta.MessageType, MessageType = meta.MessageType,
MessageLabel = meta.MessageLabel,
FunctionName = meta.FunctionName, FunctionName = meta.FunctionName,
FunctionArgs = meta.FunctionArgs, FunctionArgs = meta.FunctionArgs,
ToolCallId = meta.ToolCallId, ToolCallId = meta.ToolCallId,

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework> <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<IConversationHook, SqlDriverConversationHook>();
services.AddScoped<IAgentUtilityHook, SqlUtilityHook>(); services.AddScoped<IAgentUtilityHook, SqlUtilityHook>();
services.AddScoped<ICrontabHook, SqlDriverCrontabHook>(); 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;
global using BotSharp.Abstraction.Instructs.Models; global using BotSharp.Abstraction.Instructs.Models;
global using BotSharp.Abstraction.Routing; 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.Models;
global using BotSharp.Plugin.SqlDriver.Hooks; global using BotSharp.Plugin.SqlDriver.Hooks;

View file

@ -7,8 +7,9 @@ namespace BotSharp.Plugin.SqlDriver.UtilFunctions;
public class SqlSelect : IFunctionCallback public class SqlSelect : IFunctionCallback
{ {
public string Name => "util-db-sql_select";
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
public string Name => "util-db-sql_select";
public string Indication => "Extracting data";
public SqlSelect(IServiceProvider services) public SqlSelect(IServiceProvider services)
{ {

View file

@ -320,7 +320,9 @@
"ChartHandler": { "ChartHandler": {
"ChartPlot": { "ChartPlot": {
"LlmProvider": "openai", "LlmProvider": "openai",
"LlmModel": "gpt-5" "LlmModel": "gpt-5",
"MaxOutputTokens": 8192,
"ReasoningEffortLevel": "minimal"
} }
}, },
@ -579,6 +581,7 @@
"BotSharp.Plugin.EmailHandler", "BotSharp.Plugin.EmailHandler",
"BotSharp.Plugin.AudioHandler", "BotSharp.Plugin.AudioHandler",
"BotSharp.Plugin.ChartHandler", "BotSharp.Plugin.ChartHandler",
"BotSharp.Plugin.SqlDriver",
"BotSharp.Plugin.TencentCos" "BotSharp.Plugin.TencentCos"
] ]
} }