feat:Add Tencent Cloud object storage support

This commit is contained in:
Gil Zhang 2024-07-31 01:08:40 +08:00
parent a283abb869
commit fdab693d5c
15 changed files with 1212 additions and 1 deletions

View file

@ -101,6 +101,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.FileHandler
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Planner", "src\Plugins\BotSharp.Plugin.Planner\BotSharp.Plugin.Planner.csproj", "{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FileStorages", "FileStorages", "{38B37C0D-1930-4D47-BCBF-E358EC1096B1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.TencentCos", "src\Plugins\BotSharp.Plugin.TencentCos\BotSharp.Plugin.TencentCos.csproj", "{BF029B0A-768B-43A1-8D91-E70B95505716}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -413,6 +417,14 @@ Global
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|Any CPU.Build.0 = Release|Any CPU
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.ActiveCfg = Release|Any CPU
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.Build.0 = Release|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.ActiveCfg = Debug|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.Build.0 = Debug|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.Build.0 = Release|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.ActiveCfg = Release|Any CPU
{BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -461,6 +473,8 @@ Global
{A72B3BEB-E14B-4917-BE44-97EAE4E122D2} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{38B37C0D-1930-4D47-BCBF-E358EC1096B1} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C}
{BF029B0A-768B-43A1-8D91-E70B95505716} = {38B37C0D-1930-4D47-BCBF-E358EC1096B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>$(LangVersion)</LangVersion>
<Nullable>enable</Nullable>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.39" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,304 @@
using COSXML;
using COSXML.CosException;
using COSXML.Model.Bucket;
using COSXML.Model.Object;
using COSXML.Model.Tag;
namespace BotSharp.Plugin.TencentCos.Modules
{
public class BucketClient
{
private readonly CosXmlServer _cosXml;
private readonly string _fullBucketName;
private readonly string _appId;
private readonly string _region;
public BucketClient(CosXmlServer cosXml, string fullBucketName, string appId, string region)
{
_cosXml = cosXml;
_fullBucketName = fullBucketName;
_appId = appId;
_region = region;
}
public bool UploadBytes(string key, byte[] fileData)
{
var result = false;
try
{
var request = new PutObjectRequest(_fullBucketName, key, fileData);
var resultData = _cosXml.PutObject(request);
if (resultData != null && resultData.IsSuccessful())
{
result = true;
}
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
return result;
}
public bool UploadStream(string key, Stream stream)
{
var result = false;
try
{
var request = new PutObjectRequest(_fullBucketName, key, stream);
var resultData = _cosXml.PutObject(request);
if (resultData != null && resultData.IsSuccessful())
{
result = true;
}
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
return result;
}
public (string, byte[]) DownloadDirDefaultFileBytes(string dir)
{
try
{
var request = new GetBucketRequest(_fullBucketName);
request.SetPrefix($"{dir.TrimEnd('/')}/");
request.SetDelimiter("/");
var result = _cosXml.GetBucket(request);
var info = result.listBucket;
var objects = info.contentsList;
var objectData = objects.FirstOrDefault(o => o.size > 0);
if (objectData != null)
{
var fileName = Path.GetFileName(objectData.key);
var fileBytes = DownloadFileBytes(objectData.key);
return (fileName, fileBytes);
}
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
return (string.Empty, Array.Empty<byte>());
}
public byte[] DownloadFileBytes(string key)
{
try
{
var request = new GetObjectBytesRequest(_fullBucketName, key);
var result = _cosXml.GetObject(request);
if (result != null)
{
return result.content;
}
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
return Array.Empty<byte>();
}
public List<string> GetDirFiles(string dir)
{
try
{
var request = new GetBucketRequest(_fullBucketName);
request.SetPrefix($"{dir.TrimEnd('/')}/");
request.SetDelimiter("/");
var result = _cosXml.GetBucket(request);
var info = result.listBucket;
var objects = info.contentsList;
return objects.Where(o => o.size > 0).Select(o => o.key).ToList();
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
}
public List<string> GetDirectories(string dir)
{
var dirs = new List<string>();
try
{
var request = new GetBucketRequest(_fullBucketName);
request.SetPrefix($"{dir.TrimEnd('/')}/");
request.SetDelimiter("/");
var result = _cosXml.GetBucket(request);
var info = result.listBucket;
var objects = info.contentsList;
var list = objects.Where(o => o.size == 0 && o.key != dir).Select(o => o.key).ToList();
dirs.AddRange(list);
var commonPrefixes = info.commonPrefixesList;
dirs.AddRange(commonPrefixes.Select(c => c.prefix));
return dirs;
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
}
public bool DirExists(string dir)
{
try
{
var request = new GetBucketRequest(_fullBucketName);
request.SetPrefix($"{dir.TrimEnd('/')}/");
request.SetDelimiter("/");
var result = _cosXml.GetBucket(request);
var info = result.listBucket;
var objects = info.contentsList;
return objects.Count > 0 || info?.commonPrefixesList.Count > 0;
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
}
public void MoveDir(string sourceDir, string destDir)
{
var listRequest = new GetBucketRequest(_fullBucketName);
listRequest.SetPrefix($"{sourceDir.TrimEnd('/')}/");
var listResult = _cosXml.GetBucket(listRequest);
var info = listResult.listBucket;
var objects = info.contentsList;
foreach (var obj in objects)
{
string sourceKey = obj.key;
string destinationKey = $"{destDir.TrimEnd('/')}/{sourceKey.Substring(sourceDir.Length)}";
var copySource = new CopySourceStruct(_appId, _fullBucketName, _region, sourceKey);
var request = new CopyObjectRequest(_fullBucketName, destinationKey);
request.SetCopySource(copySource);
try
{
var result = _cosXml.CopyObject(request);
var deleteRequest = new DeleteObjectRequest(_fullBucketName, sourceKey);
var deleteResult = _cosXml.DeleteObject(deleteRequest);
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
}
}
public void DeleteDir(string dir)
{
try
{
string nextMarker = null;
do
{
var listRequest = new GetBucketRequest(_fullBucketName);
listRequest.SetPrefix($"{dir.TrimEnd('/')}/");
listRequest.SetMarker(nextMarker);
var listResult = _cosXml.GetBucket(listRequest);
var info = listResult.listBucket;
List<ListBucket.Contents> objects = info.contentsList;
nextMarker = info.nextMarker;
var deleteRequest = new DeleteMultiObjectRequest(_fullBucketName);
deleteRequest.SetDeleteQuiet(false);
var deleteObjects = new List<string>();
foreach (var content in objects)
{
deleteObjects.Add(content.key);
}
deleteRequest.SetObjectKeys(deleteObjects);
var deleteResult = _cosXml.DeleteMultiObjects(deleteRequest);
} while (nextMarker != null);
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
}
public bool DoesObjectExist(string key)
{
var request = new DoesObjectExistRequest(_fullBucketName, key);
return _cosXml.DoesObjectExist(request);
}
}
}

View file

@ -0,0 +1,70 @@
using Microsoft.AspNetCore.StaticFiles;
namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService
{
public string GetDirectory(string conversationId)
{
return $"{CONVERSATION_FOLDER}/{conversationId}/attachments/";
}
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));
}
public string GetFileContentType(string filePath)
{
string contentType;
var provider = new FileExtensionContentTypeProvider();
if (!provider.TryGetContentType(filePath, out contentType))
{
contentType = string.Empty;
}
return contentType;
}
public byte[] GetFileBytes(string fileStorageUrl)
{
try
{
var fileData = _cosClient.BucketClient.DownloadFileBytes(fileStorageUrl);
return fileData;
}
catch (Exception ex)
{
_logger.LogWarning($"Error when get file bytes: {ex.Message}\r\n{ex.InnerException}");
}
return Array.Empty<byte>();
}
public bool SavefileToPath(string filePath, Stream stream)
{
if (string.IsNullOrEmpty(filePath)) return false;
try
{
return _cosClient.BucketClient.UploadStream(filePath, stream);
}
catch (Exception ex)
{
_logger.LogWarning($"Error when saving file to path: {ex.Message}\r\n{ex.InnerException}");
return false;
}
}
}

View file

@ -0,0 +1,350 @@
using BotSharp.Abstraction.Files.Converters;
using BotSharp.Abstraction.Files.Enums;
using System.Net.Mime;
namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService
{
public async Task<IEnumerable<MessageFileModel>> GetChatFiles(string conversationId, string source,
IEnumerable<RoleDialogModel> conversations, IEnumerable<string> contentTypes,
bool includeScreenShot = false, int? offset = null)
{
var files = new List<MessageFileModel>();
if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty())
{
return files;
}
var messageIds = GetMessageIds(conversations, offset);
var pathPrefix = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}";
foreach (var messageId in messageIds)
{
var dir = $"{pathPrefix}/{messageId}/{source}";
foreach (var subDir in _cosClient.BucketClient.GetDirectories(dir))
{
var file = _cosClient.BucketClient.GetDirFiles(subDir).FirstOrDefault();
if (file == null) continue;
var contentType = GetFileContentType(file);
if (contentTypes?.Contains(contentType) != true) continue;
var foundFiles = await GetMessageFiles(file, subDir, contentType, messageId, source, includeScreenShot);
if (foundFiles.IsNullOrEmpty()) continue;
files.AddRange(foundFiles);
}
}
return files;
}
public IEnumerable<MessageFileModel> GetMessageFiles(string conversationId, IEnumerable<string> messageIds,
string source, bool imageOnly = false)
{
var files = new List<MessageFileModel>();
if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return files;
foreach (var messageId in messageIds)
{
var dir = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{messageId}/{source}";
if (!ExistDirectory(dir))
{
continue;
}
foreach (var subDir in _cosClient.BucketClient.GetDirectories(dir))
{
foreach (var file in _cosClient.BucketClient.GetDirFiles(subDir))
{
var contentType = GetFileContentType(file);
if (imageOnly && !_imageTypes.Contains(contentType))
{
continue;
}
var fileName = Path.GetFileNameWithoutExtension(file);
var fileType = Path.GetExtension(file).Substring(1);
var model = new MessageFileModel()
{
MessageId = messageId,
FileUrl = $"https://{_fullBuketName}.cos.{_settings.Region}.myqcloud.com/{file}",
FileStorageUrl = file,
FileName = fileName,
FileType = fileType,
ContentType = contentType,
FileSource = source
};
files.Add(model);
}
}
}
return files;
}
public string GetMessageFile(string conversationId, string messageId, string source, string index, string fileName)
{
var dir = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{source}/{index}/";
var fileList = _cosClient.BucketClient.GetDirFiles(dir);
var found = fileList.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName));
return found;
}
public IEnumerable<MessageFileModel> GetMessagesWithFile(string conversationId, IEnumerable<string> messageIds)
{
var foundMsgs = new List<MessageFileModel>();
if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return foundMsgs;
foreach (var messageId in messageIds)
{
var prefix = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{messageId}";
var userDir = $"{prefix}/{FileSourceType.User}/";
if (ExistDirectory(userDir))
{
foundMsgs.Add(new MessageFileModel { MessageId = messageId, FileSource = FileSourceType.User });
}
var botDir = $"{prefix}/{FileSourceType.Bot}";
if (ExistDirectory(botDir))
{
foundMsgs.Add(new MessageFileModel { MessageId = messageId, FileSource = FileSourceType.Bot });
}
}
return foundMsgs;
}
public bool SaveMessageFiles(string conversationId, string messageId, string source, List<BotSharpFile> files)
{
if (files.IsNullOrEmpty()) return false;
var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true);
for (int i = 0; i < files.Count; i++)
{
var file = files[i];
if (string.IsNullOrEmpty(file.FileData))
{
continue;
}
try
{
var (_, bytes) = GetFileInfoFromData(file.FileData);
var subDir = $"{dir}/{source}/{i + 1}";
_cosClient.BucketClient.UploadBytes($"{subDir}/{file.FileName}", bytes);
}
catch (Exception ex)
{
_logger.LogWarning($"Error when saving message file {file.FileName}: {ex.Message}\r\n{ex.InnerException}");
continue;
}
}
return true;
}
public bool DeleteMessageFiles(string conversationId, IEnumerable<string> messageIds, string targetMessageId, string? newMessageId = null)
{
if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false;
if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId))
{
var prevDir = GetConversationFileDirectory(conversationId, targetMessageId);
var newDir = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{newMessageId}/";
if (ExistDirectory(prevDir))
{
if (ExistDirectory(newDir))
{
_cosClient.BucketClient.DeleteDir(newDir);
}
_cosClient.BucketClient.MoveDir(prevDir, newDir);
var botDir = $"{newDir}/{BOT_FILE_FOLDER}";
if (ExistDirectory(botDir))
{
_cosClient.BucketClient.DeleteDir(newDir);
}
}
}
foreach (var messageId in messageIds)
{
var dir = GetConversationFileDirectory(conversationId, messageId);
if (!ExistDirectory(dir)) continue;
_cosClient.BucketClient.DeleteDir(dir);
}
return true;
}
public bool DeleteConversationFiles(IEnumerable<string> conversationIds)
{
if (conversationIds.IsNullOrEmpty()) return false;
foreach (var conversationId in conversationIds)
{
var convDir = GetConversationDirectory(conversationId);
if (!ExistDirectory(convDir)) continue;
_cosClient.BucketClient.DeleteDir(convDir);
}
return true;
}
#region Private methods
private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false)
{
if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId))
{
return string.Empty;
}
return $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{messageId}";
}
private string? GetConversationDirectory(string conversationId)
{
if (string.IsNullOrEmpty(conversationId)) return null;
var dir = $"{CONVERSATION_FOLDER}/{conversationId}";
return dir;
}
private IEnumerable<string> GetMessageIds(IEnumerable<RoleDialogModel> conversations, int? offset = null)
{
if (conversations.IsNullOrEmpty()) return Enumerable.Empty<string>();
if (offset <= 0)
{
offset = MIN_OFFSET;
}
else if (offset > MAX_OFFSET)
{
offset = MAX_OFFSET;
}
var messageIds = new List<string>();
if (offset.HasValue)
{
messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset.Value).ToList();
}
else
{
messageIds = conversations.Select(x => x.MessageId).Distinct().ToList();
}
return messageIds;
}
private async Task<IEnumerable<MessageFileModel>> GetMessageFiles(string file, string fileDir, string contentType,
string messageId, string source, bool includeScreenShot)
{
var files = new List<MessageFileModel>();
try
{
if (!_imageTypes.Contains(contentType) && includeScreenShot)
{
var screenShotDir = $"{fileDir}/{SCREENSHOT_FILE_FOLDER}/";
var fileList = _cosClient.BucketClient.GetDirFiles(screenShotDir);
if (!fileList.IsNullOrEmpty())
{
foreach (var screenShot in fileList)
{
contentType = GetFileContentType(screenShot);
if (!_imageTypes.Contains(contentType)) continue;
var fileName = Path.GetFileNameWithoutExtension(screenShot);
var fileType = Path.GetExtension(file).Substring(1);
var model = new MessageFileModel()
{
MessageId = messageId,
FileName = fileName,
FileType = fileType,
FileStorageUrl = screenShot,
ContentType = contentType,
FileSource = source
};
files.Add(model);
}
}
else if (contentType == MediaTypeNames.Application.Pdf)
{
var images = await ConvertPdfToImages(file, screenShotDir);
foreach (var image in images)
{
contentType = GetFileContentType(image);
var fileName = Path.GetFileNameWithoutExtension(image);
var fileType = Path.GetExtension(image).Substring(1);
var model = new MessageFileModel()
{
MessageId = messageId,
FileName = fileName,
FileType = fileType,
FileStorageUrl = image,
ContentType = contentType,
FileSource = source
};
files.Add(model);
}
}
}
else
{
var fileName = Path.GetFileNameWithoutExtension(file);
var fileType = Path.GetExtension(file).Substring(1);
var model = new MessageFileModel()
{
MessageId = messageId,
FileName = fileName,
FileType = fileType,
FileStorageUrl = file,
ContentType = contentType,
FileSource = source
};
files.Add(model);
}
return files;
}
catch (Exception ex)
{
_logger.LogWarning($"Error when getting message files {file} (messageId: {messageId}), Error: {ex.Message}\r\n{ex.InnerException}");
return files;
}
}
private async Task<IEnumerable<string>> ConvertPdfToImages(string pdfLoc, string imageLoc)
{
var converters = _services.GetServices<IPdf2ImageConverter>();
if (converters.IsNullOrEmpty()) return Enumerable.Empty<string>();
var converter = GetPdf2ImageConverter();
if (converter == null)
{
return Enumerable.Empty<string>();
}
return await converter.ConvertPdfToImages(pdfLoc, imageLoc);
}
private IPdf2ImageConverter? GetPdf2ImageConverter()
{
var converters = _services.GetServices<IPdf2ImageConverter>();
return converters.FirstOrDefault();
}
#endregion
}

View file

@ -0,0 +1,107 @@
namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService
{
public async Task<RoleDialogModel> GenerateImage(string? provider, string? model, string text)
{
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-3");
var message = await completion.GetImageGeneration(new Agent()
{
Id = Guid.Empty.ToString(),
}, new RoleDialogModel(AgentRole.User, text));
return message;
}
public async Task<RoleDialogModel> VaryImage(string? provider, string? model, BotSharpFile image)
{
if (string.IsNullOrWhiteSpace(image?.FileUrl) && string.IsNullOrWhiteSpace(image?.FileData))
{
throw new ArgumentException($"Cannot find image url or data!");
}
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
var bytes = await DownloadFile(image);
using var stream = new MemoryStream();
stream.Write(bytes, 0, bytes.Length);
stream.Position = 0;
var message = await completion.GetImageVariation(new Agent()
{
Id = Guid.Empty.ToString()
}, new RoleDialogModel(AgentRole.User, string.Empty), stream, image.FileName ?? string.Empty);
stream.Close();
return message;
}
public async Task<RoleDialogModel> EditImage(string? provider, string? model, string text, BotSharpFile image)
{
if (string.IsNullOrWhiteSpace(image?.FileUrl) && string.IsNullOrWhiteSpace(image?.FileData))
{
throw new ArgumentException($"Cannot find image url or data!");
}
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
var bytes = await DownloadFile(image);
using var stream = new MemoryStream();
stream.Write(bytes, 0, bytes.Length);
stream.Position = 0;
var message = await completion.GetImageEdits(new Agent()
{
Id = Guid.Empty.ToString()
}, new RoleDialogModel(AgentRole.User, text), stream, image.FileName ?? string.Empty);
stream.Close();
return message;
}
public async Task<RoleDialogModel> EditImage(string? provider, string? model, string text, BotSharpFile image, BotSharpFile mask)
{
if ((string.IsNullOrWhiteSpace(image?.FileUrl) && string.IsNullOrWhiteSpace(image?.FileData)) ||
(string.IsNullOrWhiteSpace(mask?.FileUrl) && string.IsNullOrWhiteSpace(mask?.FileData)))
{
throw new ArgumentException($"Cannot find image/mask url or data");
}
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
var imageBytes = await DownloadFile(image);
var maskBytes = await DownloadFile(mask);
using var imageStream = new MemoryStream();
imageStream.Write(imageBytes, 0, imageBytes.Length);
imageStream.Position = 0;
using var maskStream = new MemoryStream();
maskStream.Write(maskBytes, 0, maskBytes.Length);
maskStream.Position = 0;
var message = await completion.GetImageEdits(new Agent()
{
Id = Guid.Empty.ToString()
}, new RoleDialogModel(AgentRole.User, text), imageStream, image.FileName ?? string.Empty, maskStream, mask.FileName ?? string.Empty);
imageStream.Close();
maskStream.Close();
return message;
}
#region Private methods
private async Task<byte[]> DownloadFile(BotSharpFile file)
{
var bytes = new byte[0];
if (!string.IsNullOrEmpty(file.FileUrl))
{
var http = _services.GetRequiredService<IHttpClientFactory>();
using var client = http.CreateClient();
bytes = await client.GetByteArrayAsync(file.FileUrl);
}
else if (!string.IsNullOrEmpty(file.FileData))
{
(_, bytes) = GetFileInfoFromData(file.FileData);
}
return bytes;
}
#endregion
}

View file

@ -0,0 +1,130 @@
namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService
{
public async Task<string> ReadPdf(string? provider, string? model, string? modelId, string prompt, List<BotSharpFile> files)
{
var content = string.Empty;
if (string.IsNullOrWhiteSpace(prompt) || files.IsNullOrEmpty())
{
return content;
}
var guid = Guid.NewGuid().ToString();
var sessionDir = GetSessionDirectory(guid);
try
{
var pdfFiles = await DownloadFiles(sessionDir, files);
var images = await ConvertPdfToImages(pdfFiles);
if (images.IsNullOrEmpty()) return content;
var completion = CompletionProvider.GetChatCompletion(_services, provider: provider ?? "openai",
model: model, modelId: modelId ?? "gpt-4", multiModal: true);
var message = await completion.GetChatCompletions(new Agent()
{
Id = Guid.Empty.ToString(),
}, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, prompt)
{
Files = images.Select(x => new BotSharpFile { FileStorageUrl = x }).ToList()
}
});
content = message.Content;
return content;
}
catch (Exception ex)
{
_logger.LogError($"Error when analyzing pdf in file service: {ex.Message}\r\n{ex.InnerException}");
return content;
}
finally
{
Directory.Delete(sessionDir, true);
}
}
#region Private methods
private string GetSessionDirectory(string id)
{
var dir = $"{SESSION_FOLDER}/{id}";
return dir;
}
private async Task<IEnumerable<string>> DownloadFiles(string dir, List<BotSharpFile> files, string extension = "pdf")
{
if (string.IsNullOrWhiteSpace(dir) || files.IsNullOrEmpty())
{
return Enumerable.Empty<string>();
}
var locs = new List<string>();
foreach (var file in files)
{
try
{
var bytes = new byte[0];
if (!string.IsNullOrEmpty(file.FileUrl))
{
var http = _services.GetRequiredService<IHttpClientFactory>();
using var client = http.CreateClient();
bytes = await client.GetByteArrayAsync(file.FileUrl);
}
else if (!string.IsNullOrEmpty(file.FileData))
{
(_, bytes) = GetFileInfoFromData(file.FileData);
}
if (!bytes.IsNullOrEmpty())
{
var guid = Guid.NewGuid().ToString();
var fileDir = $"{dir}/{guid}";
var pdfDir = $"{fileDir}/{guid}.{extension}";
_cosClient.BucketClient.UploadBytes(pdfDir, bytes);
locs.Add(pdfDir);
}
}
catch (Exception ex)
{
_logger.LogWarning($"Error when saving pdf file: {ex.Message}\r\n{ex.InnerException}");
continue;
}
}
return locs;
}
private async Task<IEnumerable<string>> ConvertPdfToImages(IEnumerable<string> files)
{
var images = new List<string>();
var converter = GetPdf2ImageConverter();
if (converter == null || files.IsNullOrEmpty())
{
return images;
}
foreach (var file in files)
{
try
{
var segs = file.Split(Path.DirectorySeparatorChar);
var dir = string.Join(Path.DirectorySeparatorChar, segs.SkipLast(1));
var folder = Path.Combine(dir, "screenshots");
var urls = await converter.ConvertPdfToImages(file, folder);
images.AddRange(urls);
}
catch (Exception ex)
{
_logger.LogWarning($"Error when converting pdf file to images ({file}): {ex.Message}\r\n{ex.InnerException}");
continue;
}
}
return images;
}
#endregion
}

View file

@ -0,0 +1,58 @@
namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService
{
public string GetUserAvatar()
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(_user.Id);
var dir = GetUserAvatarDir(user?.Id);
if (!ExistDirectory(dir)) return string.Empty;
var found = _cosClient.BucketClient.GetDirFiles(dir).FirstOrDefault() ?? string.Empty;
return found;
}
public bool SaveUserAvatar(BotSharpFile file)
{
if (file == null || string.IsNullOrEmpty(file.FileData)) return false;
try
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(_user.Id);
var dir = GetUserAvatarDir(user?.Id);
if (string.IsNullOrEmpty(dir)) return false;
var (_, bytes) = GetFileInfoFromData(file.FileData);
var extension = Path.GetExtension(file.FileName);
var fileName = user?.Id == null ? file.FileName : $"{user?.Id}{extension}";
return _cosClient.BucketClient.UploadBytes($"{dir}/{fileName}", bytes);
}
catch (Exception ex)
{
_logger.LogWarning($"Error when saving user avatar: {ex.Message}\r\n{ex.InnerException}");
return false;
}
}
#region Private methods
private string GetUserAvatarDir(string? userId, bool createNewDir = false)
{
if (string.IsNullOrEmpty(userId))
{
return string.Empty;
}
var dir = $"{USERS_FOLDER}/{userId}/{USER_AVATAR_FOLDER}/";
return dir;
}
#endregion
}

View file

@ -0,0 +1,56 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Users;
using BotSharp.Plugin.TencentCos.Settings;
using System.Net.Mime;
namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService : IBotSharpFileService
{
private readonly TencentCosSettings _settings;
private readonly IServiceProvider _services;
private readonly IUserIdentity _user;
private readonly ILogger<TencentCosService> _logger;
private readonly string _fullBuketName;
private readonly IEnumerable<string> _imageTypes = new List<string>
{
MediaTypeNames.Image.Png,
MediaTypeNames.Image.Jpeg
};
private const string CONVERSATION_FOLDER = "conversations";
private const string FILE_FOLDER = "files";
private const string USER_FILE_FOLDER = "user";
private const string SCREENSHOT_FILE_FOLDER = "screenshot";
private const string BOT_FILE_FOLDER = "bot";
private const string USERS_FOLDER = "users";
private const string USER_AVATAR_FOLDER = "avatar";
private const string SESSION_FOLDER = "sessions";
private const int MIN_OFFSET = 1;
private const int MAX_OFFSET = 5;
private readonly TencentCosClient _cosClient;
public TencentCosService(
TencentCosSettings settings,
IUserIdentity user,
ILogger<TencentCosService> logger,
IServiceProvider services,
TencentCosClient cosClient)
{
_settings = settings;
_user = user;
_logger = logger;
_services = services;
_fullBuketName = $"{_settings.BucketName}-{_settings.AppId}";
_cosClient = cosClient;
}
#region Private methods
private bool ExistDirectory(string? dir)
{
return !string.IsNullOrEmpty(dir) && _cosClient.BucketClient.DirExists(dir);
}
#endregion
}

View file

@ -0,0 +1,12 @@
namespace BotSharp.Plugin.TencentCos.Settings
{
public class TencentCosSettings
{
public string AppId { get; set; }
public string SecretId { get; set; }
public string SecretKey { get; set; }
public string Region { get; set; }
public string BucketName { get; set; }
public int KeyDurationSecond { get; set; } = 600;
}
}

View file

@ -0,0 +1,26 @@
using BotSharp.Plugin.TencentCos.Modules;
using BotSharp.Plugin.TencentCos.Settings;
using COSXML;
using COSXML.Auth;
namespace BotSharp.Plugin.TencentCos
{
public class TencentCosClient
{
public BucketClient BucketClient { get; private set; }
public TencentCosClient(TencentCosSettings settings)
{
var cosXmlConfig = new CosXmlConfig.Builder()
.IsHttps(true)
.SetAppid(settings.AppId)
.SetRegion(settings.Region)
.Build();
var cosCredentialProvider = new DefaultQCloudCredentialProvider(
settings.SecretId, settings.SecretKey, settings.KeyDurationSecond);
var cosXml = new CosXmlServer(cosXmlConfig, cosCredentialProvider);
BucketClient = new BucketClient(cosXml, $"{settings.BucketName}-{settings.AppId}", settings.AppId, settings.Region);
}
}
}

View file

@ -0,0 +1,37 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Repositories.Enums;
using BotSharp.Abstraction.Settings;
using BotSharp.Plugin.TencentCos;
using BotSharp.Plugin.TencentCos.Services;
using BotSharp.Plugin.TencentCos.Settings;
namespace BotSharp.Plugin.TencentCosFile.Files;
public class TencentCosPlugin : IBotSharpPlugin
{
public string Id => "3f55b702-8a28-4f9a-907c-affc24f845f1";
public string Name => "TencentCos";
public string Description => "Provides connection to Tencent Cloud object storage service.";
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
var myFileStorageSettings = new FileStorageSettings();
config.Bind("FileStorage", myFileStorageSettings);
if (myFileStorageSettings.Default == FileStorageEnum.TencentCosStorage)
{
services.AddScoped(provider =>
{
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<TencentCosSettings>("TencentCos");
});
services.AddScoped<TencentCosClient>();
services.AddScoped<IBotSharpFileService, TencentCosService>();
}
}
}

View file

@ -0,0 +1,17 @@
global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Agents.Models;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Files.Models;
global using BotSharp.Abstraction.Plugins;
global using BotSharp.Abstraction.Repositories;
global using BotSharp.Abstraction.Utilities;
global using BotSharp.Core.Infrastructures;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Logging;
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading.Tasks;

View file

@ -31,6 +31,7 @@
<ProjectReference Include="..\Plugins\BotSharp.Plugin.MetaGLM\BotSharp.Plugin.MetaGLM.csproj" />
<ProjectReference Include="..\Plugins\BotSharp.Plugin.Planner\BotSharp.Plugin.Planner.csproj" />
<ProjectReference Include="..\Plugins\BotSharp.Plugin.SparkDesk\BotSharp.Plugin.SparkDesk.csproj" />
<ProjectReference Include="..\Plugins\BotSharp.Plugin.TencentCos\BotSharp.Plugin.TencentCos.csproj" />
</ItemGroup>
<ItemGroup Condition="$(SolutionName)==BotSharp">

View file

@ -233,6 +233,13 @@
"FileStorage": {
"Default": "LocalFileStorage"
},
"TencentCos": {
"AppId": "",
"SecretId": "",
"SecretKey": "",
"BucketName": "",
"Region": ""
},
"Qdrant": {
"Url": "",
"ApiKey": ""
@ -306,7 +313,8 @@
"BotSharp.Plugin.MetaGLM",
"BotSharp.Plugin.HttpHandler",
"BotSharp.Plugin.FileHandler",
"BotSharp.Plugin.EmailHandler"
"BotSharp.Plugin.EmailHandler",
"BotSharp.Plugin.TencentCos"
]
}
}