Merge pull request #454 from iceljc/features/add-fetch-conv-files

add chat files
This commit is contained in:
C. Oceania 2024-05-15 07:20:48 -05:00 committed by GitHub
commit d305e42cac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 301 additions and 117 deletions

View file

@ -3,7 +3,8 @@ namespace BotSharp.Abstraction.Files;
public interface IBotSharpFileService
{
string GetDirectory(string conversationId);
IEnumerable<OutputFileModel> GetConversationFiles(string conversationId, string messageId);
IEnumerable<MessageFileModel> GetChatImages(string conversationId, List<RoleDialogModel> conversations, int offset = 2);
IEnumerable<MessageFileModel> GetMessageFiles(string conversationId, IEnumerable<string> messageIds, bool imageOnly = false);
string? GetMessageFile(string conversationId, string messageId, string fileName);
void SaveMessageFiles(string conversationId, string messageId, List<BotSharpFile> files);
@ -17,4 +18,11 @@ public interface IBotSharpFileService
/// <returns></returns>
bool DeleteMessageFiles(string conversationId, IEnumerable<string> messageIds, string targetMessageId, string? newMessageId = null);
bool DeleteConversationFiles(IEnumerable<string> conversationIds);
/// <summary>
/// Get file bytes and content type from data, e.g., "data:image/png;base64,aaaaaaaaa"
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
(string, byte[]) GetFileInfoFromData(string data);
}

View file

@ -4,14 +4,14 @@ namespace BotSharp.Abstraction.Files.Models;
public class BotSharpFile
{
[JsonPropertyName("file_name")]
public string FileName { get; set; }
public string FileName { get; set; } = string.Empty;
/// <summary>
/// File data, e.g., "data:image/png;base64,aaaaaaaa"
/// </summary>
[JsonPropertyName("file_data")]
public string FileData { get; set; }
public string FileData { get; set; } = string.Empty;
[JsonPropertyName("content_type")]
public string ContentType { get; set; }
[JsonPropertyName("file_size")]
public int FileSize { get; set; }
[JsonPropertyName("file_url")]
public string FileUrl { get; set; } = string.Empty;
}

View file

@ -0,0 +1,32 @@
namespace BotSharp.Abstraction.Files.Models;
public class MessageFileModel
{
[JsonPropertyName("message_id")]
public string MessageId { get; set; }
[JsonPropertyName("file_url")]
public string FileUrl { get; set; }
[JsonPropertyName("file_storage_url")]
public string FileStorageUrl { get; set; }
[JsonPropertyName("file_name")]
public string FileName { get; set; }
[JsonPropertyName("file_type")]
public string FileType { get; set; }
[JsonPropertyName("content_type")]
public string ContentType { get; set; }
public MessageFileModel()
{
}
public override string ToString()
{
return $"File name: {FileName}, File type: {FileType}, Content type: {ContentType}";
}
}

View file

@ -1,13 +0,0 @@
namespace BotSharp.Abstraction.Files.Models;
public class OutputFileModel
{
[JsonPropertyName("file_url")]
public string FileUrl { get; set; }
[JsonPropertyName("file_name")]
public string FileName { get; set; }
[JsonPropertyName("file_type")]
public string FileType { get; set; }
}

View file

@ -6,6 +6,6 @@ public interface ILlmProviderService
{
LlmModelSetting GetSetting(string provider, string model);
List<string> GetProviders();
LlmModelSetting GetProviderModel(string provider, string id);
LlmModelSetting GetProviderModel(string provider, string id, bool multiModal = false);
List<LlmModelSetting> GetProviderModels(string provider);
}

View file

@ -27,6 +27,11 @@ public class LlmModelSetting
public string Endpoint { get; set; }
public LlmModelType Type { get; set; } = LlmModelType.Chat;
/// <summary>
/// If true, allow sending images/vidoes to this model
/// </summary>
public bool MultiModal { get; set; }
/// <summary>
/// Prompt cost per 1K token
/// </summary>

View file

@ -159,6 +159,7 @@
<PackageReference Include="Colorful.Console" Version="1.2.15" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.2.1" />
<PackageReference Include="Fluid.Core" Version="2.8.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Nanoid" Version="3.0.0" />
<PackageReference Include="RedLock.net" Version="2.3.2" />
</ItemGroup>

View file

@ -1,3 +1,4 @@
using Microsoft.AspNetCore.StaticFiles;
using System.IO;
using System.Threading;
@ -7,16 +8,22 @@ public class BotSharpFileService : IBotSharpFileService
{
private readonly BotSharpDatabaseSettings _dbSettings;
private readonly IServiceProvider _services;
private readonly ILogger<BotSharpFileService> _logger;
private readonly string _baseDir;
private readonly IEnumerable<string> _allowedTypes = new List<string> { "image/png", "image/jpeg" };
private const string CONVERSATION_FOLDER = "conversations";
private const string FILE_FOLDER = "files";
private const int MIN_OFFSET = 1;
private const int MAX_OFFSET = 5;
public BotSharpFileService(
BotSharpDatabaseSettings dbSettings,
ILogger<BotSharpFileService> logger,
IServiceProvider services)
{
_dbSettings = dbSettings;
_logger = logger;
_services = services;
_baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository);
}
@ -31,29 +38,67 @@ public class BotSharpFileService : IBotSharpFileService
return dir;
}
public IEnumerable<OutputFileModel> GetConversationFiles(string conversationId, string messageId)
public IEnumerable<MessageFileModel> GetChatImages(string conversationId, List<RoleDialogModel> conversations, int offset = 2)
{
var outputFiles = new List<OutputFileModel>();
var dir = GetConversationFileDirectory(conversationId, messageId);
if (string.IsNullOrEmpty(dir))
var files = new List<MessageFileModel>();
if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty())
{
return outputFiles;
return files;
}
foreach (var file in Directory.GetFiles(dir))
if (offset <= 0)
{
var fileName = Path.GetFileNameWithoutExtension(file);
var extension = Path.GetExtension(file);
var fileType = extension.Substring(1);
var model = new OutputFileModel()
{
FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}",
FileName = fileName,
FileType = fileType
};
outputFiles.Add(model);
offset = MIN_OFFSET;
}
return outputFiles;
else if (offset > MAX_OFFSET)
{
offset = MAX_OFFSET;
}
var messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset).ToList();
files = GetMessageFiles(conversationId, messageIds, imageOnly: true).ToList();
return files;
}
public IEnumerable<MessageFileModel> GetMessageFiles(string conversationId, IEnumerable<string> messageIds, bool imageOnly = false)
{
var files = new List<MessageFileModel>();
if (messageIds.IsNullOrEmpty()) return files;
foreach (var messageId in messageIds)
{
var dir = GetConversationFileDirectory(conversationId, messageId);
if (string.IsNullOrEmpty(dir))
{
continue;
}
foreach (var file in Directory.GetFiles(dir))
{
var contentType = GetFileContentType(file);
if (imageOnly && !_allowedTypes.Contains(contentType))
{
continue;
}
var fileName = Path.GetFileNameWithoutExtension(file);
var extension = Path.GetExtension(file);
var fileType = extension.Substring(1);
var model = new MessageFileModel()
{
MessageId = messageId,
FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}",
FileStorageUrl = file,
FileName = fileName,
FileType = fileType,
ContentType = contentType
};
files.Add(model);
}
}
return files;
}
public string? GetMessageFile(string conversationId, string messageId, string fileName)
@ -75,19 +120,26 @@ public class BotSharpFileService : IBotSharpFileService
var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true);
if (string.IsNullOrEmpty(dir)) return;
for (int i = 0; i < files.Count; i++)
try
{
var file = files[i];
if (string.IsNullOrEmpty(file.FileData))
for (int i = 0; i < files.Count; i++)
{
continue;
}
var file = files[i];
if (string.IsNullOrEmpty(file.FileData))
{
continue;
}
var bytes = GetFileBytes(file.FileData);
var fileType = Path.GetExtension(file.FileName);
var fileName = $"{i + 1}{fileType}";
Thread.Sleep(100);
File.WriteAllBytes(Path.Combine(dir, fileName), bytes);
var (_, bytes) = GetFileInfoFromData(file.FileData);
var fileType = Path.GetExtension(file.FileName);
var fileName = $"{i + 1}{fileType}";
Thread.Sleep(100);
File.WriteAllBytes(Path.Combine(dir, fileName), bytes);
}
}
catch (Exception ex)
{
_logger.LogError($"Error when saving conversation files: {ex.Message}");
}
}
@ -137,6 +189,23 @@ public class BotSharpFileService : IBotSharpFileService
return true;
}
public (string, byte[]) GetFileInfoFromData(string data)
{
if (string.IsNullOrEmpty(data))
{
return (string.Empty, new byte[0]);
}
var typeStartIdx = data.IndexOf(':');
var typeEndIdx = data.IndexOf(';');
var contentType = data.Substring(typeStartIdx + 1, typeEndIdx - typeStartIdx - 1);
var base64startIdx = data.IndexOf(',');
var base64Str = data.Substring(base64startIdx + 1);
return (contentType, Convert.FromBase64String(base64Str));
}
#region Private methods
private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false)
{
@ -170,54 +239,16 @@ public class BotSharpFileService : IBotSharpFileService
return dir;
}
private byte[] GetFileBytes(string data)
private string GetFileContentType(string filePath)
{
if (string.IsNullOrEmpty(data))
string contentType;
var provider = new FileExtensionContentTypeProvider();
if (!provider.TryGetContentType(filePath, out contentType))
{
return new byte[0];
contentType = string.Empty;
}
var startIdx = data.IndexOf(',');
var base64Str = data.Substring(startIdx + 1);
return Convert.FromBase64String(base64Str);
}
private string GetFileType(string data)
{
if (string.IsNullOrEmpty(data))
{
return string.Empty;
}
var startIdx = data.IndexOf(':');
var endIdx = data.IndexOf(';');
var fileType = data.Substring(startIdx + 1, endIdx - startIdx - 1);
return fileType;
}
private string ParseFileFormat(string type)
{
var parsed = string.Empty;
switch (type)
{
case "image/png":
parsed = ".png";
break;
case "image/jpeg":
case "image/jpg":
parsed = ".jpeg";
break;
case "application/pdf":
parsed = ".pdf";
break;
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
parsed = ".xlsx";
break;
case "text/plain":
parsed = ".txt";
break;
}
return parsed;
return contentType;
}
#endregion
}

View file

@ -35,10 +35,13 @@ public class CompletionProvider
public static IChatCompletion GetChatCompletion(IServiceProvider services,
string? provider = null,
string? model = null,
string? modelId = null,
bool multiModal = false,
AgentLlmConfig? agentConfig = null)
{
var completions = services.GetServices<IChatCompletion>();
(provider, model) = GetProviderAndModel(services, provider: provider, model: model, agentConfig: agentConfig);
(provider, model) = GetProviderAndModel(services, provider: provider, model: model, modelId: modelId,
multiModal: multiModal, agentConfig: agentConfig);
var completer = completions.FirstOrDefault(x => x.Provider == provider);
if (completer == null)
@ -47,7 +50,7 @@ public class CompletionProvider
logger.LogError($"Can't resolve completion provider by {provider}");
}
completer.SetModelName(model);
completer?.SetModelName(model);
return completer;
}
@ -55,6 +58,8 @@ public class CompletionProvider
private static (string, string) GetProviderAndModel(IServiceProvider services,
string? provider = null,
string? model = null,
string? modelId = null,
bool multiModal = false,
AgentLlmConfig? agentConfig = null)
{
var agentSetting = services.GetRequiredService<AgentSettings>();
@ -73,11 +78,11 @@ public class CompletionProvider
{
model = state.GetState("model", model ?? "gpt-35-turbo-4k");
}
else if (state.ContainsState("model_id"))
else if (state.ContainsState("model_id") || !string.IsNullOrEmpty(modelId))
{
var modelId = state.GetState("model_id");
var modelIdentity = state.ContainsState("model_id") ? state.GetState("model_id") : modelId;
var llmProviderService = services.GetRequiredService<ILlmProviderService>();
model = llmProviderService.GetProviderModel(provider, modelId)?.Name;
model = llmProviderService.GetProviderModel(provider, modelIdentity, multiModal)?.Name;
}
}

View file

@ -44,10 +44,10 @@ public class LlmProviderService : ILlmProviderService
?.Models ?? new List<LlmModelSetting>();
}
public LlmModelSetting GetProviderModel(string provider, string id)
public LlmModelSetting GetProviderModel(string provider, string id, bool multiModal = false)
{
var models = GetProviderModels(provider)
.Where(x => x.Id == id)
.Where(x => x.Id == id && x.MultiModal == multiModal)
.ToList();
var random = new Random();

View file

@ -17,18 +17,18 @@ public partial class RoutingService
return false;
}
var provide = agent.LlmConfig.Provider;
var provider = agent.LlmConfig.Provider;
var model = agent.LlmConfig.Model;
if (provide == null || model == null)
if (provider == null || model == null)
{
var agentSettings = _services.GetRequiredService<AgentSettings>();
provide = agentSettings.LlmConfig.Provider;
provider = agentSettings.LlmConfig.Provider;
model = agentSettings.LlmConfig.Model;
}
var chatCompletion = CompletionProvider.GetChatCompletion(_services,
provider: provide,
provider: provider,
model: model);
var message = dialogs.Last();

View file

@ -38,10 +38,11 @@ public class FileController : ControllerBase
}
[HttpGet("/conversation/{conversationId}/files/{messageId}")]
public IEnumerable<OutputFileModel> GetConversationFiles([FromRoute] string conversationId, [FromRoute] string messageId)
public IEnumerable<MessageFileViewModel> GetMessageFiles([FromRoute] string conversationId, [FromRoute] string messageId)
{
var fileService = _services.GetRequiredService<IBotSharpFileService>();
return fileService.GetConversationFiles(conversationId, messageId);
var files = fileService.GetMessageFiles(conversationId, new List<string> { messageId });
return files?.Select(x => MessageFileViewModel.Transform(x))?.ToList() ?? new List<MessageFileViewModel>();
}
[HttpGet("/conversation/{conversationId}/message/{messageId}/file/{fileName}")]

View file

@ -11,10 +11,12 @@ namespace BotSharp.OpenAPI.Controllers;
public class InstructModeController : ControllerBase
{
private readonly IServiceProvider _services;
private readonly ILogger<InstructModeController> _logger;
public InstructModeController(IServiceProvider services)
public InstructModeController(IServiceProvider services, ILogger<InstructModeController> logger)
{
_services = services;
_logger = logger;
}
[HttpPost("/instruct/{agentId}")]
@ -72,4 +74,32 @@ public class InstructModeController : ControllerBase
});
return message.Content;
}
[HttpPost("/instruct/multi-modal")]
public async Task<string> MultiModalCompletion([FromBody] IncomingMessageModel input)
{
var state = _services.GetRequiredService<IConversationStateService>();
input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
try
{
var completion = CompletionProvider.GetChatCompletion(_services, provider: "openai", modelId: "gpt-4", multiModal: true);
var message = await completion.GetChatCompletions(new Agent()
{
Id = Guid.Empty.ToString(),
}, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, input.Text)
{
Files = input.Files
}
});
return message.Content;
}
catch (Exception ex)
{
_logger.LogError($"Error in analyzing files. {ex.Message}");
return $"Error in analyzing files.";
}
}
}

View file

@ -28,4 +28,5 @@ global using BotSharp.Abstraction.Files.Models;
global using BotSharp.Abstraction.Files;
global using BotSharp.OpenAPI.ViewModels.Conversations;
global using BotSharp.OpenAPI.ViewModels.Users;
global using BotSharp.OpenAPI.ViewModels.Agents;
global using BotSharp.OpenAPI.ViewModels.Agents;
global using BotSharp.OpenAPI.ViewModels.Files;

View file

@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
namespace BotSharp.OpenAPI.ViewModels.Files;
public class MessageFileViewModel
{
[JsonPropertyName("file_url")]
public string FileUrl { get; set; }
[JsonPropertyName("file_name")]
public string FileName { get; set; }
[JsonPropertyName("file_type")]
public string FileType { get; set; }
[JsonPropertyName("content_type")]
public string ContentType { get; set; }
public MessageFileViewModel()
{
}
public static MessageFileViewModel Transform(MessageFileModel model)
{
return new MessageFileViewModel
{
FileUrl = model.FileUrl,
FileName = model.FileName,
FileType = model.FileType,
ContentType = model.ContentType
};
}
}

View file

@ -4,14 +4,19 @@ using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Files.Models;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Utilities;
using BotSharp.Plugin.AzureOpenAI.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Threading.Tasks;
namespace BotSharp.Plugin.AzureOpenAI.Providers;
@ -218,6 +223,17 @@ public class ChatCompletionProvider : IChatCompletion
protected (string, ChatCompletionsOptions) PrepareOptions(Agent agent, List<RoleDialogModel> conversations)
{
var agentService = _services.GetRequiredService<IAgentService>();
var fileService = _services.GetRequiredService<IBotSharpFileService>();
var state = _services.GetRequiredService<IConversationStateService>();
var settingsService = _services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(Provider, _model);
var allowMultiModal = settings != null && settings.MultiModal;
var chatFiles = new List<MessageFileModel>();
if (allowMultiModal)
{
chatFiles = fileService.GetChatImages(state.GetConversationId(), conversations, offset: 2).ToList();
}
var chatCompletionsOptions = new ChatCompletionsOptions();
@ -279,19 +295,52 @@ public class ChatCompletionProvider : IChatCompletion
else if (message.Role == ChatRole.User)
{
var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content;
var userMessage = new ChatRequestUserMessage(text)
var chatItems = new List<ChatMessageContentItem>()
{
new ChatMessageTextContentItem(text)
};
var files = chatFiles.Where(x => x.MessageId == message.MessageId).ToList();
if (!files.IsNullOrEmpty())
{
foreach (var file in files)
{
using var stream = File.OpenRead(file.FileStorageUrl);
chatItems.Add(new ChatMessageImageContentItem(stream, file.ContentType, ChatMessageImageDetailLevel.Low));
}
}
if (allowMultiModal && !message.Files.IsNullOrEmpty())
{
foreach (var file in message.Files)
{
if (!string.IsNullOrEmpty(file.FileUrl))
{
var uri = new Uri(file.FileUrl);
chatItems.Add(new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low));
}
else if (!string.IsNullOrEmpty(file.FileData))
{
var (contentType, bytes) = fileService.GetFileInfoFromData(file.FileData);
using var stream = new MemoryStream(bytes, 0, bytes.Length);
chatItems.Add(new ChatMessageImageContentItem(stream, contentType, ChatMessageImageDetailLevel.Low));
}
}
}
//if (!string.IsNullOrEmpty(message.ImageUrl))
//{
// var uri = new Uri(message.ImageUrl);
// userMessage.MultimodalContentItems.Add(
// new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low));
//}
var userMessage = new ChatRequestUserMessage(chatItems)
{
// To display Planner name in log
Name = message.FunctionName,
};
if (!string.IsNullOrEmpty(message.ImageUrl))
{
var uri = new Uri(message.ImageUrl);
userMessage.MultimodalContentItems.Add(
new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low));
}
chatCompletionsOptions.Messages.Add(userMessage);
}
else if (message.Role == ChatRole.Assistant)
@ -301,7 +350,7 @@ public class ChatCompletionProvider : IChatCompletion
}
// https://community.openai.com/t/cheat-sheet-mastering-temperature-and-top-p-in-chatgpt-api-a-few-tips-and-tricks-on-controlling-the-creativity-deterministic-output-of-prompt-responses/172683
var state = _services.GetRequiredService<IConversationStateService>();
//var state = _services.GetRequiredService<IConversationStateService>();
var temperature = float.Parse(state.GetState("temperature", "0.0"));
var samplingFactor = float.Parse(state.GetState("sampling_factor", "0.0"));
chatCompletionsOptions.Temperature = temperature;