refine multi modal instruct

This commit is contained in:
Jicheng Lu 2025-09-12 11:30:41 -05:00
parent 517d46a5a7
commit 98291a5e49
22 changed files with 227 additions and 203 deletions

View file

@ -132,12 +132,6 @@ 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()
{ {
@ -184,26 +178,7 @@ 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

@ -16,6 +16,8 @@ public interface IImageConverter
/// Convert an image to PNG with RGBA /// Convert an image to PNG with RGBA
/// </summary> /// </summary>
/// <param name="binary"></param> /// <param name="binary"></param>
/// <param name="options"></param>
/// <returns></returns> /// <returns></returns>
Task<BinaryData> ConvertImageToRgbaPng(BinaryData binary) => throw new NotImplementedException(); /// <exception cref="NotImplementedException"></exception>
Task<BinaryData> ConvertImage(BinaryData binary, ImageConvertOptions? options = null) => throw new NotImplementedException();
} }

View file

@ -5,8 +5,9 @@ namespace BotSharp.Abstraction.Files;
public class FileCoreSettings public class FileCoreSettings
{ {
public string Storage { get; set; } = FileStorageEnum.LocalFileStorage; public string Storage { get; set; } = FileStorageEnum.LocalFileStorage;
public SettingBase Pdf2TextConverter { get; set; } public SettingBase? Pdf2TextConverter { get; set; }
public SettingBase Pdf2ImageConverter { get; set; } public SettingBase? Pdf2ImageConverter { get; set; }
public SettingBase? ImageConverter { get; set; }
} }
public class SettingBase public class SettingBase

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Files.Models;
public class ImageConvertOptions
{
public string ImageType { get; set; } = "png";
public string ColorType { get; set; } = "rgba";
}

View file

@ -33,7 +33,7 @@ public class InstructOptions
public Dictionary<string, object> Data { get; set; } = new(); public Dictionary<string, object> Data { get; set; } = new();
/// <summary> /// <summary>
/// Image converter provider /// Image convert provider
/// </summary> /// </summary>
public string? ImageConverterProvider { get; set; } public string? ImageConvertProvider { get; set; }
} }

View file

@ -31,23 +31,12 @@ public class ConversationStorage : IConversationStorage
foreach ( var dialog in dialogs) foreach ( var dialog in dialogs)
{ {
var innerList = new List<RoleDialogModel> { dialog }; var element = BuildDialogElement(dialog);
if (dialog.AdditionalMessageWrapper != null
&& dialog.AdditionalMessageWrapper.SaveToDb
&& dialog.AdditionalMessageWrapper.Messages?.Count > 0)
{
innerList.AddRange(dialog.AdditionalMessageWrapper.Messages);
}
foreach (var item in innerList)
{
var element = BuildDialogElement(item);
if (element != null) if (element != null)
{ {
dialogElements.Add(element); dialogElements.Add(element);
} }
} }
}
db.AppendConversationDialogs(conversationId, dialogElements); db.AppendConversationDialogs(conversationId, dialogElements);
} }

View file

@ -1,6 +1,5 @@
using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Instructs; using BotSharp.Abstraction.Instructs;
using BotSharp.Abstraction.Files.Converters;
namespace BotSharp.Core.Files.Services; namespace BotSharp.Core.Files.Services;
@ -82,6 +81,15 @@ public partial class FileInstructService
var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); var innerAgentId = options?.AgentId ?? Guid.Empty.ToString();
var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "dall-e-2"); var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "dall-e-2");
var binary = await DownloadFile(image); var binary = await DownloadFile(image);
// Convert image
var converter = GetImageConverter(options?.ImageConvertProvider);
if (converter != null)
{
binary = await converter.ConvertImage(binary);
image.FileExtension = "png";
}
using var stream = binary.ToStream(); using var stream = binary.ToStream();
stream.Position = 0; stream.Position = 0;
@ -120,10 +128,10 @@ public partial class FileInstructService
var binary = await DownloadFile(image); var binary = await DownloadFile(image);
// Convert image // Convert image
var converter = _services.GetServices<IImageConverter>().FirstOrDefault(x => x.Provider == options?.ImageConverterProvider); var converter = GetImageConverter(options?.ImageConvertProvider);
if (converter != null) if (converter != null)
{ {
binary = await converter.ConvertImageToRgbaPng(binary); binary = await converter.ConvertImage(binary);
image.FileExtension = "png"; image.FileExtension = "png";
} }
@ -165,10 +173,21 @@ public partial class FileInstructService
var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); var innerAgentId = options?.AgentId ?? Guid.Empty.ToString();
var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName);
var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "dall-e-2"); var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "gpt-image-1");
var imageBinary = await DownloadFile(image); var imageBinary = await DownloadFile(image);
var maskBinary = await DownloadFile(mask); var maskBinary = await DownloadFile(mask);
// Convert image
var converter = GetImageConverter(options?.ImageConvertProvider);
if (converter != null)
{
imageBinary = await converter.ConvertImage(imageBinary);
image.FileExtension = "png";
maskBinary = await converter.ConvertImage(maskBinary);
mask.FileExtension = "png";
}
using var imageStream = imageBinary.ToStream(); using var imageStream = imageBinary.ToStream();
imageStream.Position = 0; imageStream.Position = 0;
@ -176,7 +195,7 @@ public partial class FileInstructService
maskStream.Position = 0; maskStream.Position = 0;
var imageName = BuildFileName(image.FileName, image.FileExtension, "image", "png"); var imageName = BuildFileName(image.FileName, image.FileExtension, "image", "png");
var maskName = BuildFileName(image.FileName, image.FileExtension, "mask", "png"); var maskName = BuildFileName(mask.FileName, mask.FileExtension, "mask", "png");
var textContent = text.IfNullOrEmptyAs(instruction).IfNullOrEmptyAs(string.Empty); var textContent = text.IfNullOrEmptyAs(instruction).IfNullOrEmptyAs(string.Empty);
var message = await completion.GetImageEdits(new Agent() var message = await completion.GetImageEdits(new Agent()
{ {

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Files.Converters;
using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Instructs; using BotSharp.Abstraction.Instructs;
@ -39,7 +38,7 @@ public partial class FileInstructService
var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName);
var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, var completion = CompletionProvider.GetChatCompletion(_services, provider: provider,
model: options?.Model ?? "gpt-4o", multiModal: true); model: options?.Model ?? "gpt-5-mini", multiModal: true);
var message = await completion.GetChatCompletions(new Agent() var message = await completion.GetChatCompletions(new Agent()
{ {
Id = innerAgentId, Id = innerAgentId,
@ -120,9 +119,8 @@ public partial class FileInstructService
{ {
var images = new List<string>(); var images = new List<string>();
var settings = _services.GetRequiredService<FileCoreSettings>(); var settings = _services.GetRequiredService<FileCoreSettings>();
var imageConverterProvider = options?.ImageConverterProvider;
var converter = _services.GetServices<IImageConverter>().FirstOrDefault(x => x.Provider == imageConverterProvider); var converter = GetImageConverter(options?.ImageConvertProvider);
if (converter == null || files.IsNullOrEmpty()) if (converter == null || files.IsNullOrEmpty())
{ {
return images; return images;

View file

@ -1,4 +1,6 @@
using BotSharp.Abstraction.Files.Converters;
using Microsoft.Extensions.Options;
using static System.Net.Mime.MediaTypeNames; using static System.Net.Mime.MediaTypeNames;
namespace BotSharp.Core.Files.Services; namespace BotSharp.Core.Files.Services;
@ -86,5 +88,13 @@ public partial class FileInstructService : IFileInstructService
fextension = fextension.StartsWith(".") ? fextension.Substring(1) : fextension; fextension = fextension.StartsWith(".") ? fextension.Substring(1) : fextension;
return $"{name}.{fextension}"; return $"{name}.{fextension}";
} }
private IImageConverter? GetImageConverter(string? provider)
{
var settings = _services.GetRequiredService<FileCoreSettings>();
var convertProvider = provider ?? settings?.ImageConverter?.Provider;
var converter = _services.GetServices<IImageConverter>().FirstOrDefault(x => x.Provider == convertProvider);
return converter;
}
#endregion #endregion
} }

View file

@ -60,7 +60,6 @@ public partial class RoutingService
message.CurrentAgentId = agent.Id; message.CurrentAgentId = agent.Id;
message.IsStreaming = response.IsStreaming; message.IsStreaming = response.IsStreaming;
message.MessageLabel = response.MessageLabel; message.MessageLabel = response.MessageLabel;
message.AdditionalMessageWrapper = null;
await InvokeFunction(message, dialogs, options); await InvokeFunction(message, dialogs, options);
} }
@ -77,7 +76,6 @@ public partial class RoutingService
message.CurrentAgentId = agent.Id; message.CurrentAgentId = agent.Id;
message.IsStreaming = response.IsStreaming; message.IsStreaming = response.IsStreaming;
message.MessageLabel = response.MessageLabel; message.MessageLabel = response.MessageLabel;
message.AdditionalMessageWrapper = null;
dialogs.Add(message); dialogs.Add(message);
Context.SetDialogs(dialogs); Context.SetDialogs(dialogs);
} }

View file

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

View file

@ -4,14 +4,46 @@
"description": "Chatbot is used to test the performance of different large models and does not interact with external APIs.", "description": "Chatbot is used to test the performance of different large models and does not interact with external APIs.",
"type": "task", "type": "task",
"createdDateTime": "2024-01-15T10:39:32Z", "createdDateTime": "2024-01-15T10:39:32Z",
"updatedDateTime": "2024-01-15T14:39:32Z", "updatedDateTime": "2025-09-11T20:25:59.292826Z",
"iconUrl": "/images/logo.png",
"disabled": false,
"isPublic": true,
"llmConfig": { "llmConfig": {
"is_inherit": false, "is_inherit": false,
"provider": "openai", "provider": "openai",
"model": "gpt-4o-mini", "model": "gpt-5",
"max_recursion_depth": 3 "max_recursion_depth": 3
},
"isPublic": true,
"disabled": false,
"iconUrl": "/images/logo.png",
"profiles": [],
"labels": [],
"mergeUtility": false,
"utilities": [
{
"category": "file",
"name": "image-generator",
"disabled": false,
"items": [
{
"function_name": "util-file-generate_image",
"template_name": "util-file-generate_image.fn"
} }
]
},
{
"category": "file",
"name": "image-editor",
"disabled": false,
"items": [
{
"function_name": "util-file-edit_image",
"template_name": "util-file-edit_image.fn"
}
]
}
],
"mcpTools": [],
"rules": [],
"knowledgeBases": [],
"inheritAgentId": null,
"routingRules": []
} }

View file

@ -375,7 +375,6 @@ public class ConversationController : ControllerBase
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>();
@ -434,7 +433,6 @@ public class ConversationController : ControllerBase
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);
}); });

View file

@ -144,7 +144,7 @@ public class InstructModeController : ControllerBase
} }
} }
[HttpPost("/instruct/multi-modal/upload")] [HttpPost("/instruct/multi-modal/form")]
public async Task<MultiModalViewModel> MultiModalCompletion([FromForm] IEnumerable<IFormFile> files, [FromForm] MultiModalRequest request) public async Task<MultiModalViewModel> MultiModalCompletion([FromForm] IEnumerable<IFormFile> files, [FromForm] MultiModalRequest request)
{ {
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
@ -214,29 +214,30 @@ public class InstructModeController : ControllerBase
#region Edit image #region Edit image
[HttpPost("/instruct/image-variation")] [HttpPost("/instruct/image-variation")]
public async Task<ImageGenerationViewModel> ImageVariation([FromBody] ImageVariationRequest input) public async Task<ImageGenerationViewModel> ImageVariation([FromBody] ImageVariationFileRequest request)
{ {
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
input.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External));
var imageViewModel = new ImageGenerationViewModel(); var imageViewModel = new ImageGenerationViewModel();
try try
{ {
if (input.File == null) if (request.File == null)
{ {
return new ImageGenerationViewModel { Message = "Error! Cannot find an image!" }; return new ImageGenerationViewModel { Message = "Error! Cannot find an image!" };
} }
var fileInstruct = _services.GetRequiredService<IFileInstructService>(); var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var message = await fileInstruct.VaryImage(input.File, new InstructOptions var message = await fileInstruct.VaryImage(request.File, new InstructOptions
{ {
Provider = input.Provider, Provider = request.Provider,
Model = input.Model, Model = request.Model,
AgentId = input.AgentId AgentId = request.AgentId,
ImageConvertProvider = request.ImageConvertProvider
}); });
imageViewModel.Content = message.Content; imageViewModel.Content = message.Content;
imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? [];
return imageViewModel; return imageViewModel;
} }
catch (Exception ex) catch (Exception ex)
@ -248,8 +249,8 @@ public class InstructModeController : ControllerBase
} }
} }
[HttpPost("/instruct/image-variation/upload")] [HttpPost("/instruct/image-variation/form")]
public async Task<ImageGenerationViewModel> ImageVariation(IFormFile file, [FromForm] MultiModalRequest request) public async Task<ImageGenerationViewModel> ImageVariation(IFormFile file, [FromForm] ImageVariationRequest request)
{ {
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External));
@ -269,7 +270,8 @@ public class InstructModeController : ControllerBase
{ {
Provider = request?.Provider, Provider = request?.Provider,
Model = request?.Model, Model = request?.Model,
AgentId = request?.AgentId AgentId = request?.AgentId,
ImageConvertProvider = request?.ImageConvertProvider
}); });
imageViewModel.Content = message.Content; imageViewModel.Content = message.Content;
@ -305,7 +307,7 @@ public class InstructModeController : ControllerBase
Model = request.Model, Model = request.Model,
AgentId = request.AgentId, AgentId = request.AgentId,
TemplateName = request.TemplateName, TemplateName = request.TemplateName,
ImageConverterProvider = request.ImageConverterProvider ImageConvertProvider = request.ImageConvertProvider
}); });
imageViewModel.Content = message.Content; imageViewModel.Content = message.Content;
imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? [];
@ -320,7 +322,7 @@ public class InstructModeController : ControllerBase
} }
} }
[HttpPost("/instruct/image-edit/upload")] [HttpPost("/instruct/image-edit/form")]
public async Task<ImageGenerationViewModel> ImageEdit(IFormFile file, [FromForm] ImageEditRequest request) public async Task<ImageGenerationViewModel> ImageEdit(IFormFile file, [FromForm] ImageEditRequest request)
{ {
var fileInstruct = _services.GetRequiredService<IFileInstructService>(); var fileInstruct = _services.GetRequiredService<IFileInstructService>();
@ -343,7 +345,7 @@ public class InstructModeController : ControllerBase
Model = request?.Model, Model = request?.Model,
AgentId = request?.AgentId, AgentId = request?.AgentId,
TemplateName = request?.TemplateName, TemplateName = request?.TemplateName,
ImageConverterProvider = request?.ImageConverterProvider ImageConvertProvider = request?.ImageConvertProvider
}); });
imageViewModel.Content = message.Content; imageViewModel.Content = message.Content;
@ -360,7 +362,7 @@ public class InstructModeController : ControllerBase
} }
[HttpPost("/instruct/image-mask-edit")] [HttpPost("/instruct/image-mask-edit")]
public async Task<ImageGenerationViewModel> ImageMaskEdit([FromBody] ImageMaskEditRequest request) public async Task<ImageGenerationViewModel> ImageMaskEdit([FromBody] ImageMaskEditFileRequest request)
{ {
var fileInstruct = _services.GetRequiredService<IFileInstructService>(); var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
@ -380,7 +382,8 @@ public class InstructModeController : ControllerBase
Provider = request.Provider, Provider = request.Provider,
Model = request.Model, Model = request.Model,
AgentId = request.AgentId, AgentId = request.AgentId,
TemplateName = request.TemplateName TemplateName = request.TemplateName,
ImageConvertProvider = request.ImageConvertProvider
}); });
imageViewModel.Content = message.Content; imageViewModel.Content = message.Content;
imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? [];
@ -395,7 +398,7 @@ public class InstructModeController : ControllerBase
} }
} }
[HttpPost("/instruct/image-mask-edit/upload")] [HttpPost("/instruct/image-mask-edit/form")]
public async Task<ImageGenerationViewModel> ImageMaskEdit(IFormFile image, IFormFile mask, [FromForm] ImageMaskEditRequest request) public async Task<ImageGenerationViewModel> ImageMaskEdit(IFormFile image, IFormFile mask, [FromForm] ImageMaskEditRequest request)
{ {
var fileInstruct = _services.GetRequiredService<IFileInstructService>(); var fileInstruct = _services.GetRequiredService<IFileInstructService>();
@ -425,12 +428,12 @@ public class InstructModeController : ControllerBase
Provider = request?.Provider, Provider = request?.Provider,
Model = request?.Model, Model = request?.Model,
AgentId = request?.AgentId, AgentId = request?.AgentId,
TemplateName = request?.TemplateName TemplateName = request?.TemplateName,
ImageConvertProvider = request?.ImageConvertProvider
}); });
imageViewModel.Content = message.Content; imageViewModel.Content = message.Content;
imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? [];
return imageViewModel; return imageViewModel;
} }
catch (Exception ex) catch (Exception ex)
@ -445,21 +448,22 @@ public class InstructModeController : ControllerBase
#region Pdf #region Pdf
[HttpPost("/instruct/pdf-completion")] [HttpPost("/instruct/pdf-completion")]
public async Task<PdfCompletionViewModel> PdfCompletion([FromBody] MultiModalFileRequest input) public async Task<PdfCompletionViewModel> PdfCompletion([FromBody] PdfReadFileRequest request)
{ {
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
input.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External));
var viewModel = new PdfCompletionViewModel(); var viewModel = new PdfCompletionViewModel();
try try
{ {
var fileInstruct = _services.GetRequiredService<IFileInstructService>(); var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var content = await fileInstruct.ReadPdf(input.Text, input.Files, new InstructOptions var content = await fileInstruct.ReadPdf(request.Text, request.Files, new InstructOptions
{ {
Provider = input.Provider, Provider = request.Provider,
Model = input.Model, Model = request.Model,
AgentId = input.AgentId, AgentId = request.AgentId,
TemplateName = input.TemplateName TemplateName = request.TemplateName,
ImageConvertProvider = request.ImageConvertProvider
}); });
viewModel.Content = content; viewModel.Content = content;
return viewModel; return viewModel;
@ -473,8 +477,8 @@ public class InstructModeController : ControllerBase
} }
} }
[HttpPost("/instruct/pdf-completion/upload")] [HttpPost("/instruct/pdf-completion/form")]
public async Task<PdfCompletionViewModel> PdfCompletion([FromForm] IEnumerable<IFormFile> files, [FromForm] MultiModalRequest request) public async Task<PdfCompletionViewModel> PdfCompletion([FromForm] IEnumerable<IFormFile> files, [FromForm] PdfReadRequest request)
{ {
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External));
@ -494,7 +498,8 @@ public class InstructModeController : ControllerBase
Provider = request?.Provider, Provider = request?.Provider,
Model = request?.Model, Model = request?.Model,
AgentId = request?.AgentId, AgentId = request?.AgentId,
TemplateName = request?.TemplateName TemplateName = request?.TemplateName,
ImageConvertProvider = request?.ImageConvertProvider
}); });
viewModel.Content = content; viewModel.Content = content;
return viewModel; return viewModel;
@ -511,26 +516,26 @@ public class InstructModeController : ControllerBase
#region Audio #region Audio
[HttpPost("/instruct/speech-to-text")] [HttpPost("/instruct/speech-to-text")]
public async Task<SpeechToTextViewModel> SpeechToText([FromBody] SpeechToTextRequest input) public async Task<SpeechToTextViewModel> SpeechToText([FromBody] SpeechToTextFileRequest request)
{ {
var fileInstruct = _services.GetRequiredService<IFileInstructService>(); var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
input.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External));
var viewModel = new SpeechToTextViewModel(); var viewModel = new SpeechToTextViewModel();
try try
{ {
var audio = input.File; var audio = request.File;
if (audio == null) if (audio == null)
{ {
return new SpeechToTextViewModel { Message = "Error! Cannot find a valid audio file!" }; return new SpeechToTextViewModel { Message = "Error! Cannot find a valid audio file!" };
} }
var content = await fileInstruct.SpeechToText(audio, input.Text, new InstructOptions var content = await fileInstruct.SpeechToText(audio, request.Text, new InstructOptions
{ {
Provider = input.Provider, Provider = request.Provider,
Model = input.Model, Model = request.Model,
AgentId = input.AgentId, AgentId = request.AgentId,
TemplateName = input.TemplateName TemplateName = request.TemplateName
}); });
viewModel.Content = content; viewModel.Content = content;
return viewModel; return viewModel;
@ -544,8 +549,8 @@ public class InstructModeController : ControllerBase
} }
} }
[HttpPost("/instruct/speech-to-text/upload")] [HttpPost("/instruct/speech-to-text/form")]
public async Task<SpeechToTextViewModel> SpeechToText(IFormFile file, [FromForm] MultiModalRequest request) public async Task<SpeechToTextViewModel> SpeechToText(IFormFile file, [FromForm] SpeechToTextRequest request)
{ {
var fileInstruct = _services.GetRequiredService<IFileInstructService>(); var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
@ -583,13 +588,13 @@ public class InstructModeController : ControllerBase
} }
[HttpPost("/instruct/text-to-speech")] [HttpPost("/instruct/text-to-speech")]
public async Task<IActionResult> TextToSpeech([FromBody] TextToSpeechRequest input) public async Task<IActionResult> TextToSpeech([FromBody] TextToSpeechRequest request)
{ {
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
input.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External));
var completion = CompletionProvider.GetAudioSynthesizer(_services, provider: input.Provider, model: input.Model); var completion = CompletionProvider.GetAudioSynthesizer(_services, provider: request.Provider, model: request.Model);
var binaryData = await completion.GenerateAudioAsync(input.Text); var binaryData = await completion.GenerateAudioAsync(request.Text);
var stream = binaryData.ToStream(); var stream = binaryData.ToStream();
stream.Position = 0; stream.Position = 0;

View file

@ -1,46 +1,7 @@
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

@ -21,6 +21,7 @@ public class InstructBaseRequest
public List<InstructState> States { get; set; } = []; public List<InstructState> States { get; set; } = [];
} }
public class MultiModalRequest : InstructBaseRequest public class MultiModalRequest : InstructBaseRequest
{ {
[JsonPropertyName("text")] [JsonPropertyName("text")]
@ -33,25 +34,34 @@ public class MultiModalFileRequest : MultiModalRequest
public List<InstructFileModel> Files { get; set; } = []; public List<InstructFileModel> Files { get; set; } = [];
} }
public class ImageGenerationRequest : InstructBaseRequest public class ImageGenerationRequest : InstructBaseRequest
{ {
[JsonPropertyName("text")] [JsonPropertyName("text")]
public string Text { get; set; } = string.Empty; public string Text { get; set; } = string.Empty;
} }
public class ImageVariationRequest : InstructBaseRequest public class ImageVariationRequest : InstructBaseRequest
{
[JsonPropertyName("image_convert_provider")]
public string? ImageConvertProvider { get; set; }
}
public class ImageVariationFileRequest : ImageVariationRequest
{ {
[JsonPropertyName("file")] [JsonPropertyName("file")]
public InstructFileModel File { get; set; } public InstructFileModel File { get; set; }
} }
public class ImageEditRequest : InstructBaseRequest public class ImageEditRequest : InstructBaseRequest
{ {
[JsonPropertyName("text")] [JsonPropertyName("text")]
public string Text { get; set; } = string.Empty; public string Text { get; set; } = string.Empty;
[JsonPropertyName("image_converter_provider")] [JsonPropertyName("image_convert_provider")]
public string? ImageConverterProvider { get; set; } public string? ImageConvertProvider { get; set; }
} }
public class ImageEditFileRequest : ImageEditRequest public class ImageEditFileRequest : ImageEditRequest
@ -66,6 +76,12 @@ public class ImageMaskEditRequest : InstructBaseRequest
[JsonPropertyName("text")] [JsonPropertyName("text")]
public string Text { get; set; } = string.Empty; public string Text { get; set; } = string.Empty;
[JsonPropertyName("image_convert_provider")]
public string? ImageConvertProvider { get; set; }
}
public class ImageMaskEditFileRequest : ImageMaskEditRequest
{
[JsonPropertyName("file")] [JsonPropertyName("file")]
public InstructFileModel File { get; set; } public InstructFileModel File { get; set; }
@ -73,11 +89,31 @@ public class ImageMaskEditRequest : InstructBaseRequest
public InstructFileModel Mask { get; set; } public InstructFileModel Mask { get; set; }
} }
public class PdfReadRequest : InstructBaseRequest
{
[JsonPropertyName("text")]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("image_convert_provider")]
public string? ImageConvertProvider { get; set; }
}
public class PdfReadFileRequest : PdfReadRequest
{
[JsonPropertyName("files")]
public List<InstructFileModel> Files { get; set; } = [];
}
public class SpeechToTextRequest : InstructBaseRequest public class SpeechToTextRequest : InstructBaseRequest
{ {
[JsonPropertyName("text")] [JsonPropertyName("text")]
public string? Text { get; set; } public string? Text { get; set; }
}
public class SpeechToTextFileRequest : SpeechToTextRequest
{
[JsonPropertyName("file")] [JsonPropertyName("file")]
public InstructFileModel File { get; set; } public InstructFileModel File { get; set; }
} }

View file

@ -126,49 +126,6 @@ public class ChatHubConversationHook : ConversationHookBase
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

@ -20,17 +20,45 @@ public class FileHandlerImageConverter : IImageConverter
public string Provider => "file-handler"; public string Provider => "file-handler";
public async Task<BinaryData> ConvertImageToRgbaPng(BinaryData binary) public async Task<BinaryData> ConvertImage(BinaryData binary, ImageConvertOptions? options = null)
{ {
try try
{ {
using var image = Image.Load<Rgba32>(binary.ToArray()); using var image = Image.Load<Rgba32>(binary.ToArray());
using var memoryStream = new MemoryStream(); using var memoryStream = new MemoryStream();
if (options?.ImageType == "png")
{
var colorType = PngColorType.RgbWithAlpha;
switch (options?.ColorType)
{
case "grayscale":
colorType = PngColorType.Grayscale;
break;
case "grayscaleWithAlpha":
colorType = PngColorType.GrayscaleWithAlpha;
break;
case "rgb":
colorType = PngColorType.Rgb;
break;
case "palette":
colorType = PngColorType.Palette;
break;
}
image.SaveAsPng(memoryStream, new PngEncoder
{
ColorType = colorType
});
}
else
{
image.SaveAsPng(memoryStream, new PngEncoder image.SaveAsPng(memoryStream, new PngEncoder
{ {
ColorType = PngColorType.RgbWithAlpha ColorType = PngColorType.RgbWithAlpha
}); });
}
var convertedBinary = BinaryData.FromBytes(memoryStream.ToArray()); var convertedBinary = BinaryData.FromBytes(memoryStream.ToArray());
return await Task.FromResult(convertedBinary); return await Task.FromResult(convertedBinary);
} }

View file

@ -83,7 +83,7 @@ public class EditImageFn : IFunctionCallback
var fileStorage = _services.GetRequiredService<IFileStorageService>(); var fileStorage = _services.GetRequiredService<IFileStorageService>();
var fileBinary = fileStorage.GetFileBytes(image.FileStorageUrl); var fileBinary = fileStorage.GetFileBytes(image.FileStorageUrl);
var rgbaBinary = await ConvertImageToRgbaWithPng(fileBinary); var rgbaBinary = await ConvertImageToPngWithRgba(fileBinary);
image.FileExtension = "png"; image.FileExtension = "png";
using var stream = rgbaBinary.ToStream(); using var stream = rgbaBinary.ToStream();
@ -147,7 +147,7 @@ public class EditImageFn : IFunctionCallback
fileStorage.SaveMessageFiles(_conversationId, _messageId, FileSourceType.Bot, files); fileStorage.SaveMessageFiles(_conversationId, _messageId, FileSourceType.Bot, files);
} }
private async Task<BinaryData> ConvertImageToRgbaWithPng(BinaryData binaryFile) private async Task<BinaryData> ConvertImageToPngWithRgba(BinaryData binaryFile)
{ {
var provider = _settings?.ImageConverter?.Provider; var provider = _settings?.ImageConverter?.Provider;
var converter = _services.GetServices<IImageConverter>().FirstOrDefault(x => x.Provider == provider); var converter = _services.GetServices<IImageConverter>().FirstOrDefault(x => x.Provider == provider);
@ -156,6 +156,6 @@ public class EditImageFn : IFunctionCallback
return binaryFile; return binaryFile;
} }
return await converter.ConvertImageToRgbaPng(binaryFile); return await converter.ConvertImage(binaryFile);
} }
} }

View file

@ -40,7 +40,7 @@ public class AudioSynthesisProvider : IAudioSynthesis
var options = new SpeechGenerationOptions var options = new SpeechGenerationOptions
{ {
ResponseFormat = responseFormat, ResponseFormat = responseFormat,
SpeedRatio = speed, SpeedRatio = speed
}; };
return (voice, options); return (voice, options);

View file

@ -57,6 +57,12 @@ public class AudioTranscriptionProvider : IAudioTranscription
switch (value) switch (value)
{ {
case "json": case "json":
format = new AudioTranscriptionFormat("json");
break;
case "text":
format = new AudioTranscriptionFormat("text");
break;
case "simple":
format = AudioTranscriptionFormat.Simple; format = AudioTranscriptionFormat.Simple;
break; break;
case "srt": case "srt":

View file

@ -437,6 +437,9 @@
}, },
"Pdf2ImageConverter": { "Pdf2ImageConverter": {
"Provider": "" "Provider": ""
},
"ImageConverter": {
"Provider": ""
} }
}, },
@ -469,7 +472,7 @@
} }
}, },
"ImageConverter": { "ImageConverter": {
"Provider": null "Provider": "file-handler"
} }
}, },