refine code and settings

This commit is contained in:
Jicheng Lu 2025-09-15 13:28:57 -05:00
parent f12e395e46
commit 349e58eee8
19 changed files with 501 additions and 150 deletions

View file

@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="EntityFramework" Version="6.4.4" />
<PackageVersion Include="Google_GenerativeAI" Version="2.5.8" />
<PackageVersion Include="Google_GenerativeAI.Live" Version="2.5.8" />
<PackageVersion Include="Google_GenerativeAI" Version="3.2.0" />
<PackageVersion Include="Google_GenerativeAI.Live" Version="3.2.0" />
<PackageVersion Include="LLMSharp.Google.Palm" Version="1.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
@ -45,7 +45,7 @@
<PackageVersion Include="Whisper.net" Version="1.8.1" />
<PackageVersion Include="Whisper.net.Runtime" Version="1.8.1" />
<PackageVersion Include="NCrontab" Version="3.3.3" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.2.0-beta.5" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.3.0-beta.2" />
<PackageVersion Include="OpenAI" Version="2.4.0" />
<PackageVersion Include="MailKit" Version="4.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />

View file

@ -46,9 +46,6 @@ public class ChatResponseDto : InstructResult
[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;
}

View file

@ -14,6 +14,7 @@ public class ConversationSetting
public bool EnableTranslationMemory { get; set; }
public CleanConversationSetting CleanSetting { get; set; } = new();
public RateLimitSetting RateLimit { get; set; } = new();
public FileSelectSetting? FileSelect { get; set; }
}
public class CleanConversationSetting
@ -24,3 +25,8 @@ public class CleanConversationSetting
public int BufferHours { get; set; }
public IEnumerable<string> ExcludeAgentIds { get; set; } = new List<string>();
}
public class FileSelectSetting : LlmConfigBase
{
public int? MessageLimit { get; set; }
}

View file

@ -1,27 +1,7 @@
namespace BotSharp.Abstraction.Files.Models;
public class SelectFileOptions
public class SelectFileOptions : LlmConfigBase
{
/// <summary>
/// Llm provider
/// </summary>
public string? Provider { get; set; }
/// <summary>
/// Llm model
/// </summary>
public string? Model { get; set; }
/// <summary>
/// Llm maximum output tokens
/// </summary>
public int? MaxOutputTokens { get; set; }
/// <summary>
/// Llm reasoning effort level
/// </summary>
public string? ReasoningEffortLevel { get; set; }
/// <summary>
/// Agent id
/// </summary>

View file

@ -0,0 +1,24 @@
namespace BotSharp.Abstraction.Models;
public class LlmConfigBase
{
/// <summary>
/// Llm provider
/// </summary>
public string? LlmProvider { get; set; }
/// <summary>
/// Llm model
/// </summary>
public string? LlmModel { get; set; }
/// <summary>
/// Llm maximum output tokens
/// </summary>
public int? MaxOutputTokens { get; set; }
/// <summary>
/// Llm reasoning effort level
/// </summary>
public string? ReasoningEffortLevel { get; set; }
}

View file

@ -91,8 +91,8 @@ public partial class FileInstructService
var promptMessages = innerDialogs.Select(x =>
{
var text = $"[Role] '{x.Role}': {x.RichContent?.Message?.Text ?? x.Payload ?? x.Content}";
var fileDescs = x.Files?.Select((f, fidx) => $"message_id: '{x.MessageId}', file_index: '{f.FileIndex}', " +
$"file_name: '{f.FileFullName}', content_type: '{f.ContentType}', author: '{(x.Role == AgentRole.User ? FileSourceType.User : FileSourceType.Bot)}'");
var fileDescs = x.Files?.Select((f, fidx) => $"- message_id: '{x.MessageId}', file_index: '{f.FileIndex}', " +
$"content_type: '{f.ContentType}', author: '{(x.Role == AgentRole.User ? FileSourceType.User : FileSourceType.Bot)}'");
var desc = string.Empty;
if (!fileDescs.IsNullOrEmpty())
@ -138,8 +138,8 @@ public partial class FileInstructService
// Get ai response
var provider = options.Provider ?? "openai";
var model = options?.Model ?? "gpt-5-mini";
var provider = options.LlmProvider ?? "openai";
var model = options?.LlmModel ?? "gpt-5-mini";
var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, model: model);
var response = await completion.GetChatCompletions(agent, innerDialogs);

View file

@ -12,6 +12,7 @@
"is_inherit": false,
"provider": "openai",
"model": "gpt-5-mini",
"max_recursion_depth": 3
"max_recursion_depth": 3,
"reasoning_effort_level": "minimal"
}
}

View file

@ -9,9 +9,10 @@ You are a File Selector designed to identify and return the most relevant files
[REQUIREMENTS] (Follow All Precisely)
- Infer what files the user is asking for based on the chat context. ALWAYS select files from [MESSAGES AND FILES].
- Begin from the most recent message and go backward. Newer files are generally more relevant.
- You must check the content of the message files to determine if they are relevant.
- You must select files in descending order of relevance based on the chat context. Do not include irrelevant files.
- You must not include duplicate files (even across different messages).
- You must ensure the information (e.g., message_id, file_source, file_index, file_name) of each selected file is from the same message.
- You must ensure the information (e.g., message_id, file_source, file_index) of each selected file is from the same message.
- You may select more than one file if required to satisfy the user's request.
- If you think no files are relevant, output an empty list [] for "selected_files".
@ -32,7 +33,6 @@ You are a File Selector designed to identify and return the most relevant files
{
"message_id": "the message_id of the file",
"file_source": "the file_source of the file: 'user' | 'bot'",
"file_index": "the file_index of the selected file",
"file_name": "the file_name of the selected file"
"file_index": "the file_index of the selected file"
}
}

View file

@ -1,3 +1,4 @@
#pragma warning disable OPENAI001
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Hooks;
@ -347,17 +348,7 @@ public class ChatCompletionProvider : IChatCompletion
renderedInstructions = [];
var messages = new List<ChatMessage>();
var temperature = float.Parse(state.GetState("temperature", "0.0"));
var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens)
? tokens
: agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN;
var options = new ChatCompletionOptions()
{
Temperature = temperature,
MaxOutputTokenCount = maxTokens
};
var options = InitChatCompletionOption(agent);
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
@ -367,6 +358,22 @@ public class ChatCompletionProvider : IChatCompletion
messages.Add(new SystemChatMessage(instruction));
}
// Render functions
if (options.WebSearchOptions == null)
{
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
var property = agentService.RenderFunctionProperty(agent, function);
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,
functionDescription: function.Description,
functionParameters: BinaryData.FromObjectAsJson(property)));
}
}
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
@ -397,6 +404,12 @@ public class ChatCompletionProvider : IChatCompletion
filteredMessages = filteredMessages.Where((_, idx) => idx >= firstUserMsgIdx).ToList();
}
var imageDetailLevel = ChatImageDetailLevel.Auto;
if (allowMultiModal)
{
imageDetailLevel = ParseChatImageDetailLevel(state.GetState("chat_image_detail_level"));
}
foreach (var message in filteredMessages)
{
if (message.Role == AgentRole.Function)
@ -416,34 +429,21 @@ public class ChatCompletionProvider : IChatCompletion
if (allowMultiModal && !message.Files.IsNullOrEmpty())
{
foreach (var file in message.Files)
{
if (!string.IsNullOrEmpty(file.FileData))
{
var (contentType, binary) = FileUtility.GetFileInfoFromData(file.FileData);
var contentPart = ChatMessageContentPart.CreateImagePart(binary, contentType.IfNullOrEmptyAs(file.ContentType), ChatImageDetailLevel.Auto);
contentParts.Add(contentPart);
}
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
{
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
var binary = fileStorage.GetFileBytes(file.FileStorageUrl);
var contentPart = ChatMessageContentPart.CreateImagePart(binary, contentType.IfNullOrEmptyAs(file.ContentType), ChatImageDetailLevel.Auto);
contentParts.Add(contentPart);
}
else if (!string.IsNullOrEmpty(file.FileUrl))
{
var uri = new Uri(file.FileUrl);
var contentPart = ChatMessageContentPart.CreateImagePart(uri, ChatImageDetailLevel.Auto);
contentParts.Add(contentPart);
}
}
CollectMessageContentParts(contentParts, message.Files, imageDetailLevel);
}
messages.Add(new UserChatMessage(contentParts) { ParticipantName = message.FunctionName });
}
else if (message.Role == AgentRole.Assistant)
{
messages.Add(new AssistantChatMessage(message.Content));
var text = message.Content;
var textPart = ChatMessageContentPart.CreateTextPart(text);
var contentParts = new List<ChatMessageContentPart> { textPart };
if (allowMultiModal && !message.Files.IsNullOrEmpty())
{
CollectMessageContentParts(contentParts, message.Files, imageDetailLevel);
}
messages.Add(new AssistantChatMessage(contentParts));
}
}
@ -451,6 +451,34 @@ public class ChatCompletionProvider : IChatCompletion
return (prompt, messages, options);
}
private void CollectMessageContentParts(List<ChatMessageContentPart> contentParts, List<BotSharpFile> files, ChatImageDetailLevel imageDetailLevel)
{
foreach (var file in files)
{
if (!string.IsNullOrEmpty(file.FileData))
{
var (contentType, binary) = FileUtility.GetFileInfoFromData(file.FileData);
var contentPart = ChatMessageContentPart.CreateImagePart(binary, contentType.IfNullOrEmptyAs(file.ContentType), imageDetailLevel);
contentParts.Add(contentPart);
}
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
{
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var binary = fileStorage.GetFileBytes(file.FileStorageUrl);
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
var contentPart = ChatMessageContentPart.CreateImagePart(binary, contentType.IfNullOrEmptyAs(file.ContentType), imageDetailLevel);
contentParts.Add(contentPart);
}
else if (!string.IsNullOrEmpty(file.FileUrl))
{
var uri = new Uri(file.FileUrl);
var contentPart = ChatMessageContentPart.CreateImagePart(uri, imageDetailLevel);
contentParts.Add(contentPart);
}
}
}
private string GetPrompt(IEnumerable<ChatMessage> messages, ChatCompletionOptions options)
{
var prompt = string.Empty;
@ -518,6 +546,95 @@ public class ChatCompletionProvider : IChatCompletion
return prompt;
}
private ChatCompletionOptions InitChatCompletionOption(Agent agent)
{
var state = _services.GetRequiredService<IConversationStateService>();
var settingsService = _services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(Provider, _model);
// Reasoning effort
ChatReasoningEffortLevel? reasoningEffortLevel = null;
float? temperature = float.Parse(state.GetState("temperature", "0.0"));
if (settings?.Reasoning != null)
{
temperature = settings.Reasoning.Temperature;
var level = state.GetState("reasoning_effort_level")
.IfNullOrEmptyAs(agent?.LlmConfig?.ReasoningEffortLevel)
.IfNullOrEmptyAs(settings?.Reasoning?.EffortLevel);
reasoningEffortLevel = ParseReasoningEffortLevel(level);
}
// Web search
ChatWebSearchOptions? webSearchOptions = null;
if (settings?.WebSearch != null)
{
temperature = null;
reasoningEffortLevel = null;
webSearchOptions = new();
}
var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens)
? tokens
: agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN;
return new ChatCompletionOptions()
{
Temperature = temperature,
MaxOutputTokenCount = maxTokens,
ReasoningEffortLevel = reasoningEffortLevel,
WebSearchOptions = webSearchOptions
};
}
private ChatReasoningEffortLevel? ParseReasoningEffortLevel(string? level)
{
if (string.IsNullOrWhiteSpace(level))
{
return null;
}
var effortLevel = new ChatReasoningEffortLevel("minimal");
switch (level.ToLower())
{
case "low":
effortLevel = ChatReasoningEffortLevel.Low;
break;
case "medium":
effortLevel = ChatReasoningEffortLevel.Medium;
break;
case "high":
effortLevel = ChatReasoningEffortLevel.High;
break;
default:
break;
}
return effortLevel;
}
private ChatImageDetailLevel ParseChatImageDetailLevel(string? level)
{
if (string.IsNullOrWhiteSpace(level))
{
return ChatImageDetailLevel.Auto;
}
var imageLevel = ChatImageDetailLevel.Auto;
switch (level.ToLower())
{
case "low":
imageLevel = ChatImageDetailLevel.Low;
break;
case "high":
imageLevel = ChatImageDetailLevel.High;
break;
default:
break;
}
return imageLevel;
}
public void SetModelName(string model)
{
_model = model;

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Models;
namespace BotSharp.Plugin.ChartHandler.Settings;
public class ChartHandlerSettings
@ -5,11 +7,7 @@ public class ChartHandlerSettings
public ChartPlotSetting ChartPlot { get; set; }
}
public class ChartPlotSetting
public class ChartPlotSetting : LlmConfigBase
{
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; }
}

View file

@ -1,5 +1,8 @@
#pragma warning disable OPENAI001
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Files.Models;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Hooks;
using BotSharp.Abstraction.MessageHub.Models;
using BotSharp.Core.Infrastructures.Streams;
@ -313,23 +316,13 @@ public class ChatCompletionProvider : IChatCompletion
{
var agentService = _services.GetRequiredService<IAgentService>();
var state = _services.GetRequiredService<IConversationStateService>();
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var settingsService = _services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(Provider, _model);
var allowMultiModal = settings != null && settings.MultiModal;
renderedInstructions = [];
var messages = new List<ChatMessage>();
var temperature = float.Parse(state.GetState("temperature", "0.0"));
var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens)
? tokens
: agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN;
var options = new ChatCompletionOptions()
{
Temperature = temperature,
MaxOutputTokenCount = maxTokens
};
var options = InitChatCompletionOption(agent);
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
@ -339,16 +332,20 @@ public class ChatCompletionProvider : IChatCompletion
messages.Add(new SystemChatMessage(instruction));
}
foreach (var function in functions)
// Render functions
if (options.WebSearchOptions == null)
{
if (!agentService.RenderFunction(agent, function)) continue;
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
var property = agentService.RenderFunctionProperty(agent, function);
var property = agentService.RenderFunctionProperty(agent, function);
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,
functionDescription: function.Description,
functionParameters: BinaryData.FromObjectAsJson(property)));
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,
functionDescription: function.Description,
functionParameters: BinaryData.FromObjectAsJson(property)));
}
}
if (!string.IsNullOrEmpty(agent.Knowledges))
@ -363,6 +360,12 @@ public class ChatCompletionProvider : IChatCompletion
filteredMessages = filteredMessages.Where((_, idx) => idx >= firstUserMsgIdx).ToList();
}
var imageDetailLevel = ChatImageDetailLevel.Auto;
if (allowMultiModal)
{
imageDetailLevel = ParseChatImageDetailLevel(state.GetState("chat_image_detail_level"));
}
foreach (var message in filteredMessages)
{
if (message.Role == AgentRole.Function)
@ -377,11 +380,26 @@ public class ChatCompletionProvider : IChatCompletion
else if (message.Role == AgentRole.User)
{
var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content;
messages.Add(new UserChatMessage(text));
var textPart = ChatMessageContentPart.CreateTextPart(text);
var contentParts = new List<ChatMessageContentPart> { textPart };
if (allowMultiModal && !message.Files.IsNullOrEmpty())
{
CollectMessageContentParts(contentParts, message.Files, imageDetailLevel);
}
messages.Add(new UserChatMessage(contentParts) { ParticipantName = message.FunctionName });
}
else if (message.Role == AgentRole.Assistant)
{
messages.Add(new AssistantChatMessage(message.Content));
var text = message.Content;
var textPart = ChatMessageContentPart.CreateTextPart(text);
var contentParts = new List<ChatMessageContentPart> { textPart };
if (allowMultiModal && !message.Files.IsNullOrEmpty())
{
CollectMessageContentParts(contentParts, message.Files, imageDetailLevel);
}
messages.Add(new AssistantChatMessage(contentParts));
}
}
@ -389,6 +407,32 @@ public class ChatCompletionProvider : IChatCompletion
return (prompt, messages, options);
}
private void CollectMessageContentParts(List<ChatMessageContentPart> contentParts, List<BotSharpFile> files, ChatImageDetailLevel imageDetailLevel)
{
foreach (var file in files)
{
if (!string.IsNullOrEmpty(file.FileData))
{
var (contentType, binary) = FileUtility.GetFileInfoFromData(file.FileData);
var contentPart = ChatMessageContentPart.CreateImagePart(binary, contentType.IfNullOrEmptyAs(file.ContentType), imageDetailLevel);
contentParts.Add(contentPart);
}
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
{
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var binary = fileStorage.GetFileBytes(file.FileStorageUrl);
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
var contentPart = ChatMessageContentPart.CreateImagePart(binary, contentType.IfNullOrEmptyAs(file.ContentType), imageDetailLevel);
contentParts.Add(contentPart);
}
else if (!string.IsNullOrEmpty(file.FileUrl))
{
var uri = new Uri(file.FileUrl);
var contentPart = ChatMessageContentPart.CreateImagePart(uri, imageDetailLevel);
contentParts.Add(contentPart);
}
}
}
private string GetPrompt(IEnumerable<ChatMessage> messages, ChatCompletionOptions options)
{
@ -456,4 +500,93 @@ public class ChatCompletionProvider : IChatCompletion
return prompt;
}
private ChatCompletionOptions InitChatCompletionOption(Agent agent)
{
var state = _services.GetRequiredService<IConversationStateService>();
var settingsService = _services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(Provider, _model);
// Reasoning effort
ChatReasoningEffortLevel? reasoningEffortLevel = null;
float? temperature = float.Parse(state.GetState("temperature", "0.0"));
if (settings?.Reasoning != null)
{
temperature = settings.Reasoning.Temperature;
var level = state.GetState("reasoning_effort_level")
.IfNullOrEmptyAs(agent?.LlmConfig?.ReasoningEffortLevel)
.IfNullOrEmptyAs(settings?.Reasoning?.EffortLevel);
reasoningEffortLevel = ParseReasoningEffortLevel(level);
}
// Web search
ChatWebSearchOptions? webSearchOptions = null;
if (settings?.WebSearch != null)
{
temperature = null;
reasoningEffortLevel = null;
webSearchOptions = new();
}
var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens)
? tokens
: agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN;
return new ChatCompletionOptions()
{
Temperature = temperature,
MaxOutputTokenCount = maxTokens,
ReasoningEffortLevel = reasoningEffortLevel,
WebSearchOptions = webSearchOptions
};
}
private ChatReasoningEffortLevel? ParseReasoningEffortLevel(string? level)
{
if (string.IsNullOrWhiteSpace(level))
{
return null;
}
var effortLevel = new ChatReasoningEffortLevel("minimal");
switch (level.ToLower())
{
case "low":
effortLevel = ChatReasoningEffortLevel.Low;
break;
case "medium":
effortLevel = ChatReasoningEffortLevel.Medium;
break;
case "high":
effortLevel = ChatReasoningEffortLevel.High;
break;
default:
break;
}
return effortLevel;
}
private ChatImageDetailLevel ParseChatImageDetailLevel(string? level)
{
if (string.IsNullOrWhiteSpace(level))
{
return ChatImageDetailLevel.Auto;
}
var imageLevel = ChatImageDetailLevel.Auto;
switch (level.ToLower())
{
case "low":
imageLevel = ChatImageDetailLevel.Low;
break;
case "high":
imageLevel = ChatImageDetailLevel.High;
break;
default:
break;
}
return imageLevel;
}
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Settings;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
@ -67,18 +68,18 @@ public class HandleEmailSenderFn : IFunctionCallback
private async Task<IEnumerable<MessageFileModel>> GetConversationFiles()
{
var convService = _services.GetRequiredService<IConversationService>();
var conversationId = convService.ConversationId;
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var selecteds = await fileInstruct.SelectMessageFiles(conversationId, new SelectFileOptions
var convSettings = _services.GetRequiredService<ConversationSetting>();
var selecteds = await fileInstruct.SelectMessageFiles(convService.ConversationId, new SelectFileOptions
{
IsIncludeBotFiles = true,
IsAttachFiles = true,
MessageLimit = 50,
Provider = "openai",
Model = "gpt-5-mini",
MaxOutputTokens = 8192,
ReasoningEffortLevel = "low"
MessageLimit = convSettings?.FileSelect?.MessageLimit,
LlmProvider = convSettings?.FileSelect?.LlmProvider,
LlmModel = convSettings?.FileSelect?.LlmModel,
MaxOutputTokens = convSettings?.FileSelect?.MaxOutputTokens,
ReasoningEffortLevel = convSettings?.FileSelect?.ReasoningEffortLevel
});
return selecteds;
}

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Conversations.Settings;
namespace BotSharp.Plugin.FileHandler.Functions;
public class EditImageFn : IFunctionCallback
@ -9,7 +11,7 @@ public class EditImageFn : IFunctionCallback
private readonly ILogger<EditImageFn> _logger;
private readonly FileHandlerSettings _settings;
private string _agentId;
private Agent _agent;
private string _conversationId;
private string _messageId;
@ -27,7 +29,7 @@ public class EditImageFn : IFunctionCallback
{
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs);
var descrpition = args?.UserRequest ?? string.Empty;
Init(message);
await Init(message);
SetImageOptions();
var image = await SelectImage(descrpition);
@ -37,10 +39,12 @@ public class EditImageFn : IFunctionCallback
return true;
}
private void Init(RoleDialogModel message)
private async Task Init(RoleDialogModel message)
{
var agentService = _services.GetRequiredService<IAgentService>();
var convService = _services.GetRequiredService<IConversationService>();
_agentId = message.CurrentAgentId;
_agent = await agentService.GetAgent(message.CurrentAgentId);
_conversationId = convService.ConversationId;
_messageId = message.MessageId;
}
@ -55,17 +59,19 @@ public class EditImageFn : IFunctionCallback
private async Task<MessageFileModel?> SelectImage(string? description)
{
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var convSettings = _services.GetRequiredService<ConversationSetting>();
var selecteds = await fileInstruct.SelectMessageFiles(_conversationId, new SelectFileOptions
{
Description = description,
IsIncludeBotFiles = true,
IsAttachFiles = true,
ContentTypes = [MediaTypeNames.Image.Png, MediaTypeNames.Image.Jpeg],
MessageLimit = 50,
Provider = "openai",
Model = "gpt-5-mini",
MaxOutputTokens = 8192,
ReasoningEffortLevel = "low"
MessageLimit = convSettings?.FileSelect?.MessageLimit,
LlmProvider = convSettings?.FileSelect?.LlmProvider,
LlmModel = convSettings?.FileSelect?.LlmModel,
MaxOutputTokens = convSettings?.FileSelect?.MaxOutputTokens,
ReasoningEffortLevel = convSettings?.FileSelect?.ReasoningEffortLevel
});
return selecteds?.FirstOrDefault();
}
@ -85,8 +91,8 @@ public class EditImageFn : IFunctionCallback
var dialog = RoleDialogModel.From(message, AgentRole.User, text);
var agent = new Agent
{
Id = BuiltInAgentId.UtilityAssistant,
Name = "Utility Assistant"
Id = _agent?.Id ?? BuiltInAgentId.UtilityAssistant,
Name = _agent?.Name ?? "Utility Assistant"
};
var fileStorage = _services.GetRequiredService<IFileStorageService>();
@ -98,14 +104,15 @@ public class EditImageFn : IFunctionCallback
stream.Position = 0;
var response = await completion.GetImageEdits(agent, dialog, stream, image.FileFullName);
stream.Close();
SaveGeneratedImage(response?.GeneratedImages?.FirstOrDefault());
var savedFiles = SaveGeneratedImage(response?.GeneratedImages?.FirstOrDefault());
if (!string.IsNullOrWhiteSpace(response?.Content))
{
return response.Content;
}
return await GetImageEditGreetingResponse(description);
return await GetImageEditResponse(description, defaultContent: null);
}
catch (Exception ex)
{
@ -115,19 +122,28 @@ public class EditImageFn : IFunctionCallback
}
}
private async Task<string> GetImageEditGreetingResponse(string description)
private async Task<string> GetImageEditResponse(string description, string? defaultContent)
{
if (defaultContent != null)
{
return defaultContent;
}
var llmConfig = _agent.LlmConfig;
var agent = new Agent
{
Id = BuiltInAgentId.UtilityAssistant,
Name = "Utility Assistant"
Id = _agent?.Id ?? BuiltInAgentId.UtilityAssistant,
Name = _agent?.Name ?? "Utility Assistant",
LlmConfig = new AgentLlmConfig
{
Provider = llmConfig?.Provider ?? "openai",
Model = llmConfig?.Model ?? "gpt-4o-mini",
MaxOutputTokens = llmConfig?.MaxOutputTokens,
ReasoningEffortLevel = llmConfig?.ReasoningEffortLevel
}
};
var text = $"Please generate a user-friendly response from the following description to inform user that you have completed the required image: {description}";
var completion = CompletionProvider.GetChatCompletion(_services, provider: "openai", model: "gpt-4o-mini");
var response = await completion.GetChatCompletions(agent, [new RoleDialogModel(AgentRole.User, text)]);
return response?.Content ?? "Your image is successfully edited.";
return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description);
}
private (string, string) GetLlmProviderModel()
@ -158,9 +174,12 @@ public class EditImageFn : IFunctionCallback
return (provider, model);
}
private void SaveGeneratedImage(ImageGeneration? image)
private IEnumerable<string> SaveGeneratedImage(ImageGeneration? image)
{
if (image == null) return;
if (image == null)
{
return [];
}
var files = new List<FileDataModel>()
{
@ -173,6 +192,7 @@ public class EditImageFn : IFunctionCallback
var fileStorage = _services.GetRequiredService<IFileStorageService>();
fileStorage.SaveMessageFiles(_conversationId, _messageId, FileSourceType.Bot, files);
return files.Select(x => x.FileName);
}
private async Task<BinaryData> ConvertImageToPngWithRgba(BinaryData binaryFile)

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Agents.Models;
namespace BotSharp.Plugin.FileHandler.Functions;
public class GenerateImageFn : IFunctionCallback
@ -7,6 +9,8 @@ public class GenerateImageFn : IFunctionCallback
private readonly IServiceProvider _services;
private readonly ILogger<GenerateImageFn> _logger;
private Agent _agent;
private string _conversationId;
private string _messageId;
@ -25,21 +29,9 @@ public class GenerateImageFn : IFunctionCallback
SetImageOptions();
var agentService = _services.GetRequiredService<IAgentService>();
_agent = await agentService.GetAgent(message.CurrentAgentId);
Agent? fromAgent = null;
if (!string.IsNullOrEmpty(message.CurrentAgentId))
{
fromAgent = await agentService.GetAgent(message.CurrentAgentId);
}
var agent = new Agent
{
Id = fromAgent?.Id ?? BuiltInAgentId.UtilityAssistant,
Name = fromAgent?.Name ?? "Utility Assistant",
Instruction = args?.ImageDescription
};
var response = await GetImageGeneration(agent, message, args?.ImageDescription);
var response = await GetImageGeneration(message, args?.ImageDescription);
message.Content = response;
message.StopCompletion = true;
return true;
@ -60,17 +52,30 @@ public class GenerateImageFn : IFunctionCallback
state.SetState("image_response_format", "bytes");
}
private async Task<string> GetImageGeneration(Agent agent, RoleDialogModel message, string? description)
private async Task<string> GetImageGeneration(RoleDialogModel message, string? description)
{
try
{
var agent = new Agent
{
Id = _agent?.Id ?? BuiltInAgentId.UtilityAssistant,
Name = _agent?.Name ?? "Utility Assistant",
Instruction = description
};
var (provider, model) = GetLlmProviderModel();
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider, model: model);
var text = !string.IsNullOrWhiteSpace(description) ? description : message.Content;
var dialog = RoleDialogModel.From(message, AgentRole.User, text);
var result = await completion.GetImageGeneration(agent, dialog);
SaveGeneratedImages(result?.GeneratedImages);
return !string.IsNullOrWhiteSpace(result?.Content) ? result.Content : "Here is the image you asked for";
var savedFiles = SaveGeneratedImages(result?.GeneratedImages);
if (!string.IsNullOrWhiteSpace(result?.Content))
{
return result.Content;
}
return await GetImageGenerationResponse(description, defaultContent: null);
}
catch (Exception ex)
{
@ -80,6 +85,30 @@ public class GenerateImageFn : IFunctionCallback
}
}
private async Task<string> GetImageGenerationResponse(string description, string? defaultContent)
{
if (defaultContent != null)
{
return defaultContent;
}
var llmConfig = _agent.LlmConfig;
var agent = new Agent
{
Id = _agent?.Id ?? BuiltInAgentId.UtilityAssistant,
Name = _agent?.Name ?? "Utility Assistant",
LlmConfig = new AgentLlmConfig
{
Provider = llmConfig?.Provider ?? "openai",
Model = llmConfig?.Model ?? "gpt-4o-mini",
MaxOutputTokens = llmConfig?.MaxOutputTokens,
ReasoningEffortLevel = llmConfig?.ReasoningEffortLevel
}
};
return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description);
}
private (string, string) GetLlmProviderModel()
{
var state = _services.GetRequiredService<IConversationStateService>();
@ -108,9 +137,12 @@ public class GenerateImageFn : IFunctionCallback
return (provider, model);
}
private void SaveGeneratedImages(List<ImageGeneration>? images)
private IEnumerable<string> SaveGeneratedImages(List<ImageGeneration>? images)
{
if (images.IsNullOrEmpty()) return;
if (images.IsNullOrEmpty())
{
return [];
}
var files = images.Where(x => !string.IsNullOrEmpty(x?.ImageData)).Select(x => new FileDataModel
{
@ -120,5 +152,6 @@ public class GenerateImageFn : IFunctionCallback
var fileStorage = _services.GetRequiredService<IFileStorageService>();
fileStorage.SaveMessageFiles(_conversationId, _messageId, FileSourceType.Bot, files);
return files.Select(x => x.FileName);
}
}

View file

@ -0,0 +1,33 @@
using BotSharp.Abstraction.Agents.Models;
namespace BotSharp.Plugin.FileHandler.Helpers;
internal static class AiResponseHelper
{
internal static string GetDefaultResponse(IEnumerable<string> files)
{
if (files.IsNullOrEmpty())
{
return $"No image is generated.";
}
if (files.Count() > 1)
{
return $"Here are the images you asked for: {string.Join(", ", files)}";
}
return $"Here is the image you asked for: {string.Join(", ", files)}";
}
internal static async Task<string> GetImageGenerationResponse(IServiceProvider services, Agent agent, string description)
{
var text = $"Please generate a user-friendly response from the following description to " +
$"inform user that you have completed the required image: {description}";
var provider = agent?.LlmConfig?.Provider ?? "openai";
var model = agent?.LlmConfig?.Model ?? "gpt-4o-mini";
var completion = CompletionProvider.GetChatCompletion(services, provider: provider, model: model);
var response = await completion.GetChatCompletions(agent, [new RoleDialogModel(AgentRole.User, text)]);
return response.Content;
}
}

View file

@ -30,4 +30,5 @@ global using BotSharp.Abstraction.Options;
global using BotSharp.Core.Infrastructures;
global using BotSharp.Plugin.FileHandler.Enums;
global using BotSharp.Plugin.FileHandler.Settings;
global using BotSharp.Plugin.FileHandler.LlmContexts;
global using BotSharp.Plugin.FileHandler.LlmContexts;
global using BotSharp.Plugin.FileHandler.Helpers;

View file

@ -4,7 +4,6 @@ using BotSharp.Abstraction.MessageHub.Models;
using BotSharp.Core.Infrastructures.Streams;
using BotSharp.Core.MessageHub;
using OpenAI.Chat;
using Pipelines.Sockets.Unofficial.Arenas;
namespace BotSharp.Plugin.OpenAI.Providers.Chat;
@ -420,8 +419,6 @@ public class ChatCompletionProvider : IChatCompletion
private void CollectMessageContentParts(List<ChatMessageContentPart> contentParts, List<BotSharpFile> files, ChatImageDetailLevel imageDetailLevel)
{
var fileStorage = _services.GetRequiredService<IFileStorageService>();
foreach (var file in files)
{
if (!string.IsNullOrEmpty(file.FileData))
@ -432,8 +429,9 @@ public class ChatCompletionProvider : IChatCompletion
}
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
{
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var binary = fileStorage.GetFileBytes(file.FileStorageUrl);
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
var contentPart = ChatMessageContentPart.CreateImagePart(binary, contentType.IfNullOrEmptyAs(file.ContentType), imageDetailLevel);
contentParts.Add(contentPart);
}

View file

@ -62,6 +62,7 @@ public partial class TencentCosService
var fileName = Path.GetFileNameWithoutExtension(file);
var fileExtension = Path.GetExtension(file).Substring(1);
var fileIndex = subDir.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty;
var model = new MessageFileModel()
{
MessageId = messageId,
@ -71,7 +72,8 @@ public partial class TencentCosService
FileName = fileName,
FileExtension = fileExtension,
ContentType = contentType,
FileSource = source
FileSource = source,
FileIndex = fileIndex
};
files.Add(model);
}

View file

@ -299,6 +299,13 @@
"MaxConversationPerDay": 100,
"MaxInputLengthPerRequest": 256,
"MinTimeSecondsBetweenMessages": 2
},
"FileSelect": {
"LlmProvider": "openai",
"LlmModel": "gpt-5-mini",
"MaxOutputTokens": 8192,
"ReasoningEffortLevel": "low",
"MessageLimit": 50
}
},