resolve conflict.
This commit is contained in:
commit
52d1387121
11
BotSharp.sln
11
BotSharp.sln
|
|
@ -97,6 +97,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.OpenAI", "s
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.EmailHandler", "src\Plugins\BotSharp.Plugin.EmailHandler\BotSharp.Plugin.EmailHandler.csproj", "{A72B3BEB-E14B-4917-BE44-97EAE4E122D2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.FileHandler", "src\Plugins\BotSharp.Plugin.FileHandler\BotSharp.Plugin.FileHandler.csproj", "{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -393,6 +395,14 @@ Global
|
|||
{A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -439,6 +449,7 @@ Global
|
|||
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
|
||||
{6507D336-3A4D-41D4-81C0-2B900173A5FE} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
|
||||
{A72B3BEB-E14B-4917-BE44-97EAE4E122D2} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
|
||||
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ public class RoleDialogModel : ITrackableMessage
|
|||
{
|
||||
if (Role == AgentRole.Function)
|
||||
{
|
||||
return $"{Role}: {FunctionName}({FunctionArgs}) => {Content}";
|
||||
return $"{Role}: {Content} <= {FunctionName}({FunctionArgs})";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ namespace BotSharp.Abstraction.Files;
|
|||
public interface IBotSharpFileService
|
||||
{
|
||||
string GetDirectory(string conversationId);
|
||||
Task<IEnumerable<MessageFileModel>> GetChatImages(string conversationId, string source, IEnumerable<string> fileTypes, List<RoleDialogModel> conversations, int? offset = null);
|
||||
Task<IEnumerable<MessageFileModel>> GetChatImages(string conversationId, string source,
|
||||
IEnumerable<RoleDialogModel> conversations, IEnumerable<string> contentTypes,
|
||||
bool includeScreenShot = false, int? offset = null);
|
||||
IEnumerable<MessageFileModel> GetMessageFiles(string conversationId, IEnumerable<string> messageIds, string source, bool imageOnly = false);
|
||||
string GetMessageFile(string conversationId, string messageId, string source, string index, string fileName);
|
||||
IEnumerable<MessageFileModel> GetMessagesWithFile(string conversationId, IEnumerable<string> messageIds);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
|
|
@ -48,10 +48,8 @@
|
|||
<ItemGroup>
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\agent.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\generate_image.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\read_file.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\instruction.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\generate_image.fn.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\read_file.fn.liquid" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\agent.json" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\functions.json" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\functions\human_intervention_needed.json" />
|
||||
|
|
@ -159,12 +157,6 @@
|
|||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\instruction.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\read_file.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\read_file.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\generate_image.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
@ -191,7 +183,6 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Nanoid" Version="3.1.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -65,14 +65,14 @@ public class EvaluatingService : IEvaluatingService
|
|||
|
||||
if (roundCount > 10)
|
||||
{
|
||||
Console.WriteLine($"Conversation ended due to execced max round count {roundCount}", Color.Red);
|
||||
Console.WriteLine($"Conversation ended due to execced max round count {roundCount}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.FunctionName == "conversation_end" ||
|
||||
response.FunctionName == "human_intervention_needed")
|
||||
{
|
||||
Console.WriteLine($"Conversation ended by function {response.FunctionName}", Color.Green);
|
||||
Console.WriteLine($"Conversation ended by function {response.FunctionName}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ public class FilePlugin : IBotSharpPlugin
|
|||
{
|
||||
services.AddScoped<IBotSharpFileService, BotSharpFileService>();
|
||||
|
||||
services.AddScoped<IAgentHook, FileReaderHook>();
|
||||
services.AddScoped<IAgentUtilityHook, FileReaderUtilityHook>();
|
||||
services.AddScoped<IAgentHook, ImageGeneratorHook>();
|
||||
services.AddScoped<IAgentUtilityHook, ImageGeneratorUtilityHook>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
namespace BotSharp.Core.Files.Hooks;
|
||||
|
||||
public class FileReaderHook : AgentHookBase
|
||||
{
|
||||
private static string FUNCTION_NAME = "read_file";
|
||||
|
||||
public override string SelfId => string.Empty;
|
||||
|
||||
public FileReaderHook(IServiceProvider services, AgentSettings settings)
|
||||
: base(services, settings)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnAgentLoaded(Agent agent)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var isConvMode = conv.IsConversationMode();
|
||||
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(AgentUtility.FileReader);
|
||||
|
||||
if (isConvMode && isEnabled)
|
||||
{
|
||||
var (prompt, fn) = GetPromptAndFunction();
|
||||
if (fn != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n";
|
||||
}
|
||||
|
||||
if (agent.Functions == null)
|
||||
{
|
||||
agent.Functions = new List<FunctionDef> { fn };
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.Functions.Add(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.OnAgentLoaded(agent);
|
||||
}
|
||||
|
||||
private (string, FunctionDef?) GetPromptAndFunction()
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant);
|
||||
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty;
|
||||
var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME));
|
||||
return (prompt, loadAttachmentFn);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
namespace BotSharp.Core.Files.Hooks;
|
||||
|
||||
public class FileReaderUtilityHook : IAgentUtilityHook
|
||||
{
|
||||
public void AddUtilities(List<string> utilities)
|
||||
{
|
||||
utilities.Add(AgentUtility.FileReader);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +1,43 @@
|
|||
using AspectInjector.Broker;
|
||||
using BotSharp.Abstraction.Files.Converters;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace BotSharp.Core.Files.Services;
|
||||
|
||||
public partial class BotSharpFileService
|
||||
{
|
||||
public async Task<IEnumerable<MessageFileModel>> GetChatImages(string conversationId, string source, IEnumerable<string> fileTypes,
|
||||
List<RoleDialogModel> conversations, int? offset = null)
|
||||
public async Task<IEnumerable<MessageFileModel>> GetChatImages(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 new List<MessageFileModel>();
|
||||
return files;
|
||||
}
|
||||
|
||||
if (offset <= 0)
|
||||
var messageIds = GetMessageIds(conversations, offset);
|
||||
var pathPrefix = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER);
|
||||
|
||||
foreach (var messageId in messageIds)
|
||||
{
|
||||
offset = MIN_OFFSET;
|
||||
}
|
||||
else if (offset > MAX_OFFSET)
|
||||
{
|
||||
offset = MAX_OFFSET;
|
||||
}
|
||||
var dir = Path.Combine(pathPrefix, messageId, source);
|
||||
if (!ExistDirectory(dir)) continue;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
files = await GetMessageFiles(conversationId, messageIds, source, fileTypes);
|
||||
return files;
|
||||
}
|
||||
|
||||
private async Task<List<MessageFileModel>> GetMessageFiles(string conversationId, IEnumerable<string> messageIds, string source, IEnumerable<string> fileTypes)
|
||||
{
|
||||
var files = new List<MessageFileModel>();
|
||||
if (string.IsNullOrEmpty(conversationId) || messageIds.IsNullOrEmpty() || fileTypes.IsNullOrEmpty()) return files;
|
||||
|
||||
var isNeedScreenShot = fileTypes.Any(x => _allowScreenShotTypes.Contains(x));
|
||||
var onlyScreenShot = fileTypes.All(x => _allowScreenShotTypes.Contains(x));
|
||||
|
||||
try
|
||||
{
|
||||
var preFixPath = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER);
|
||||
|
||||
foreach (var messageId in messageIds)
|
||||
foreach (var subDir in Directory.GetDirectories(dir))
|
||||
{
|
||||
var dir = Path.Combine(preFixPath, messageId, source);
|
||||
if (!ExistDirectory(dir)) continue;
|
||||
var file = Directory.GetFiles(subDir).FirstOrDefault();
|
||||
if (file == null) continue;
|
||||
|
||||
foreach (var subDir in Directory.GetDirectories(dir))
|
||||
{
|
||||
var file = Directory.GetFiles(subDir).FirstOrDefault();
|
||||
if (file == null) continue;
|
||||
var contentType = GetFileContentType(file);
|
||||
if (contentTypes?.Contains(contentType) != true) continue;
|
||||
|
||||
var index = subDir.Split(Path.DirectorySeparatorChar).Last();
|
||||
var contentType = GetFileContentType(file);
|
||||
var foundFiles = await GetMessageFiles(file, subDir, contentType, messageId, source, includeScreenShot);
|
||||
if (foundFiles.IsNullOrEmpty()) continue;
|
||||
|
||||
if ((!isNeedScreenShot || (isNeedScreenShot && !onlyScreenShot)) && _allowedImageTypes.Contains(contentType))
|
||||
{
|
||||
var model = new MessageFileModel()
|
||||
{
|
||||
MessageId = messageId,
|
||||
FileStorageUrl = file,
|
||||
ContentType = contentType
|
||||
};
|
||||
files.Add(model);
|
||||
}
|
||||
else if ((isNeedScreenShot && !onlyScreenShot || onlyScreenShot) && !_allowedImageTypes.Contains(contentType))
|
||||
{
|
||||
var screenShotDir = Path.Combine(subDir, SCREENSHOT_FILE_FOLDER);
|
||||
if (ExistDirectory(screenShotDir) && Directory.GetFiles(screenShotDir).Any())
|
||||
{
|
||||
foreach (var screenShot in Directory.GetFiles(screenShotDir))
|
||||
{
|
||||
contentType = GetFileContentType(screenShot);
|
||||
if (!_allowedImageTypes.Contains(contentType)) continue;
|
||||
|
||||
var model = new MessageFileModel()
|
||||
{
|
||||
MessageId = messageId,
|
||||
FileStorageUrl = screenShot,
|
||||
ContentType = contentType
|
||||
};
|
||||
files.Add(model);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var screenShotPath = Path.Combine(subDir, SCREENSHOT_FILE_FOLDER);
|
||||
var images = await ConvertPdfToImages(file, screenShotPath);
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
contentType = GetFileContentType(image);
|
||||
var model = new MessageFileModel()
|
||||
{
|
||||
MessageId = messageId,
|
||||
FileStorageUrl = image,
|
||||
ContentType = contentType
|
||||
};
|
||||
files.Add(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
files.AddRange(foundFiles);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning($"Error when reading conversation ({conversationId}) files: {ex.Message}\r\n{ex.InnerException}\r\n{ex.StackTrace}");
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
|
@ -144,7 +63,7 @@ public partial class BotSharpFileService
|
|||
foreach (var file in Directory.GetFiles(subDir))
|
||||
{
|
||||
var contentType = GetFileContentType(file);
|
||||
if (imageOnly && !_allowedImageTypes.Contains(contentType))
|
||||
if (imageOnly && !_imageTypes.Contains(contentType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -327,6 +246,102 @@ public partial class BotSharpFileService
|
|||
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 = Path.Combine(fileDir, SCREENSHOT_FILE_FOLDER);
|
||||
if (ExistDirectory(screenShotDir) && !Directory.GetFiles(screenShotDir).IsNullOrEmpty())
|
||||
{
|
||||
foreach (var screenShot in Directory.GetFiles(screenShotDir))
|
||||
{
|
||||
contentType = GetFileContentType(screenShot);
|
||||
if (!_imageTypes.Contains(contentType)) continue;
|
||||
|
||||
var model = new MessageFileModel()
|
||||
{
|
||||
MessageId = messageId,
|
||||
FileName = Path.GetFileName(screenShot),
|
||||
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 model = new MessageFileModel()
|
||||
{
|
||||
MessageId = messageId,
|
||||
FileName = Path.GetFileName(image),
|
||||
FileStorageUrl = image,
|
||||
ContentType = contentType,
|
||||
FileSource = source
|
||||
};
|
||||
files.Add(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var model = new MessageFileModel()
|
||||
{
|
||||
MessageId = messageId,
|
||||
FileName = Path.GetFileName(file),
|
||||
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>();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
|
||||
namespace BotSharp.Core.Files.Services;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ public partial class BotSharpFileService : IBotSharpFileService
|
|||
private readonly IUserIdentity _user;
|
||||
private readonly ILogger<BotSharpFileService> _logger;
|
||||
private readonly string _baseDir;
|
||||
private readonly IEnumerable<string> _allowedImageTypes = new List<string> { "image/png", "image/jpeg" };
|
||||
private readonly IEnumerable<string> _allowScreenShotTypes = new List<string> { "pdf" };
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public class PluginLoader
|
|||
|
||||
if (!_plugins.Any(x => x.Assembly == plugin))
|
||||
{
|
||||
Console.WriteLine($"Load dependent plugin {plugin} failed by {module.Name}.", Color.Red);
|
||||
Console.WriteLine($"Load dependent plugin {plugin} failed by {module.Name}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ public class PluginLoader
|
|||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Can't find assemble {assemblyPath}.", Color.Red);
|
||||
Console.WriteLine($"Can't find assemble {assemblyPath}.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ public class PluginLoader
|
|||
AgentIds = module.AgentIds
|
||||
});
|
||||
Console.Write($"Loaded plugin ");
|
||||
Console.Write(name, Color.Green);
|
||||
Console.Write(name);
|
||||
Console.WriteLine($" from {assembly}.");
|
||||
if (!string.IsNullOrEmpty(module.Description))
|
||||
{
|
||||
|
|
@ -258,7 +258,7 @@ public class PluginLoader
|
|||
{
|
||||
if (_modules.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"No plugin loaded. Please check whether the Load() method is called.", Color.Yellow);
|
||||
Console.WriteLine($"No plugin loaded. Please check whether the Load() method is called.");
|
||||
}
|
||||
|
||||
_modules.ForEach(module =>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ public partial class RouteToAgentFn : IFunctionCallback
|
|||
// Stack redirection agent
|
||||
_context.Push(agentId, reason: $"REDIRECTION {reason}");
|
||||
message.Content = reason;
|
||||
message.Role = AgentRole.Function;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,12 +59,8 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
|
|||
message.FunctionArgs = JsonSerializer.Serialize(inst);
|
||||
if (message.FunctionName != null)
|
||||
{
|
||||
var msg = RoleDialogModel.From(message);
|
||||
var msg = RoleDialogModel.From(message, role: AgentRole.Function);
|
||||
var ret = await routing.InvokeFunction(message.FunctionName, msg);
|
||||
if (msg.Role == AgentRole.Function)
|
||||
{
|
||||
_dialogs.Add(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var agentId = routing.Context.GetCurrentAgentId();
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ public partial class RoutingService
|
|||
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "redirect_to", record.Name);
|
||||
agentId = routingRule.RedirectTo;
|
||||
#if DEBUG
|
||||
Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***", Color.Yellow);
|
||||
Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***");
|
||||
#else
|
||||
logger.LogInformation($"*** Routing redirect to {record.Name.ToUpper()} ***");
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ global using System.Text;
|
|||
global using System.Threading.Tasks;
|
||||
global using System.Linq;
|
||||
global using System.Text.Json;
|
||||
global using System.Net.Mime;
|
||||
global using System.Net.Http;
|
||||
global using System.Threading;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
global using EntityFrameworkCore.BootKit;
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "read_file",
|
||||
"description": "If the user's request is related to analyzing files and/or images, you can call this function to analyze files and images.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_request": {
|
||||
"type": "string",
|
||||
"description": "The request posted by user, which is related to analyzing requested files. User can request for multiple files to process at one time."
|
||||
},
|
||||
"file_types": {
|
||||
"type": "string",
|
||||
"description": "The file types requested by user to analyze, such as image, png, jpeg, and pdf. There can be multiple file types in a single request. An example output is, 'image,pdf'."
|
||||
}
|
||||
},
|
||||
"required": [ "user_request", "file_types" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
Please call read_file if user wants to describe files, such as images, pdf.
|
||||
|
|
@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace BotSharp.Plugin.EmailHandler.Enums
|
||||
{
|
||||
public class Utility
|
||||
public class UtilityName
|
||||
{
|
||||
public const string EmailHandler = "email-handler";
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ public class EmailHandlerHook : AgentHookBase
|
|||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var isConvMode = conv.IsConversationMode();
|
||||
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(Utility.EmailHandler);
|
||||
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.EmailHandler);
|
||||
|
||||
if (isConvMode && isEnabled)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace BotSharp.Plugin.EmailHandler.Hooks
|
|||
{
|
||||
public void AddUtilities(List<string> utilities)
|
||||
{
|
||||
utilities.Add(Utility.EmailHandler);
|
||||
utilities.Add(UtilityName.EmailHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>$(LangVersion)</LangVersion>
|
||||
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
|
||||
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
|
||||
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
|
||||
<OutputPath>$(SolutionDir)packages</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\read_image.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\read_pdf.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\read_image.fn.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\read_pdf.fn.liquid" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\read_image.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\read_image.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\read_pdf.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\read_pdf.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace BotSharp.Plugin.FileHandler.Enums;
|
||||
|
||||
public class UtilityName
|
||||
{
|
||||
public const string ImageReader = "image-reader";
|
||||
public const string PdfReader = "pdf-reader";
|
||||
}
|
||||
26
src/Plugins/BotSharp.Plugin.FileHandler/FileHandlerPlugin.cs
Normal file
26
src/Plugins/BotSharp.Plugin.FileHandler/FileHandlerPlugin.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using BotSharp.Plugin.FileHandler.Hooks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BotSharp.Plugin.FileHandler;
|
||||
|
||||
public class FileHandlerPlugin : IBotSharpPlugin
|
||||
{
|
||||
public string Id => "65be5aee-48df-4ff8-a50a-05c8bcd2a793";
|
||||
public string Name => "File Handler";
|
||||
public string Description => "AI reads files, such as image, pdf, excel";
|
||||
public string IconUrl => "https://lirp.cdn-website.com/6f8d6d8a/dms3rep/multi/opt/API_Icon-640w.png";
|
||||
public string[] AgentIds => [];
|
||||
|
||||
public void RegisterDI(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddScoped(provider =>
|
||||
{
|
||||
var settingService = provider.GetRequiredService<ISettingService>();
|
||||
return settingService.Bind<FileHandlerSettings>("FileHandler");
|
||||
});
|
||||
|
||||
services.AddScoped<IAgentHook, FileHandlerHook>();
|
||||
services.AddScoped<IAgentUtilityHook, FileHandlerUtilityHook>();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
namespace BotSharp.Plugin.FileHandler.Functions;
|
||||
|
||||
public class ReadImageFn : IFunctionCallback
|
||||
{
|
||||
public string Name => "read_image";
|
||||
public string Indication => "Reading images";
|
||||
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<ReadImageFn> _logger;
|
||||
|
||||
private readonly IEnumerable<string> _imageContentTypes = new List<string>
|
||||
{
|
||||
MediaTypeNames.Image.Png,
|
||||
MediaTypeNames.Image.Jpeg,
|
||||
};
|
||||
|
||||
public ReadImageFn(
|
||||
IServiceProvider services,
|
||||
ILogger<ReadImageFn> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Execute(RoleDialogModel message)
|
||||
{
|
||||
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs);
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
|
||||
var wholeDialogs = conv.GetDialogHistory();
|
||||
var dialogs = await AssembleFiles(conv.ConversationId, wholeDialogs);
|
||||
var agent = await agentService.LoadAgent(BuiltInAgentId.UtilityAssistant);
|
||||
var fileAgent = new Agent
|
||||
{
|
||||
Id = agent?.Id ?? Guid.Empty.ToString(),
|
||||
Name = agent?.Name ?? "Unkown",
|
||||
Instruction = !string.IsNullOrWhiteSpace(args?.UserRequest) ? args.UserRequest : "Please describe the image(s).",
|
||||
TemplateDict = new Dictionary<string, object>()
|
||||
};
|
||||
|
||||
var response = await GetChatCompletion(fileAgent, dialogs);
|
||||
message.Content = response;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<List<RoleDialogModel>> AssembleFiles(string conversationId, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
if (dialogs.IsNullOrEmpty())
|
||||
{
|
||||
return new List<RoleDialogModel>();
|
||||
}
|
||||
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
var images = await fileService.GetChatImages(conversationId, FileSourceType.User, dialogs, _imageContentTypes);
|
||||
|
||||
foreach (var dialog in dialogs)
|
||||
{
|
||||
var found = images.Where(x => x.MessageId == dialog.MessageId).ToList();
|
||||
if (found.IsNullOrEmpty()) continue;
|
||||
|
||||
dialog.Files = found.Select(x => new BotSharpFile
|
||||
{
|
||||
ContentType = x.ContentType,
|
||||
FileStorageUrl = x.FileStorageUrl
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return dialogs;
|
||||
}
|
||||
|
||||
private async Task<string> GetChatCompletion(Agent agent, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var llmProviderService = _services.GetRequiredService<ILlmProviderService>();
|
||||
var provider = llmProviderService.GetProviders().FirstOrDefault(x => x == "openai");
|
||||
var model = llmProviderService.GetProviderModel(provider: provider, id: "gpt-4", multiModal: true);
|
||||
var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, model: model.Name);
|
||||
var response = await completion.GetChatCompletions(agent, dialogs);
|
||||
return response.Content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"Error when analyzing images.";
|
||||
_logger.LogWarning($"{error} {ex.Message}\r\n{ex.InnerException}");
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
using BotSharp.Abstraction.Functions;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
namespace BotSharp.Plugin.FileHandler.Functions;
|
||||
|
||||
namespace BotSharp.Core.Files.Functions;
|
||||
|
||||
public class ReadFileFn : IFunctionCallback
|
||||
public class ReadPdfFn : IFunctionCallback
|
||||
{
|
||||
public string Name => "read_file";
|
||||
public string Indication => "Reading files";
|
||||
public string Name => "read_pdf";
|
||||
public string Indication => "Reading pdf";
|
||||
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<ReadFileFn> _logger;
|
||||
private readonly IEnumerable<string> _imageTypes = new List<string> { "image", "images", "png", "jpg", "jpeg" };
|
||||
private readonly IEnumerable<string> _pdfTypes = new List<string> { "pdf" };
|
||||
private readonly ILogger<ReadPdfFn> _logger;
|
||||
|
||||
public ReadFileFn(
|
||||
private readonly IEnumerable<string> _pdfContentTypes = new List<string>
|
||||
{
|
||||
MediaTypeNames.Application.Pdf
|
||||
};
|
||||
|
||||
public ReadPdfFn(
|
||||
IServiceProvider services,
|
||||
ILogger<ReadFileFn> logger)
|
||||
ILogger<ReadPdfFn> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
|
|
@ -23,38 +23,35 @@ public class ReadFileFn : IFunctionCallback
|
|||
|
||||
public async Task<bool> Execute(RoleDialogModel message)
|
||||
{
|
||||
var args = JsonSerializer.Deserialize<LlmFileContext>(message.FunctionArgs);
|
||||
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs);
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
|
||||
|
||||
var wholeDialogs = conv.GetDialogHistory();
|
||||
var fileTypes = args?.FileTypes?.Split(",", StringSplitOptions.RemoveEmptyEntries)?.ToList() ?? new List<string>();
|
||||
var dialogs = await AssembleFiles(conv.ConversationId, wholeDialogs, fileTypes);
|
||||
var dialogs = await AssembleFiles(conv.ConversationId, wholeDialogs);
|
||||
var agent = await agentService.LoadAgent(BuiltInAgentId.UtilityAssistant);
|
||||
var fileAgent = new Agent
|
||||
{
|
||||
Id = agent?.Id ?? Guid.Empty.ToString(),
|
||||
Name = agent?.Name ?? "Unkown",
|
||||
Instruction = !string.IsNullOrWhiteSpace(args?.UserRequest) ? args.UserRequest : "Please describe the files.",
|
||||
Instruction = !string.IsNullOrWhiteSpace(args?.UserRequest) ? args.UserRequest : "Please describe the pdf file(s).",
|
||||
TemplateDict = new Dictionary<string, object>()
|
||||
};
|
||||
|
||||
var response = await GetChatCompletion(fileAgent, dialogs);
|
||||
message.Content = response;
|
||||
message.StopCompletion = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<List<RoleDialogModel>> AssembleFiles(string conversationId, List<RoleDialogModel> dialogs, List<string> fileTypes)
|
||||
private async Task<List<RoleDialogModel>> AssembleFiles(string conversationId, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
if (dialogs.IsNullOrEmpty())
|
||||
{
|
||||
return new List<RoleDialogModel>();
|
||||
}
|
||||
|
||||
var parsedTypes = ParseFileTypes(fileTypes);
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
var files = await fileService.GetChatImages(conversationId, FileSourceType.User, parsedTypes, dialogs);
|
||||
var files = await fileService.GetChatImages(conversationId, FileSourceType.User, dialogs, _pdfContentTypes, includeScreenShot: true);
|
||||
|
||||
foreach (var dialog in dialogs)
|
||||
{
|
||||
|
|
@ -71,38 +68,6 @@ public class ReadFileFn : IFunctionCallback
|
|||
return dialogs;
|
||||
}
|
||||
|
||||
private IEnumerable<string> ParseFileTypes(IEnumerable<string> fileTypes)
|
||||
{
|
||||
var imageType = "image";
|
||||
var pdfType = "pdf";
|
||||
var parsed = new List<string>();
|
||||
|
||||
if (fileTypes.IsNullOrEmpty())
|
||||
{
|
||||
return new List<string> { imageType };
|
||||
}
|
||||
|
||||
foreach (var fileType in fileTypes)
|
||||
{
|
||||
var type = fileType?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(type) || _imageTypes.Any(x => type.IsEqualTo(x)))
|
||||
{
|
||||
parsed.Add(imageType);
|
||||
}
|
||||
else if (_pdfTypes.Any(x => type.IsEqualTo(x)))
|
||||
{
|
||||
parsed.Add(pdfType);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.IsNullOrEmpty())
|
||||
{
|
||||
parsed.Add(imageType);
|
||||
}
|
||||
|
||||
return parsed.Distinct();
|
||||
}
|
||||
|
||||
private async Task<string> GetChatCompletion(Agent agent, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
try
|
||||
|
|
@ -116,8 +81,8 @@ public class ReadFileFn : IFunctionCallback
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"Error when analyzing files.";
|
||||
_logger.LogWarning($"{error} {ex.Message}");
|
||||
var error = $"Error when analyzing pdf file(s).";
|
||||
_logger.LogWarning($"{error} {ex.Message}\r\n{ex.InnerException}");
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
namespace BotSharp.Plugin.FileHandler.Hooks;
|
||||
|
||||
public class FileHandlerHook : AgentHookBase, IAgentHook
|
||||
{
|
||||
private const string READ_IMAGE_FN = "read_image";
|
||||
private const string READ_PDF_FN = "read_pdf";
|
||||
|
||||
public override string SelfId => string.Empty;
|
||||
|
||||
public FileHandlerHook(IServiceProvider services, AgentSettings settings) : base(services, settings)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnAgentLoaded(Agent agent)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var isConvMode = conv.IsConversationMode();
|
||||
|
||||
if (isConvMode)
|
||||
{
|
||||
AddUtility(agent, UtilityName.ImageReader, READ_IMAGE_FN);
|
||||
AddUtility(agent, UtilityName.PdfReader, READ_PDF_FN);
|
||||
}
|
||||
|
||||
base.OnAgentLoaded(agent);
|
||||
}
|
||||
|
||||
private void AddUtility(Agent agent, string utility, string functionName)
|
||||
{
|
||||
if (!IsEnableUtility(agent, utility)) return;
|
||||
|
||||
var (prompt, fn) = GetPromptAndFunction(functionName);
|
||||
if (fn != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n";
|
||||
}
|
||||
|
||||
if (agent.Functions == null)
|
||||
{
|
||||
agent.Functions = new List<FunctionDef> { fn };
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.Functions.Add(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEnableUtility(Agent agent, string utility)
|
||||
{
|
||||
return !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(utility);
|
||||
}
|
||||
|
||||
private (string, FunctionDef?) GetPromptAndFunction(string functionName)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant);
|
||||
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty;
|
||||
var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName));
|
||||
return (prompt, loadAttachmentFn);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace BotSharp.Plugin.FileHandler.Hooks;
|
||||
|
||||
public class FileHandlerUtilityHook : IAgentUtilityHook
|
||||
{
|
||||
public void AddUtilities(List<string> utilities)
|
||||
{
|
||||
utilities.Add(UtilityName.ImageReader);
|
||||
utilities.Add(UtilityName.PdfReader);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BotSharp.Plugin.FileHandler.LlmContexts;
|
||||
|
||||
public class LlmContextIn
|
||||
{
|
||||
[JsonPropertyName("user_request")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? UserRequest { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
namespace BotSharp.Plugin.FileHandler.Settings;
|
||||
|
||||
public class FileHandlerSettings
|
||||
{
|
||||
}
|
||||
32
src/Plugins/BotSharp.Plugin.FileHandler/Using.cs
Normal file
32
src/Plugins/BotSharp.Plugin.FileHandler/Using.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Text;
|
||||
global using System.Linq;
|
||||
global using System.Text.Json;
|
||||
global using System.Net.Mime;
|
||||
global using System.Threading.Tasks;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
global using BotSharp.Abstraction.Agents;
|
||||
global using BotSharp.Abstraction.Conversations;
|
||||
global using BotSharp.Abstraction.Plugins;
|
||||
global using BotSharp.Abstraction.Conversations.Models;
|
||||
global using BotSharp.Abstraction.Functions;
|
||||
global using BotSharp.Abstraction.Agents.Models;
|
||||
global using BotSharp.Abstraction.Agents.Enums;
|
||||
global using BotSharp.Abstraction.Files.Enums;
|
||||
global using BotSharp.Abstraction.Files.Models;
|
||||
global using BotSharp.Abstraction.Files;
|
||||
global using BotSharp.Abstraction.MLTasks;
|
||||
global using BotSharp.Abstraction.Utilities;
|
||||
global using BotSharp.Abstraction.Agents.Settings;
|
||||
global using BotSharp.Abstraction.Functions.Models;
|
||||
global using BotSharp.Abstraction.Repositories;
|
||||
global using BotSharp.Abstraction.Settings;
|
||||
global using BotSharp.Abstraction.Messaging;
|
||||
global using BotSharp.Abstraction.Messaging.Models.RichContent;
|
||||
global using BotSharp.Abstraction.Options;
|
||||
global using BotSharp.Core.Infrastructures;
|
||||
global using BotSharp.Plugin.FileHandler.Enums;
|
||||
global using BotSharp.Plugin.FileHandler.Settings;
|
||||
global using BotSharp.Plugin.FileHandler.LlmContexts;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "read_image",
|
||||
"description": "If the user's request is related to analyzing images, you can call this function to analyze images.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_request": {
|
||||
"type": "string",
|
||||
"description": "The request posted by user, which is related to analyzing requested images. User can request for multiple images to process at one time."
|
||||
}
|
||||
},
|
||||
"required": [ "user_request" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "read_pdf",
|
||||
"description": "If the user's request is related to analyzing pdf files, you can call this function to analyze pdf.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_request": {
|
||||
"type": "string",
|
||||
"description": "The request posted by user, which is related to analyzing requested a pdf file or pdf files."
|
||||
}
|
||||
},
|
||||
"required": [ "user_request" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Please call function read_image if user wants to describe an image or images.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Please call function read_pdf if user wants to describe an pdf file or pdf files.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
namespace BotSharp.Plugin.HttpHandler.Enums;
|
||||
|
||||
public class Utility
|
||||
public class UtilityName
|
||||
{
|
||||
public const string HttpHandler = "http-handler";
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -40,7 +41,6 @@ public class HandleHttpRequestFn : IFunctionCallback
|
|||
var response = await SendHttpRequest(url, method, content);
|
||||
var responseContent = await HandleHttpResponse(response);
|
||||
message.Content = responseContent;
|
||||
message.StopCompletion = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -48,7 +48,6 @@ public class HandleHttpRequestFn : IFunctionCallback
|
|||
var msg = $"Fail when sending http request. Url: {url}, method: {method}, content: {content}";
|
||||
_logger.LogWarning($"{msg}\n(Error: {ex.Message})");
|
||||
message.Content = msg;
|
||||
message.StopCompletion = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -152,7 +151,7 @@ public class HandleHttpRequestFn : IFunctionCallback
|
|||
_logger.LogWarning($"Error when build http content: {content}\n(Error: {ex.Message})");
|
||||
}
|
||||
|
||||
return new StringContent(str, Encoding.UTF8, "application/json");
|
||||
return new StringContent(str, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
}
|
||||
|
||||
private string BuildQuery(string url, string? content)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ public class HttpHandlerHook : AgentHookBase
|
|||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var isConvMode = conv.IsConversationMode();
|
||||
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(Utility.HttpHandler);
|
||||
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.HttpHandler);
|
||||
|
||||
if (isConvMode && isEnabled)
|
||||
{
|
||||
var (prompt, fn) = GetPromptAndFunction();
|
||||
var (prompt, fn) = GetPromptAndFunction(FUNCTION_NAME);
|
||||
if (fn != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(prompt))
|
||||
|
|
@ -47,12 +47,12 @@ public class HttpHandlerHook : AgentHookBase
|
|||
base.OnAgentLoaded(agent);
|
||||
}
|
||||
|
||||
private (string, FunctionDef?) GetPromptAndFunction()
|
||||
private (string, FunctionDef?) GetPromptAndFunction(string functionName)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant);
|
||||
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty;
|
||||
var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME));
|
||||
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty;
|
||||
var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName));
|
||||
return (prompt, loadAttachmentFn);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ public class HttpHandlerUtilityHook : IAgentUtilityHook
|
|||
{
|
||||
public void AddUtilities(List<string> utilities)
|
||||
{
|
||||
utilities.Add(Utility.HttpHandler);
|
||||
utilities.Add(UtilityName.HttpHandler);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Text;
|
||||
global using System.Linq;
|
||||
global using System.Text.Json;
|
||||
global using System.Threading.Tasks;
|
||||
global using BotSharp.Abstraction.Conversations;
|
||||
global using BotSharp.Abstraction.Plugins;
|
||||
global using System.Text.Json;
|
||||
global using BotSharp.Abstraction.Conversations.Models;
|
||||
global using System.Threading.Tasks;
|
||||
global using BotSharp.Abstraction.Functions;
|
||||
global using BotSharp.Abstraction.Agents.Models;
|
||||
global using BotSharp.Abstraction.Templating;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using System.Linq;
|
||||
global using BotSharp.Abstraction.Utilities;
|
||||
global using BotSharp.Abstraction.Messaging;
|
||||
global using BotSharp.Abstraction.Messaging.Models.RichContent;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<PackageReference Include="Sdcb.PaddleInference.runtime.win64.mkl" Version="2.5.1" />
|
||||
<PackageReference Include="Sdcb.PaddleOCR" Version="2.7.0.1" />
|
||||
<PackageReference Include="Sdcb.PaddleOCR.Models.LocalV3" Version="2.7.0.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.5" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using ImageMagick;
|
||||
using OpenCvSharp;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Sdcb.PaddleInference;
|
||||
using Sdcb.PaddleOCR.Models;
|
||||
using Sdcb.PaddleOCR.Models.LocalV3;
|
||||
using Sdcb.PaddleOCR;
|
||||
using System.Threading.Tasks;
|
||||
using BotSharp.Abstraction.Knowledges;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\sql_select.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\sql_executor.fn.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\agent.json" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\functions\get_table_columns.json" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\functions\lookup_dictionary.json" />
|
||||
|
|
@ -42,6 +44,12 @@
|
|||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\functions\sql_select.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\sql_select.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\sql_executor.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
6
src/Plugins/BotSharp.Plugin.SqlDriver/Enum/Utility.cs
Normal file
6
src/Plugins/BotSharp.Plugin.SqlDriver/Enum/Utility.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Plugin.SqlDriver.Enum;
|
||||
|
||||
public class Utility
|
||||
{
|
||||
public const string SqlExecutor = "sql-executor";
|
||||
}
|
||||
|
|
@ -1,12 +1,3 @@
|
|||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using BotSharp.Abstraction.Functions;
|
||||
using BotSharp.Plugin.SqlDriver.Models;
|
||||
using BotSharp.Plugin.SqlHero.Settings;
|
||||
using Dapper;
|
||||
using MySqlConnector;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BotSharp.Plugin.SqlDriver.Functions;
|
||||
|
||||
public class ExecuteQueryFn : IFunctionCallback
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Agents.Settings;
|
||||
using BotSharp.Abstraction.Functions.Models;
|
||||
using BotSharp.Abstraction.Repositories;
|
||||
|
||||
namespace BotSharp.Plugin.SqlDriver.Hooks;
|
||||
|
||||
public class SqlExecutorHook : AgentHookBase, IAgentHook
|
||||
{
|
||||
private const string SQL_EXECUTOR_TEMPLATE = "sql_executor.fn";
|
||||
private IEnumerable<string> _targetSqlExecutorFunctions = new List<string>
|
||||
{
|
||||
"sql_select"
|
||||
};
|
||||
|
||||
public override string SelfId => string.Empty;
|
||||
|
||||
public SqlExecutorHook(IServiceProvider services, AgentSettings settings) : base(services, settings)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnAgentLoaded(Agent agent)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var isConvMode = conv.IsConversationMode();
|
||||
var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(Utility.SqlExecutor);
|
||||
|
||||
if (isConvMode && isEnabled)
|
||||
{
|
||||
var (prompt, fns) = GetPromptAndFunctions();
|
||||
if (!fns.IsNullOrEmpty())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n";
|
||||
}
|
||||
|
||||
if (agent.Functions == null)
|
||||
{
|
||||
agent.Functions = fns;
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.Functions.AddRange(fns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.OnAgentLoaded(agent);
|
||||
}
|
||||
|
||||
private (string, List<FunctionDef>?) GetPromptAndFunctions()
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant);
|
||||
var fns = agent?.Functions?.Where(x => _targetSqlExecutorFunctions.Contains(x.Name))?.ToList();
|
||||
|
||||
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(SQL_EXECUTOR_TEMPLATE))?.Content ?? string.Empty;
|
||||
var dbType = GetDatabaseType();
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
prompt = render.Render(prompt, new Dictionary<string, object>
|
||||
{
|
||||
{ "db_type", dbType }
|
||||
});
|
||||
|
||||
return (prompt, fns);
|
||||
}
|
||||
|
||||
private string GetDatabaseType()
|
||||
{
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
var dbType = "MySQL";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings?.SqlServerConnectionString))
|
||||
{
|
||||
dbType = "SQL Server";
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(settings?.SqlLiteConnectionString))
|
||||
{
|
||||
dbType = "SQL Lite";
|
||||
}
|
||||
return dbType;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace BotSharp.Plugin.SqlDriver.Hooks;
|
||||
|
||||
public class SqlExecutorUtilityHook : IAgentUtilityHook
|
||||
{
|
||||
public void AddUtilities(List<string> utilities)
|
||||
{
|
||||
utilities.Add(Utility.SqlExecutor);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,21 +25,21 @@ public class SqlDriverService
|
|||
|
||||
Console.WriteLine($"{sql.Reason}");
|
||||
|
||||
Console.WriteLine(sql.Statement, Color.Yellow);
|
||||
Console.WriteLine(sql.Statement);
|
||||
foreach (var p in sql.Parameters)
|
||||
{
|
||||
Console.WriteLine($"@{p.Name} = '{p.Value}'", Color.Green);
|
||||
Console.WriteLine($"@{p.Name} = '{p.Value}'");
|
||||
}
|
||||
if (sql.Return != null)
|
||||
{
|
||||
Console.Write($"Return: ");
|
||||
if (!string.IsNullOrEmpty(sql.Return.Value))
|
||||
{
|
||||
Console.WriteLine($" {sql.Return.Value}", Color.Red);
|
||||
Console.WriteLine($" {sql.Return.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"{sql.Return.Name} as @{sql.Return.Alias}", Color.Green);
|
||||
Console.WriteLine($"{sql.Return.Name} as @{sql.Return.Alias}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,6 @@ namespace BotSharp.Plugin.SqlHero.Settings;
|
|||
public class SqlDriverSetting
|
||||
{
|
||||
public string MySqlConnectionString { get; set; }
|
||||
public string SqlServerConnectionString { get; set; }
|
||||
public string SqlLiteConnectionString { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,7 @@ public class SqlDriverPlugin : IBotSharpPlugin
|
|||
|
||||
services.AddScoped<SqlDriverService>();
|
||||
services.AddScoped<IKnowledgeHook, SqlDriverKnowledgeHook>();
|
||||
services.AddScoped<IAgentHook, SqlExecutorHook>();
|
||||
services.AddScoped<IAgentUtilityHook, SqlExecutorUtilityHook>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@ global using BotSharp.Abstraction.Knowledges.Models;
|
|||
global using BotSharp.Abstraction.Settings;
|
||||
global using BotSharp.Plugin.SqlDriver.Hooks;
|
||||
global using BotSharp.Plugin.SqlDriver.Services;
|
||||
global using BotSharp.Plugin.SqlDriver.Enum;
|
||||
global using BotSharp.Plugin.SqlHero.Settings;
|
||||
global using System.Drawing;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "sql_select",
|
||||
"description": "Get the specific value from table",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql_statement": {
|
||||
"type": "string",
|
||||
"description": "SQL statement with SELECT"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "reason"
|
||||
},
|
||||
"table": {
|
||||
"type": "string",
|
||||
"description": "related table"
|
||||
},
|
||||
"parameters": {
|
||||
"type": "array",
|
||||
"description": "data criteria for the query",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "the name and value for the parameter",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "field name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "real value inferred by the context"
|
||||
}
|
||||
},
|
||||
"required": [ "name", "value" ]
|
||||
}
|
||||
},
|
||||
"return_field": {
|
||||
"type": "object",
|
||||
"description": "the name and alias for the return field",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "field in the table"
|
||||
},
|
||||
"alias": {
|
||||
"type": "string",
|
||||
"description": "meaningful field alias"
|
||||
}
|
||||
},
|
||||
"required": [ "name", "value" ]
|
||||
}
|
||||
},
|
||||
"required": [ "sql_statement", "reason", "table", "parameters", "return_field" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
You are connecting to {{ db_type }} database. Please generate SQL statements following {{ db_type }} rules.
|
||||
|
||||
Please call function sql_select if user wants to get or retrieve data from data tables.
|
||||
If there are any parameters, please add them in the WHERE clause, each of which starts with "@".
|
||||
For example, SELECT * FROM table WHERE Id=@Id AND Name=@Name
|
||||
|
|
@ -55,11 +55,13 @@
|
|||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.Twilio\BotSharp.Plugin.Twilio.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.TelegramBots\BotSharp.Plugin.TelegramBots.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.ChatHub\BotSharp.Plugin.ChatHub.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.HttpHandler\BotSharp.Plugin.HttpHandler.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.LLamaSharp\BotSharp.Plugin.LLamaSharp.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.SqlDriver\BotSharp.Plugin.SqlDriver.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.WebDriver\BotSharp.Plugin.WebDriver.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.AnthropicAI\BotSharp.Plugin.AnthropicAI.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.HttpHandler\BotSharp.Plugin.HttpHandler.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.FileHandler\BotSharp.Plugin.FileHandler.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.EmailHandler\BotSharp.Plugin.EmailHandler.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -159,6 +159,12 @@
|
|||
"Origin": ""
|
||||
},
|
||||
|
||||
"SqlDriver": {
|
||||
"MySqlConnectionString": "",
|
||||
"SqlServerConnectionString": "",
|
||||
"SqlLiteConnectionString": ""
|
||||
},
|
||||
|
||||
"Statistics": {
|
||||
"DataDir": "stats"
|
||||
},
|
||||
|
|
@ -295,7 +301,9 @@
|
|||
"BotSharp.Plugin.LLamaSharp",
|
||||
"BotSharp.Plugin.SparkDesk",
|
||||
"BotSharp.Plugin.MetaGLM",
|
||||
"BotSharp.Plugin.HttpHandler"
|
||||
"BotSharp.Plugin.HttpHandler",
|
||||
"BotSharp.Plugin.FileHandler",
|
||||
"BotSharp.Plugin.EmailHandler"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue