diff --git a/BotSharp.sln b/BotSharp.sln index e4653913f..69974a1df 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -151,6 +151,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.GiteeAI", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{242F2D93-FCCE-4982-8075-F3052ECCA92C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.FuzzySharp", "src\Plugins\BotSharp.Plugin.FuzzySharp\BotSharp.Plugin.FuzzySharp.csproj", "{E7C243B9-E751-B3B4-8F16-95C76CA90D31}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -639,6 +641,14 @@ Global {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|Any CPU.Build.0 = Release|Any CPU {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|x64.ActiveCfg = Release|Any CPU {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|x64.Build.0 = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.Build.0 = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.Build.0 = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.ActiveCfg = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -712,6 +722,7 @@ Global {FC63C875-E880-D8BB-B8B5-978AB7B62983} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {242F2D93-FCCE-4982-8075-F3052ECCA92C} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Build.props b/Directory.Build.props index 0879abfcc..d93cb20fb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net8.0 12.0 - 5.1.0 + 5.2.0 true false diff --git a/Directory.Packages.props b/Directory.Packages.props index 60ef84da7..53e2bb5be 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,31 +5,33 @@ true + + - - - + + - + - + - + + - + - + - - - + + + @@ -38,82 +40,82 @@ - + - - - - - - + + + + + + - + - - + + - - - + + + - - - - - - + + + + + + - + - + - - + + - + - - + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 89fcc0e82..81e8bafd6 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,11 @@ BotSharp uses component design, the kernel is kept to a minimum, and business fu - BotSharp.Plugin.Dashboard - BotSharp.Plugin.RoutingSpeeder - BotSharp.Plugin.AudioHandler +- BotSharp.Plugin.ChartHandler - BotSharp.Plugin.EmailHandler +- BotSharp.Plugin.ExcelHandler - BotSharp.Plugin.FileHandler +- BotSharp.Plugin.ImageHandler - BotSharp.Plugin.HttpHandler - BotSharp.Plugin.SqlDriver - BotSharp.Plugin.WebDriver @@ -130,7 +133,7 @@ BotSharp uses component design, the kernel is kept to a minimum, and business fu - [x] MCP - [x] Realtime - [ ] Computer Use -- [x] Browser Use +- [ ] Browser Use - [x] Database Assistant - [x] Code Interpreter - [x] Conversation Management diff --git a/src/BotSharp.AppHost/BotSharp.AppHost.csproj b/src/BotSharp.AppHost/BotSharp.AppHost.csproj index 26ff84a82..962ffa903 100644 --- a/src/BotSharp.AppHost/BotSharp.AppHost.csproj +++ b/src/BotSharp.AppHost/BotSharp.AppHost.csproj @@ -1,5 +1,6 @@ - + + Exe net8.0 diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs index 770acd016..ff3ac9408 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs @@ -29,7 +29,7 @@ public virtual bool OnAgentLoading(ref string id) return true; } - public virtual bool OnInstructionLoaded(string template, Dictionary dict) + public virtual bool OnInstructionLoaded(string template, IDictionary dict) { dict["current_date"] = $"{DateTime.Now:MMM dd, yyyy}"; dict["current_time"] = $"{DateTime.Now:hh:mm tt}"; diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs index 19e29642c..ca6f042a7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs @@ -16,7 +16,7 @@ public interface IAgentHook : IHookBase /// bool OnAgentLoading(ref string id); - bool OnInstructionLoaded(string template, Dictionary dict); + bool OnInstructionLoaded(string template, IDictionary dict); bool OnFunctionsLoaded(List functions); diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index e7b054d79..d0284b224 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -1,4 +1,6 @@ using BotSharp.Abstraction.Agents.Options; +using BotSharp.Abstraction.Coding.Models; +using BotSharp.Abstraction.Coding.Options; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; @@ -30,18 +32,18 @@ public interface IAgentService /// Task InheritAgent(Agent agent); - string RenderInstruction(Agent agent, Dictionary? renderData = null); + string RenderInstruction(Agent agent, IDictionary? renderData = null); - string RenderTemplate(Agent agent, string templateName, Dictionary? renderData = null); + string RenderTemplate(Agent agent, string templateName, IDictionary? renderData = null); - bool RenderFunction(Agent agent, FunctionDef def, Dictionary? renderData = null); + bool RenderFunction(Agent agent, FunctionDef def, IDictionary? renderData = null); - FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, Dictionary? renderData = null); + FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, IDictionary? renderData = null); - (string, IEnumerable) PrepareInstructionAndFunctions(Agent agent, Dictionary? renderData = null, StringComparer? comparer = null); + (string, IEnumerable) PrepareInstructionAndFunctions(Agent agent, IDictionary? renderData = null, StringComparer? comparer = null); - bool RenderVisibility(string? visibilityExpression, Dictionary dict); - Dictionary CollectRenderData(Agent agent); + bool RenderVisibility(string? visibilityExpression, IDictionary dict); + IDictionary CollectRenderData(Agent agent); /// @@ -51,7 +53,7 @@ public interface IAgentService /// Original agent information Task GetAgent(string id); - Task DeleteAgent(string id); + Task DeleteAgent(string id, AgentDeleteOptions? options = null); Task UpdateAgent(Agent agent, AgentField updateField); /// @@ -70,9 +72,15 @@ public interface IAgentService Task> GetAgentCodeScripts(string agentId, AgentCodeScriptFilter? filter = null) => Task.FromResult(new List()); - Task GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) - => Task.FromResult(string.Empty); + Task GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) + => Task.FromResult((AgentCodeScript?)null); Task UpdateAgentCodeScripts(string agentId, List codeScripts, AgentCodeScriptUpdateOptions? options = null) => Task.FromResult(false); + + Task DeleteAgentCodeScripts(string agentId, List? codeScripts = null) + => Task.FromResult(false); + + Task GenerateCodeScript(string agentId, string text, CodeGenHandleOptions? options = null) + => Task.FromResult(new CodeGenerationResult()); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentCodeScript.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentCodeScript.cs index 840d67210..aa673e32b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentCodeScript.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentCodeScript.cs @@ -5,13 +5,16 @@ public class AgentCodeScript : AgentCodeScriptBase public string Id { get; set; } public string AgentId { get; set; } = null!; + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + public DateTime UpdatedTime { get; set; } = DateTime.UtcNow; + public AgentCodeScript() : base() { } public override string ToString() { - return $"{CodePath}"; + return base.ToString(); } } @@ -26,4 +29,9 @@ public class AgentCodeScriptBase public string ScriptType { get; set; } = null!; public string CodePath => $"{ScriptType}/{Name}"; + + public override string ToString() + { + return $"{CodePath}"; + } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs index 6ed259a3f..dcd716bb9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentLlmConfig.cs @@ -43,18 +43,11 @@ public class AgentLlmConfig public string? ReasoningEffortLevel { get; set; } /// - /// Image generation config + /// Image composition config /// - [JsonPropertyName("image_generation")] + [JsonPropertyName("image_composition")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public LlmImageGenerationConfig? ImageGeneration { get; set; } - - /// - /// Image edit config - /// - [JsonPropertyName("image_edit")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public LlmImageEditConfig? ImageEdit { get; set; } + public LlmImageCompositionConfig? ImageComposition { get; set; } /// /// Audio transcription config @@ -71,12 +64,7 @@ public class AgentLlmConfig public LlmRealtimeConfig? Realtime { get; set; } } - -public class LlmImageGenerationConfig : LlmProviderModel -{ -} - -public class LlmImageEditConfig : LlmProviderModel +public class LlmImageCompositionConfig : LlmProviderModel { } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 87a08a894..75c0985a8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -3,11 +3,11 @@ namespace BotSharp.Abstraction.Agents.Models; public class AgentRule { [JsonPropertyName("trigger_name")] - public string TriggerName { get; set; } + public string TriggerName { get; set; } = string.Empty; [JsonPropertyName("disabled")] public bool Disabled { get; set; } [JsonPropertyName("criteria")] - public string Criteria { get; set; } + public string Criteria { get; set; } = string.Empty; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Options/AgentCodeScriptUpdateOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Options/AgentCodeScriptUpdateOptions.cs index 476e42fc9..153694e9d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Options/AgentCodeScriptUpdateOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Options/AgentCodeScriptUpdateOptions.cs @@ -4,5 +4,6 @@ namespace BotSharp.Abstraction.Agents.Options; public class AgentCodeScriptUpdateOptions : AgentCodeScriptDbUpdateOptions { + [JsonPropertyName("delete_if_not_included")] public bool DeleteIfNotIncluded { get; set; } -} +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Options/AgentDeleteOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Options/AgentDeleteOptions.cs new file mode 100644 index 000000000..5df6737c7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Options/AgentDeleteOptions.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Abstraction.Agents.Options; + +public class AgentDeleteOptions +{ + [JsonPropertyName("delete_role_agents")] + public bool DeleteRoleAgents { get; set; } + + [JsonPropertyName("delete_user_agents")] + public bool DeleteUserAgents { get; set; } + + [JsonPropertyName("to_delete_code_scripts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? ToDeleteCodeScripts { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs index 887c437ec..f6d4835ec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs @@ -10,6 +10,5 @@ public class AgentSettings /// /// This is the default LLM config for agent /// - public AgentLlmConfig LlmConfig { get; set; } - = new AgentLlmConfig(); + public AgentLlmConfig LlmConfig { get; set; } = new(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs deleted file mode 100644 index 0e1cab8c4..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Chart/IBotSharpChartService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using BotSharp.Abstraction.Chart.Models; -using BotSharp.Abstraction.Chart.Options; - -namespace BotSharp.Abstraction.Chart; - -public interface IBotSharpChartService -{ - public string Provider { get; } - - Task GetConversationChartData(string conversationId, string messageId, ChartDataOptions options) - => throw new NotImplementedException(); - - Task GetConversationChartCode(string conversationId, string messageId, ChartCodeOptions options) - => throw new NotImplementedException(); -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/IChartProcessor.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/IChartProcessor.cs new file mode 100644 index 000000000..7f8cfd4d8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Chart/IChartProcessor.cs @@ -0,0 +1,12 @@ +using BotSharp.Abstraction.Chart.Models; +using BotSharp.Abstraction.Chart.Options; + +namespace BotSharp.Abstraction.Chart; + +public interface IChartProcessor +{ + public string Provider { get; } + + Task GetConversationChartDataAsync(string conversationId, string messageId, ChartDataOptions? options = null) + => throw new NotImplementedException(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeResult.cs b/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeResult.cs deleted file mode 100644 index 143ba218d..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Chart/Models/ChartCodeResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BotSharp.Abstraction.Chart.Models; - -public class ChartCodeResult -{ - public string Code { get; set; } - public string Language { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Contexts/CodeExecutionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Contexts/CodeExecutionContext.cs new file mode 100644 index 000000000..e2ec38a5a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Contexts/CodeExecutionContext.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Coding.Contexts; + +public class CodeExecutionContext +{ + public AgentCodeScript CodeScript { get; set; } + public List Arguments { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Enums/BuiltInCodeProcessor.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Enums/BuiltInCodeProcessor.cs new file mode 100644 index 000000000..cdbf76a99 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Enums/BuiltInCodeProcessor.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Coding.Enums; + +public static class BuiltInCodeProcessor +{ + public const string PyInterpreter = "botsharp-py-interpreter"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs index 1fe083f01..bd34eb257 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/ICodeProcessor.cs @@ -1,5 +1,7 @@ +using BotSharp.Abstraction.Coding.Models; using BotSharp.Abstraction.Coding.Options; using BotSharp.Abstraction.Coding.Responses; +using System.Threading; namespace BotSharp.Abstraction.Coding; @@ -7,6 +9,24 @@ public interface ICodeProcessor { string Provider { get; } - Task RunAsync(string codeScript, CodeInterpretOptions? options = null) + /// + /// Run code script + /// + /// The code script to run + /// Code script execution options + /// The cancellation token + /// + /// + CodeInterpretResponse Run(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + /// + /// Generate code script + /// + /// User requirement to generate code script + /// Code script generation options + /// + /// + Task GenerateCodeScriptAsync(string text, CodeGenerationOptions? options = null) => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeExecutionResponseModel.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeExecutionResponseModel.cs new file mode 100644 index 000000000..7045f1ac9 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeExecutionResponseModel.cs @@ -0,0 +1,12 @@ +using BotSharp.Abstraction.Coding.Responses; + +namespace BotSharp.Abstraction.Coding.Models; + +public class CodeExecutionResponseModel +{ + public string CodeProcessor { get; set; } = default!; + public AgentCodeScript CodeScript { get; set; } + public IDictionary? Arguments { get; set; } + public string Text { get; set; } = default!; + public CodeInterpretResponse? ExecutionResult { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeGenerationResult.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeGenerationResult.cs new file mode 100644 index 000000000..83544f985 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeGenerationResult.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Coding.Models; + +public class CodeGenerationResult : ResponseBase +{ + public string Content { get; set; } + public string Language { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenHandleOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenHandleOptions.cs new file mode 100644 index 000000000..963cb8a15 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenHandleOptions.cs @@ -0,0 +1,28 @@ +namespace BotSharp.Abstraction.Coding.Options; + +public class CodeGenHandleOptions : CodeGenerationOptions +{ + /// + /// Code processor provider + /// + [JsonPropertyName("processor")] + public string? Processor { get; set; } + + /// + /// Whether to save the generated code script to db + /// + [JsonPropertyName("save_to_db")] + public bool SaveToDb { get; set; } + + /// + /// Code script name (e.g., demo.py) + /// + [JsonPropertyName("script_name")] + public string? ScriptName { get; set; } + + /// + /// Code script type (i.e., src, test) + /// + [JsonPropertyName("script_type")] + public string? ScriptType { get; set; } = AgentCodeScriptType.Src; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs new file mode 100644 index 000000000..de886f5ef --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs @@ -0,0 +1,31 @@ +namespace BotSharp.Abstraction.Coding.Options; + +public class CodeGenerationOptions : LlmConfigBase +{ + /// + /// Agent id to get instruction + /// + [JsonPropertyName("agent_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? AgentId { get; set; } + + /// + /// Template (prompt) name + /// + [JsonPropertyName("template_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TemplateName { get; set; } + + /// + /// Programming language + /// + [JsonPropertyName("programming_language")] + public string? ProgrammingLanguage { get; set; } + + /// + /// Data that can be used to fill in the prompt + /// + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Data { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs index b563b03de..7152c46ef 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeInterpretOptions.cs @@ -1,11 +1,9 @@ -using System.Threading; - namespace BotSharp.Abstraction.Coding.Options; public class CodeInterpretOptions { public string? ScriptName { get; set; } public IEnumerable? Arguments { get; set; } - public bool UseMutex { get; set; } - public CancellationToken? CancellationToken { get; set; } + public bool UseLock { get; set; } + public bool UseProcess { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs index a07c7a1fe..8d41f9853 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs @@ -1,8 +1,11 @@ namespace BotSharp.Abstraction.Coding.Responses; -public class CodeInterpretResponse +public class CodeInterpretResponse : ResponseBase { public string Result { get; set; } = string.Empty; - public bool Success { get; set; } - public string? ErrorMsg { get; set; } + + public override string ToString() + { + return Result.IfNullOrEmptyAs(ErrorMsg.IfNullOrEmptyAs($"Success: {Success}"))!; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs new file mode 100644 index 000000000..49747798e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs @@ -0,0 +1,22 @@ +namespace BotSharp.Abstraction.Coding.Settings; + +public class CodingSettings +{ + public CodeScriptGenerationSettings? CodeGeneration { get; set; } + + public CodeScriptExecutionSettings? CodeExecution { get; set; } +} + +public class CodeScriptGenerationSettings : LlmConfigBase +{ + public string? Processor { get; set; } + public int? MessageLimit { get; set; } +} + +public class CodeScriptExecutionSettings +{ + public string? Processor { get; set; } + public bool UseLock { get; set; } + public bool UseProcess { get; set; } + public int TimeoutSeconds { get; set; } = 3; +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Options/FileHandleOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Options/FileHandleOptions.cs index 31a2ab46c..60b3ffc34 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Options/FileHandleOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Options/FileHandleOptions.cs @@ -1,27 +1,7 @@ namespace BotSharp.Abstraction.Files.Options; -public class FileHandleOptions +public class FileHandleOptions : LlmConfigBase { - /// - /// Llm provider - /// - public string? Provider { get; set; } - - /// - /// llm model - /// - public string? Model { get; set; } - - /// - /// Llm maximum output tokens - /// - public int? MaxOutputTokens { get; set; } - - /// - /// Reasoning effort level - /// - public string? ReasoningEfforLevel { get; set; } - /// /// Instruction /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Options/SelectFileOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Options/SelectFileOptions.cs index 2f8d348ea..2c769cb8d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Options/SelectFileOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Options/SelectFileOptions.cs @@ -13,7 +13,7 @@ public class SelectFileOptions : LlmConfigBase public string? TemplateName { get; set; } /// - /// Description that user provides to select files + /// User description to select files /// public string? Description { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Proccessors/IFileProcessor.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Proccessors/IFileProcessor.cs index 19ab437c9..fbcced77a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Proccessors/IFileProcessor.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Proccessors/IFileProcessor.cs @@ -1,5 +1,7 @@ using BotSharp.Abstraction.Files.Options; using BotSharp.Abstraction.Files.Responses; +using BotSharp.Abstraction.Knowledges.Options; +using BotSharp.Abstraction.Knowledges.Responses; namespace BotSharp.Abstraction.Files.Proccessors; @@ -9,4 +11,7 @@ public interface IFileProcessor Task HandleFilesAsync(Agent agent, string text, IEnumerable files, FileHandleOptions? options = null) => throw new NotImplementedException(); + + Task GetFileKnowledgeAsync(FileBinaryDataModel file, FileKnowledgeHandleOptions? options = null) + => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Responses/FileHandleResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Responses/FileHandleResponse.cs index 6957ed0fc..67de6eb7d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Responses/FileHandleResponse.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Responses/FileHandleResponse.cs @@ -1,8 +1,6 @@ namespace BotSharp.Abstraction.Files.Responses; -public class FileHandleResponse +public class FileHandleResponse : ResponseBase { public string Result { get; set; } = string.Empty; - public bool Success { get; set; } - public string? ErrorMsg { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Contexts/CodeInstructContext.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Contexts/CodeInstructContext.cs deleted file mode 100644 index d6dae85f6..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Contexts/CodeInstructContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BotSharp.Abstraction.Instructs.Contexts; - -public class CodeInstructContext -{ - public string CodeScript { get; set; } - public List Arguments { get; set; } = []; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs index c275691b4..54fc80a90 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs @@ -1,5 +1,6 @@ +using BotSharp.Abstraction.Coding.Contexts; +using BotSharp.Abstraction.Coding.Models; using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Instructs.Contexts; using BotSharp.Abstraction.Instructs.Models; namespace BotSharp.Abstraction.Instructs; @@ -10,6 +11,6 @@ public interface IInstructHook : IHookBase Task AfterCompletion(Agent agent, InstructResult result) => Task.CompletedTask; Task OnResponseGenerated(InstructResponseModel response) => Task.CompletedTask; - Task BeforeCodeExecution(Agent agent, RoleDialogModel message, CodeInstructContext context) => Task.CompletedTask; - Task AfterCodeExecution(Agent agent, InstructResult result) => Task.CompletedTask; + Task BeforeCodeExecution(Agent agent, CodeExecutionContext context) => Task.CompletedTask; + Task AfterCodeExecution(Agent agent, CodeExecutionResponseModel response) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs index 9209684cf..f5758434c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Coding.Contexts; +using BotSharp.Abstraction.Coding.Models; using BotSharp.Abstraction.Instructs.Models; namespace BotSharp.Abstraction.Instructs; @@ -20,4 +22,14 @@ public virtual async Task OnResponseGenerated(InstructResponseModel response) { await Task.CompletedTask; } + + public virtual async Task BeforeCodeExecution(Agent agent, CodeExecutionContext context) + { + await Task.CompletedTask; + } + + public virtual async Task AfterCodeExecution(Agent agent, CodeExecutionResponseModel response) + { + await Task.CompletedTask; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs index 89684cfdf..182196753 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/CodeInstructOptions.cs @@ -2,7 +2,31 @@ namespace BotSharp.Abstraction.Instructs.Options; public class CodeInstructOptions { + /// + /// Code processor provider + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("processor")] public string? Processor { get; set; } - public string? CodeScriptName { get; set; } + + /// + /// Code script name + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("script_name")] + public string? ScriptName { get; set; } + + /// + /// Code script name + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("script_type")] + public string? ScriptType { get; set; } + + /// + /// Arguments + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("arguments")] public List? Arguments { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/FileInstructOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/FileInstructOptions.cs index bb575f49d..b673589db 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/FileInstructOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/FileInstructOptions.cs @@ -2,5 +2,10 @@ namespace BotSharp.Abstraction.Instructs.Options; public class FileInstructOptions { + /// + /// File processor provider + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("processor")] public string? Processor { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/InstructOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/InstructOptions.cs index cc5a76cd5..b2ff66fba 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/InstructOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Options/InstructOptions.cs @@ -35,5 +35,5 @@ public class InstructOptions /// /// Image convert provider /// - public string? ImageConvertProvider { get; set; } + public string? ImageConverter { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs index 767615fe5..393898b5f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs @@ -36,8 +36,9 @@ public interface IKnowledgeService /// /// /// + /// /// - Task UploadDocumentsToKnowledge(string collectionName, IEnumerable files, ChunkOption? option = null); + Task UploadDocumentsToKnowledge(string collectionName, IEnumerable files, KnowledgeDocOptions? options = null); /// /// Save document content to knowledgebase without saving the document /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IPhraseCollection.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IPhraseCollection.cs new file mode 100644 index 000000000..9238e3220 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IPhraseCollection.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Knowledges; + +public interface IPhraseCollection +{ + Task>> LoadVocabularyAsync(); + Task> LoadSynonymMappingAsync(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IPhraseService.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IPhraseService.cs new file mode 100644 index 000000000..1ca84024a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IPhraseService.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Knowledges; + +public interface IPhraseService +{ + Task> SearchPhrasesAsync(string term); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/FileKnowledgeModel.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/FileKnowledgeModel.cs new file mode 100644 index 000000000..0b79001e5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/FileKnowledgeModel.cs @@ -0,0 +1,18 @@ +using BotSharp.Abstraction.VectorStorage.Models; + +namespace BotSharp.Abstraction.Knowledges.Models; + +public class FileKnowledgeModel +{ + public IEnumerable Contents { get; set; } = []; + public IDictionary? Payload { get; set; } +} + + +public class FileKnowledgeWrapper +{ + public Guid FileId { get; set; } + public string? FileSource { get; set; } + public FileBinaryDataModel FileData { get; set; } + public IEnumerable FileKnowledges { get; set; } = []; +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/KnowledgeFileModel.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeFileModel.cs similarity index 87% rename from src/Infrastructure/BotSharp.Abstraction/Files/Models/KnowledgeFileModel.cs rename to src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeFileModel.cs index c3ca76fc7..49f9f1615 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Models/KnowledgeFileModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeFileModel.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Abstraction.Files.Models; +namespace BotSharp.Abstraction.Knowledges.Models; public class KnowledgeFileModel { diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/SearchPhrasesResult.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/SearchPhrasesResult.cs new file mode 100644 index 000000000..64dc0c18f --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/SearchPhrasesResult.cs @@ -0,0 +1,11 @@ + +namespace BotSharp.Abstraction.Knowledges.Models; + +public class SearchPhrasesResult +{ + public string Token { get; set; } = string.Empty; + public List Sources { get; set; } = new(); + public string CanonicalForm { get; set; } = string.Empty; + public string MatchType { get; set; } = string.Empty; + public double Confidence { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Options/FileKnowledgeHandleOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Options/FileKnowledgeHandleOptions.cs new file mode 100644 index 000000000..9b891ab28 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Options/FileKnowledgeHandleOptions.cs @@ -0,0 +1,34 @@ +namespace BotSharp.Abstraction.Knowledges.Options; + +public class FileKnowledgeHandleOptions : LlmConfigBase +{ + /// + /// Agent id + /// + public string? AgentId { get; set; } + + /// + /// Instruction + /// + public string? Instruction { get; set; } + + /// + /// Message from user + /// + public string? UserMessage { get; set; } + + /// + /// Template name in Agent + /// + public string? TemplateName { get; set; } + + /// + /// The upstream where the file llm is invoked + /// + public string? InvokeFrom { get; set; } + + /// + /// Data that is used to render instruction + /// + public Dictionary? Data { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Options/KnowledgeDocOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Options/KnowledgeDocOptions.cs new file mode 100644 index 000000000..52c123426 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Options/KnowledgeDocOptions.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Knowledges.Options; + +public class KnowledgeDocOptions : FileKnowledgeHandleOptions +{ + public string? Processor { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Responses/FileKnowledgeResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Responses/FileKnowledgeResponse.cs new file mode 100644 index 000000000..27b5d6967 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Responses/FileKnowledgeResponse.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Knowledges.Responses; + +public class FileKnowledgeResponse : ResponseBase +{ + public IEnumerable Knowledges { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Filters/LlmConfigFilter.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Filters/LlmConfigFilter.cs new file mode 100644 index 000000000..89ec04103 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Filters/LlmConfigFilter.cs @@ -0,0 +1,13 @@ +using BotSharp.Abstraction.MLTasks.Settings; + +namespace BotSharp.Abstraction.MLTasks.Filters; + +public class LlmConfigFilter +{ + public List? Providers { get; set; } + public List? ModelIds { get; set; } + public List? ModelNames { get; set; } + public List? ModelTypes { get; set; } + public List? ModelCapabilities { get; set; } + public bool? MultiModal { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs index f70a9496e..da8a8f534 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs @@ -1,12 +1,13 @@ +using BotSharp.Abstraction.MLTasks.Filters; using BotSharp.Abstraction.MLTasks.Settings; namespace BotSharp.Abstraction.MLTasks; public interface ILlmProviderService { - LlmModelSetting GetSetting(string provider, string model); + LlmModelSetting? GetSetting(string provider, string model); List GetProviders(); - LlmModelSetting GetProviderModel(string provider, string id, bool? multiModal = null, LlmModelType? modelType = null, bool imageGenerate = false); + LlmModelSetting? GetProviderModel(string provider, string id, bool? multiModal = null, LlmModelType? modelType = null, IEnumerable? capabilities = null); List GetProviderModels(string provider); - List GetLlmConfigs(LlmConfigOptions? options = null); + List GetLlmConfigs(LlmConfigFilter? filter = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmConfigOptions.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmConfigOptions.cs deleted file mode 100644 index eae417a41..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmConfigOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BotSharp.Abstraction.MLTasks.Settings; - -public class LlmConfigOptions -{ - public LlmModelType? Type { get; set; } - public bool? MultiModal { get; set; } - public bool? ImageGeneration { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs index 1bc7ef743..77bd46b28 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs @@ -31,17 +31,13 @@ public class LlmModelSetting public string ApiKey { get; set; } = null!; public string? Endpoint { get; set; } public LlmModelType Type { get; set; } = LlmModelType.Chat; + public List Capabilities { get; set; } = []; /// - /// If true, allow sending images/vidoes to this model + /// If true, allow sending images/videos to this model /// public bool MultiModal { get; set; } - /// - /// If true, allow generating images - /// - public bool ImageGeneration { get; set; } - /// /// Settings for embedding /// @@ -122,6 +118,7 @@ public class ImageGenerationSetting public class ImageEditSetting { public ModelSettingBase? Size { get; set; } + public ModelSettingBase? Quality { get; set; } public ModelSettingBase? ResponseFormat { get; set; } public ModelSettingBase? Background { get; set; } } @@ -172,10 +169,30 @@ public class LlmCostSetting public enum LlmModelType { + All = 0, Text = 1, Chat = 2, Image = 3, Embedding = 4, Audio = 5, Realtime = 6, + Web = 7 } + +public enum LlmModelCapability +{ + All = 0, + Text = 1, + Chat = 2, + ImageReading = 3, + ImageGeneration = 4, + ImageEdit = 5, + ImageVariation = 6, + ImageComposition = 7, + Embedding = 8, + AudioTranscription = 9, + AudioGeneration = 10, + Realtime = 11, + WebSearch = 12, + PdfReading = 13 +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/ResponseBase.cs b/src/Infrastructure/BotSharp.Abstraction/Models/ResponseBase.cs new file mode 100644 index 000000000..cb381aebe --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Models/ResponseBase.cs @@ -0,0 +1,11 @@ +namespace BotSharp.Abstraction.Models; + +public class ResponseBase +{ + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("error_message")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ErrorMsg { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs index b8609422d..63469bcd8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs @@ -9,9 +9,11 @@ public class BotSharpOptions PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, AllowTrailingCommas = true, - WriteIndented = true + WriteIndented = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles }; + private JsonSerializerOptions _jsonSerializerOptions; public JsonSerializerOptions JsonSerializerOptions diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/AgentCodeScriptFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/AgentCodeScriptFilter.cs index 530bb4aa5..14848d717 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/AgentCodeScriptFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/AgentCodeScriptFilter.cs @@ -7,6 +7,6 @@ public class AgentCodeScriptFilter public static AgentCodeScriptFilter Empty() { - return new AgentCodeScriptFilter(); + return new(); } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs index aeec7b18a..4ff78e4dd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/InstructLogFilter.cs @@ -8,6 +8,7 @@ public class InstructLogFilter : Pagination public List? TemplateNames { get; set; } public List? UserIds { get; set; } public List? States { get; set; } + public string? SimilarTemplateName { get; set; } public DateTime? StartTime { get; set; } public DateTime? EndTime { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 31b1da97a..374271b9f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Agents.Options; using BotSharp.Abstraction.Knowledges.Filters; using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; @@ -81,7 +82,7 @@ void BulkInsertUserAgents(List userAgents) => throw new NotImplementedException(); bool DeleteAgents() => throw new NotImplementedException(); - bool DeleteAgent(string agentId) + bool DeleteAgent(string agentId, AgentDeleteOptions? options = null) => throw new NotImplementedException(); List GetAgentResponses(string agentId, string prefix, string intent) => throw new NotImplementedException(); @@ -113,7 +114,7 @@ bool DeleteAgentTasks(string agentId, List? taskIds = null) #region Agent Code List GetAgentCodeScripts(string agentId, AgentCodeScriptFilter? filter = null) => throw new NotImplementedException(); - string? GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) + AgentCodeScript? GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) => throw new NotImplementedException(); bool UpdateAgentCodeScripts(string agentId, List scripts, AgentCodeScriptDbUpdateOptions? options = null) => throw new NotImplementedException(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Options/AgentCodeScriptDbUpdateOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Options/AgentCodeScriptDbUpdateOptions.cs index 5f6045129..2dde049bf 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Options/AgentCodeScriptDbUpdateOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Options/AgentCodeScriptDbUpdateOptions.cs @@ -2,5 +2,6 @@ namespace BotSharp.Abstraction.Repositories.Options; public class AgentCodeScriptDbUpdateOptions { + [JsonPropertyName("is_upsert")] public bool IsUpsert { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index c870f145f..c7a6d847b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -2,5 +2,15 @@ namespace BotSharp.Abstraction.Rules; public interface IRuleEngine { - Task> Triggered(IRuleTrigger trigger, string data, List? states = null); + /// + /// Trigger the rule that is subscribed by agents. + /// + /// + /// + /// + /// + /// + /// + Task> Triggered(IRuleTrigger trigger, string text, IEnumerable? states = null, RuleTriggerOptions? options = null) + => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleTrigger.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleTrigger.cs index 7b6c9bc25..c7ad59d9a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleTrigger.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleTrigger.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Rules; public interface IRuleTrigger @@ -9,4 +11,14 @@ public interface IRuleTrigger string EntityType { get; set; } string EntityId { get; set; } + + /// + /// The default arguments as input to code trigger (display purpose) + /// + JsonDocument OutputArgs => JsonDocument.Parse("{}"); + + /// + /// Explain the purpose of rule trigger (display purpose) + /// + string Statement => string.Empty; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs new file mode 100644 index 000000000..068052b0b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -0,0 +1,26 @@ +using System.Text.Json; + +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleTriggerOptions +{ + /// + /// Code processor provider + /// + public string? CodeProcessor { get; set; } + + /// + /// Code script name + /// + public string? CodeScriptName { get; set; } + + /// + /// Argument name as an input key to the code script + /// + public string? ArgumentName { get; set; } + + /// + /// Json arguments as an input value to the code script + /// + public JsonDocument? ArgumentContent { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs b/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs index fecc36c45..9a555f9c7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs @@ -2,6 +2,6 @@ namespace BotSharp.Abstraction.Templating; public interface ITemplateRender { - string Render(string template, Dictionary dict); + string Render(string template, IDictionary dict); void RegisterType(Type type); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs index a8403dfa1..18e92b28c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs @@ -15,6 +15,15 @@ public interface IAuthenticationHook Task Authenticate(string id, string password) => Task.FromResult(new User()); + /// + /// Renew token for authentication + /// + /// + /// + /// + Task RenewAuthentication(string refreshToken, string? accessToken = null) + => Task.FromResult((User?)null); + /// /// Add extra claims to user /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index d03687ffa..a7b3da1f7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -19,8 +19,8 @@ public interface IUserService Task GetAffiliateToken(string authorization); Task GetAdminToken(string authorization); Task GetToken(string authorization); + Task RenewToken(string refreshToken, string? accessToken = null); Task CreateTokenByUser(User user); - Task RenewToken(); Task GetMyProfile(); Task VerifyUserNameExisting(string userName); Task VerifyEmailExisting(string email); diff --git a/src/Infrastructure/BotSharp.Abstraction/Using.cs b/src/Infrastructure/BotSharp.Abstraction/Using.cs index d20775375..ca0cbe7f7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Using.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Using.cs @@ -21,4 +21,6 @@ global using BotSharp.Abstraction.Knowledges.Models; global using BotSharp.Abstraction.Crontab.Models; global using BotSharp.Abstraction.MCP.Models; -global using BotSharp.Abstraction.Settings; \ No newline at end of file +global using BotSharp.Abstraction.Settings; +global using BotSharp.Abstraction.Rules.Options; +global using BotSharp.Abstraction.Coding.Settings; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs new file mode 100644 index 000000000..a36516c8a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs @@ -0,0 +1,68 @@ +using BotSharp.Abstraction.Options; +using System.Text.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace BotSharp.Abstraction.Utilities; + +public static class ObjectExtensions +{ + private static readonly JsonSerializerOptions _defaultJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + + public static T? DeepClone(this T? inputObj, Action? modifier = null, BotSharpOptions? options = null) where T : class + { + if (inputObj == null) + { + return null; + } + + try + { + var json = JsonSerializer.Serialize(inputObj, options?.JsonSerializerOptions ?? _defaultJsonOptions); + var outputObj = JsonSerializer.Deserialize(json, options?.JsonSerializerOptions ?? _defaultJsonOptions); + if (modifier != null && outputObj != null) + { + modifier(outputObj); + } + + return outputObj; + } + catch (Exception ex) + { + Console.WriteLine($"DeepClone Error in {nameof(DeepClone)} for {typeof(T).Name}: {ex}"); + return null; + } + } + + public static TOutput? DeepClone(this TInput? inputObj, Action? modifier = null, BotSharpOptions? options = null) + where TInput : class + where TOutput : class + { + if (inputObj == null) + { + return null; + } + + try + { + var json = JsonSerializer.Serialize(inputObj, options?.JsonSerializerOptions ?? _defaultJsonOptions); + var outputObj = JsonSerializer.Deserialize(json, options?.JsonSerializerOptions ?? _defaultJsonOptions); + if (modifier != null && outputObj != null) + { + modifier(outputObj); + } + + return outputObj; + } + catch (Exception ex) + { + Console.WriteLine($"DeepClone Error in {nameof(DeepClone)} for {typeof(TInput).Name} and {typeof(TOutput).Name}: {ex}"); + return null; + } + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs index 8e2fd9bbf..e26f663d1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs @@ -11,12 +11,18 @@ public static class StringExtensions public static string SubstringMax(this string str, int maxLength) { if (string.IsNullOrEmpty(str)) + { return str; + } if (str.Length > maxLength) + { return str.Substring(0, maxLength); + } else + { return str; + } } public static string[] SplitByNewLine(this string input) @@ -39,14 +45,20 @@ public static string RemoveNewLine(this string input) public static bool IsEqualTo(this string? str1, string? str2, StringComparison option = StringComparison.OrdinalIgnoreCase) { - if (str1 == null) return str2 == null; + if (str1 == null) + { + return str2 == null; + } return str1.Equals(str2, option); } public static string CleanStr(this string? str) { - if (string.IsNullOrWhiteSpace(str)) return string.Empty; + if (string.IsNullOrWhiteSpace(str)) + { + return string.Empty; + } return str.Replace(" ", "").Replace("\t", "").Replace("\n", "").Replace("\r", ""); } diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSearchParamModel.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSearchParamModel.cs new file mode 100644 index 000000000..8e14b05ca --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorSearchParamModel.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorSearchParamModel +{ + [JsonPropertyName("exact_search")] + public bool? ExactSearch { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Options/VectorSearchOptions.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Options/VectorSearchOptions.cs index 1e0141a86..4ba0c81c8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Options/VectorSearchOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Options/VectorSearchOptions.cs @@ -10,6 +10,7 @@ public class VectorSearchOptions public int? Limit { get; set; } = 5; public float? Confidence { get; set; } = 0.5f; public bool WithVector { get; set; } + public VectorSearchParamModel? SearchParam { get; set; } public static VectorSearchOptions Default() { diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs index 288458430..ccd89cf45 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs @@ -36,7 +36,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await RunCronChecker(scope.ServiceProvider); await Task.Delay(1000, stoppingToken); }); - if (isLocked == false) + + if (!isLocked) { await Task.Delay(1000, stoppingToken); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 1b3006663..2d52ef6d2 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,25 +1,41 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Coding; +using BotSharp.Abstraction.Coding.Contexts; +using BotSharp.Abstraction.Coding.Enums; +using BotSharp.Abstraction.Coding.Models; +using BotSharp.Abstraction.Coding.Settings; using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Repositories.Filters; +using BotSharp.Abstraction.Rules.Options; using BotSharp.Abstraction.Utilities; using Microsoft.Extensions.Logging; using System.Data; +using System.Text.Json; namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine { private readonly IServiceProvider _services; - private readonly ILogger _logger; + private readonly ILogger _logger; + private readonly CodingSettings _codingSettings; - public RuleEngine(IServiceProvider services, ILogger logger) + public RuleEngine( + IServiceProvider services, + ILogger logger, + CodingSettings codingSettings) { _services = services; _logger = logger; + _codingSettings = codingSettings; } - public async Task> Triggered(IRuleTrigger trigger, string data, List? states = null) + public async Task> Triggered(IRuleTrigger trigger, string text, IEnumerable? states = null, RuleTriggerOptions? options = null) { + var newConversationIds = new List(); + // Pull all user defined rules var agentService = _services.GetRequiredService(); var agents = await agentService.GetAgents(new AgentFilter @@ -30,25 +46,29 @@ public async Task> Triggered(IRuleTrigger trigger, string da } }); - var preFilteredAgents = agents.Items.Where(x => - x.Rules.Exists(r => r.TriggerName == trigger.Name && - !x.Disabled)).ToList(); - - // Trigger the agents - var instructService = _services.GetRequiredService(); - var newConversationIds = new List(); - - foreach (var agent in preFilteredAgents) + // Trigger agents + var filteredAgents = agents.Items.Where(x => x.Rules.Exists(r => r.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled)).ToList(); + foreach (var agent in filteredAgents) { + // Code trigger + if (options != null) + { + var isTriggered = await TriggerCodeScript(agent, trigger.Name, options); + if (!isTriggered) + { + continue; + } + } + var convService = _services.GetRequiredService(); var conv = await convService.NewConversation(new Conversation { Channel = trigger.Channel, - Title = data, + Title = text, AgentId = agent.Id }); - var message = new RoleDialogModel(AgentRole.User, data); + var message = new RoleDialogModel(AgentRole.User, text); var allStates = new List { @@ -69,27 +89,127 @@ await convService.SendMessage(agent.Id, convService.SaveStates(); newConversationIds.Add(conv.Id); + } + + return newConversationIds; + } - /*foreach (var rule in agent.Rules) + #region Private methods + private async Task TriggerCodeScript(Agent agent, string triggerName, RuleTriggerOptions options) + { + if (string.IsNullOrWhiteSpace(agent?.Id)) + { + return false; + } + + var provider = options.CodeProcessor ?? BuiltInCodeProcessor.PyInterpreter; + var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); + if (processor == null) + { + _logger.LogWarning($"Unable to find code processor: {provider}."); + return false; + } + + var agentService = _services.GetRequiredService(); + var scriptName = options.CodeScriptName ?? $"{triggerName}_rule.py"; + var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); + + var msg = $"rule trigger ({triggerName}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; + + if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) + { + _logger.LogWarning($"Unable to find {msg}."); + return false; + } + + try + { + var hooks = _services.GetHooks(agent.Id); + + var arguments = BuildArguments(options.ArgumentName, options.ArgumentContent); + var context = new CodeExecutionContext { - var userSay = $"===Input data with Before and After values===\r\n{data}\r\n\r\n===Trigger Criteria===\r\n{rule.Criteria}\r\n\r\nJust output 1 or 0 without explanation: "; + CodeScript = codeScript, + Arguments = arguments + }; - var result = await instructService.Execute(BuiltInAgentId.RulesInterpreter, new RoleDialogModel(AgentRole.User, userSay), "criteria_check", "#TEMPLATE#"); + foreach (var hook in hooks) + { + await hook.BeforeCodeExecution(agent, context); + } - // Check if meet the criteria - if (result.Text == "1") - { - // Hit rule - _logger.LogInformation($"Hit rule {rule.TriggerName} {rule.EntityType} {rule.EventName}, {data}"); + var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); + var response = processor.Run(codeScript.Content, options: new() + { + ScriptName = scriptName, + Arguments = arguments, + UseLock = useLock, + UseProcess = useProcess + }, cancellationToken: cts.Token); - await convService.SendMessage(agent.Id, - new RoleDialogModel(AgentRole.User, $"The conversation was triggered by {rule.Criteria}"), - null, - msg => Task.CompletedTask); - } - }*/ + var codeResponse = new CodeExecutionResponseModel + { + CodeProcessor = processor.Provider, + CodeScript = codeScript, + Arguments = arguments.DistinctBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value ?? string.Empty), + ExecutionResult = response + }; + + foreach (var hook in hooks) + { + await hook.AfterCodeExecution(agent, codeResponse); + } + + if (response == null || !response.Success) + { + _logger.LogWarning($"Failed to handle {msg}"); + return false; + } + + bool result; + LogLevel logLevel; + if (response.Result.IsEqualTo("true")) + { + logLevel = LogLevel.Information; + result = true; + } + else + { + logLevel = LogLevel.Warning; + result = false; + } + + _logger.Log(logLevel, $"Code script execution result ({response}) from {msg}"); + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when handling {msg}"); + return false; } + } - return newConversationIds; + private List BuildArguments(string? name, JsonDocument? args) + { + var keyValues = new List(); + if (args != null) + { + keyValues.Add(new KeyValue(name ?? "trigger_args", args.RootElement.GetRawText())); + } + return keyValues; + } + + private (bool, bool, int) GetCodeExecutionConfig() + { + var codeExecution = _codingSettings.CodeExecution; + var defaultTimeoutSeconds = 3; + + var useLock = codeExecution?.UseLock ?? false; + var useProcess = codeExecution?.UseProcess ?? false; + var timeoutSeconds = codeExecution?.TimeoutSeconds > 0 ? codeExecution.TimeoutSeconds : defaultTimeoutSeconds; + + return (useLock, useProcess, timeoutSeconds); } + #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index 41e13da4c..575385455 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -57,9 +57,10 @@ public bool AttachMenu(List menu) { SubMenu = new List { + new PluginMenuDef("Agents", link: "page/agent"), // icon: "bx bx-bot", new PluginMenuDef("Routing", link: "page/agent/router"), // icon: "bx bx-map-pin" - new PluginMenuDef("Evaluating", link: "page/agent/evaluator") { Roles = new List { UserRole.Root, UserRole.Admin } }, // icon: "bx bx-task" - new PluginMenuDef("Agents", link: "page/agent"), // icon: "bx bx-bot" + new PluginMenuDef("Evaluating", link: "page/agent/evaluator") { Roles = [UserRole.Root, UserRole.Admin] }, // icon: "bx bx-task" + new PluginMenuDef("Coding", link: "page/agent/code-scripts") { Roles = [UserRole.Root, UserRole.Admin] }, } }); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs b/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs index a8bafdd25..cadeb3b47 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs @@ -70,12 +70,18 @@ public override void OnAgentUtilityLoaded(Agent agent) foreach (var utility in innerUtilities) { var isVisible = agentService.RenderVisibility(utility.VisibilityExpression, renderDict); - if (!isVisible || utility.Items.IsNullOrEmpty()) continue; + if (!isVisible || utility.Items.IsNullOrEmpty()) + { + continue; + } foreach (var item in utility.Items) { isVisible = agentService.RenderVisibility(item.VisibilityExpression, renderDict); - if (!isVisible) continue; + if (!isVisible) + { + continue; + } if (item.FunctionName?.StartsWith(UTIL_PREFIX) == true) { diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CodeScripts.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CodeScripts.cs deleted file mode 100644 index 5c7c8bc00..000000000 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CodeScripts.cs +++ /dev/null @@ -1,46 +0,0 @@ -using BotSharp.Abstraction.Agents.Options; - -namespace BotSharp.Core.Agents.Services; - -public partial class AgentService -{ - public async Task> GetAgentCodeScripts(string agentId, AgentCodeScriptFilter? filter = null) - { - var db = _services.GetRequiredService(); - var scripts = db.GetAgentCodeScripts(agentId, filter); - return await Task.FromResult(scripts); - } - - public async Task GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) - { - var db = _services.GetRequiredService(); - var script = db.GetAgentCodeScript(agentId, scriptName, scriptType); - return await Task.FromResult(script); - } - - public async Task UpdateAgentCodeScripts(string agentId, List codeScripts, AgentCodeScriptUpdateOptions? options = null) - { - if (string.IsNullOrWhiteSpace(agentId) || codeScripts.IsNullOrEmpty()) - { - return false; - } - - var db = _services.GetRequiredService(); - - var toDeleteScripts = new List(); - if (options?.DeleteIfNotIncluded == true) - { - var curDbScripts = await GetAgentCodeScripts(agentId); - var codePaths = codeScripts.Select(x => x.CodePath).ToList(); - toDeleteScripts = curDbScripts.Where(x => !codePaths.Contains(x.CodePath)).ToList(); - } - - var updateResult = db.UpdateAgentCodeScripts(agentId, codeScripts, options); - if (!toDeleteScripts.IsNullOrEmpty()) - { - db.DeleteAgentCodeScripts(agentId, toDeleteScripts); - } - - return updateResult; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Coding.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Coding.cs new file mode 100644 index 000000000..ea9d336f0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Coding.cs @@ -0,0 +1,113 @@ +using BotSharp.Abstraction.Agents.Options; +using BotSharp.Abstraction.Coding; +using BotSharp.Abstraction.Coding.Enums; +using BotSharp.Abstraction.Coding.Options; + +namespace BotSharp.Core.Agents.Services; + +public partial class AgentService +{ + public async Task> GetAgentCodeScripts(string agentId, AgentCodeScriptFilter? filter = null) + { + var db = _services.GetRequiredService(); + var scripts = db.GetAgentCodeScripts(agentId, filter); + return await Task.FromResult(scripts); + } + + public async Task GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) + { + var db = _services.GetRequiredService(); + var script = db.GetAgentCodeScript(agentId, scriptName, scriptType); + return await Task.FromResult(script); + } + + public async Task UpdateAgentCodeScripts(string agentId, List codeScripts, AgentCodeScriptUpdateOptions? options = null) + { + if (string.IsNullOrWhiteSpace(agentId)) + { + return false; + } + + codeScripts ??= new(); + var db = _services.GetRequiredService(); + + if (options?.DeleteIfNotIncluded == true && codeScripts.IsNullOrEmpty()) + { + // Delete all code scripts in this agent + db.DeleteAgentCodeScripts(agentId); + return true; + } + + var toDeleteScripts = new List(); + if (options?.DeleteIfNotIncluded == true) + { + var curDbScripts = await GetAgentCodeScripts(agentId); + var codePaths = codeScripts.Select(x => x.CodePath).ToList(); + toDeleteScripts = curDbScripts.Where(x => !codePaths.Contains(x.CodePath)).ToList(); + } + + var updateResult = db.UpdateAgentCodeScripts(agentId, codeScripts, options); + if (!toDeleteScripts.IsNullOrEmpty()) + { + db.DeleteAgentCodeScripts(agentId, toDeleteScripts); + } + + return updateResult; + } + + public async Task DeleteAgentCodeScripts(string agentId, List? codeScripts = null) + { + if (string.IsNullOrWhiteSpace(agentId)) + { + return false; + } + + var db = _services.GetRequiredService(); + var deleted = db.DeleteAgentCodeScripts(agentId, codeScripts); + return await Task.FromResult(deleted); + } + + public async Task GenerateCodeScript(string agentId, string text, CodeGenHandleOptions? options = null) + { + if (string.IsNullOrWhiteSpace(agentId)) + { + return new CodeGenerationResult + { + ErrorMsg = "Agent id cannot be empty." + }; + } + + var settings = _services.GetRequiredService(); + var processor = options?.Processor ?? settings?.CodeGeneration?.Processor; + processor = !string.IsNullOrEmpty(processor) ? processor : BuiltInCodeProcessor.PyInterpreter; + var codeProcessor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(processor)); + if (codeProcessor == null) + { + var errorMsg = $"Unable to find code processor {processor}."; + _logger.LogWarning(errorMsg); + return new CodeGenerationResult + { + ErrorMsg = errorMsg + }; + } + + var result = await codeProcessor.GenerateCodeScriptAsync(text, options); + if (result.Success && options?.SaveToDb == true) + { + var db = _services.GetRequiredService(); + var scripts = new List + { + new AgentCodeScript + { + Name = options?.ScriptName ?? $"{Guid.NewGuid()}.py", + Content = result.Content, + ScriptType = options?.ScriptType ?? AgentCodeScriptType.Src + } + }; + var saved = db.UpdateAgentCodeScripts(agentId, scripts, new() { IsUpsert = true }); + result.Success = saved; + } + + return result; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs index d61f0f3dd..5404c0d44 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs @@ -127,7 +127,7 @@ private List GetFunctionsFromFile(string fileDir) if (extension != "json") continue; var json = File.ReadAllText(file); - var function = JsonSerializer.Deserialize(json, _options); + var function = JsonSerializer.Deserialize(json, _options.JsonSerializerOptions); functions.Add(function); } catch diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs index 6783bf916..5237dbdc9 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Agents.Options; using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; @@ -5,7 +6,7 @@ namespace BotSharp.Core.Agents.Services; public partial class AgentService { - public async Task DeleteAgent(string id) + public async Task DeleteAgent(string id, AgentDeleteOptions? options = null) { var userService = _services.GetRequiredService(); var auth = await userService.GetUserAuthorizations(new List { id }); @@ -15,7 +16,7 @@ public async Task DeleteAgent(string id) return false; } - var deleted = _db.DeleteAgent(id); + var deleted = _db.DeleteAgent(id, options); return await Task.FromResult(deleted); } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs index 5fae2184e..e3defd95d 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs @@ -41,7 +41,7 @@ public async Task> GetAgentOptions(List? agentIdsOrNames, b return agents?.Select(x => new IdName(x.Id, x.Name))?.OrderBy(x => x.Name)?.ToList() ?? []; } - [SharpCache(10)] + [SharpCache(10, perInstanceCache: true)] public async Task GetAgent(string id) { if (string.IsNullOrWhiteSpace(id) || id == Guid.Empty.ToString()) @@ -83,17 +83,34 @@ public async Task GetAgent(string id) private void AddDefaultInstruction(Agent agent, string instruction) { //check if instruction is empty - if (string.IsNullOrWhiteSpace(instruction)) return; + if (string.IsNullOrWhiteSpace(instruction)) + { + return; + } + //check if instruction is already set - if (agent.ChannelInstructions.Exists(p => p.Channel == string.Empty)) return; + var instructions = new List(agent.ChannelInstructions); + if (instructions.Exists(p => string.IsNullOrEmpty(p.Channel))) + { + return; + } + //Add default instruction to ChannelInstructions - var defaultInstruction = new ChannelInstruction() { Channel = string.Empty, Instruction = instruction }; - agent.ChannelInstructions.Insert(0, defaultInstruction); + var defaultInstruction = new ChannelInstruction() + { + Channel = string.Empty, + Instruction = instruction + }; + instructions.Insert(0, defaultInstruction); + agent.ChannelInstructions = instructions; } public async Task InheritAgent(Agent agent) { - if (string.IsNullOrWhiteSpace(agent?.InheritAgentId)) return; + if (string.IsNullOrWhiteSpace(agent?.InheritAgentId)) + { + return; + } var inheritedAgent = await GetAgent(agent.InheritAgentId); agent.Templates.AddRange(inheritedAgent.Templates diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs index ef81adafd..031ee9bcf 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs @@ -10,12 +10,18 @@ public partial class AgentService // [SharpCache(10, perInstanceCache: true)] public async Task LoadAgent(string id, bool loadUtility = true) { - if (string.IsNullOrEmpty(id) || id == Guid.Empty.ToString()) return null; + if (string.IsNullOrEmpty(id) || id == Guid.Empty.ToString()) + { + return null; + } HookEmitter.Emit(_services, hook => hook.OnAgentLoading(ref id), id); var agent = await GetAgent(id); - if (agent == null) return null; + if (agent == null) + { + return null; + } agent.TemplateDict = []; agent.SecondaryInstructions = []; @@ -25,8 +31,8 @@ public async Task LoadAgent(string id, bool loadUtility = true) OverrideInstructionByChannel(agent); AddOrUpdateParameters(agent); - // Populate state into dictionary - PopulateState(agent.TemplateDict); + // Populate state + PopulateState(agent); // After agent is loaded HookEmitter.Emit(_services, hook => { @@ -68,30 +74,32 @@ public async Task LoadAgent(string id, bool loadUtility = true) private void OverrideInstructionByChannel(Agent agent) { - var instructions = agent.ChannelInstructions; - if (instructions.IsNullOrEmpty()) return; + var instructions = new List(agent.ChannelInstructions); + if (instructions.IsNullOrEmpty()) + { + return; + } var state = _services.GetRequiredService(); var channel = state.GetState("channel"); var found = instructions.FirstOrDefault(x => x.Channel.IsEqualTo(channel)); - var defaultInstruction = instructions.FirstOrDefault(x => x.Channel == string.Empty); + var defaultInstruction = instructions.FirstOrDefault(x => string.IsNullOrEmpty(x.Channel)); agent.Instruction = !string.IsNullOrWhiteSpace(found?.Instruction) ? found.Instruction : defaultInstruction?.Instruction; } - private void PopulateState(Dictionary dict) + private void PopulateState(Agent agent) { - var conv = _services.GetRequiredService(); - foreach (var t in conv.States.GetStates()) - { - dict[t.Key] = t.Value; - } + agent.TemplateDict = new(CollectRenderData(agent)); } private void AddOrUpdateParameters(Agent agent) { var agentId = agent.Id ?? agent.Name; - if (AgentParameterTypes.ContainsKey(agentId)) return; + if (AgentParameterTypes.ContainsKey(agentId)) + { + return; + } AddOrUpdateRoutesParameters(agentId, agent.RoutingRules); AddOrUpdateFunctionsParameters(agentId, agent.Functions); @@ -103,7 +111,10 @@ private void AddOrUpdateRoutesParameters(string agentId, List routi foreach (var rule in routingRules.Where(x => x.Required)) { - if (string.IsNullOrEmpty(rule.FieldType)) continue; + if (string.IsNullOrEmpty(rule.FieldType)) + { + continue; + } parameterTypes[rule.Field] = rule.FieldType; } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index cc38a151a..266f69935 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -39,7 +39,7 @@ public async Task RefreshAgents(IEnumerable? agentIds = null) try { var agentJson = File.ReadAllText(Path.Combine(dir, "agent.json")); - var agent = JsonSerializer.Deserialize(agentJson, _options); + var agent = JsonSerializer.Deserialize(agentJson, _options.JsonSerializerOptions); if (agent == null) { @@ -62,7 +62,13 @@ public async Task RefreshAgents(IEnumerable? agentIds = null) var tasks = GetTasksFromFile(dir); var codeScripts = GetCodeScriptsFromFile(dir); - var isAgentDeleted = _db.DeleteAgent(agent.Id); + var isAgentDeleted = _db.DeleteAgent(agent.Id, options: new() + { + DeleteRoleAgents = false, + DeleteUserAgents = false, + ToDeleteCodeScripts = codeScripts + }); + if (isAgentDeleted) { await Task.Delay(100); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs index 00f50a1db..83e83ef5a 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs @@ -7,7 +7,7 @@ namespace BotSharp.Core.Agents.Services; public partial class AgentService { - public string RenderInstruction(Agent agent, Dictionary? renderData = null) + public string RenderInstruction(Agent agent, IDictionary? renderData = null) { var render = _services.GetRequiredService(); var conv = _services.GetRequiredService(); @@ -18,16 +18,18 @@ public string RenderInstruction(Agent agent, Dictionary? renderD instructions.AddRange(secondaryInstructions); // update states - var renderDict = renderData != null ? new Dictionary(renderData ?? []) : CollectRenderData(agent); + var renderDict = renderData != null + ? new Dictionary(renderData) + : CollectRenderData(agent); renderDict[TemplateRenderConstant.RENDER_AGENT] = agent; var res = render.Render(string.Join("\r\n", instructions), renderDict); return res; } - public bool RenderFunction(Agent agent, FunctionDef def, Dictionary? renderData = null) + public bool RenderFunction(Agent agent, FunctionDef def, IDictionary? renderData = null) { - var renderDict = renderData ?? agent.TemplateDict; + var renderDict = new Dictionary(renderData ?? agent.TemplateDict ?? []); var isRender = true; var channels = def.Channels; @@ -41,7 +43,10 @@ public bool RenderFunction(Agent agent, FunctionDef def, Dictionary? renderData = null) + public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, IDictionary? renderData = null) { - var parameterDef = def?.Parameters; + var parameterDef = def?.Parameters?.DeepClone(options: _options); var propertyDef = parameterDef?.Properties; - if (propertyDef == null) return null; + if (propertyDef == null) + { + return null; + } - var renderDict = renderData ?? agent.TemplateDict; + var renderDict = new Dictionary(renderData ?? agent.TemplateDict ?? []); var visibleExpress = "visibility_expression"; var root = propertyDef.RootElement; var iterator = root.EnumerateObject(); @@ -105,7 +113,7 @@ public bool RenderFunction(Agent agent, FunctionDef def, Dictionary) PrepareInstructionAndFunctions(Agent agent, Dictionary? renderData = null, StringComparer ? comparer = null) + public (string, IEnumerable) PrepareInstructionAndFunctions(Agent agent, IDictionary? renderData = null, StringComparer ? comparer = null) { var text = string.Empty; if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty()) @@ -117,7 +125,7 @@ public bool RenderFunction(Agent agent, FunctionDef def, Dictionary? renderData = null) + public string RenderTemplate(Agent agent, string templateName, IDictionary? renderData = null) { var conv = _services.GetRequiredService(); var render = _services.GetRequiredService(); @@ -125,7 +133,9 @@ public string RenderTemplate(Agent agent, string templateName, Dictionary x.Name == templateName)?.Content ?? string.Empty; // update states - var renderDict = renderData != null ? new Dictionary(renderData ?? []) : CollectRenderData(agent); + var renderDict = renderData != null + ? new Dictionary(renderData) + : CollectRenderData(agent); renderDict[TemplateRenderConstant.RENDER_AGENT] = agent; // render liquid template @@ -137,7 +147,7 @@ public string RenderTemplate(Agent agent, string templateName, Dictionary dict) + public bool RenderVisibility(string? visibilityExpression, IDictionary dict) { if (string.IsNullOrWhiteSpace(visibilityExpression)) { @@ -145,25 +155,33 @@ public bool RenderVisibility(string? visibilityExpression, Dictionary(); + var copy = dict != null ? new Dictionary(dict) : []; var result = render.Render(visibilityExpression, new Dictionary { - { "states", dict ?? [] } + { "states", copy } }); return result.IsEqualTo("visible"); } - public Dictionary CollectRenderData(Agent agent) + public IDictionary CollectRenderData(Agent agent) { var state = _services.GetRequiredService(); - var renderDict = new Dictionary(agent?.TemplateDict ?? []); - foreach (var t in state.GetStates()) + var innerDict = new Dictionary(); + var dict = new Dictionary(agent.TemplateDict ?? []); + foreach (var p in dict) + { + innerDict[p.Key] = p.Value; + } + + var states = new Dictionary(state.GetStates()); + foreach (var p in states) { - renderDict[t.Key] = t.Value; + innerDict[p.Key] = p.Value; } - return renderDict; + return innerDict; } #region Private methods diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs index ca025447c..d06cc3764 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs @@ -1,6 +1,6 @@ +using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Repositories.Settings; using System.IO; -using System.Reflection; namespace BotSharp.Core.Agents.Services; @@ -10,24 +10,19 @@ public partial class AgentService : IAgentService private readonly IBotSharpRepository _db; private readonly ILogger _logger; private readonly AgentSettings _agentSettings; - private readonly JsonSerializerOptions _options; + private readonly BotSharpOptions _options; public AgentService(IServiceProvider services, IBotSharpRepository db, ILogger logger, - AgentSettings agentSettings) + AgentSettings agentSettings, + BotSharpOptions options) { _services = services; _db = db; _logger = logger; _agentSettings = agentSettings; - _options = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - AllowTrailingCommas = true - }; + _options = options; } public string GetDataDir() @@ -49,7 +44,10 @@ public string GetAgentDataDir(string agentId) public async Task> GetUserAgents(string userId) { - if (string.IsNullOrEmpty(userId)) return []; + if (string.IsNullOrEmpty(userId)) + { + return []; + } var userAgents = _db.GetUserAgents(userId); return userAgents; diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 8d045f856..d0d517f4a 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -62,50 +62,59 @@ - - - - - - - - - - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + @@ -118,129 +127,135 @@ PreserveNewest - + + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest PreserveNewest - + + PreserveNewest - + PreserveNewest - + + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + + PreserveNewest - + PreserveNewest - + PreserveNewest - + + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - - - + PreserveNewest - + + PreserveNewest - + PreserveNewest - + + PreserveNewest diff --git a/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs b/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs deleted file mode 100644 index bc38416dc..000000000 --- a/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace BotSharp.Core.Coding; - -public class CodeScriptExecutor -{ - private readonly ILogger _logger; - private readonly SemaphoreSlim _semLock = new(initialCount: 1, maxCount: 1); - - public CodeScriptExecutor( - ILogger logger) - { - _logger = logger; - } - - public async Task ExecuteAsync(Func> func, CancellationToken cancellationToken = default) - { - await _semLock.WaitAsync(cancellationToken); - - try - { - return await func(); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error in {nameof(CodeScriptExecutor)}."); - return default(T); - } - finally - { - _semLock.Release(); - } - } -} diff --git a/src/Infrastructure/BotSharp.Core/Coding/CodingPlugin.cs b/src/Infrastructure/BotSharp.Core/Coding/CodingPlugin.cs new file mode 100644 index 000000000..efbe13371 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Coding/CodingPlugin.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Core.Coding; + +public class CodingPlugin : IBotSharpPlugin +{ + public string Id => "31bc334b-9462-4191-beac-cb4a139b78c1"; + public string Name => "Coding"; + public string Description => "Handling execution and generation of code scripts"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + var coding = new CodingSettings(); + config.Bind("Coding", coding); + services.AddSingleton(provider => coding); + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs index e1c44a9a1..6de7da9d6 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs @@ -69,7 +69,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); - services.AddSingleton(); } public bool AttachMenu(List menu) diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Audio.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Audio.cs index 62a1356cc..a1cc821e2 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Audio.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Audio.cs @@ -9,9 +9,13 @@ public async Task SpeechToText(InstructFileModel audio, string? text = n if (string.IsNullOrWhiteSpace(text)) { var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); - text = await GetAgentTemplate(innerAgentId, options?.TemplateName); + text = await RenderAgentTemplate(innerAgentId, options?.TemplateName, options?.Data); } - + else + { + text = RenderText(text, options?.Data); + } + var completion = CompletionProvider.GetAudioTranscriber(_services, provider: options?.Provider, model: options?.Model); var audioBinary = await DownloadFile(audio); using var stream = audioBinary.ToStream(); diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs index dbd9de90f..50e912565 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Image.cs @@ -10,7 +10,8 @@ public partial class FileInstructService public async Task ReadImages(string text, IEnumerable images, InstructOptions? options = null) { var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); - var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); + var instruction = await RenderAgentTemplate(innerAgentId, options?.TemplateName, options?.Data); + text = RenderText(text, options?.Data); var completion = CompletionProvider.GetChatCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "gpt-4o", multiModal: true); var message = await completion.GetChatCompletions(new Agent() @@ -48,7 +49,8 @@ await hook.OnResponseGenerated(new InstructResponseModel public async Task GenerateImage(string text, InstructOptions? options = null) { var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); - var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); + var instruction = await RenderAgentTemplate(innerAgentId, options?.TemplateName, options?.Data); + text = RenderText(text, options?.Data); var textContent = text.IfNullOrEmptyAs(instruction).IfNullOrEmptyAs(string.Empty); var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "gpt-image-1-mini"); @@ -85,7 +87,7 @@ public async Task VaryImage(InstructFileModel image, InstructOp var binary = await DownloadFile(image); // Convert image - var converter = GetImageConverter(options?.ImageConvertProvider); + var converter = GetImageConverter(options?.ImageConverter); if (converter != null) { binary = await converter.ConvertImage(binary); @@ -124,13 +126,14 @@ public async Task EditImage(string text, InstructFileModel imag } var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); - var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); + var instruction = await RenderAgentTemplate(innerAgentId, options?.TemplateName, options?.Data); + text = RenderText(text, options?.Data); var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "gpt-image-1-mini"); var binary = await DownloadFile(image); // Convert image - var converter = GetImageConverter(options?.ImageConvertProvider); + var converter = GetImageConverter(options?.ImageConverter); if (converter != null) { binary = await converter.ConvertImage(binary); @@ -173,14 +176,15 @@ public async Task EditImage(string text, InstructFileModel imag } var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); - var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); + var instruction = await RenderAgentTemplate(innerAgentId, options?.TemplateName, options?.Data); + text = RenderText(text, options?.Data); var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "gpt-image-1-mini"); var imageBinary = await DownloadFile(image); var maskBinary = await DownloadFile(mask); // Convert image - var converter = GetImageConverter(options?.ImageConvertProvider); + var converter = GetImageConverter(options?.ImageConverter); if (converter != null) { imageBinary = await converter.ConvertImage(imageBinary); @@ -225,7 +229,8 @@ await hook.OnResponseGenerated(new InstructResponseModel public async Task ComposeImages(string text, InstructFileModel[] images, InstructOptions? options = null) { var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); - var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); + var instruction = await RenderAgentTemplate(innerAgentId, options?.TemplateName, options?.Data); + text = RenderText(text, options?.Data); var completion = CompletionProvider.GetImageCompletion(_services, provider: options?.Provider ?? "openai", model: options?.Model ?? "gpt-image-1-mini"); @@ -236,7 +241,7 @@ public async Task ComposeImages(string text, InstructFileModel[ var binary = await DownloadFile(image); // Convert image - var converter = GetImageConverter(options?.ImageConvertProvider); + var converter = GetImageConverter(options?.ImageConverter); if (converter != null) { binary = await converter.ConvertImage(binary); diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs index 2e4792222..bee60f1af 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs @@ -26,7 +26,7 @@ public async Task ReadPdf(string text, List files, In var pdfFiles = await DownloadAndSaveFiles(sessionDir, files); var targetFiles = pdfFiles; - var converter = GetImageConverter(options?.ImageConvertProvider); + var converter = GetImageConverter(options?.ImageConverter); if (converter == null && provider == "openai") { var fileCoreSettings = _services.GetRequiredService(); @@ -40,7 +40,8 @@ public async Task ReadPdf(string text, List files, In } var innerAgentId = options?.AgentId ?? Guid.Empty.ToString(); - var instruction = await GetAgentTemplate(innerAgentId, options?.TemplateName); + var instruction = await RenderAgentTemplate(innerAgentId, options?.TemplateName, options?.Data); + text = RenderText(text, options?.Data); var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, model: options?.Model ?? "gpt-5-mini", multiModal: true); diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs index 170a0fc0c..48cf5e83c 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs @@ -1,7 +1,6 @@ - +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Files.Converters; -using Microsoft.Extensions.Options; -using static System.Net.Mime.MediaTypeNames; +using BotSharp.Abstraction.Templating; namespace BotSharp.Core.Files.Services; @@ -63,7 +62,7 @@ private async Task DownloadFile(InstructFileModel file) } } - private async Task GetAgentTemplate(string agentId, string? templateName) + private async Task RenderAgentTemplate(string agentId, string? templateName, IDictionary? data = null) { if (string.IsNullOrWhiteSpace(agentId) || string.IsNullOrWhiteSpace(templateName)) { @@ -77,21 +76,32 @@ private async Task DownloadFile(InstructFileModel file) return null; } - var instruction = agentService.RenderTemplate(agent, templateName); + var instruction = agentService.RenderTemplate(agent, templateName, data); return instruction; } + private string RenderText(string text, IDictionary? data = null) + { + var agentService = _services.GetRequiredService(); + var render = _services.GetRequiredService(); + + var renderData = data != null + ? new Dictionary(data) + : agentService.CollectRenderData(new Agent()); + return render.Render(text, renderData); + } + private string BuildFileName(string? name, string? extension, string defaultName, string defaultExtension) { var fname = name.IfNullOrEmptyAs(defaultName); - var fextension = extension.IfNullOrEmptyAs(defaultExtension); + var fextension = extension.IfNullOrEmptyAs(defaultExtension)!; fextension = fextension.StartsWith(".") ? fextension.Substring(1) : fextension; return $"{name}.{fextension}"; } private IImageConverter? GetImageConverter(string? provider) { - var converter = _services.GetServices().FirstOrDefault(x => x.Provider == (provider ?? "file-handler")); + var converter = _services.GetServices().FirstOrDefault(x => x.Provider == (provider ?? "image-handler")); return converter; } #endregion diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs index a62744a51..0cd8a8c70 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Audio.cs @@ -15,7 +15,10 @@ public bool SaveSpeechFile(string conversationId, string fileName, BinaryData da } var filePath = Path.Combine(dir, fileName); - if (File.Exists(filePath)) return false; + if (File.Exists(filePath)) + { + return false; + } using var fs = File.Create(filePath); using var ds = data.ToStream(); diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Common.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Common.cs index 80a10deeb..afed03f54 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Common.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Common.cs @@ -40,7 +40,10 @@ public BinaryData GetFileBytes(string fileStorageUrl) public bool SaveFileStreamToPath(string filePath, Stream stream) { - if (string.IsNullOrEmpty(filePath)) return false; + if (string.IsNullOrEmpty(filePath)) + { + return false; + } using (var fileStream = new FileStream(filePath, FileMode.Create)) { diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs index e4f5fc5c9..23135e8c9 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs @@ -34,9 +34,9 @@ public async Task> GetMessageFileScreenshotsAsync( continue; } - foreach (var subDir in Directory.GetDirectories(dir)) + foreach (var subDir in Directory.EnumerateDirectories(dir)) { - var file = Directory.GetFiles(subDir).FirstOrDefault(); + var file = Directory.EnumerateFiles(subDir).FirstOrDefault(); if (file == null) { continue; @@ -79,7 +79,7 @@ public IEnumerable GetMessageFiles(string conversationId, IEnu var sources = options?.Sources != null ? options.Sources - : Directory.GetDirectories(baseDir).Select(x => x.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last()); + : Directory.EnumerateDirectories(baseDir).Select(x => x.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last()); if (sources.IsNullOrEmpty()) { continue; @@ -93,11 +93,11 @@ public IEnumerable GetMessageFiles(string conversationId, IEnu continue; } - foreach (var subDir in Directory.GetDirectories(dir)) + foreach (var subDir in Directory.EnumerateDirectories(dir)) { var fileIndex = subDir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last(); - foreach (var file in Directory.GetFiles(subDir)) + foreach (var file in Directory.EnumerateFiles(subDir)) { var contentType = FileUtility.GetFileContentType(file); if (options?.ContentTypes != null && !options.ContentTypes.Contains(contentType)) @@ -143,7 +143,7 @@ public string GetMessageFile(string conversationId, string messageId, string sou return string.Empty; } - var found = Directory.GetFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName)); + var found = Directory.EnumerateFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName)); return found; } @@ -308,9 +308,9 @@ private async Task> GetScreenshotsAsync(string fil var contentType = FileUtility.GetFileContentType(file); var screenshotDir = Path.Combine(parentDir, SCREENSHOT_FILE_FOLDER); - if (ExistDirectory(screenshotDir) && !Directory.GetFiles(screenshotDir).IsNullOrEmpty()) + if (ExistDirectory(screenshotDir)) { - foreach (var screenshot in Directory.GetFiles(screenshotDir)) + foreach (var screenshot in Directory.EnumerateFiles(screenshotDir)) { var fileName = Path.GetFileNameWithoutExtension(screenshot); var fileExtension = Path.GetExtension(screenshot).Substring(1); diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.KnowledgeBase.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.KnowledgeBase.cs index 8fc6bb0ac..72170fea0 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.KnowledgeBase.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.KnowledgeBase.cs @@ -46,7 +46,10 @@ public bool DeleteKnowledgeFile(string collectionName, string vectorStoreProvide } var dir = BuildKnowledgeCollectionFileDir(collectionName, vectorStoreProvider); - if (!ExistDirectory(dir)) return false; + if (!ExistDirectory(dir)) + { + return false; + } if (fileId == null) { diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs index 9384bd837..6935a1ad2 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs @@ -10,7 +10,10 @@ public string GetUserAvatar() var user = db.GetUserById(_user.Id); var dir = GetUserAvatarDir(user?.Id); - if (!ExistDirectory(dir)) return string.Empty; + if (!ExistDirectory(dir)) + { + return string.Empty; + } var found = Directory.GetFiles(dir).FirstOrDefault() ?? string.Empty; return found; @@ -18,7 +21,10 @@ public string GetUserAvatar() public bool SaveUserAvatar(FileDataModel file) { - if (file == null || string.IsNullOrEmpty(file.FileData)) return false; + if (file == null || string.IsNullOrEmpty(file.FileData)) + { + return false; + } try { @@ -26,7 +32,10 @@ public bool SaveUserAvatar(FileDataModel file) var user = db.GetUserById(_user.Id); var dir = GetUserAvatarDir(user?.Id); - if (string.IsNullOrEmpty(dir)) return false; + if (string.IsNullOrEmpty(dir)) + { + return false; + } if (Directory.Exists(dir)) { diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs index 60645bcf2..b5d989298 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs @@ -5,7 +5,8 @@ namespace BotSharp.Core.Infrastructures; public class CompletionProvider { - public static object GetCompletion(IServiceProvider services, + public static object GetCompletion( + IServiceProvider services, string? provider = null, string? model = null, AgentLlmConfig? agentConfig = null) @@ -42,7 +43,8 @@ public static object GetCompletion(IServiceProvider services, } } - public static IChatCompletion GetChatCompletion(IServiceProvider services, + public static IChatCompletion GetChatCompletion( + IServiceProvider services, string? provider = null, string? model = null, string? modelId = null, @@ -66,7 +68,8 @@ public static IChatCompletion GetChatCompletion(IServiceProvider services, return completer; } - public static ITextCompletion GetTextCompletion(IServiceProvider services, + public static ITextCompletion GetTextCompletion( + IServiceProvider services, string? provider = null, string? model = null, AgentLlmConfig? agentConfig = null) @@ -86,15 +89,16 @@ public static ITextCompletion GetTextCompletion(IServiceProvider services, return completer; } - public static IImageCompletion GetImageCompletion(IServiceProvider services, + public static IImageCompletion GetImageCompletion( + IServiceProvider services, string? provider = null, string? model = null, string? modelId = null, - bool imageGenerate = false) + IEnumerable? capabilities = null) { var completions = services.GetServices(); (provider, model) = GetProviderAndModel(services, provider: provider, - model: model, modelId: modelId, imageGenerate: imageGenerate); + model: model, modelId: modelId, capabilities: capabilities); var completer = completions.FirstOrDefault(x => x.Provider == provider); if (completer == null) @@ -107,7 +111,8 @@ public static IImageCompletion GetImageCompletion(IServiceProvider services, return completer; } - public static ITextEmbedding GetTextEmbedding(IServiceProvider services, + public static ITextEmbedding GetTextEmbedding( + IServiceProvider services, string? provider = null, string? model = null) { @@ -166,7 +171,8 @@ public static IAudioSynthesis GetAudioSynthesizer( return completer; } - public static IRealTimeCompletion GetRealTimeCompletion(IServiceProvider services, + public static IRealTimeCompletion GetRealTimeCompletion( + IServiceProvider services, string? provider = null, string? model = null, string? modelId = null, @@ -176,7 +182,7 @@ public static IRealTimeCompletion GetRealTimeCompletion(IServiceProvider service var completions = services.GetServices(); (provider, model) = GetProviderAndModel(services, provider: provider, model: model, modelId: modelId, multiModal: multiModal, - modelType: LlmModelType.Realtime, + modelType: LlmModelType.Realtime, agentConfig: agentConfig); var completer = completions.FirstOrDefault(x => x.Provider == provider); @@ -190,13 +196,14 @@ public static IRealTimeCompletion GetRealTimeCompletion(IServiceProvider service return completer; } - private static (string, string) GetProviderAndModel(IServiceProvider services, + private static (string, string) GetProviderAndModel( + IServiceProvider services, string? provider = null, string? model = null, string? modelId = null, bool? multiModal = null, LlmModelType? modelType = null, - bool imageGenerate = false, + IEnumerable? capabilities = null, AgentLlmConfig? agentConfig = null) { var agentSetting = services.GetRequiredService(); @@ -220,9 +227,9 @@ private static (string, string) GetProviderAndModel(IServiceProvider services, var modelIdentity = state.ContainsState("model_id") ? state.GetState("model_id") : modelId; var llmProviderService = services.GetRequiredService(); model = llmProviderService.GetProviderModel(provider, modelIdentity, - multiModal: multiModal, + multiModal: multiModal, modelType: modelType, - imageGenerate: imageGenerate)?.Name; + capabilities: capabilities)?.Name; } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs index 3615faf52..66ce90446 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.Infrastructures; namespace BotSharp.Core.Infrastructures; diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs index 95cfd9c04..3445d3c00 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.MLTasks.Filters; using BotSharp.Abstraction.MLTasks.Settings; using BotSharp.Abstraction.Settings; @@ -40,14 +41,12 @@ public List GetProviderModels(string provider) { var settingService = _services.GetRequiredService(); return settingService.Bind>($"LlmProviders") - .FirstOrDefault(x => x.Provider.Equals(provider)) - ?.Models ?? new List(); + .FirstOrDefault(x => x.Provider.Equals(provider))?.Models ?? []; } - public LlmModelSetting GetProviderModel(string provider, string id, bool? multiModal = null, LlmModelType? modelType = null, bool imageGenerate = false) + public LlmModelSetting? GetProviderModel(string provider, string id, bool? multiModal = null, LlmModelType? modelType = null, IEnumerable? capabilities = null) { - var models = GetProviderModels(provider) - .Where(x => x.Id == id); + var models = GetProviderModels(provider).Where(x => x.Id == id); if (multiModal.HasValue) { @@ -59,7 +58,15 @@ public LlmModelSetting GetProviderModel(string provider, string id, bool? multiM models = models.Where(x => x.Type == modelType.Value); } - models = models.Where(x => x.ImageGeneration == imageGenerate); + if (capabilities != null) + { + models = models.Where(x => x.Capabilities != null && capabilities.Any(y => x.Capabilities.Contains(y))); + } + + if (models.IsNullOrEmpty()) + { + return null; + } var random = new Random(); var index = random.Next(0, models.Count()); @@ -72,14 +79,14 @@ public LlmModelSetting GetProviderModel(string provider, string id, bool? multiM var settings = _services.GetRequiredService>(); var providerSetting = settings.FirstOrDefault(p => p.Provider.Equals(provider, StringComparison.CurrentCultureIgnoreCase)); + if (providerSetting == null) { _logger.LogError($"Can't find provider settings for {provider}"); return null; } - var modelSetting = providerSetting.Models.FirstOrDefault(m => - m.Name.Equals(model, StringComparison.CurrentCultureIgnoreCase)); + var modelSetting = providerSetting.Models.FirstOrDefault(m => m.Name.Equals(model, StringComparison.CurrentCultureIgnoreCase)); if (modelSetting == null) { _logger.LogError($"Can't find model settings for {provider}.{model}"); @@ -95,42 +102,67 @@ public LlmModelSetting GetProviderModel(string provider, string id, bool? multiM m.Group.Equals(modelSetting.Group, StringComparison.CurrentCultureIgnoreCase)) .ToList(); - // pick one model randomly - var random = new Random(); - var index = random.Next(0, models.Count()); - modelSetting = models.ElementAt(index); + if (!models.IsNullOrEmpty()) + { + // pick one model randomly + var random = new Random(); + var index = random.Next(0, models.Count()); + modelSetting = models.ElementAt(index); + } } return modelSetting; } - public List GetLlmConfigs(LlmConfigOptions? options = null) + public List GetLlmConfigs(LlmConfigFilter? filter = null) { var settingService = _services.GetRequiredService(); var providers = settingService.Bind>($"LlmProviders"); var configs = new List(); + var comparer = StringComparer.OrdinalIgnoreCase; + + if (providers.IsNullOrEmpty()) + { + return configs; + } - if (providers.IsNullOrEmpty()) return configs; + if (filter == null) + { + return providers ?? []; + } - if (options == null) return providers ?? []; + if (filter.Providers != null) + { + providers = providers.Where(x => filter.Providers.Contains(x.Provider, comparer)).ToList(); + } foreach (var provider in providers) { - var models = provider.Models ?? []; - if (options.Type.HasValue) + IEnumerable models = provider.Models ?? []; + if (filter.ModelTypes != null) + { + models = models.Where(x => filter.ModelTypes.Contains(x.Type)); + } + + if (filter.ModelIds != null) + { + models = models.Where(x => filter.ModelIds.Contains(x.Id, comparer)); + } + + if (filter.ModelNames != null) { - models = models.Where(x => x.Type == options.Type.Value).ToList(); + models = models.Where(x => filter.ModelNames.Contains(x.Name, comparer)); } - if (options.MultiModal.HasValue) + if (filter.ModelCapabilities != null) { - models = models.Where(x => x.MultiModal == options.MultiModal.Value).ToList(); + models = models.Where(x => x.Capabilities != null && filter.ModelCapabilities.Any(y => x.Capabilities.Contains(y))); } - if (options.ImageGeneration.HasValue) + if (filter.MultiModal.HasValue) { - models = models.Where(x => x.ImageGeneration == options.ImageGeneration.Value).ToList(); + models = models.Where(x => x.MultiModal == filter.MultiModal.Value); } if (models.IsNullOrEmpty()) @@ -138,7 +170,7 @@ public List GetLlmConfigs(LlmConfigOptions? options = null) continue; } - provider.Models = models; + provider.Models = models.ToList(); configs.Add(provider); } diff --git a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs index 3556cabc6..a0b7dc8cd 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs @@ -1,8 +1,9 @@ using BotSharp.Abstraction.Coding; +using BotSharp.Abstraction.Coding.Enums; +using BotSharp.Abstraction.Coding.Contexts; using BotSharp.Abstraction.Files.Options; using BotSharp.Abstraction.Files.Proccessors; using BotSharp.Abstraction.Instructs; -using BotSharp.Abstraction.Instructs.Contexts; using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Options; using BotSharp.Abstraction.MLTasks; @@ -23,7 +24,7 @@ public async Task Execute( { var agentService = _services.GetRequiredService(); var state = _services.GetRequiredService(); - Agent agent = await agentService.LoadAgent(agentId); + var agent = await agentService.LoadAgent(agentId); var response = new InstructResult { @@ -48,7 +49,7 @@ public async Task Execute( // Run code template var codeResponse = await RunCode(agent, message, templateName, codeOptions); - if (codeResponse != null) + if (!string.IsNullOrWhiteSpace(codeResponse?.Text)) { return codeResponse; } @@ -168,18 +169,21 @@ await hook.OnResponseGenerated(new InstructResponseModel string templateName, CodeInstructOptions? codeOptions) { - InstructResult? response = null; + InstructResult? instructResult = null; if (agent == null) { - return response; + return instructResult; } var agentService = _services.GetRequiredService(); var state = _services.GetRequiredService(); + var codingSettings = _services.GetRequiredService(); var hooks = _services.GetHooks(agent.Id); - var codeProvider = codeOptions?.Processor ?? "botsharp-py-interpreter"; + var codeProvider = codeOptions?.Processor ?? codingSettings.CodeExecution?.Processor; + codeProvider = !string.IsNullOrEmpty(codeProvider) ? codeProvider : BuiltInCodeProcessor.PyInterpreter; + var codeProcessor = _services.GetServices() .FirstOrDefault(x => x.Provider.IsEqualTo(codeProvider)); @@ -188,14 +192,14 @@ await hook.OnResponseGenerated(new InstructResponseModel #if DEBUG _logger.LogWarning($"No code processor found. (Agent: {agent.Id}, Code processor: {codeProvider})"); #endif - return response; + return instructResult; } // Get code script name var scriptName = string.Empty; - if (!string.IsNullOrEmpty(codeOptions?.CodeScriptName)) + if (!string.IsNullOrEmpty(codeOptions?.ScriptName)) { - scriptName = codeOptions.CodeScriptName; + scriptName = codeOptions.ScriptName; } else if (!string.IsNullOrEmpty(templateName)) { @@ -207,17 +211,18 @@ await hook.OnResponseGenerated(new InstructResponseModel #if DEBUG _logger.LogWarning($"Empty code script name. (Agent: {agent.Id}, {scriptName})"); #endif - return response; + return instructResult; } // Get code script - var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); - if (string.IsNullOrWhiteSpace(codeScript)) + var scriptType = codeOptions?.ScriptType ?? AgentCodeScriptType.Src; + var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType); + if (string.IsNullOrWhiteSpace(codeScript?.Content)) { #if DEBUG _logger.LogWarning($"Empty code script. (Agent: {agent.Id}, {scriptName})"); #endif - return response; + return instructResult; } // Get code arguments @@ -227,7 +232,7 @@ await hook.OnResponseGenerated(new InstructResponseModel arguments = state.GetStates().Select(x => new KeyValue(x.Key, x.Value)).ToList(); } - var context = new CodeInstructContext + var context = new CodeExecutionContext { CodeScript = codeScript, Arguments = arguments @@ -237,7 +242,7 @@ await hook.OnResponseGenerated(new InstructResponseModel foreach (var hook in hooks) { await hook.BeforeCompletion(agent, message); - await hook.BeforeCodeExecution(agent, message, context); + await hook.BeforeCodeExecution(agent, context); // Interrupted by hook if (message.StopCompletion) @@ -251,42 +256,40 @@ await hook.OnResponseGenerated(new InstructResponseModel } // Run code script - var codeResponse = await codeProcessor.RunAsync(context.CodeScript, options: new() + var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(codingSettings); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); + var codeResponse = codeProcessor.Run(context.CodeScript?.Content ?? string.Empty, options: new() { - ScriptName = scriptName, - Arguments = context.Arguments - }); + ScriptName = context.CodeScript?.Name, + Arguments = context.Arguments, + UseLock = useLock, + UseProcess = useProcess + }, cancellationToken: cts.Token); - response = new InstructResult + instructResult = new InstructResult { MessageId = message.MessageId, - Template = scriptName, - Text = codeResponse?.Result ?? codeResponse?.ErrorMsg + Template = context.CodeScript?.Name, + Text = codeResponse?.Result ?? string.Empty }; - if (context?.Arguments != null) + var codeExecution = new CodeExecutionResponseModel { - context.Arguments.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - } + CodeProcessor = codeProcessor.Provider, + CodeScript = context.CodeScript, + ExecutionResult = codeResponse, + Text = message.Content, + Arguments = context.Arguments?.DistinctBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value ?? string.Empty) + }; // After code execution foreach (var hook in hooks) { - await hook.AfterCompletion(agent, response); - await hook.AfterCodeExecution(agent, response); - await hook.OnResponseGenerated(new InstructResponseModel - { - AgentId = agent.Id, - Provider = codeProcessor.Provider, - Model = string.Empty, - TemplateName = scriptName, - UserMessage = message.Content, - SystemInstruction = context?.CodeScript, - CompletionText = response.Text - }); + await hook.AfterCompletion(agent, instructResult); + await hook.AfterCodeExecution(agent, codeExecution); } - return response; + return instructResult; } private async Task GetTextCompletion( @@ -324,4 +327,20 @@ private async Task GetChatCompletion( return result.Content; } + + /// + /// Returns (useLock, useProcess, timeoutSeconds) + /// + /// + private (bool, bool, int) GetCodeExecutionConfig(CodingSettings settings) + { + var codeExecution = settings.CodeExecution; + var defaultTimeoutSeconds = 3; + + var useLock = codeExecution?.UseLock ?? false; + var useProcess = codeExecution?.UseProcess ?? false; + var timeoutSeconds = codeExecution?.TimeoutSeconds > 0 ? codeExecution.TimeoutSeconds : defaultTimeoutSeconds; + + return (useLock, useProcess, timeoutSeconds); + } } diff --git a/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs b/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs index 743bf4c06..b117555fd 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs @@ -27,7 +27,7 @@ public override void OnAgentMcpToolLoaded(Agent agent) agent.SecondaryFunctions ??= []; - var functions = GetMcpContent(agent).Result; + var functions = GetMcpContent(agent).ConfigureAwait(false).GetAwaiter().GetResult(); agent.SecondaryFunctions = agent.SecondaryFunctions.Concat(functions).DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase).ToList(); } diff --git a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs index 083ff85aa..96c703121 100644 --- a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs +++ b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs @@ -159,13 +159,13 @@ public PluginDef UpdatePluginStatus(IServiceProvider services, string id, bool e var agentService = services.GetRequiredService(); foreach (var agentId in dependentAgentIds) { - var agent = agentService.LoadAgent(agentId).Result; + var agent = agentService.LoadAgent(agentId).ConfigureAwait(false).GetAwaiter().GetResult(); agent.Disabled = false; agentService.UpdateAgent(agent, AgentField.Disabled); if (agent.InheritAgentId != null) { - agent = agentService.LoadAgent(agent.InheritAgentId).Result; + agent = agentService.LoadAgent(agent.InheritAgentId).ConfigureAwait(false).GetAwaiter().GetResult(); agent.Disabled = false; agentService.UpdateAgent(agent, AgentField.Disabled); } @@ -183,7 +183,7 @@ public PluginDef UpdatePluginStatus(IServiceProvider services, string id, bool e var agentService = services.GetRequiredService(); foreach (var agentId in plugin.AgentIds) { - var agent = agentService.LoadAgent(agentId).Result; + var agent = agentService.LoadAgent(agentId).ConfigureAwait(false).GetAwaiter().GetResult(); if (agent != null) { agent.Disabled = true; diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index bd2599526..c6b1e3a45 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Agents.Options; using BotSharp.Abstraction.Routing.Models; using System.IO; using System.Text.RegularExpressions; @@ -91,10 +92,16 @@ public void UpdateAgent(Agent agent, AgentField field) #region Update Agent Fields private void UpdateAgentName(string agentId, string name) { - if (string.IsNullOrWhiteSpace(name)) return; + if (string.IsNullOrWhiteSpace(name)) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Name = name; agent.UpdatedDateTime = DateTime.UtcNow; @@ -104,10 +111,16 @@ private void UpdateAgentName(string agentId, string name) private void UpdateAgentDescription(string agentId, string description) { - if (string.IsNullOrWhiteSpace(description)) return; + if (string.IsNullOrWhiteSpace(description)) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Description = description; agent.UpdatedDateTime = DateTime.UtcNow; @@ -118,7 +131,10 @@ private void UpdateAgentDescription(string agentId, string description) private void UpdateAgentIsPublic(string agentId, bool isPublic) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.IsPublic = isPublic; agent.UpdatedDateTime = DateTime.UtcNow; @@ -129,7 +145,10 @@ private void UpdateAgentIsPublic(string agentId, bool isPublic) private void UpdateAgentDisabled(string agentId, bool disabled) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Disabled = disabled; agent.UpdatedDateTime = DateTime.UtcNow; @@ -140,7 +159,10 @@ private void UpdateAgentDisabled(string agentId, bool disabled) private void UpdateAgentType(string agentId, string type) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Type = type; agent.UpdatedDateTime = DateTime.UtcNow; @@ -151,7 +173,10 @@ private void UpdateAgentType(string agentId, string type) private void UpdateAgentRoutingMode(string agentId, string? mode) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Mode = mode; agent.UpdatedDateTime = DateTime.UtcNow; @@ -162,7 +187,10 @@ private void UpdateAgentRoutingMode(string agentId, string? mode) private void UpdateAgentFuncVisMode(string agentId, string? visMode) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.FuncVisMode = visMode; agent.UpdatedDateTime = DateTime.UtcNow; @@ -173,7 +201,10 @@ private void UpdateAgentFuncVisMode(string agentId, string? visMode) private void UpdateAgentInheritAgentId(string agentId, string? inheritAgentId) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.InheritAgentId = inheritAgentId; agent.UpdatedDateTime = DateTime.UtcNow; @@ -183,10 +214,16 @@ private void UpdateAgentInheritAgentId(string agentId, string? inheritAgentId) private void UpdateAgentProfiles(string agentId, List profiles) { - if (profiles == null) return; + if (profiles == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Profiles = profiles; agent.UpdatedDateTime = DateTime.UtcNow; @@ -196,10 +233,16 @@ private void UpdateAgentProfiles(string agentId, List profiles) public bool UpdateAgentLabels(string agentId, List labels) { - if (labels == null) return false; + if (labels == null) + { + return false; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return false; + if (agent == null) + { + return false; + } agent.Labels = labels; agent.UpdatedDateTime = DateTime.UtcNow; @@ -210,10 +253,16 @@ public bool UpdateAgentLabels(string agentId, List labels) private void UpdateAgentUtilities(string agentId, bool mergeUtility, List utilities) { - if (utilities == null) return; + if (utilities == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.MergeUtility = mergeUtility; agent.Utilities = utilities; @@ -224,11 +273,16 @@ private void UpdateAgentUtilities(string agentId, bool mergeUtility, List mcptools) { - if (mcptools == null) return; + if (mcptools == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; - + if (agent == null) + { + return; + } agent.McpTools = mcptools; agent.UpdatedDateTime = DateTime.UtcNow; @@ -238,10 +292,16 @@ private void UpdateAgentMcpTools(string agentId, List mcptools) private void UpdateAgentKnowledgeBases(string agentId, List knowledgeBases) { - if (knowledgeBases == null) return; + if (knowledgeBases == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.KnowledgeBases = knowledgeBases; agent.UpdatedDateTime = DateTime.UtcNow; @@ -251,10 +311,16 @@ private void UpdateAgentKnowledgeBases(string agentId, List private void UpdateAgentRules(string agentId, List rules) { - if (rules == null) return; + if (rules == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Rules = rules; agent.UpdatedDateTime = DateTime.UtcNow; @@ -264,10 +330,16 @@ private void UpdateAgentRules(string agentId, List rules) private void UpdateAgentRoutingRules(string agentId, List rules) { - if (rules == null) return; + if (rules == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.RoutingRules = rules; agent.UpdatedDateTime = DateTime.UtcNow; @@ -277,10 +349,16 @@ private void UpdateAgentRoutingRules(string agentId, List rules) private void UpdateAgentInstructions(string agentId, string instruction, List channelInstructions) { - if (string.IsNullOrWhiteSpace(instruction)) return; + if (string.IsNullOrWhiteSpace(instruction)) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } var instructionDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_INSTRUCTIONS_FOLDER); DeleteBeforeCreateDirectory(instructionDir); @@ -293,7 +371,10 @@ private void UpdateAgentInstructions(string agentId, string instruction, List inputFunctions) { - if (inputFunctions == null) return; + if (inputFunctions == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } var functionDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_FUNCTIONS_FOLDER); DeleteBeforeCreateDirectory(functionDir); foreach (var func in inputFunctions) { - if (string.IsNullOrWhiteSpace(func.Name)) continue; + if (string.IsNullOrWhiteSpace(func.Name)) + { + continue; + } var text = JsonSerializer.Serialize(func, _options); var file = Path.Combine(functionDir, $"{func.Name}.json"); @@ -324,10 +414,16 @@ private void UpdateAgentFunctions(string agentId, List inputFunctio private void UpdateAgentTemplates(string agentId, List templates) { - if (templates == null) return; + if (templates == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } var templateDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_TEMPLATES_FOLDER); DeleteBeforeCreateDirectory(templateDir); @@ -341,10 +437,16 @@ private void UpdateAgentTemplates(string agentId, List templates) private void UpdateAgentResponses(string agentId, List responses) { - if (responses == null) return; + if (responses == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } var responseDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_RESPONSES_FOLDER); DeleteBeforeCreateDirectory(responseDir); @@ -360,10 +462,16 @@ private void UpdateAgentResponses(string agentId, List responses) private void UpdateAgentSamples(string agentId, List samples) { - if (samples == null) return; + if (samples == null) + { + return; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } var file = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_SAMPLES_FILE); File.WriteAllLines(file, samples); @@ -372,7 +480,10 @@ private void UpdateAgentSamples(string agentId, List samples) private void UpdateAgentLlmConfig(string agentId, AgentLlmConfig? config) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.LlmConfig = config; agent.UpdatedDateTime = DateTime.UtcNow; @@ -383,7 +494,10 @@ private void UpdateAgentLlmConfig(string agentId, AgentLlmConfig? config) private void UpdateAgentMaxMessageCount(string agentId, int? maxMessageCount) { var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return; + if (agent == null) + { + return; + } agent.MaxMessageCount = maxMessageCount; agent.UpdatedDateTime = DateTime.UtcNow; @@ -394,7 +508,10 @@ private void UpdateAgentMaxMessageCount(string agentId, int? maxMessageCount) private void UpdateAgentAllFields(Agent inputAgent) { var (agent, agentFile) = GetAgentFromFile(inputAgent.Id); - if (agent == null) return; + if (agent == null) + { + return; + } agent.Name = inputAgent.Name; agent.Type = inputAgent.Type; @@ -429,9 +546,12 @@ public List GetAgentResponses(string agentId, string prefix, string inte { var responses = new List(); var dir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_RESPONSES_FOLDER); - if (!Directory.Exists(dir)) return responses; + if (!Directory.Exists(dir)) + { + return responses; + } - foreach (var file in Directory.GetFiles(dir)) + foreach (var file in Directory.EnumerateFiles(dir)) { if (file.Split(Path.DirectorySeparatorChar) .Last() @@ -447,17 +567,26 @@ public List GetAgentResponses(string agentId, string prefix, string inte public Agent? GetAgent(string agentId, bool basicsOnly = false) { var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir); - var dir = Directory.GetDirectories(agentDir).FirstOrDefault(x => x.Split(Path.DirectorySeparatorChar).Last() == agentId); + var dir = Directory.EnumerateDirectories(agentDir).FirstOrDefault(x => x.Split(Path.DirectorySeparatorChar).Last() == agentId); if (!string.IsNullOrEmpty(dir)) { var json = File.ReadAllText(Path.Combine(dir, AGENT_FILE)); - if (string.IsNullOrEmpty(json)) return null; + if (string.IsNullOrEmpty(json)) + { + return null; + } var record = JsonSerializer.Deserialize(json, _options); - if (record == null) return null; + if (record == null) + { + return null; + } - if (basicsOnly) return record; + if (basicsOnly) + { + return record; + } var (defaultInstruction, channelInstructions) = FetchInstructions(dir); var functions = FetchFunctions(dir); @@ -529,14 +658,20 @@ join u in Users on ua.UserId equals u.Id where ua.UserId == userId || u.ExternalId == userId select ua).ToList(); - if (found.IsNullOrEmpty()) return []; + if (found.IsNullOrEmpty()) + { + return []; + } var agentIds = found.Select(x => x.AgentId).Distinct().ToList(); var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); foreach (var item in found) { var agent = agents.FirstOrDefault(x => x.Id == item.AgentId); - if (agent == null) continue; + if (agent == null) + { + continue; + } item.Agent = agent; } @@ -554,9 +689,12 @@ public string GetAgentTemplate(string agentId, string templateName) } var dir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_TEMPLATES_FOLDER); - if (!Directory.Exists(dir)) return string.Empty; + if (!Directory.Exists(dir)) + { + return string.Empty; + } - foreach (var file in Directory.GetFiles(dir)) + foreach (var file in Directory.EnumerateFiles(dir)) { var fileName = file.Split(Path.DirectorySeparatorChar).Last(); var splitIdx = fileName.LastIndexOf("."); @@ -573,19 +711,28 @@ public string GetAgentTemplate(string agentId, string templateName) public bool PatchAgentTemplate(string agentId, AgentTemplate template) { - if (string.IsNullOrEmpty(agentId) || template == null) return false; + if (string.IsNullOrEmpty(agentId) || template == null) + { + return false; + } var dir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId, AGENT_TEMPLATES_FOLDER); - if (!Directory.Exists(dir)) return false; + if (!Directory.Exists(dir)) + { + return false; + } - var foundTemplate = Directory.GetFiles(dir).FirstOrDefault(f => + var foundTemplate = Directory.EnumerateFiles(dir).FirstOrDefault(f => { var fileName = Path.GetFileNameWithoutExtension(f); var extension = Path.GetExtension(f).Substring(1); return fileName.IsEqualTo(template.Name) && extension.IsEqualTo(_agentSettings.TemplateFormat); }); - if (foundTemplate == null) return false; + if (foundTemplate == null) + { + return false; + } File.WriteAllText(foundTemplate, template.Content); return true; @@ -593,10 +740,16 @@ public bool PatchAgentTemplate(string agentId, AgentTemplate template) public bool AppendAgentLabels(string agentId, List labels) { - if (labels.IsNullOrEmpty()) return false; + if (labels.IsNullOrEmpty()) + { + return false; + } var (agent, agentFile) = GetAgentFromFile(agentId); - if (agent == null) return false; + if (agent == null) + { + return false; + } var prevLabels = agent.Labels ?? []; var curLabels = prevLabels.Concat(labels).Distinct().ToList(); @@ -609,13 +762,19 @@ public bool AppendAgentLabels(string agentId, List labels) public void BulkInsertAgents(List agents) { - if (agents.IsNullOrEmpty()) return; + if (agents.IsNullOrEmpty()) + { + return; + } var baseDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir); foreach (var agent in agents) { var dir = Path.Combine(baseDir, agent.Id); - if (Directory.Exists(dir)) continue; + if (Directory.Exists(dir)) + { + continue; + } Directory.CreateDirectory(dir); Thread.Sleep(50); @@ -638,7 +797,10 @@ public void BulkInsertAgents(List agents) public void BulkInsertUserAgents(List userAgents) { - if (userAgents.IsNullOrEmpty()) return; + if (userAgents.IsNullOrEmpty()) + { + return; + } var groups = userAgents.GroupBy(x => x.UserId); var usersDir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER); @@ -646,12 +808,18 @@ public void BulkInsertUserAgents(List userAgents) foreach (var group in groups) { var filtered = group.Where(x => !string.IsNullOrEmpty(x.UserId) && !string.IsNullOrEmpty(x.AgentId)).ToList(); - if (filtered.IsNullOrEmpty()) continue; + if (filtered.IsNullOrEmpty()) + { + continue; + } filtered.ForEach(x => x.Id = Guid.NewGuid().ToString()); var userId = filtered.First().UserId; var userDir = Path.Combine(usersDir, userId); - if (!Directory.Exists(userDir)) continue; + if (!Directory.Exists(userDir)) + { + continue; + } var userAgentFile = Path.Combine(userDir, USER_AGENT_FILE); var list = new List(); @@ -674,48 +842,72 @@ public bool DeleteAgents() return false; } - public bool DeleteAgent(string agentId) + public bool DeleteAgent(string agentId, AgentDeleteOptions? options = null) { - if (string.IsNullOrEmpty(agentId)) return false; + if (string.IsNullOrEmpty(agentId)) + { + return false; + } try { var agentDir = GetAgentDataDir(agentId); - if (string.IsNullOrEmpty(agentDir)) return false; + if (string.IsNullOrEmpty(agentDir)) + { + return false; + } - // Delete user agents - var usersDir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER); - if (Directory.Exists(usersDir)) + if (options == null || options.DeleteUserAgents) { - foreach (var userDir in Directory.GetDirectories(usersDir)) + // Delete user agents + var usersDir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER); + if (Directory.Exists(usersDir)) { - var userAgentFile = Directory.GetFiles(userDir).FirstOrDefault(x => Path.GetFileName(x) == USER_AGENT_FILE); - if (string.IsNullOrEmpty(userAgentFile)) continue; - - var text = File.ReadAllText(userAgentFile); - var userAgents = JsonSerializer.Deserialize>(text, _options); - if (userAgents.IsNullOrEmpty()) continue; - - userAgents = userAgents?.Where(x => x.AgentId != agentId)?.ToList() ?? []; - File.WriteAllText(userAgentFile, JsonSerializer.Serialize(userAgents, _options)); + foreach (var userDir in Directory.EnumerateDirectories(usersDir)) + { + var userAgentFile = Directory.GetFiles(userDir).FirstOrDefault(x => Path.GetFileName(x) == USER_AGENT_FILE); + if (string.IsNullOrEmpty(userAgentFile)) + { + continue; + } + + var text = File.ReadAllText(userAgentFile); + var userAgents = JsonSerializer.Deserialize>(text, _options); + if (userAgents.IsNullOrEmpty()) + { + continue; + } + + userAgents = userAgents?.Where(x => x.AgentId != agentId)?.ToList() ?? []; + File.WriteAllText(userAgentFile, JsonSerializer.Serialize(userAgents, _options)); + } } } - - // Delete role agents - var rolesDir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER); - if (Directory.Exists(rolesDir)) + + if (options == null || options.DeleteRoleAgents) { - foreach (var roleDir in Directory.GetDirectories(rolesDir)) + // Delete role agents + var rolesDir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER); + if (Directory.Exists(rolesDir)) { - var roleAgentFile = Directory.GetFiles(roleDir).FirstOrDefault(x => Path.GetFileName(x) == ROLE_AGENT_FILE); - if (string.IsNullOrEmpty(roleAgentFile)) continue; - - var text = File.ReadAllText(roleAgentFile); - var roleAgents = JsonSerializer.Deserialize>(text, _options); - if (roleAgents.IsNullOrEmpty()) continue; - - roleAgents = roleAgents?.Where(x => x.AgentId != agentId)?.ToList() ?? []; - File.WriteAllText(roleAgentFile, JsonSerializer.Serialize(roleAgents, _options)); + foreach (var roleDir in Directory.EnumerateDirectories(rolesDir)) + { + var roleAgentFile = Directory.GetFiles(roleDir).FirstOrDefault(x => Path.GetFileName(x) == ROLE_AGENT_FILE); + if (string.IsNullOrEmpty(roleAgentFile)) + { + continue; + } + + var text = File.ReadAllText(roleAgentFile); + var roleAgents = JsonSerializer.Deserialize>(text, _options); + if (roleAgents.IsNullOrEmpty()) + { + continue; + } + + roleAgents = roleAgents?.Where(x => x.AgentId != agentId)?.ToList() ?? []; + File.WriteAllText(roleAgentFile, JsonSerializer.Serialize(roleAgents, _options)); + } } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentCodeScript.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentCodeScript.cs index b085ad34b..fa4d11a9f 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentCodeScript.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentCodeScript.cs @@ -50,7 +50,7 @@ public List GetAgentCodeScripts(string agentId, AgentCodeScript return results; } - public string? GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) + public AgentCodeScript? GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) { if (string.IsNullOrWhiteSpace(agentId) || string.IsNullOrWhiteSpace(scriptName) @@ -65,12 +65,21 @@ public List GetAgentCodeScripts(string agentId, AgentCodeScript return null; } - var foundFile = Directory.GetFiles(dir).FirstOrDefault(file => scriptName.IsEqualTo(Path.GetFileName(file))); - if (!string.IsNullOrEmpty(foundFile)) + var foundFile = Directory.EnumerateFiles(dir).FirstOrDefault(file => scriptName.IsEqualTo(Path.GetFileName(file))); + if (!File.Exists(foundFile)) { - return File.ReadAllText(foundFile); + return null; } - return string.Empty; + + return new AgentCodeScript + { + AgentId = agentId, + Name = scriptName, + ScriptType = scriptType, + Content = File.ReadAllText(foundFile), + CreatedTime = File.GetCreationTimeUtc(foundFile), + UpdatedTime = File.GetLastWriteTimeUtc(foundFile) + }; } public bool UpdateAgentCodeScripts(string agentId, List scripts, AgentCodeScriptDbUpdateOptions? options = null) @@ -91,7 +100,14 @@ public bool UpdateAgentCodeScripts(string agentId, List scripts var dir = BuildAgentCodeScriptDir(agentId, script.ScriptType); if (!Directory.Exists(dir)) { - continue; + if (options?.IsUpsert == true) + { + Directory.CreateDirectory(dir); + } + else + { + continue; + } } var file = Path.Combine(dir, script.Name); @@ -106,7 +122,7 @@ public bool UpdateAgentCodeScripts(string agentId, List scripts public bool BulkInsertAgentCodeScripts(string agentId, List scripts) { - return UpdateAgentCodeScripts(agentId, scripts); + return UpdateAgentCodeScripts(agentId, scripts, options: new() { IsUpsert = true }); } public bool DeleteAgentCodeScripts(string agentId, List? scripts = null) @@ -117,14 +133,12 @@ public bool DeleteAgentCodeScripts(string agentId, List? script } var dir = BuildAgentCodeScriptDir(agentId); - if (!Directory.Exists(dir)) - { - return false; - } - if (scripts == null) { - Directory.Delete(dir, true); + if (Directory.Exists(dir)) + { + Directory.Delete(dir, true); + } return true; } else if (!scripts.Any()) @@ -132,6 +146,11 @@ public bool DeleteAgentCodeScripts(string agentId, List? script return false; } + if (!Directory.Exists(dir)) + { + return false; + } + var dict = scripts.DistinctBy(x => x.CodePath).ToDictionary(x => x.CodePath, x => x); foreach (var pair in dict) { diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs index b3926e66f..b847b0be6 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentTask.cs @@ -21,12 +21,18 @@ public async ValueTask> GetAgentTasks(AgentTaskFilter filt var matched = true; var dir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir); - if (!Directory.Exists(dir)) return new PagedItems(); + if (!Directory.Exists(dir)) + { + return new PagedItems(); + } - foreach (var agentDir in Directory.GetDirectories(dir)) + foreach (var agentDir in Directory.EnumerateDirectories(dir)) { var taskDir = Path.Combine(agentDir, AGENT_TASKS_FOLDER); - if (!Directory.Exists(taskDir)) continue; + if (!Directory.Exists(taskDir)) + { + continue; + } var agentId = agentDir.Split(Path.DirectorySeparatorChar).Last(); @@ -36,13 +42,19 @@ public async ValueTask> GetAgentTasks(AgentTaskFilter filt matched = agentId == filter.AgentId; } - if (!matched) continue; + if (!matched) + { + continue; + } var curTasks = new List(); - foreach (var taskFile in Directory.GetFiles(taskDir)) + foreach (var taskFile in Directory.EnumerateFiles(taskDir)) { var task = ParseAgentTask(taskFile); - if (task == null) continue; + if (task == null) + { + continue; + } matched = true; if (filter?.Enabled != null) @@ -55,10 +67,16 @@ public async ValueTask> GetAgentTasks(AgentTaskFilter filt matched = matched && task.Status == filter.Status; } - if (!matched) continue; + if (!matched) + { + continue; + } totalCount++; - if (takeCount >= pager.Size) continue; + if (takeCount >= pager.Size) + { + continue; + } if (skipCount < pager.Offset) { @@ -71,7 +89,10 @@ public async ValueTask> GetAgentTasks(AgentTaskFilter filt } } - if (curTasks.IsNullOrEmpty()) continue; + if (curTasks.IsNullOrEmpty()) + { + continue; + } var agent = ParseAgent(agentDir); curTasks.ForEach(t => @@ -92,16 +113,28 @@ public async ValueTask> GetAgentTasks(AgentTaskFilter filt public AgentTask? GetAgentTask(string agentId, string taskId) { var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agentId); - if (!Directory.Exists(agentDir)) return null; + if (!Directory.Exists(agentDir)) + { + return null; + } var taskDir = Path.Combine(agentDir, AGENT_TASKS_FOLDER); - if (!Directory.Exists(taskDir)) return null; + if (!Directory.Exists(taskDir)) + { + return null; + } var taskFile = FindTaskFileById(taskDir, taskId); - if (taskFile == null) return null; + if (taskFile == null) + { + return null; + } var task = ParseAgentTask(taskFile); - if (task == null) return null; + if (task == null) + { + return null; + } var agent = ParseAgent(agentDir); task.AgentId = agentId; @@ -111,10 +144,16 @@ public async ValueTask> GetAgentTasks(AgentTaskFilter filt public void InsertAgentTask(AgentTask task) { - if (task == null || string.IsNullOrEmpty(task.AgentId)) return; + if (task == null || string.IsNullOrEmpty(task.AgentId)) + { + return; + } var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, task.AgentId); - if (!Directory.Exists(agentDir)) return; + if (!Directory.Exists(agentDir)) + { + return; + } var taskDir = Path.Combine(agentDir, AGENT_TASKS_FOLDER); if (!Directory.Exists(taskDir)) @@ -144,19 +183,34 @@ public void BulkInsertAgentTasks(string agentId, List tasks) public void UpdateAgentTask(AgentTask task, AgentTaskField field) { - if (task == null || string.IsNullOrEmpty(task.Id)) return; + if (task == null || string.IsNullOrEmpty(task.Id)) + { + return; + } var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, task.AgentId); - if (!Directory.Exists(agentDir)) return; + if (!Directory.Exists(agentDir)) + { + return; + } var taskDir = Path.Combine(agentDir, AGENT_TASKS_FOLDER); - if (!Directory.Exists(taskDir)) return; + if (!Directory.Exists(taskDir)) + { + return; + } var taskFile = FindTaskFileById(taskDir, task.Id); - if (string.IsNullOrEmpty(taskFile)) return; + if (string.IsNullOrEmpty(taskFile)) + { + return; + } var parsedTask = ParseAgentTask(taskFile); - if (parsedTask == null) return; + if (parsedTask == null) + { + return; + } var metaData = new AgentTask { @@ -218,7 +272,10 @@ public bool DeleteAgentTasks(string agentId, List? taskIds = null) foreach (var taskId in taskIds) { var taskFile = FindTaskFileById(taskDir, taskId); - if (string.IsNullOrWhiteSpace(taskFile)) continue; + if (string.IsNullOrWhiteSpace(taskFile)) + { + continue; + } File.Delete(taskFile); deletedTasks.Add(taskId); @@ -229,9 +286,12 @@ public bool DeleteAgentTasks(string agentId, List? taskIds = null) private string? FindTaskFileById(string taskDir, string taskId) { - if (!Directory.Exists(taskDir) || string.IsNullOrEmpty(taskId)) return null; + if (!Directory.Exists(taskDir) || string.IsNullOrEmpty(taskId)) + { + return null; + } - var taskFile = Directory.GetFiles(taskDir).FirstOrDefault(file => + var taskFile = Directory.EnumerateFiles(taskDir).FirstOrDefault(file => { var fileName = file.Split(Path.DirectorySeparatorChar).Last(); var id = fileName.Split('.').First(); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 68881ea37..a3cece2c1 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -1,6 +1,4 @@ using BotSharp.Abstraction.Loggers.Models; -using BotSharp.Abstraction.Users.Models; -using System; using System.IO; namespace BotSharp.Core.Repository; @@ -53,12 +51,18 @@ public void CreateNewConversation(Conversation conversation) public bool DeleteConversations(IEnumerable conversationIds) { - if (conversationIds.IsNullOrEmpty()) return false; + if (conversationIds.IsNullOrEmpty()) + { + return false; + } foreach (var conversationId in conversationIds) { var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) continue; + if (string.IsNullOrEmpty(convDir)) + { + continue; + } Directory.Delete(convDir, true); } @@ -161,13 +165,22 @@ public void UpdateConversationTitleAlias(string conversationId, string titleAlia public bool UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags) { - if (string.IsNullOrEmpty(conversationId)) return false; + if (string.IsNullOrEmpty(conversationId)) + { + return false; + } var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return false; + if (string.IsNullOrEmpty(convDir)) + { + return false; + } var convFile = Path.Combine(convDir, CONVERSATION_FILE); - if (!File.Exists(convFile)) return false; + if (!File.Exists(convFile)) + { + return false; + } var json = File.ReadAllText(convFile); var conv = JsonSerializer.Deserialize(json, _options); @@ -183,13 +196,22 @@ public bool UpdateConversationTags(string conversationId, List toAddTags public bool AppendConversationTags(string conversationId, List tags) { - if (string.IsNullOrEmpty(conversationId) || tags.IsNullOrEmpty()) return false; + if (string.IsNullOrEmpty(conversationId) || tags.IsNullOrEmpty()) + { + return false; + } var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return false; + if (string.IsNullOrEmpty(convDir)) + { + return false; + } var convFile = Path.Combine(convDir, CONVERSATION_FILE); - if (!File.Exists(convFile)) return false; + if (!File.Exists(convFile)) + { + return false; + } var json = File.ReadAllText(convFile); var conv = JsonSerializer.Deserialize(json, _options); @@ -204,14 +226,20 @@ public bool AppendConversationTags(string conversationId, List tags) public bool UpdateConversationMessage(string conversationId, UpdateMessageRequest request) { - if (string.IsNullOrEmpty(conversationId)) return false; + if (string.IsNullOrEmpty(conversationId)) + { + return false; + } var dialogs = GetConversationDialogs(conversationId); var candidates = dialogs.Where(x => x.MetaData.MessageId == request.Message.MetaData.MessageId && x.MetaData.Role == request.Message.MetaData.Role).ToList(); var found = candidates.Where((_, idx) => idx == request.InnderIndex).FirstOrDefault(); - if (found == null) return false; + if (found == null) + { + return false; + } found.Content = request.Message.Content; found.RichContent = request.Message.RichContent; @@ -227,7 +255,10 @@ public bool UpdateConversationMessage(string conversationId, UpdateMessageReques } var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return false; + if (string.IsNullOrEmpty(convDir)) + { + return false; + } var dialogFile = Path.Combine(convDir, DIALOG_FILE); File.WriteAllText(dialogFile, JsonSerializer.Serialize(dialogs, _options)); @@ -309,10 +340,16 @@ public ConversationState GetConversationStates(string conversationId) [SideCar] public void UpdateConversationStates(string conversationId, List states) { - if (states.IsNullOrEmpty()) return; + if (states.IsNullOrEmpty()) + { + return; + } var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return; + if (string.IsNullOrEmpty(convDir)) + { + return; + } var stateFile = Path.Combine(convDir, STATE_FILE); if (File.Exists(stateFile)) @@ -350,7 +387,10 @@ public void UpdateConversationStatus(string conversationId, string status) public Conversation GetConversation(string conversationId, bool isLoadStates = false) { var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return null; + if (string.IsNullOrEmpty(convDir)) + { + return null; + } var convFile = Path.Combine(convDir, CONVERSATION_FILE); var content = File.ReadAllText(convFile); @@ -401,15 +441,20 @@ public async ValueTask> GetConversations(ConversationFi filter.AgentIds.Add(filter.AgentId); } - var totalDirs = Directory.GetDirectories(dir); - foreach (var d in totalDirs) + foreach (var d in Directory.EnumerateDirectories(dir)) { var convFile = Path.Combine(d, CONVERSATION_FILE); - if (!File.Exists(convFile)) continue; + if (!File.Exists(convFile)) + { + continue; + } var json = File.ReadAllText(convFile); var record = JsonSerializer.Deserialize(json, _options); - if (record == null) continue; + if (record == null) + { + continue; + } var matched = true; if (filter?.Id != null) @@ -475,7 +520,10 @@ public async ValueTask> GetConversations(ConversationFi { foreach (var pair in filter.States) { - if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) continue; + if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) + { + continue; + } var components = pair.Key.Split(".").ToList(); var primaryKey = components[0]; @@ -518,12 +566,18 @@ public async ValueTask> GetConversations(ConversationFi matched = false; } - if (!matched) break; + if (!matched) + { + break; + } } } } - if (!matched) continue; + if (!matched) + { + continue; + } if (filter.IsLoadLatestStates) { @@ -555,14 +609,20 @@ public List GetLastConversations() var records = new List(); var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir); - foreach (var d in Directory.GetDirectories(dir)) + foreach (var d in Directory.EnumerateDirectories(dir)) { var path = Path.Combine(d, CONVERSATION_FILE); - if (!File.Exists(path)) continue; + if (!File.Exists(path)) + { + continue; + } var json = File.ReadAllText(path); var record = JsonSerializer.Deserialize(json, _options); - if (record == null) continue; + if (record == null) + { + continue; + } records.Add(record); } @@ -588,7 +648,7 @@ public List GetIdleConversations(int batchSize, int messageLimit, int bu batchSize = batchLimit; } - foreach (var d in Directory.GetDirectories(dir)) + foreach (var d in Directory.EnumerateDirectories(dir)) { var convFile = Path.Combine(d, CONVERSATION_FILE); if (!File.Exists(convFile)) @@ -687,12 +747,15 @@ public List TruncateConversation(string conversationId, string messageId public List GetConversationStateSearchKeys(ConversationStateKeysFilter filter) { var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir); - if (!Directory.Exists(dir)) return []; + if (!Directory.Exists(dir)) + { + return []; + } var count = 0; var keys = new List(); - foreach (var d in Directory.GetDirectories(dir)) + foreach (var d in Directory.EnumerateDirectories(dir)) { var convFile = Path.Combine(d, CONVERSATION_FILE); var latestStateFile = Path.Combine(d, CONV_LATEST_STATE_FILE); @@ -733,18 +796,25 @@ public List GetConversationStateSearchKeys(ConversationStateKeysFilter f public List GetConversationsToMigrate(int batchSize = 100) { var baseDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir); - if (!Directory.Exists(baseDir)) return []; + if (!Directory.Exists(baseDir)) + { + return []; + } var convIds = new List(); - var dirs = Directory.GetDirectories(baseDir); - - foreach (var dir in dirs) + foreach (var dir in Directory.EnumerateDirectories(baseDir)) { var latestStateFile = Path.Combine(dir, CONV_LATEST_STATE_FILE); - if (File.Exists(latestStateFile)) continue; + if (File.Exists(latestStateFile)) + { + continue; + } var convId = dir.Split(Path.DirectorySeparatorChar).Last(); - if (string.IsNullOrEmpty(convId)) continue; + if (string.IsNullOrEmpty(convId)) + { + continue; + } convIds.Add(convId); if (convIds.Count >= batchSize) @@ -759,7 +829,10 @@ public List GetConversationsToMigrate(int batchSize = 100) public bool MigrateConvsersationLatestStates(string conversationId) { - if (string.IsNullOrEmpty(conversationId)) return false; + if (string.IsNullOrEmpty(conversationId)) + { + return false; + } var convDir = FindConversationDirectory(conversationId); if (string.IsNullOrEmpty(convDir)) @@ -774,7 +847,6 @@ public bool MigrateConvsersationLatestStates(string conversationId) var latestStateFile = Path.Combine(convDir, CONV_LATEST_STATE_FILE); var stateStr = JsonSerializer.Serialize(latestStates, _options); File.WriteAllText(latestStateFile, stateStr); - return true; } @@ -782,10 +854,16 @@ public bool MigrateConvsersationLatestStates(string conversationId) #region Private methods private string? FindConversationDirectory(string conversationId) { - if (string.IsNullOrEmpty(conversationId)) return null; + if (string.IsNullOrEmpty(conversationId)) + { + return null; + } var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, conversationId); - if (!Directory.Exists(dir)) return null; + if (!Directory.Exists(dir)) + { + return null; + } return dir; } @@ -794,7 +872,10 @@ private List CollectDialogElements(string dialogDir) { var dialogs = new List(); - if (!File.Exists(dialogDir)) return dialogs; + if (!File.Exists(dialogDir)) + { + return dialogs; + } var texts = File.ReadAllText(dialogDir); dialogs = JsonSerializer.Deserialize>(texts) ?? new List(); @@ -803,7 +884,10 @@ private List CollectDialogElements(string dialogDir) private string ParseDialogElements(List dialogs) { - if (dialogs.IsNullOrEmpty()) return "[]"; + if (dialogs.IsNullOrEmpty()) + { + return "[]"; + } return JsonSerializer.Serialize(dialogs, _options) ?? "[]"; } @@ -811,10 +895,16 @@ private string ParseDialogElements(List dialogs) private List CollectConversationStates(string stateFile) { var states = new List(); - if (!File.Exists(stateFile)) return states; + if (!File.Exists(stateFile)) + { + return states; + } var stateStr = File.ReadAllText(stateFile); - if (string.IsNullOrEmpty(stateStr)) return states; + if (string.IsNullOrEmpty(stateStr)) + { + return states; + } states = JsonSerializer.Deserialize>(stateStr, _options); return states ?? new List(); @@ -823,10 +913,16 @@ private List CollectConversationStates(string stateFile) private List CollectConversationBreakpoints(string breakpointFile) { var breakpoints = new List(); - if (!File.Exists(breakpointFile)) return breakpoints; + if (!File.Exists(breakpointFile)) + { + return breakpoints; + } var content = File.ReadAllText(breakpointFile); - if (string.IsNullOrEmpty(content)) return breakpoints; + if (string.IsNullOrEmpty(content)) + { + return breakpoints; + } breakpoints = JsonSerializer.Deserialize>(content, _options); return breakpoints ?? new List(); @@ -861,7 +957,10 @@ private bool HandleTruncatedStates(string stateDir, string latestStateDir, List< var values = state.Values.Where(x => x.MessageId != refMsgId) .Where(x => x.UpdateTime < refTime) .ToList(); - if (values.Count == 0) continue; + if (values.Count == 0) + { + continue; + } state.Values = values; truncatedStates.Add(state); @@ -891,11 +990,14 @@ private bool HandleTruncatedLogs(string convDir, DateTime refTime) if (Directory.Exists(contentLogDir)) { - foreach (var file in Directory.GetFiles(contentLogDir)) + foreach (var file in Directory.EnumerateFiles(contentLogDir)) { var text = File.ReadAllText(file); var log = JsonSerializer.Deserialize(text); - if (log == null) continue; + if (log == null) + { + continue; + } if (log.CreatedTime >= refTime) { @@ -906,11 +1008,14 @@ private bool HandleTruncatedLogs(string convDir, DateTime refTime) if (Directory.Exists(stateLogDir)) { - foreach (var file in Directory.GetFiles(stateLogDir)) + foreach (var file in Directory.EnumerateFiles(stateLogDir)) { var text = File.ReadAllText(file); var log = JsonSerializer.Deserialize(text); - if (log == null) continue; + if (log == null) + { + continue; + } if (log.CreatedTime >= refTime) { @@ -924,7 +1029,10 @@ private bool HandleTruncatedLogs(string convDir, DateTime refTime) private bool SaveTruncatedDialogs(string dialogDir, List dialogs) { - if (string.IsNullOrEmpty(dialogDir) || dialogs == null) return false; + if (string.IsNullOrEmpty(dialogDir) || dialogs == null) + { + return false; + } var texts = ParseDialogElements(dialogs); File.WriteAllText(dialogDir, texts); @@ -933,7 +1041,10 @@ private bool SaveTruncatedDialogs(string dialogDir, List dialogs) private bool SaveTruncatedStates(string stateDir, List states) { - if (string.IsNullOrEmpty(stateDir) || states == null) return false; + if (string.IsNullOrEmpty(stateDir) || states == null) + { + return false; + } var stateStr = JsonSerializer.Serialize(states, _options); File.WriteAllText(stateDir, stateStr); @@ -942,7 +1053,10 @@ private bool SaveTruncatedStates(string stateDir, List states) private bool SaveTruncatedLatestStates(string latestStateDir, List states) { - if (string.IsNullOrEmpty(latestStateDir) || states == null) return false; + if (string.IsNullOrEmpty(latestStateDir) || states == null) + { + return false; + } var latestStates = BuildLatestStates(states); var stateStr = JsonSerializer.Serialize(latestStates, _options); @@ -952,7 +1066,10 @@ private bool SaveTruncatedLatestStates(string latestStateDir, List breakpoints) { - if (string.IsNullOrEmpty(breakpointDir) || breakpoints == null) return false; + if (string.IsNullOrEmpty(breakpointDir) || breakpoints == null) + { + return false; + } var breakpointStr = JsonSerializer.Serialize(breakpoints, _options); File.WriteAllText(breakpointDir, breakpointStr); @@ -961,7 +1078,10 @@ private bool SaveTruncatedBreakpoints(string breakpointDir, List CollectConversationLatestStates(string latestStateDir) { - if (string.IsNullOrEmpty(latestStateDir) || !File.Exists(latestStateDir)) return []; + if (string.IsNullOrEmpty(latestStateDir) || !File.Exists(latestStateDir)) + { + return []; + } var str = File.ReadAllText(latestStateDir); var states = JsonSerializer.Deserialize>(str, _options); @@ -979,7 +1099,10 @@ private Dictionary BuildLatestStates(List s foreach (var pair in states) { var value = pair.Values?.LastOrDefault(); - if (value == null || !value.Active) continue; + if (value == null || !value.Active) + { + continue; + } try { @@ -1009,7 +1132,10 @@ private Dictionary BuildLatestStates(List s for (int i = 0; i < paths.Count(); i++) { - if (elem == null) return null; + if (elem == null) + { + return null; + } var field = paths.ElementAt(i); if (elem.Value.ValueKind == JsonValueKind.Array) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs index 84f2794c3..ded762c85 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs @@ -65,7 +65,6 @@ public bool DeleteCrontabItem(string conversationId) public async ValueTask> GetCrontabItems(CrontabItemFilter filter) { - if (filter == null) { filter = CrontabItemFilter.Empty(); @@ -79,15 +78,20 @@ public async ValueTask> GetCrontabItems(CrontabItemFilte Directory.CreateDirectory(baseDir); } - var totalDirs = Directory.GetDirectories(baseDir); - foreach (var d in totalDirs) + foreach (var d in Directory.EnumerateDirectories(baseDir)) { var file = Path.Combine(d, CRON_FILE); - if (!File.Exists(file)) continue; + if (!File.Exists(file)) + { + continue; + } var json = File.ReadAllText(file); var record = JsonSerializer.Deserialize(json, _options); - if (record == null) continue; + if (record == null) + { + continue; + } var matched = true; if (filter?.AgentIds != null) @@ -103,7 +107,10 @@ public async ValueTask> GetCrontabItems(CrontabItemFilte matched = matched && filter.UserIds.Contains(record.UserId); } - if (!matched) continue; + if (!matched) + { + continue; + } records.Add(record); } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs index f245477f2..3c3ba64bc 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs @@ -54,11 +54,17 @@ public bool AddKnowledgeCollectionConfigs(List configs, public bool DeleteKnowledgeCollectionConfig(string collectionName) { - if (string.IsNullOrWhiteSpace(collectionName)) return false; + if (string.IsNullOrWhiteSpace(collectionName)) + { + return false; + } var vectorDir = BuildKnowledgeCollectionConfigDir(); var configFile = Path.Combine(vectorDir, COLLECTION_CONFIG_FILE); - if (!File.Exists(configFile)) return false; + if (!File.Exists(configFile)) + { + return false; + } var str = File.ReadAllText(configFile); var savedConfigs = JsonSerializer.Deserialize>(str, _options) ?? new(); @@ -139,7 +145,10 @@ public bool DeleteKnolwedgeBaseFileMeta(string collectionName, string vectorStor } var dir = BuildKnowledgeCollectionFileDir(collectionName, vectorStoreProvider); - if (!Directory.Exists(dir)) return false; + if (!Directory.Exists(dir)) + { + return false; + } if (fileId == null) { @@ -172,14 +181,20 @@ public async ValueTask> GetKnowledgeBaseFileMet } var records = new List(); - foreach (var folder in Directory.GetDirectories(dir)) + foreach (var folder in Directory.EnumerateDirectories(dir)) { var metaFile = Path.Combine(folder, KNOWLEDGE_DOC_META_FILE); - if (!File.Exists(metaFile)) continue; + if (!File.Exists(metaFile)) + { + continue; + } var content = File.ReadAllText(metaFile); var metaData = JsonSerializer.Deserialize(content, _options); - if (metaData == null) continue; + if (metaData == null) + { + continue; + } var matched = true; @@ -208,7 +223,10 @@ public async ValueTask> GetKnowledgeBaseFileMet } - if (!matched) continue; + if (!matched) + { + continue; + } records.Add(metaData); } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs index c08514012..7c999b85f 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Loggers.Models; -using Microsoft.IdentityModel.Logging; using System.IO; +using System.Text.RegularExpressions; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace BotSharp.Core.Repository { @@ -9,7 +10,10 @@ public partial class FileRepository #region LLM Completion Log public void SaveLlmCompletionLog(LlmCompletionLog log) { - if (log == null) return; + if (log == null) + { + return; + } log.ConversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString()); log.MessageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString()); @@ -36,13 +40,19 @@ public void SaveLlmCompletionLog(LlmCompletionLog log) #region Conversation Content Log public void SaveConversationContentLog(ContentLogOutputModel log) { - if (log == null) return; + if (log == null) + { + return; + } log.ConversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString()); log.MessageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString()); var convDir = FindConversationDirectory(log.ConversationId); - if (string.IsNullOrEmpty(convDir)) return; + if (string.IsNullOrEmpty(convDir)) + { + return; + } var logDir = Path.Combine(convDir, "content_log"); if (!Directory.Exists(logDir)) @@ -57,20 +67,32 @@ public void SaveConversationContentLog(ContentLogOutputModel log) public DateTimePagination GetConversationContentLogs(string conversationId, ConversationLogFilter filter) { - if (string.IsNullOrEmpty(conversationId)) return new(); + if (string.IsNullOrEmpty(conversationId)) + { + return new(); + } var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return new(); + if (string.IsNullOrEmpty(convDir)) + { + return new(); + } var logDir = Path.Combine(convDir, "content_log"); - if (!Directory.Exists(logDir)) return new(); + if (!Directory.Exists(logDir)) + { + return new(); + } var logs = new List(); - foreach (var file in Directory.GetFiles(logDir)) + foreach (var file in Directory.EnumerateFiles(logDir)) { var text = File.ReadAllText(file); var log = JsonSerializer.Deserialize(text); - if (log == null || log.CreatedTime >= filter.StartTime) continue; + if (log == null || log.CreatedTime >= filter.StartTime) + { + continue; + } logs.Add(log); } @@ -89,13 +111,19 @@ public DateTimePagination GetConversationContentLogs(stri #region Conversation State Log public void SaveConversationStateLog(ConversationStateLogModel log) { - if (log == null) return; + if (log == null) + { + return; + } log.ConversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString()); log.MessageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString()); var convDir = FindConversationDirectory(log.ConversationId); - if (string.IsNullOrEmpty(convDir)) return; + if (string.IsNullOrEmpty(convDir)) + { + return; + } var logDir = Path.Combine(convDir, "state_log"); if (!Directory.Exists(logDir)) @@ -110,20 +138,32 @@ public void SaveConversationStateLog(ConversationStateLogModel log) public DateTimePagination GetConversationStateLogs(string conversationId, ConversationLogFilter filter) { - if (string.IsNullOrEmpty(conversationId)) return new(); + if (string.IsNullOrEmpty(conversationId)) + { + return new(); + } var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) return new(); + if (string.IsNullOrEmpty(convDir)) + { + return new(); + } var logDir = Path.Combine(convDir, "state_log"); - if (!Directory.Exists(logDir)) return new(); + if (!Directory.Exists(logDir)) + { + return new(); + } var logs = new List(); - foreach (var file in Directory.GetFiles(logDir)) + foreach (var file in Directory.EnumerateFiles(logDir)) { var text = File.ReadAllText(file); var log = JsonSerializer.Deserialize(text); - if (log == null || log.CreatedTime >= filter.StartTime) continue; + if (log == null || log.CreatedTime >= filter.StartTime) + { + continue; + } logs.Add(log); } @@ -142,7 +182,10 @@ public DateTimePagination GetConversationStateLogs(st #region Instruction Log public bool SaveInstructionLogs(IEnumerable logs) { - if (logs.IsNullOrEmpty()) return false; + if (logs.IsNullOrEmpty()) + { + return false; + } var baseDir = Path.Combine(_dbSettings.FileRepository, INSTRUCTION_LOG_FOLDER); if (!Directory.Exists(baseDir)) @@ -174,12 +217,14 @@ public async ValueTask> GetInstructionLogs(Instr } var logs = new List(); - var files = Directory.GetFiles(baseDir); - foreach (var file in files) + foreach (var file in Directory.EnumerateFiles(baseDir)) { var json = File.ReadAllText(file); var log = JsonSerializer.Deserialize(json, _options); - if (log == null) continue; + if (log == null) + { + continue; + } var matched = true; if (!filter.AgentIds.IsNullOrEmpty()) @@ -198,6 +243,11 @@ public async ValueTask> GetInstructionLogs(Instr { matched = matched && filter.TemplateNames.Contains(log.TemplateName); } + if (!string.IsNullOrEmpty(filter.SimilarTemplateName)) + { + var regex = new Regex(filter.SimilarTemplateName, RegexOptions.Compiled | RegexOptions.IgnoreCase); + matched = matched && !string.IsNullOrEmpty(log.TemplateName) && regex.IsMatch(log.TemplateName); + } if (!filter.UserIds.IsNullOrEmpty()) { matched = matched && filter.UserIds.Contains(log.UserId); @@ -223,7 +273,10 @@ public async ValueTask> GetInstructionLogs(Instr { foreach (var pair in filter.States) { - if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) continue; + if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) + { + continue; + } var components = pair.Key.Split(".").ToList(); var primaryKey = components[0]; @@ -266,12 +319,18 @@ public async ValueTask> GetInstructionLogs(Instr matched = false; } - if (!matched) break; + if (!matched) + { + break; + } } } } - if (!matched) continue; + if (!matched) + { + continue; + } log.Id = Path.GetFileNameWithoutExtension(file); logs.Add(log); @@ -306,12 +365,14 @@ public List GetInstructionLogSearchKeys(InstructLogKeysFilter filter) } var count = 0; - var files = Directory.GetFiles(baseDir); - foreach (var file in files) + foreach (var file in Directory.EnumerateFiles(baseDir)) { var json = File.ReadAllText(file); var log = JsonSerializer.Deserialize(json, _options); - if (log == null) continue; + if (log == null) + { + continue; + } if (log == null || log.InnerStates.IsNullOrEmpty() @@ -340,11 +401,7 @@ public List GetInstructionLogSearchKeys(InstructLogKeysFilter filter) #region Private methods private int GetNextLogIndex(string logDir, string id) { - var files = Directory.GetFiles(logDir); - if (files.IsNullOrEmpty()) - return 0; - - var logIndexes = files.Where(file => + var logIndexes = Directory.EnumerateFiles(logDir).Where(file => { var fileName = ParseFileNameByPath(file); return fileName[0].IsEqualTo(id); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs index c797ce5aa..825df02f0 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs @@ -6,11 +6,17 @@ public partial class FileRepository { public bool RefreshRoles(IEnumerable roles) { - if (roles.IsNullOrEmpty()) return false; + if (roles.IsNullOrEmpty()) + { + return false; + } var validRoles = roles.Where(x => !string.IsNullOrWhiteSpace(x.Id) && !string.IsNullOrWhiteSpace(x.Name)).ToList(); - if (validRoles.IsNullOrEmpty()) return false; + if (validRoles.IsNullOrEmpty()) + { + return false; + } var baseDir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER); if (Directory.Exists(baseDir)) @@ -58,10 +64,16 @@ public IEnumerable GetRoles(RoleFilter filter) public Role? GetRoleDetails(string roleId, bool includeAgent = false) { - if (string.IsNullOrWhiteSpace(roleId)) return null; + if (string.IsNullOrWhiteSpace(roleId)) + { + return null; + } var role = Roles.FirstOrDefault(x => x.Id == roleId); - if (role == null) return null; + if (role == null) + { + return null; + } var agentActions = new List(); var roleAgents = RoleAgents?.Where(x => x.RoleId == roleId)?.ToList() ?? []; diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Translation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Translation.cs index e52e09c24..e4f016a02 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Translation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Translation.cs @@ -112,7 +112,10 @@ public bool SaveTranslationMemories(IEnumerable inputs) else { var foundItem = foundMemory.Translations?.FirstOrDefault(x => x.Language.Equals(input.Language)); - if (foundItem != null) continue; + if (foundItem != null) + { + continue; + } if (foundMemory.Translations == null) { diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index 09212e202..16520b232 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -254,7 +254,10 @@ public List SearchLoginUsers(User filter, string source = UserSource.Inter public bool UpdateUser(User user, bool updateUserAgents = false) { - if (string.IsNullOrEmpty(user?.Id)) return false; + if (string.IsNullOrEmpty(user?.Id)) + { + return false; + } var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, user.Id); if (!Directory.Exists(dir)) @@ -265,7 +268,10 @@ public bool UpdateUser(User user, bool updateUserAgents = false) var userFile = Path.Combine(dir, USER_FILE); var userJson = File.ReadAllText(userFile); var curUser = JsonSerializer.Deserialize(userJson, _options); - if (curUser == null) return false; + if (curUser == null) + { + return false; + } curUser.Type = user.Type; curUser.Role = user.Role; @@ -297,13 +303,19 @@ public bool UpdateUser(User user, bool updateUserAgents = false) public void AddDashboardConversation(string userId, string conversationId) { var user = GetUserById(userId); - if (user == null) return; + if (user == null) + { + return; + } // one user only has one dashboard currently var dash = Dashboards.FirstOrDefault(); dash ??= new(); var existingConv = dash.ConversationList.FirstOrDefault(x => string.Equals(x.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase)); - if (existingConv != null) return; + if (existingConv != null) + { + return; + } var dashconv = new DashboardConversation { @@ -321,15 +333,24 @@ public void AddDashboardConversation(string userId, string conversationId) public void RemoveDashboardConversation(string userId, string conversationId) { var user = GetUserById(userId); - if (user == null) return; + if (user == null) + { + return; + } // one user only has one dashboard currently var dash = Dashboards.FirstOrDefault(); - if (dash == null) return; + if (dash == null) + { + return; + } var dashconv = dash.ConversationList.FirstOrDefault( c => string.Equals(c.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase)); - if (dashconv == null) return; + if (dashconv == null) + { + return; + } dash.ConversationList.Remove(dashconv); @@ -341,15 +362,24 @@ public void RemoveDashboardConversation(string userId, string conversationId) public void UpdateDashboardConversation(string userId, DashboardConversation dashConv) { var user = GetUserById(userId); - if (user == null) return; + if (user == null) + { + return; + } // one user only has one dashboard currently var dash = Dashboards.FirstOrDefault(); - if (dash == null) return; + if (dash == null) + { + return; + } var curIdx = dash.ConversationList.ToList().FindIndex( x => string.Equals(x.ConversationId, dashConv.ConversationId, StringComparison.OrdinalIgnoreCase)); - if (curIdx < 0) return; + if (curIdx < 0) + { + return; + } dash.ConversationList[curIdx] = dashConv; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs index 53a54669e..4fc978b1c 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Routing.Hooks; @@ -14,7 +13,7 @@ public RoutingAgentHook(IServiceProvider services, AgentSettings settings, Routi _routingSetting = routingSetting; } - public override bool OnInstructionLoaded(string template, Dictionary dict) + public override bool OnInstructionLoaded(string template, IDictionary dict) { if (_agent.Type != AgentType.Routing) { @@ -74,7 +73,7 @@ public override bool OnFunctionsLoaded(List functions) if (rule != null) { var agentService = _services.GetRequiredService(); - var redirectAgent = agentService.GetAgent(rule.RedirectTo).Result; + var redirectAgent = agentService.GetAgent(rule.RedirectTo).ConfigureAwait(false).GetAwaiter().GetResult(); var json = JsonSerializer.Serialize(new { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs index a37e24077..278fd155b 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs @@ -12,7 +12,7 @@ public static void FixMalformedResponse(IServiceProvider services, FunctionCallF var agents = agentService.GetAgents(new AgentFilter { Types = [AgentType.Task] - }).Result.Items.ToList(); + }).ConfigureAwait(false).GetAwaiter().GetResult().Items.ToList(); var malformed = false; // Sometimes it populate malformed Function in Agent name diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index 4212c08c5..167866ddb 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -46,7 +46,7 @@ public string OriginAgentId { Types = [AgentType.Routing], Pager = new Pagination { Size = 100 } - }).Result.Items.Select(x => x.Id).ToArray(); + }).ConfigureAwait(false).GetAwaiter().GetResult().Items.Select(x => x.Id).ToArray(); } return _stack.Where(x => !_routerAgentIds.Contains(x)).LastOrDefault() ?? string.Empty; @@ -87,7 +87,7 @@ public void Push(string agentId, string? reason = null, bool updateLazyRouting = if (!Guid.TryParse(agentId, out _)) { var agentService = _services.GetRequiredService(); - var agents = agentService.GetAgentOptions([agentId], byName: true).Result; + var agents = agentService.GetAgentOptions([agentId], byName: true).ConfigureAwait(false).GetAwaiter().GetResult(); if (agents.Count > 0) { @@ -129,8 +129,8 @@ public void Pop(string? reason = null, bool updateLazyRouting = true) } // Run the routing rule - var agency = _services.GetRequiredService(); - var agent = agency.GetAgent(currentAgentId).Result; + var agentService = _services.GetRequiredService(); + var agent = agentService.GetAgent(currentAgentId).ConfigureAwait(false).GetAwaiter().GetResult(); var message = new RoleDialogModel(AgentRole.User, $"Try to route to agent {agent.Name}") { diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index 2124da16e..797b76fc2 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -46,7 +46,7 @@ public TemplateRender(IServiceProvider services, ILogger logger) }); } - public string Render(string template, Dictionary dict) + public string Render(string template, IDictionary dict) { if (_parser.TryParse(template, out var t, out var error)) { diff --git a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs index 7b81ac10a..91cb9c4a6 100644 --- a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs +++ b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs @@ -380,11 +380,11 @@ private async Task InnerTranslate(List text Id = Guid.Empty.ToString(), Name = "Translator", Instruction = "You are a translation expert.", - TemplateDict = new Dictionary + TemplateDict = new() { - { "text_list", jsonString }, - { "text_list_size", texts.Count }, - { StateConst.LANGUAGE, language } + ["text_list"] = jsonString, + ["text_list_size"] = texts.Count, + [StateConst.LANGUAGE] = language } }; diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.Token.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.Token.cs new file mode 100644 index 000000000..03380fca2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.Token.cs @@ -0,0 +1,381 @@ +using BotSharp.Abstraction.Infrastructures; +using BotSharp.Abstraction.Users.Enums; +using BotSharp.Abstraction.Users.Models; +using BotSharp.OpenAPI.ViewModels.Users; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace BotSharp.Core.Users.Services; + +public partial class UserService +{ + public async Task GetToken(string authorization) + { + var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); + var (id, password, regionCode) = base64.SplitAsTuple(":"); + + var db = _services.GetRequiredService(); + var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); + if (record == null) + { + record = db.GetUserByPhone(id, regionCode: regionCode); + } + + if (record != null && record.Type == UserType.Affiliate) + { + return default; + } + + var hooks = _services.GetServices(); + //verify password is correct or not. + if (record != null && !hooks.Any()) + { + var hashPassword = Utilities.HashTextMd5($"{password}{record.Salt}"); + if (hashPassword != record.Password) + { + return default; + } + } + + User? user = record; + var isAuthenticatedByHook = false; + if (record == null || record.Source != UserSource.Internal) + { + // check 3rd party user + foreach (var hook in hooks) + { + user = await hook.Authenticate(id, password); + if (user == null) + { + continue; + } + + if (string.IsNullOrEmpty(user.Source) || user.Source == UserSource.Internal) + { + _logger.LogError($"Please set source name in the Authenticate hook."); + return null; + } + + if (record == null) + { + // create a local user record + record = new User + { + UserName = user.UserName, + Email = user.Email, + FirstName = user.FirstName, + LastName = user.LastName, + Source = user.Source, + ExternalId = user.ExternalId, + Password = user.Password, + Type = user.Type, + Role = user.Role, + RegionCode = user.RegionCode + }; + await CreateUser(record); + } + + isAuthenticatedByHook = true; + break; + } + } + + if ((hooks.Any() && user == null) || record == null) + { + return default; + } + + if (!isAuthenticatedByHook && _setting.NewUserVerification && !record.Verified) + { + return default; + } + + if (!isAuthenticatedByHook && Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) + { + return default; + } + + var (token, jwt) = BuildToken(record); + foreach (var hook in hooks) + { + hook.UserAuthenticated(record, token); + } + + return token; + } + + public async Task RenewToken(string refreshToken, string? accessToken = null) + { + if (string.IsNullOrWhiteSpace(refreshToken)) + { + return null; + } + + try + { + User? user = null; + + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + user = await hook.RenewAuthentication(refreshToken, accessToken); + if (user != null) + { + break; + } + } + + if (user == null) + { + // Validate the incoming JWT (signature, issuer, audience, lifetime) + var config = _services.GetRequiredService(); + var validationParameters = new TokenValidationParameters + { + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config["Jwt:Key"])), + ValidateIssuerSigningKey = true, + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ClockSkew = TimeSpan.Zero + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var principal = tokenHandler.ValidateToken(refreshToken, validationParameters, out var validatedToken); + var userId = principal?.Claims? + .FirstOrDefault(x => x.Type.IsEqualTo(JwtRegisteredClaimNames.NameId) + || x.Type.IsEqualTo(ClaimTypes.NameIdentifier) + || x.Type.IsEqualTo("uid") + || x.Type.IsEqualTo("user_id") + || x.Type.IsEqualTo("userId"))?.Value; + + if (string.IsNullOrEmpty(userId)) + { + return null; + } + + user = await GetUser(userId); + if (user == null + || user.IsDisabled + || (user.Id != userId && user.ExternalId != userId)) + { + return null; + } + } + + // Issue a new access token + var (newToken, _) = BuildToken(user); + + // Notify hooks for token issuance + foreach (var hook in hooks) + { + hook.UserAuthenticated(user, newToken); + } + + return newToken; + } + catch (SecurityTokenException ex) + { + _logger.LogWarning(ex, "Invalid token presented for refresh."); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh token."); + return null; + } + } + + public async Task ActiveUser(UserActivationModel model) + { + var id = model.UserName; + var db = _services.GetRequiredService(); + var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); + + if (record == null) + { + record = db.GetUserByPhone(id, regionCode: (string.IsNullOrWhiteSpace(model.RegionCode) ? "CN" : model.RegionCode)); + } + + //if (record == null) + //{ + // record = db.GetUserByPhoneV2(id, regionCode: (string.IsNullOrWhiteSpace(model.RegionCode) ? "CN" : model.RegionCode)); + //} + + if (record == null) + { + return default; + } + + if (record.VerificationCode != model.VerificationCode || (record.VerificationCodeExpireAt != null && DateTime.UtcNow > record.VerificationCodeExpireAt)) + { + return default; + } + + if (record.Verified) + { + return default; + } + + db.UpdateUserVerified(record.Id); + + var accessToken = GenerateJwtToken(record); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + var token = new Token + { + AccessToken = accessToken, + ExpireTime = jwt.Payload.Exp.Value, + TokenType = "Bearer", + Scope = "api" + }; + return token; + } + + public async Task CreateTokenByUser(User user) + { + var accessToken = GenerateJwtToken(user); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + var token = new Token + { + AccessToken = accessToken, + ExpireTime = jwt.Payload.Exp.Value, + TokenType = "Bearer", + Scope = "api" + }; + return token; + } + + public async Task GetAffiliateToken(string authorization) + { + var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); + var (id, password, regionCode) = base64.SplitAsTuple(":"); + var db = _services.GetRequiredService(); + var record = db.GetAffiliateUserByPhone(id); + var isCanLogin = record != null && !record.IsDisabled && record.Type == UserType.Affiliate; + if (!isCanLogin) + { + return default; + } + + if (Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) + { + return default; + } + + var (token, jwt) = BuildToken(record); + + return await Task.FromResult(token); + } + + public async Task GetAdminToken(string authorization) + { + var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); + var (id, password, regionCode) = base64.SplitAsTuple(":"); + var db = _services.GetRequiredService(); + var record = db.GetUserByPhone(id, type: UserType.Internal); + var isCanLogin = record != null && !record.IsDisabled + && record.Type == UserType.Internal && new List + { + UserRole.Root,UserRole.Admin + }.Contains(record.Role); + if (!isCanLogin) + { + return default; + } + + if (Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) + { + return default; + } + + var (token, jwt) = BuildToken(record); + + return await Task.FromResult(token); + } + + public async Task GetUserTokenExpires() + { + var _cacheService = _services.GetRequiredService(); + return await _cacheService.GetAsync(GetUserTokenExpiresCacheKey(_user.Id)); + } + + #region Private methods + private (Token, JwtSecurityToken) BuildToken(User record) + { + var accessToken = GenerateJwtToken(record); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + var token = new Token + { + AccessToken = accessToken, + ExpireTime = jwt.Payload.Exp.Value, + TokenType = "Bearer", + Scope = "api" + }; + return (token, jwt); + } + + private string GenerateJwtToken(User user) + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.NameId, user.Id), + new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName), + new Claim(JwtRegisteredClaimNames.Email, user?.Email ?? string.Empty), + new Claim(JwtRegisteredClaimNames.GivenName, user?.FirstName ?? string.Empty), + new Claim(JwtRegisteredClaimNames.FamilyName, user?.LastName ?? string.Empty), + new Claim("source", user.Source), + new Claim("external_id", user.ExternalId ?? string.Empty), + new Claim("type", user.Type ?? UserType.Client), + new Claim("role", user.Role ?? UserRole.User), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim("phone", user.Phone ?? string.Empty), + new Claim("affiliate_id", user.AffiliateId ?? string.Empty), + new Claim("employee_id", user.EmployeeId ?? string.Empty), + new Claim("regionCode", user.RegionCode ?? "CN") + }; + + var validators = _services.GetServices(); + foreach (var validator in validators) + { + validator.AddClaims(claims); + } + + var config = _services.GetRequiredService(); + var issuer = config["Jwt:Issuer"]; + var audience = config["Jwt:Audience"]; + var expireInMinutes = int.Parse(config["Jwt:ExpireInMinutes"] ?? "120"); + var key = Encoding.ASCII.GetBytes(config["Jwt:Key"]); + var expires = DateTime.UtcNow.AddMinutes(expireInMinutes); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = expires, + Issuer = issuer, + Audience = audience, + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256Signature) + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + SaveUserTokenExpiresCache(user.Id, expires, expireInMinutes).GetAwaiter().GetResult(); + return tokenHandler.WriteToken(token); + } + + private async Task SaveUserTokenExpiresCache(string userId, DateTime expires, int expireInMinutes) + { + var config = _services.GetService(); + var enableSingleLogin = bool.Parse(config["Jwt:EnableSingleLogin"] ?? "false"); + if (enableSingleLogin) + { + var _cacheService = _services.GetRequiredService(); + await _cacheService.SetAsync(GetUserTokenExpiresCacheKey(userId), expires, TimeSpan.FromMinutes(expireInMinutes)); + } + } + + private string GetUserTokenExpiresCacheKey(string userId) + { + return $"user:{userId}_token_expires"; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index f7dd61c63..51b720b89 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -12,7 +12,7 @@ namespace BotSharp.Core.Users.Services; -public class UserService : IUserService +public partial class UserService : IUserService { private readonly IServiceProvider _services; private readonly IUserIdentity _user; @@ -156,232 +156,6 @@ public async Task UpdatePassword(string password, string verificationCode) return true; } - public async Task GetAffiliateToken(string authorization) - { - var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); - var (id, password, regionCode) = base64.SplitAsTuple(":"); - var db = _services.GetRequiredService(); - var record = db.GetAffiliateUserByPhone(id); - var isCanLogin = record != null && !record.IsDisabled && record.Type == UserType.Affiliate; - if (!isCanLogin) - { - return default; - } - - if (Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) - { - return default; - } - - var (token, jwt) = BuildToken(record); - - return await Task.FromResult(token); - } - - public async Task GetAdminToken(string authorization) - { - var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); - var (id, password, regionCode) = base64.SplitAsTuple(":"); - var db = _services.GetRequiredService(); - var record = db.GetUserByPhone(id, type: UserType.Internal); - var isCanLogin = record != null && !record.IsDisabled - && record.Type == UserType.Internal && new List - { - UserRole.Root,UserRole.Admin - }.Contains(record.Role); - if (!isCanLogin) - { - return default; - } - - if (Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) - { - return default; - } - - var (token, jwt) = BuildToken(record); - - return await Task.FromResult(token); - } - - private (Token, JwtSecurityToken) BuildToken(User record) - { - var accessToken = GenerateJwtToken(record); - var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); - var token = new Token - { - AccessToken = accessToken, - ExpireTime = jwt.Payload.Exp.Value, - TokenType = "Bearer", - Scope = "api" - }; - return (token, jwt); - } - - public async Task GetToken(string authorization) - { - var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); - var (id, password, regionCode) = base64.SplitAsTuple(":"); - - var db = _services.GetRequiredService(); - var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); - if (record == null) - { - record = db.GetUserByPhone(id, regionCode: regionCode); - } - - if (record != null && record.Type == UserType.Affiliate) - { - return default; - } - - var hooks = _services.GetServices(); - //verify password is correct or not. - if (record != null && !hooks.Any()) - { - var hashPassword = Utilities.HashTextMd5($"{password}{record.Salt}"); - if (hashPassword != record.Password) - { - return default; - } - } - - User? user = record; - var isAuthenticatedByHook = false; - if (record == null || record.Source != UserSource.Internal) - { - // check 3rd party user - foreach (var hook in hooks) - { - user = await hook.Authenticate(id, password); - if (user == null) - { - continue; - } - - if (string.IsNullOrEmpty(user.Source) || user.Source == UserSource.Internal) - { - _logger.LogError($"Please set source name in the Authenticate hook."); - return null; - } - - if (record == null) - { - // create a local user record - record = new User - { - UserName = user.UserName, - Email = user.Email, - FirstName = user.FirstName, - LastName = user.LastName, - Source = user.Source, - ExternalId = user.ExternalId, - Password = user.Password, - Type = user.Type, - Role = user.Role, - RegionCode = user.RegionCode - }; - await CreateUser(record); - } - - isAuthenticatedByHook = true; - break; - } - } - - if ((hooks.Any() && user == null) || record == null) - { - return default; - } - - if (!isAuthenticatedByHook && _setting.NewUserVerification && !record.Verified) - { - return default; - } - - if (!isAuthenticatedByHook && Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) - { - return default; - } - - var (token, jwt) = BuildToken(record); - foreach (var hook in hooks) - { - hook.UserAuthenticated(record, token); - } - - return token; - } - - private string GenerateJwtToken(User user) - { - var claims = new List - { - new Claim(JwtRegisteredClaimNames.NameId, user.Id), - new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName), - new Claim(JwtRegisteredClaimNames.Email, user?.Email ?? string.Empty), - new Claim(JwtRegisteredClaimNames.GivenName, user?.FirstName ?? string.Empty), - new Claim(JwtRegisteredClaimNames.FamilyName, user?.LastName ?? string.Empty), - new Claim("source", user.Source), - new Claim("external_id", user.ExternalId ?? string.Empty), - new Claim("type", user.Type ?? UserType.Client), - new Claim("role", user.Role ?? UserRole.User), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim("phone", user.Phone ?? string.Empty), - new Claim("affiliate_id", user.AffiliateId ?? string.Empty), - new Claim("employee_id", user.EmployeeId ?? string.Empty), - new Claim("regionCode", user.RegionCode ?? "CN") - }; - - var validators = _services.GetServices(); - foreach (var validator in validators) - { - validator.AddClaims(claims); - } - - var config = _services.GetRequiredService(); - var issuer = config["Jwt:Issuer"]; - var audience = config["Jwt:Audience"]; - var expireInMinutes = int.Parse(config["Jwt:ExpireInMinutes"] ?? "120"); - var key = Encoding.ASCII.GetBytes(config["Jwt:Key"]); - var expires = DateTime.UtcNow.AddMinutes(expireInMinutes); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(claims), - Expires = expires, - Issuer = issuer, - Audience = audience, - SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha256Signature) - }; - var tokenHandler = new JwtSecurityTokenHandler(); - var token = tokenHandler.CreateToken(tokenDescriptor); - SaveUserTokenExpiresCache(user.Id, expires, expireInMinutes).GetAwaiter().GetResult(); - return tokenHandler.WriteToken(token); - } - - private async Task SaveUserTokenExpiresCache(string userId, DateTime expires, int expireInMinutes) - { - var config = _services.GetService(); - var enableSingleLogin = bool.Parse(config["Jwt:EnableSingleLogin"] ?? "false"); - if (enableSingleLogin) - { - var _cacheService = _services.GetRequiredService(); - await _cacheService.SetAsync(GetUserTokenExpiresCacheKey(userId), expires, TimeSpan.FromMinutes(expireInMinutes)); - } - } - - private string GetUserTokenExpiresCacheKey(string userId) - { - return $"user:{userId}_token_expires"; - } - - public async Task GetUserTokenExpires() - { - var _cacheService = _services.GetRequiredService(); - return await _cacheService.GetAsync(GetUserTokenExpiresCacheKey(_user.Id)); - } - [SharpCache(10, perInstanceCache: true)] public async Task GetMyProfile() { @@ -486,75 +260,6 @@ public async Task UpdateUser(User user, bool isUpdateUserAgents = false) return db.UpdateUser(user, isUpdateUserAgents); } - public async Task ActiveUser(UserActivationModel model) - { - var id = model.UserName; - var db = _services.GetRequiredService(); - var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); - - if (record == null) - { - record = db.GetUserByPhone(id, regionCode: (string.IsNullOrWhiteSpace(model.RegionCode) ? "CN" : model.RegionCode)); - } - - //if (record == null) - //{ - // record = db.GetUserByPhoneV2(id, regionCode: (string.IsNullOrWhiteSpace(model.RegionCode) ? "CN" : model.RegionCode)); - //} - - if (record == null) - { - return default; - } - - if (record.VerificationCode != model.VerificationCode || (record.VerificationCodeExpireAt != null && DateTime.UtcNow > record.VerificationCodeExpireAt)) - { - return default; - } - - if (record.Verified) - { - return default; - } - - db.UpdateUserVerified(record.Id); - - var accessToken = GenerateJwtToken(record); - var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); - var token = new Token - { - AccessToken = accessToken, - ExpireTime = jwt.Payload.Exp.Value, - TokenType = "Bearer", - Scope = "api" - }; - return token; - } - - public async Task CreateTokenByUser(User user) - { - var accessToken = GenerateJwtToken(user); - var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); - var token = new Token - { - AccessToken = accessToken, - ExpireTime = jwt.Payload.Exp.Value, - TokenType = "Bearer", - Scope = "api" - }; - return token; - } - - public async Task RenewToken() - { - var newToken = GenerateJwtToken(await GetMyProfile()); - var newJwt = new JwtSecurityTokenHandler().ReadJwtToken(newToken); - Token token = new Token(); - token.AccessToken = newToken; - token.ExpireTime = newJwt.Payload.Exp.Value; - return token; - } - public async Task VerifyUserNameExisting(string userName) { if (string.IsNullOrEmpty(userName)) diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index 07b0f1642..39ac00cc8 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -5,6 +5,8 @@ global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Conversations.Settings; +global using BotSharp.Abstraction.Coding.Models; +global using BotSharp.Abstraction.Coding.Settings; global using BotSharp.Abstraction.Crontab.Models; global using BotSharp.Abstraction.Files; global using BotSharp.Abstraction.Files.Enums; diff --git a/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid new file mode 100644 index 000000000..2293ede7b --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid @@ -0,0 +1,15 @@ +Based on user's request, help generate a refined python code of function definition, only using base python packages, that can return a boolean value of true or false to determine if based on known states, the function can determine if conditions are met. + +User's request is {{user_request}} + +Couple of notes to address: +1. You need to generate a function named check_trigger_criterion(args), where input is a json object. The example of this json object is {{args_example}}. +2. The input to this function comes from the arguments. You must use "ArgumentParser" to take an argument named "trigger_args", and then use "parse_known_args" to get the raw args. +3. After getting the raw args, you need to use 'clean_args = bytes(raw_args, "utf-8").decode("unicode_escape")' to clean the raw argument, and then use 'args = json.loads(clean_args)' to get the json args. +4. Based on the user's request and input args, generate the function logic and ONLY return a boolean value. +5. You must only call check_trigger_criterion with the parsed json args in "if __name__ == '__main__'" block, and print only the function output. If any error occurs, print "Error". +6. Refine the code so it will return for certain errors if detected, for example, when input is not a valid json, return "Error: Input is not a valid JSON string."; or when certain attributes are missing from json args, so I can better track of this. +7. You can use try-except blocks to catch any errors, and return "Error" if any error occurs. Do not use sys.exit(0) to exit the program. +8. Use as fewer comments and try-except blocks as possible. + +Output the executable code directly in order to directly save it to a py file. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs index 484e82a86..8ba2dfdfa 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Coding.Models; using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Settings; using BotSharp.Abstraction.Loggers.Models; @@ -27,11 +28,7 @@ public InstructionLogHook( public override async Task OnResponseGenerated(InstructResponseModel response) { var settings = _services.GetRequiredService(); - if (response == null - || string.IsNullOrWhiteSpace(response.AgentId) - || settings == null - || !settings.Logging.Enabled - || settings.Logging.ExcludedAgentIds.Contains(response.AgentId)) + if (response == null || !IsLoggingEnabled(response.AgentId)) { return; } @@ -63,4 +60,43 @@ public override async Task OnResponseGenerated(InstructResponseModel response) await base.OnResponseGenerated(response); } + + public override async Task AfterCodeExecution(Agent agent, CodeExecutionResponseModel response) + { + if (response == null || !IsLoggingEnabled(agent?.Id)) + { + return; + } + + var db = _services.GetRequiredService(); + var codeScriptVersion = response.CodeScript?.UpdatedTime ?? DateTime.UtcNow; + var user = db.GetUserById(_user.Id); + + db.SaveInstructionLogs(new List + { + new InstructionLogModel + { + AgentId = agent?.Id, + Provider = response.CodeProcessor, + Model = string.Empty, + TemplateName = response.CodeScript?.Name, + UserMessage = response.Text, + SystemInstruction = $"Code script name: {response.CodeScript}, Version: {codeScriptVersion.ToString("o")}", + CompletionText = response.ExecutionResult?.ToString() ?? string.Empty, + States = response.Arguments?.ToDictionary() ?? [], + UserId = user?.Id + } + }); + + await base.AfterCodeExecution(agent, response); + } + + private bool IsLoggingEnabled(string? agentId) + { + var settings = _services.GetRequiredService(); + return !string.IsNullOrWhiteSpace(agentId) + && settings != null + && settings.Logging.Enabled + && !settings.Logging.ExcludedAgentIds.Contains(agentId); + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs new file mode 100644 index 000000000..40800d35c --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs @@ -0,0 +1,70 @@ +using BotSharp.Abstraction.Coding.Models; +using BotSharp.Abstraction.Infrastructures.Attributes; + +namespace BotSharp.OpenAPI.Controllers; + +public partial class AgentController +{ + /// + /// Get agent code scripts + /// + /// + /// + /// + [HttpGet("/agent/{agentId}/code-scripts")] + public async Task> GetAgentCodeScripts([FromRoute] string agentId, [FromQuery] AgentCodeScriptFilter request) + { + var scripts = await _agentService.GetAgentCodeScripts(agentId, request); + return scripts.Select(x => AgentCodeScriptViewModel.From(x)).ToList(); + } + + /// + /// Update agent code scripts + /// + /// + /// + /// + [BotSharpAuth] + [HttpPost("/agent/{agentId}/code-scripts")] + public async Task UpdateAgentCodeScripts([FromRoute] string agentId, [FromBody] AgentCodeScriptUpdateModel request) + { + var scripts = request?.CodeScripts?.Select(x => AgentCodeScriptViewModel.To(x))?.ToList() ?? []; + var updated = await _agentService.UpdateAgentCodeScripts(agentId, scripts, request?.Options); + return updated; + } + + /// + /// Delete agent code scripts + /// + /// + /// + /// + [BotSharpAuth] + [HttpDelete("/agent/{agentId}/code-scripts")] + public async Task DeleteAgentCodeScripts([FromRoute] string agentId, [FromBody] AgentCodeScriptDeleteModel request) + { + var scripts = request?.CodeScripts?.Select(x => AgentCodeScriptViewModel.To(x))?.ToList(); + var updated = await _agentService.DeleteAgentCodeScripts(agentId, scripts); + return updated; + } + + /// + /// Generate agent code script + /// + /// + /// + /// + [HttpPost("/agent/{agentId}/code-script/generate")] + public async Task GenerateAgentCodeScript([FromRoute] string agentId, [FromBody] AgentCodeScriptGenerationRequest request) + { + request ??= new(); + var states = request.Options?.Data?.ToList(); + var state = _services.GetRequiredService(); + states?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); + state.SetState("code_processor", request.Options?.Processor, source: StateSource.External); + state.SetState("programming_language", request.Options?.ProgrammingLanguage, source: StateSource.External); + + var result = await _agentService.GenerateCodeScript(agentId, request.Text, request?.Options); + return result; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs new file mode 100644 index 000000000..52fd719fd --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -0,0 +1,26 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Rules; + +namespace BotSharp.OpenAPI.Controllers; + +public partial class AgentController +{ + [HttpGet("/rule/triggers")] + public IEnumerable GetRuleTriggers() + { + var triggers = _services.GetServices(); + return triggers.Select(x => new AgentRuleViewModel + { + TriggerName = x.Name, + Channel = x.Channel, + Statement = x.Statement, + OutputArgs = x.OutputArgs + }).OrderBy(x => x.TriggerName); + } + + [HttpGet("/rule/formalization")] + public async Task GetFormalizedRuleDefinition([FromBody] AgentRule rule) + { + return "{}"; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentTaskController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Task.cs similarity index 87% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentTaskController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Task.cs index 2322dae3c..6e6ecc3c0 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentTaskController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Task.cs @@ -1,20 +1,7 @@ -using BotSharp.Abstraction.Tasks; - namespace BotSharp.OpenAPI.Controllers; -[Authorize] -[ApiController] -public class AgentTaskController : ControllerBase +public partial class AgentController { - private readonly IAgentTaskService _agentTaskService; - private readonly IServiceProvider _services; - - public AgentTaskController(IAgentTaskService agentTaskService, IServiceProvider services) - { - _agentTaskService = agentTaskService; - _services = services; - } - /// /// Get an agent task /// @@ -25,7 +12,10 @@ public AgentTaskController(IAgentTaskService agentTaskService, IServiceProvider public async Task GetAgentTask([FromRoute] string agentId, [FromRoute] string taskId) { var task = await _agentTaskService.GetTask(agentId, taskId); - if (task == null) return null; + if (task == null) + { + return null; + } return AgentTaskViewModel.From(task); } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.cs similarity index 92% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.cs index 6aff56aee..1fcdb1285 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.cs @@ -1,22 +1,26 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Infrastructures.Attributes; +using BotSharp.Abstraction.Tasks; namespace BotSharp.OpenAPI.Controllers; [Authorize] [ApiController] -public class AgentController : ControllerBase +public partial class AgentController : ControllerBase { private readonly IAgentService _agentService; + private readonly IAgentTaskService _agentTaskService; private readonly IUserIdentity _user; private readonly IServiceProvider _services; public AgentController( IAgentService agentService, + IAgentTaskService agentTaskService, IUserIdentity user, IServiceProvider services) { _agentService = agentService; + _agentTaskService = agentTaskService; _user = user; _services = services; } @@ -140,9 +144,9 @@ public async Task PatchAgentTemplates([FromRoute] string agentId, [FromB } [HttpDelete("/agent/{agentId}")] - public async Task DeleteAgent([FromRoute] string agentId) + public async Task DeleteAgent([FromRoute] string agentId, [FromBody] AgentDeleteRequest request) { - return await _agentService.DeleteAgent(agentId); + return await _agentService.DeleteAgent(agentId, request?.Options); } [HttpGet("/agent/options")] @@ -159,12 +163,12 @@ public async Task> GetAgentUtilityOptions() } [HttpGet("/agent/labels")] - public async Task> GetAgentLabels() + public async Task> GetAgentLabels([FromQuery] int? size = null) { var agentService = _services.GetRequiredService(); var agents = await agentService.GetAgents(new AgentFilter { - Pager = new Pagination { Size = 1000 } + Pager = new Pagination { Size = size ?? 1000 } }); var labels = agents.Items?.SelectMany(x => x.Labels) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ApplicationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Application/ApplicationController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/ApplicationController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Application/ApplicationController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/GoogleController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Application/GoogleController.cs similarity index 93% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/GoogleController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Application/GoogleController.cs index 13faeb2d1..5497d6c2e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/GoogleController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Application/GoogleController.cs @@ -11,15 +11,18 @@ public class GoogleController : ControllerBase private readonly IServiceProvider _services; private readonly BotSharpOptions _options; private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; + private readonly ILogger _logger; - public GoogleController(IServiceProvider services, + public GoogleController( + IServiceProvider services, + ILogger logger, IHttpClientFactory httpClientFactory, BotSharpOptions options) { _services = services; - _options = options; + _logger = logger; _httpClientFactory = httpClientFactory; + _options = options; } [HttpGet("/address/options")] diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.File.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.File.cs new file mode 100644 index 000000000..0a259304b --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.File.cs @@ -0,0 +1,117 @@ +using BotSharp.Abstraction.Files.Enums; +using BotSharp.Abstraction.Files.Utilities; + +namespace BotSharp.OpenAPI.Controllers; + +public partial class ConversationController +{ + #region Files and attachments + [HttpGet("/conversation/{conversationId}/attachments")] + public List ListAttachments([FromRoute] string conversationId) + { + var fileStorage = _services.GetRequiredService(); + var dir = fileStorage.GetDirectory(conversationId); + + // List files in the directory + var files = Directory.Exists(dir) + ? Directory.GetFiles(dir).Select(f => new MessageFileViewModel + { + FileName = Path.GetFileName(f), + FileExtension = Path.GetExtension(f).TrimStart('.').ToLower(), + ContentType = FileUtility.GetFileContentType(f), + FileDownloadUrl = $"/conversation/{conversationId}/attachments/file/{Path.GetFileName(f)}", + }).ToList() + : new List(); + + return files; + } + + [AllowAnonymous] + [HttpGet("/conversation/{conversationId}/attachments/file/{fileName}")] + public IActionResult GetAttachment([FromRoute] string conversationId, [FromRoute] string fileName) + { + var fileStorage = _services.GetRequiredService(); + var dir = fileStorage.GetDirectory(conversationId); + var filePath = Path.Combine(dir, fileName); + if (!System.IO.File.Exists(filePath)) + { + return NotFound(); + } + return BuildFileResult(filePath); + } + + [HttpPost("/conversation/{conversationId}/attachments")] + public IActionResult UploadAttachments([FromRoute] string conversationId, IFormFile[] files) + { + if (files != null && files.Length > 0) + { + var fileStorage = _services.GetRequiredService(); + var dir = fileStorage.GetDirectory(conversationId); + foreach (var file in files) + { + // Save the file, process it, etc. + var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); + var filePath = Path.Combine(dir, fileName); + + fileStorage.SaveFileStreamToPath(filePath, file.OpenReadStream()); + } + + return Ok(new { message = "File uploaded successfully." }); + } + + return BadRequest(new { message = "Invalid file." }); + } + + [HttpPost("/agent/{agentId}/conversation/{conversationId}/upload")] + public async Task UploadConversationMessageFiles([FromRoute] string agentId, [FromRoute] string conversationId, [FromBody] InputMessageFiles input) + { + var convService = _services.GetRequiredService(); + convService.SetConversationId(conversationId, input.States); + var conv = await convService.GetConversationRecordOrCreateNew(agentId); + var fileStorage = _services.GetRequiredService(); + var messageId = Guid.NewGuid().ToString(); + var isSaved = fileStorage.SaveMessageFiles(conv.Id, messageId, FileSource.User, input.Files); + return Ok(new { messageId = isSaved ? messageId : string.Empty }); + } + + [HttpGet("/conversation/{conversationId}/files/{messageId}/{source}")] + public IEnumerable GetConversationMessageFiles([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source) + { + var fileStorage = _services.GetRequiredService(); + var files = fileStorage.GetMessageFiles(conversationId, [messageId], options: new() { Sources = [source] }); + return files?.Select(x => MessageFileViewModel.Transform(x))?.ToList() ?? []; + } + + [HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}")] + public IActionResult GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName) + { + var fileStorage = _services.GetRequiredService(); + var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName); + if (string.IsNullOrEmpty(file)) + { + return NotFound(); + } + return BuildFileResult(file); + } + + [HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}/download")] + public IActionResult DownloadMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName) + { + var fileStorage = _services.GetRequiredService(); + var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName); + if (string.IsNullOrEmpty(file)) + { + return NotFound(); + } + + var fName = file.Split(Path.DirectorySeparatorChar).Last(); + var contentType = FileUtility.GetFileContentType(fName); + var stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); + var bytes = new byte[stream.Length]; + stream.Read(bytes, 0, (int)stream.Length); + stream.Position = 0; + + return new FileStreamResult(stream, contentType) { FileDownloadName = fName }; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.State.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.State.cs new file mode 100644 index 000000000..be01ca7e3 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.State.cs @@ -0,0 +1,24 @@ +namespace BotSharp.OpenAPI.Controllers; + +public partial class ConversationController +{ + #region Search state keys + [HttpGet("/conversation/state/keys")] + public async Task> GetConversationStateKeys([FromQuery] ConversationStateKeysFilter request) + { + var convService = _services.GetRequiredService(); + var keys = await convService.GetConversationStateSearhKeys(request); + return keys; + } + #endregion + + #region Migrate Latest States + [HttpPost("/conversation/latest-state/migrate")] + public async Task MigrateConversationLatestStates([FromBody] MigrateLatestStateRequest request) + { + var convService = _services.GetRequiredService(); + var res = await convService.MigrateLatestStates(request.BatchSize, request.ErrorLimit); + return res; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.Visualization.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.Visualization.cs new file mode 100644 index 000000000..42930b66e --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.Visualization.cs @@ -0,0 +1,40 @@ +using BotSharp.Abstraction.Chart; + +namespace BotSharp.OpenAPI.Controllers; + +public partial class ConversationController +{ + #region Chart + [AllowAnonymous] + [HttpGet("/conversation/{conversationId}/message/{messageId}/user/chart/data")] + public async Task GetConversationChartData( + [FromRoute] string conversationId, + [FromRoute] string messageId, + [FromQuery] ConversationChartDataRequest request) + { + var chart = _services.GetServices().FirstOrDefault(x => x.Provider == request?.ChartProvider); + if (chart == null) return null; + + var result = await chart.GetConversationChartDataAsync(conversationId, messageId, request); + return ConversationChartDataResponse.From(result); + } + #endregion + + #region Dashboard + [HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")] + public async Task PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId) + { + var userService = _services.GetRequiredService(); + var pinned = await userService.AddDashboardConversation(conversationId); + return pinned; + } + + [HttpDelete("/agent/{agentId}/conversation/{conversationId}/dashboard")] + public async Task UnpinConversationFromDashboard([FromRoute] string agentId, [FromRoute] string conversationId) + { + var userService = _services.GetRequiredService(); + var unpinned = await userService.RemoveDashboardConversation(conversationId); + return unpinned; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.cs similarity index 67% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.cs index 6351457a9..18d17d14f 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.cs @@ -1,28 +1,24 @@ -using BotSharp.Abstraction.Chart; using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Files.Enums; -using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Abstraction.MessageHub.Services; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Dtos; using BotSharp.Core.Infrastructures; -using BotSharp.Core.Users.Services; -using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.OpenAPI.Controllers; [Authorize] [ApiController] -public class ConversationController : ControllerBase +public partial class ConversationController : ControllerBase { private readonly IServiceProvider _services; private readonly IUserIdentity _user; private readonly JsonSerializerOptions _jsonOptions; - public ConversationController(IServiceProvider services, + public ConversationController( + IServiceProvider services, IUserIdentity user, BotSharpOptions options) { @@ -46,12 +42,8 @@ public async Task NewConversation([FromRoute] string agen }; conv = await service.NewConversation(conv); service.SetConversationId(conv.Id, config.States); - using (var trace = new ActivitySource("BotSharp").StartActivity("NewUserSession", ActivityKind.Internal)) - { - trace?.SetTag("user_id", _user.FullName); - trace?.SetTag("conversation_id", conv.Id); - return ConversationViewModel.FromSession(conv); - } + + return ConversationViewModel.FromSession(conv); } [HttpGet("/conversations")] @@ -371,34 +363,25 @@ public async Task SendMessage( conv.SetConversationId(conversationId, input.States); SetStates(conv, input); - using (var trace = new ActivitySource("BotSharp").StartActivity("UserSession", ActivityKind.Internal)) - { - trace?.SetTag("user.id", _user.FullName); - trace?.SetTag("session.id", conversationId); - trace?.SetTag("input", inputMsg.Content); - trace?.SetTag(ModelDiagnosticsTags.AgentId, agentId); - - var response = new ChatResponseModel(); - await conv.SendMessage(agentId, inputMsg, - replyMessage: input.Postback, - async msg => - { - response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; - response.Function = msg.FunctionName; - response.MessageLabel = msg.MessageLabel; - response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; - response.Instruction = msg.Instruction; - response.Data = msg.Data; - }); + var response = new ChatResponseModel(); + await conv.SendMessage(agentId, inputMsg, + replyMessage: input.Postback, + async msg => + { + response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.Function = msg.FunctionName; + response.MessageLabel = msg.MessageLabel; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; + response.Instruction = msg.Instruction; + response.Data = msg.Data; + }); - var state = _services.GetRequiredService(); - response.States = state.GetStates(); - response.MessageId = inputMsg.MessageId; - response.ConversationId = conversationId; + var state = _services.GetRequiredService(); + response.States = state.GetStates(); + response.MessageId = inputMsg.MessageId; + response.ConversationId = conversationId; - trace?.SetTag("output", response.Data); - return response; - } + return response; } @@ -449,7 +432,7 @@ await conv.SendMessage(agentId, inputMsg, response.Instruction = msg.Instruction; response.Data = msg.Data; response.States = state.GetStates(); - + await OnChunkReceived(Response, response); }); @@ -475,183 +458,6 @@ private async Task OnReceiveToolCallIndication(string conversationId, RoleDialog } #endregion - #region Files and attachments - [HttpGet("/conversation/{conversationId}/attachments")] - public List ListAttachments([FromRoute] string conversationId) - { - var fileStorage = _services.GetRequiredService(); - var dir = fileStorage.GetDirectory(conversationId); - - // List files in the directory - var files = Directory.Exists(dir) - ? Directory.GetFiles(dir).Select(f => new MessageFileViewModel - { - FileName = Path.GetFileName(f), - FileExtension = Path.GetExtension(f).TrimStart('.').ToLower(), - ContentType = FileUtility.GetFileContentType(f), - FileDownloadUrl = $"/conversation/{conversationId}/attachments/file/{Path.GetFileName(f)}", - }).ToList() - : new List(); - - return files; - } - - [AllowAnonymous] - [HttpGet("/conversation/{conversationId}/attachments/file/{fileName}")] - public IActionResult GetAttachment([FromRoute] string conversationId, [FromRoute] string fileName) - { - var fileStorage = _services.GetRequiredService(); - var dir = fileStorage.GetDirectory(conversationId); - var filePath = Path.Combine(dir, fileName); - if (!System.IO.File.Exists(filePath)) - { - return NotFound(); - } - return BuildFileResult(filePath); - } - - [HttpPost("/conversation/{conversationId}/attachments")] - public IActionResult UploadAttachments([FromRoute] string conversationId, IFormFile[] files) - { - if (files != null && files.Length > 0) - { - var fileStorage = _services.GetRequiredService(); - var dir = fileStorage.GetDirectory(conversationId); - foreach (var file in files) - { - // Save the file, process it, etc. - var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); - var filePath = Path.Combine(dir, fileName); - - fileStorage.SaveFileStreamToPath(filePath, file.OpenReadStream()); - } - - return Ok(new { message = "File uploaded successfully." }); - } - - return BadRequest(new { message = "Invalid file." }); - } - - [HttpPost("/agent/{agentId}/conversation/{conversationId}/upload")] - public async Task UploadConversationMessageFiles([FromRoute] string agentId, [FromRoute] string conversationId, [FromBody] InputMessageFiles input) - { - var convService = _services.GetRequiredService(); - convService.SetConversationId(conversationId, input.States); - var conv = await convService.GetConversationRecordOrCreateNew(agentId); - var fileStorage = _services.GetRequiredService(); - var messageId = Guid.NewGuid().ToString(); - var isSaved = fileStorage.SaveMessageFiles(conv.Id, messageId, FileSource.User, input.Files); - return Ok(new { messageId = isSaved ? messageId : string.Empty }); - } - - [HttpGet("/conversation/{conversationId}/files/{messageId}/{source}")] - public IEnumerable GetConversationMessageFiles([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source) - { - var fileStorage = _services.GetRequiredService(); - var files = fileStorage.GetMessageFiles(conversationId, [messageId], options: new() { Sources = [source] }); - return files?.Select(x => MessageFileViewModel.Transform(x))?.ToList() ?? []; - } - - [HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}")] - public IActionResult GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName) - { - var fileStorage = _services.GetRequiredService(); - var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName); - if (string.IsNullOrEmpty(file)) - { - return NotFound(); - } - return BuildFileResult(file); - } - - [HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}/download")] - public IActionResult DownloadMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName) - { - var fileStorage = _services.GetRequiredService(); - var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName); - if (string.IsNullOrEmpty(file)) - { - return NotFound(); - } - - var fName = file.Split(Path.DirectorySeparatorChar).Last(); - var contentType = FileUtility.GetFileContentType(fName); - var stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); - var bytes = new byte[stream.Length]; - stream.Read(bytes, 0, (int)stream.Length); - stream.Position = 0; - - return new FileStreamResult(stream, contentType) { FileDownloadName = fName }; - } - #endregion - - #region Chart - [AllowAnonymous] - [HttpGet("/conversation/{conversationId}/message/{messageId}/user/chart/data")] - public async Task GetConversationChartData( - [FromRoute] string conversationId, - [FromRoute] string messageId, - [FromQuery] ConversationChartDataRequest request) - { - var chart = _services.GetServices().FirstOrDefault(x => x.Provider == request?.ChartProvider); - if (chart == null) return null; - - var result = await chart.GetConversationChartData(conversationId, messageId, request); - return ConversationChartDataResponse.From(result); - } - - [HttpPost("/conversation/{conversationId}/message/{messageId}/user/chart/code")] - public async Task GetConversationChartCode( - [FromRoute] string conversationId, - [FromRoute] string messageId, - [FromBody] ConversationChartCodeRequest request) - { - var chart = _services.GetServices().FirstOrDefault(x => x.Provider == request?.ChartProvider); - if (chart == null) return null; - - var result = await chart.GetConversationChartCode(conversationId, messageId, request); - return ConversationChartCodeResponse.From(result); - } - #endregion - - #region Dashboard - [HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")] - public async Task PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId) - { - var userService = _services.GetRequiredService(); - var pinned = await userService.AddDashboardConversation(conversationId); - return pinned; - } - - [HttpDelete("/agent/{agentId}/conversation/{conversationId}/dashboard")] - public async Task UnpinConversationFromDashboard([FromRoute] string agentId, [FromRoute] string conversationId) - { - var userService = _services.GetRequiredService(); - var unpinned = await userService.RemoveDashboardConversation(conversationId); - return unpinned; - } - #endregion - - #region Search state keys - [HttpGet("/conversation/state/keys")] - public async Task> GetConversationStateKeys([FromQuery] ConversationStateKeysFilter request) - { - var convService = _services.GetRequiredService(); - var keys = await convService.GetConversationStateSearhKeys(request); - return keys; - } - #endregion - - #region Migrate Latest States - [HttpPost("/conversation/latest-state/migrate")] - public async Task MigrateConversationLatestStates([FromBody] MigrateLatestStateRequest request) - { - var convService = _services.GetRequiredService(); - var res = await convService.MigrateLatestStates(request.BatchSize, request.ErrorLimit); - return res; - } - #endregion - #region Private methods private void SetStates(IConversationService conv, NewMessageModel input) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/DashboardController.cs similarity index 99% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/DashboardController.cs index 93cfefde9..ee7818897 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/DashboardController.cs @@ -17,6 +17,7 @@ public DashboardController(IServiceProvider services, _user = user; } + #region User Components [HttpGet("/dashboard/components")] public async Task GetComponents() diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/EvaluatorController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/EvaluatorController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/EvaluatorController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/EvaluatorController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/TranslationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/TranslationController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/TranslationController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/TranslationController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/TextEmbeddingController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Embedding/TextEmbeddingController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/TextEmbeddingController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Embedding/TextEmbeddingController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/File/FileController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/File/FileController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ImageGenerationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ImageGenerationController.cs deleted file mode 100644 index 6dd03f641..000000000 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ImageGenerationController.cs +++ /dev/null @@ -1,55 +0,0 @@ -using BotSharp.Abstraction.Instructs.Models; -using BotSharp.Abstraction.Instructs.Options; -using BotSharp.OpenAPI.ViewModels.Instructs; - -namespace BotSharp.OpenAPI.Controllers; - -[Authorize] -[ApiController] -public class ImageGenerationController -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - - public ImageGenerationController(IServiceProvider services, ILogger logger) - { - _services = services; - _logger = logger; - } - - [HttpPost("/instruct/image-composition")] - public async Task ComposeImages([FromBody] ImageCompositionRequest request) - { - var fileInstruct = _services.GetRequiredService(); - var state = _services.GetRequiredService(); - request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - var imageViewModel = new ImageGenerationViewModel(); - - try - { - if (request.Files.Length < 1) - { - return new ImageGenerationViewModel { Message = "No image found" }; - } - - var message = await fileInstruct.ComposeImages(request.Text, request.Files, new InstructOptions - { - Provider = request.Provider, - Model = request.Model, - AgentId = request.AgentId, - TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider - }); - imageViewModel.Content = message.Content; - imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; - return imageViewModel; - } - catch (Exception ex) - { - var error = $"Error in image edit. {ex.Message}"; - _logger.LogError(ex, error); - imageViewModel.Message = error; - return imageViewModel; - } - } -} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.Audio.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.Audio.cs new file mode 100644 index 000000000..027e9e40b --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.Audio.cs @@ -0,0 +1,101 @@ +using BotSharp.Abstraction.Files.Utilities; +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Instructs.Options; +using BotSharp.Core.Infrastructures; +using BotSharp.OpenAPI.ViewModels.Instructs; + +namespace BotSharp.OpenAPI.Controllers; + +public partial class InstructModeController +{ + #region Audio + [HttpPost("/instruct/speech-to-text")] + public async Task SpeechToText([FromBody] SpeechToTextFileRequest request) + { + var fileInstruct = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); + var viewModel = new SpeechToTextViewModel(); + + try + { + var audio = request.File; + if (audio == null) + { + return new SpeechToTextViewModel { ErrorMsg = "Error! Cannot find a valid audio file!" }; + } + var content = await fileInstruct.SpeechToText(audio, request.Text, new InstructOptions + { + Provider = request.Provider, + Model = request.Model, + AgentId = request.AgentId, + TemplateName = request.TemplateName + }); + + viewModel.Success = true; + viewModel.Content = content; + return viewModel; + } + catch (Exception ex) + { + var error = $"Error in speech to text. {ex.Message}"; + _logger.LogError(ex, error); + viewModel.ErrorMsg = error; + return viewModel; + } + } + + [HttpPost("/instruct/speech-to-text/form")] + public async Task SpeechToText(IFormFile file, [FromForm] SpeechToTextRequest request) + { + var fileInstruct = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); + var viewModel = new SpeechToTextViewModel(); + + try + { + var audioData = FileUtility.BuildFileDataFromFile(file); + var content = await fileInstruct.SpeechToText(new InstructFileModel + { + FileData = audioData, + FileName = Path.GetFileNameWithoutExtension(file.FileName), + FileExtension = Path.GetExtension(file.FileName) + }, + request?.Text ?? string.Empty, + new InstructOptions + { + Provider = request?.Provider, + Model = request?.Model, + AgentId = request?.AgentId, + TemplateName = request?.TemplateName + }); + + viewModel.Success = true; + viewModel.Content = content; + return viewModel; + } + catch (Exception ex) + { + var error = $"Error in speech-to-text. {ex.Message}"; + _logger.LogError(ex, error); + viewModel.ErrorMsg = error; + return viewModel; + } + } + + [HttpPost("/instruct/text-to-speech")] + public async Task TextToSpeech([FromBody] TextToSpeechRequest request) + { + var state = _services.GetRequiredService(); + request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); + + var completion = CompletionProvider.GetAudioSynthesizer(_services, provider: request.Provider, model: request.Model); + var binaryData = await completion.GenerateAudioAsync(request.Text); + var stream = binaryData.ToStream(); + stream.Position = 0; + + return new FileStreamResult(stream, "audio/mpeg") { FileDownloadName = "output.mp3" }; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.File.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.File.cs new file mode 100644 index 000000000..9c348d8b6 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.File.cs @@ -0,0 +1,81 @@ +using BotSharp.Abstraction.Files.Utilities; +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Instructs.Options; +using BotSharp.OpenAPI.ViewModels.Instructs; + +namespace BotSharp.OpenAPI.Controllers; + +public partial class InstructModeController +{ + #region Pdf + [HttpPost("/instruct/pdf-completion")] + public async Task PdfCompletion([FromBody] PdfReadFileRequest request) + { + var state = _services.GetRequiredService(); + request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); + var viewModel = new PdfCompletionViewModel(); + + try + { + var fileInstruct = _services.GetRequiredService(); + var content = await fileInstruct.ReadPdf(request.Text, request.Files, new InstructOptions + { + Provider = request.Provider, + Model = request.Model, + AgentId = request.AgentId, + TemplateName = request.TemplateName, + ImageConverter = request.ImageConverter + }); + + viewModel.Success = true; + viewModel.Content = content; + return viewModel; + } + catch (Exception ex) + { + var error = $"Error in pdf completion. {ex.Message}"; + _logger.LogError(ex, error); + viewModel.ErrorMsg = error; + return viewModel; + } + } + + [HttpPost("/instruct/pdf-completion/form")] + public async Task PdfCompletion([FromForm] IEnumerable files, [FromForm] PdfReadRequest request) + { + var state = _services.GetRequiredService(); + request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); + var viewModel = new PdfCompletionViewModel(); + + try + { + var fileModels = files.Select(x => new InstructFileModel + { + FileData = FileUtility.BuildFileDataFromFile(x), + ContentType = x.ContentType + }).ToList(); + + var fileInstruct = _services.GetRequiredService(); + var content = await fileInstruct.ReadPdf(request?.Text ?? string.Empty, fileModels, new InstructOptions + { + Provider = request?.Provider, + Model = request?.Model, + AgentId = request?.AgentId, + TemplateName = request?.TemplateName, + ImageConverter = request?.ImageConverter + }); + + viewModel.Success = true; + viewModel.Content = content; + return viewModel; + } + catch (Exception ex) + { + var error = $"Error in pdf completion. {ex.Message}"; + _logger.LogError(ex, error); + viewModel.ErrorMsg = error; + return viewModel; + } + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.Image.cs similarity index 53% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.Image.cs index cdb1dab49..d039c50cb 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.Image.cs @@ -1,188 +1,54 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Files.Utilities; -using BotSharp.Abstraction.Instructs; using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Options; -using BotSharp.Core.Infrastructures; using BotSharp.OpenAPI.ViewModels.Instructs; namespace BotSharp.OpenAPI.Controllers; -[Authorize] -[ApiController] -public class InstructModeController : ControllerBase -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - - public InstructModeController(IServiceProvider services, ILogger logger) - { - _services = services; - _logger = logger; - } - - [HttpPost("/instruct/{agentId}")] - public async Task InstructCompletion([FromRoute] string agentId, [FromBody] InstructMessageModel input) - { - var state = _services.GetRequiredService(); - input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); - state.SetState("provider", input.Provider, source: StateSource.External) - .SetState("model", input.Model, source: StateSource.External) - .SetState("model_id", input.ModelId, source: StateSource.External) - .SetState("instruction", input.Instruction, source: StateSource.External) - .SetState("input_text", input.Text, source: StateSource.External) - .SetState("template_name", input.Template, source: StateSource.External) - .SetState("channel", input.Channel, source: StateSource.External); - - var instructor = _services.GetRequiredService(); - var result = await instructor.Execute(agentId, - new RoleDialogModel(AgentRole.User, input.Text), - instruction: input.Instruction, - templateName: input.Template, - files: input.Files, - codeOptions: input.CodeOptions, - fileOptions: input.FileOptions); - - result.States = state.GetStates(); - return result; - } - [HttpPost("/instruct/text-completion")] - public async Task TextCompletion([FromBody] IncomingInstructRequest input) +public partial class InstructModeController +{ + #region Image composition + [HttpPost("/instruct/image-composition")] + public async Task ComposeImages([FromBody] ImageCompositionRequest request) { + var fileInstruct = _services.GetRequiredService(); var state = _services.GetRequiredService(); - input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); - state.SetState("provider", input.Provider ?? "azure-openai", source: StateSource.External) - .SetState("model", input.Model, source: StateSource.External) - .SetState("model_id", input.ModelId, source: StateSource.External); - - var agentId = input.AgentId ?? Guid.Empty.ToString(); - var textCompletion = CompletionProvider.GetTextCompletion(_services); - var response = await textCompletion.GetCompletion(input.Text, agentId, Guid.NewGuid().ToString()); - - await HookEmitter.Emit(_services, async hook => - await hook.OnResponseGenerated(new InstructResponseModel - { - AgentId = agentId, - Provider = textCompletion.Provider, - Model = textCompletion.Model, - TemplateName = input.Template, - UserMessage = input.Text, - CompletionText = response - }), agentId); - - return response; - } + request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); + var imageViewModel = new ImageGenerationViewModel(); - #region Chat - [HttpPost("/instruct/chat-completion")] - public async Task ChatCompletion([FromBody] IncomingInstructRequest input) - { - var state = _services.GetRequiredService(); - input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); - state.SetState("provider", input.Provider, source: StateSource.External) - .SetState("model", input.Model, source: StateSource.External) - .SetState("model_id", input.ModelId, source: StateSource.External); - - var agentId = input.AgentId ?? Guid.Empty.ToString(); - var completion = CompletionProvider.GetChatCompletion(_services); - var message = await completion.GetChatCompletions(new Agent() - { - Id = agentId, - Instruction = input.Instruction - }, new List + try { - new RoleDialogModel(AgentRole.User, input.Text) + if (request.Files.IsNullOrEmpty()) { - Files = input.Files?.Select(x => new BotSharpFile - { - FileUrl = x.FileUrl, - FileData = x.FileData, - ContentType = x.ContentType - }).ToList() ?? [] + return new ImageGenerationViewModel { ErrorMsg = "No image found" }; } - }); - await HookEmitter.Emit(_services, async hook => - await hook.OnResponseGenerated(new InstructResponseModel + var message = await fileInstruct.ComposeImages(request.Text, request.Files, new InstructOptions { - AgentId = agentId, - Provider = completion.Provider, - Model = completion.Model, - TemplateName = input.Template, - UserMessage = input.Text, - SystemInstruction = message.RenderedInstruction, - CompletionText = message.Content - }), agentId); - - return message.Content; - } - #endregion - - #region Read image - [HttpPost("/instruct/multi-modal")] - public async Task MultiModalCompletion([FromBody] MultiModalFileRequest input) - { - var state = _services.GetRequiredService(); - input.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - - try - { - var fileInstruct = _services.GetRequiredService(); - var content = await fileInstruct.ReadImages(input.Text, input.Files, new InstructOptions - { - Provider = input.Provider, - Model = input.Model, - AgentId = input.AgentId, - TemplateName = input.TemplateName + Provider = request.Provider, + Model = request.Model, + AgentId = request.AgentId, + TemplateName = request.TemplateName, + ImageConverter = request.ImageConverter }); - return content; - } - catch (Exception ex) - { - var error = $"Error in reading multi-modal files. {ex.Message}"; - _logger.LogError(ex, error); - return error; - } - } - - [HttpPost("/instruct/multi-modal/form")] - public async Task MultiModalCompletion([FromForm] IEnumerable files, [FromForm] MultiModalRequest request) - { - var state = _services.GetRequiredService(); - request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - var viewModel = new MultiModalViewModel(); - - try - { - var fileModels = files.Select(x => new InstructFileModel - { - FileData = FileUtility.BuildFileDataFromFile(x), - ContentType = x.ContentType - }).ToList(); - var fileInstruct = _services.GetRequiredService(); - var content = await fileInstruct.ReadImages(request?.Text ?? string.Empty, fileModels, new InstructOptions - { - Provider = request?.Provider, - Model = request?.Model, - AgentId = request?.AgentId, - TemplateName = request?.TemplateName - }); - viewModel.Content = content; - return viewModel; + imageViewModel.Success = true; + imageViewModel.Content = message.Content; + imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; + return imageViewModel; } catch (Exception ex) { - var error = $"Error in reading multi-modal files. {ex.Message}"; + var error = $"Error in image composition. {ex.Message}"; _logger.LogError(ex, error); - viewModel.Message = error; - return viewModel; + imageViewModel.ErrorMsg = error; + return imageViewModel; } } #endregion - #region Generate image + #region Image generation [HttpPost("/instruct/image-generation")] public async Task ImageGeneration([FromBody] ImageGenerationRequest request) { @@ -200,6 +66,8 @@ public async Task ImageGeneration([FromBody] ImageGene AgentId = request.AgentId, TemplateName = request.TemplateName }); + + imageViewModel.Success = true; imageViewModel.Content = message.Content; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; return imageViewModel; @@ -208,13 +76,13 @@ public async Task ImageGeneration([FromBody] ImageGene { var error = $"Error in image generation. {ex.Message}"; _logger.LogError(ex, error); - imageViewModel.Message = error; + imageViewModel.ErrorMsg = error; return imageViewModel; } } #endregion - #region Edit image + #region Image variation [HttpPost("/instruct/image-variation")] public async Task ImageVariation([FromBody] ImageVariationFileRequest request) { @@ -226,7 +94,7 @@ public async Task ImageVariation([FromBody] ImageVaria { if (request.File == null) { - return new ImageGenerationViewModel { Message = "Error! Cannot find an image!" }; + return new ImageGenerationViewModel { ErrorMsg = "Error! Cannot find an image!" }; } var fileInstruct = _services.GetRequiredService(); @@ -235,9 +103,10 @@ public async Task ImageVariation([FromBody] ImageVaria Provider = request.Provider, Model = request.Model, AgentId = request.AgentId, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); + imageViewModel.Success = true; imageViewModel.Content = message.Content; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; return imageViewModel; @@ -246,7 +115,7 @@ public async Task ImageVariation([FromBody] ImageVaria { var error = $"Error in image variation. {ex.Message}"; _logger.LogError(ex, error); - imageViewModel.Message = error; + imageViewModel.ErrorMsg = error; return imageViewModel; } } @@ -273,9 +142,10 @@ public async Task ImageVariation(IFormFile file, [From Provider = request?.Provider, Model = request?.Model, AgentId = request?.AgentId, - ImageConvertProvider = request?.ImageConvertProvider + ImageConverter = request?.ImageConverter }); + imageViewModel.Success = true; imageViewModel.Content = message.Content; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; return imageViewModel; @@ -284,11 +154,13 @@ public async Task ImageVariation(IFormFile file, [From { var error = $"Error in image variation upload. {ex.Message}"; _logger.LogError(ex, error); - imageViewModel.Message = error; + imageViewModel.ErrorMsg = error; return imageViewModel; } } + #endregion + #region Image edit [HttpPost("/instruct/image-edit")] public async Task ImageEdit([FromBody] ImageEditFileRequest request) { @@ -301,16 +173,19 @@ public async Task ImageEdit([FromBody] ImageEditFileRe { if (request.File == null) { - return new ImageGenerationViewModel { Message = "Error! Cannot find a valid image file!" }; + return new ImageGenerationViewModel { ErrorMsg = "Error! Cannot find a valid image file!" }; } + var message = await fileInstruct.EditImage(request.Text, request.File, new InstructOptions { Provider = request.Provider, Model = request.Model, AgentId = request.AgentId, TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); + + imageViewModel.Success = true; imageViewModel.Content = message.Content; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; return imageViewModel; @@ -319,7 +194,7 @@ public async Task ImageEdit([FromBody] ImageEditFileRe { var error = $"Error in image edit. {ex.Message}"; _logger.LogError(ex, error); - imageViewModel.Message = error; + imageViewModel.ErrorMsg = error; return imageViewModel; } } @@ -347,9 +222,10 @@ public async Task ImageEdit(IFormFile file, [FromForm] Model = request?.Model, AgentId = request?.AgentId, TemplateName = request?.TemplateName, - ImageConvertProvider = request?.ImageConvertProvider + ImageConverter = request?.ImageConverter }); + imageViewModel.Success = true; imageViewModel.Content = message.Content; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; return imageViewModel; @@ -358,11 +234,13 @@ public async Task ImageEdit(IFormFile file, [FromForm] { var error = $"Error in image edit upload. {ex.Message}"; _logger.LogError(ex, error); - imageViewModel.Message = error; + imageViewModel.ErrorMsg = error; return imageViewModel; } } + #endregion + #region Image mask edit [HttpPost("/instruct/image-mask-edit")] public async Task ImageMaskEdit([FromBody] ImageMaskEditFileRequest request) { @@ -377,16 +255,19 @@ public async Task ImageMaskEdit([FromBody] ImageMaskEd var mask = request.Mask; if (image == null || mask == null) { - return new ImageGenerationViewModel { Message = "Error! Cannot find a valid image or mask!" }; + return new ImageGenerationViewModel { ErrorMsg = "Error! Cannot find a valid image or mask!" }; } + var message = await fileInstruct.EditImage(request.Text, image, mask, new InstructOptions { Provider = request.Provider, Model = request.Model, AgentId = request.AgentId, TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); + + imageViewModel.Success = true; imageViewModel.Content = message.Content; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; return imageViewModel; @@ -395,7 +276,7 @@ public async Task ImageMaskEdit([FromBody] ImageMaskEd { var error = $"Error in image mask edit. {ex.Message}"; _logger.LogError(ex, error); - imageViewModel.Message = error; + imageViewModel.ErrorMsg = error; return imageViewModel; } } @@ -431,9 +312,10 @@ public async Task ImageMaskEdit(IFormFile image, IForm Model = request?.Model, AgentId = request?.AgentId, TemplateName = request?.TemplateName, - ImageConvertProvider = request?.ImageConvertProvider + ImageConverter = request?.ImageConverter }); + imageViewModel.Success = true; imageViewModel.Content = message.Content; imageViewModel.Images = message.GeneratedImages?.Select(x => ImageViewModel.ToViewModel(x)) ?? []; return imageViewModel; @@ -442,49 +324,45 @@ public async Task ImageMaskEdit(IFormFile image, IForm { var error = $"Error in image mask edit upload. {ex.Message}"; _logger.LogError(ex, error); - imageViewModel.Message = error; + imageViewModel.ErrorMsg = error; return imageViewModel; } } #endregion - #region Pdf - [HttpPost("/instruct/pdf-completion")] - public async Task PdfCompletion([FromBody] PdfReadFileRequest request) + #region Read image + [HttpPost("/instruct/multi-modal")] + public async Task MultiModalCompletion([FromBody] MultiModalFileRequest input) { var state = _services.GetRequiredService(); - request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - var viewModel = new PdfCompletionViewModel(); + input.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); try { var fileInstruct = _services.GetRequiredService(); - var content = await fileInstruct.ReadPdf(request.Text, request.Files, new InstructOptions + var content = await fileInstruct.ReadImages(input.Text, input.Files, new InstructOptions { - Provider = request.Provider, - Model = request.Model, - AgentId = request.AgentId, - TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider + Provider = input.Provider, + Model = input.Model, + AgentId = input.AgentId, + TemplateName = input.TemplateName }); - viewModel.Content = content; - return viewModel; + return content; } catch (Exception ex) { - var error = $"Error in pdf completion. {ex.Message}"; + var error = $"Error in reading multi-modal files. {ex.Message}"; _logger.LogError(ex, error); - viewModel.Message = error; - return viewModel; + return error; } } - [HttpPost("/instruct/pdf-completion/form")] - public async Task PdfCompletion([FromForm] IEnumerable files, [FromForm] PdfReadRequest request) + [HttpPost("/instruct/multi-modal/form")] + public async Task MultiModalCompletion([FromForm] IEnumerable files, [FromForm] MultiModalRequest request) { var state = _services.GetRequiredService(); request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - var viewModel = new PdfCompletionViewModel(); + var viewModel = new MultiModalViewModel(); try { @@ -495,112 +373,24 @@ public async Task PdfCompletion([FromForm] IEnumerable(); - var content = await fileInstruct.ReadPdf(request?.Text ?? string.Empty, fileModels, new InstructOptions - { - Provider = request?.Provider, - Model = request?.Model, - AgentId = request?.AgentId, - TemplateName = request?.TemplateName, - ImageConvertProvider = request?.ImageConvertProvider - }); - viewModel.Content = content; - return viewModel; - } - catch (Exception ex) - { - var error = $"Error in pdf completion. {ex.Message}"; - _logger.LogError(ex, error); - viewModel.Message = error; - return viewModel; - } - } - #endregion - - #region Audio - [HttpPost("/instruct/speech-to-text")] - public async Task SpeechToText([FromBody] SpeechToTextFileRequest request) - { - var fileInstruct = _services.GetRequiredService(); - var state = _services.GetRequiredService(); - request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - var viewModel = new SpeechToTextViewModel(); - - try - { - var audio = request.File; - if (audio == null) - { - return new SpeechToTextViewModel { Message = "Error! Cannot find a valid audio file!" }; - } - var content = await fileInstruct.SpeechToText(audio, request.Text, new InstructOptions - { - Provider = request.Provider, - Model = request.Model, - AgentId = request.AgentId, - TemplateName = request.TemplateName - }); - viewModel.Content = content; - return viewModel; - } - catch (Exception ex) - { - var error = $"Error in speech to text. {ex.Message}"; - _logger.LogError(ex, error); - viewModel.Message = error; - return viewModel; - } - } - - [HttpPost("/instruct/speech-to-text/form")] - public async Task SpeechToText(IFormFile file, [FromForm] SpeechToTextRequest request) - { - var fileInstruct = _services.GetRequiredService(); - var state = _services.GetRequiredService(); - request?.States?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - var viewModel = new SpeechToTextViewModel(); - - try - { - var audioData = FileUtility.BuildFileDataFromFile(file); - var content = await fileInstruct.SpeechToText(new InstructFileModel - { - FileData = audioData, - FileName = Path.GetFileNameWithoutExtension(file.FileName), - FileExtension = Path.GetExtension(file.FileName) - }, - request?.Text ?? string.Empty, - new InstructOptions + var content = await fileInstruct.ReadImages(request?.Text ?? string.Empty, fileModels, new InstructOptions { Provider = request?.Provider, Model = request?.Model, AgentId = request?.AgentId, TemplateName = request?.TemplateName }); - + viewModel.Success = true; viewModel.Content = content; return viewModel; } catch (Exception ex) { - var error = $"Error in speech-to-text. {ex.Message}"; + var error = $"Error in reading multi-modal files. {ex.Message}"; _logger.LogError(ex, error); - viewModel.Message = error; + viewModel.ErrorMsg = error; return viewModel; } } - - [HttpPost("/instruct/text-to-speech")] - public async Task TextToSpeech([FromBody] TextToSpeechRequest request) - { - var state = _services.GetRequiredService(); - request.States.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - - var completion = CompletionProvider.GetAudioSynthesizer(_services, provider: request.Provider, model: request.Model); - var binaryData = await completion.GenerateAudioAsync(request.Text); - var stream = binaryData.ToStream(); - stream.Position = 0; - - return new FileStreamResult(stream, "audio/mpeg") { FileDownloadName = "output.mp3" }; - } #endregion } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.cs new file mode 100644 index 000000000..c51a26fa3 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.cs @@ -0,0 +1,122 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Instructs; +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Core.Infrastructures; +using BotSharp.OpenAPI.ViewModels.Instructs; + +namespace BotSharp.OpenAPI.Controllers; + +[Authorize] +[ApiController] +public partial class InstructModeController : ControllerBase +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public InstructModeController(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + [HttpPost("/instruct/{agentId}")] + public async Task InstructCompletion([FromRoute] string agentId, [FromBody] InstructMessageModel input) + { + var state = _services.GetRequiredService(); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + state.SetState("provider", input.Provider, source: StateSource.External) + .SetState("model", input.Model, source: StateSource.External) + .SetState("model_id", input.ModelId, source: StateSource.External) + .SetState("instruction", input.Instruction, source: StateSource.External) + .SetState("input_text", input.Text, source: StateSource.External) + .SetState("template_name", input.Template, source: StateSource.External) + .SetState("channel", input.Channel, source: StateSource.External) + .SetState("code_options", input.CodeOptions, source: StateSource.External) + .SetState("file_options", input.FileOptions, source: StateSource.External) + .SetState("file_count", !input.Files.IsNullOrEmpty() ? input.Files.Count : (int?)null, source: StateSource.External); + + var instructor = _services.GetRequiredService(); + var result = await instructor.Execute(agentId, + new RoleDialogModel(AgentRole.User, input.Text), + instruction: input.Instruction, + templateName: input.Template, + files: input.Files, + codeOptions: input.CodeOptions, + fileOptions: input.FileOptions); + + result.States = state.GetStates(); + return result; + } + + [HttpPost("/instruct/text-completion")] + public async Task TextCompletion([FromBody] IncomingInstructRequest input) + { + var state = _services.GetRequiredService(); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + state.SetState("provider", input.Provider ?? "azure-openai", source: StateSource.External) + .SetState("model", input.Model, source: StateSource.External) + .SetState("model_id", input.ModelId, source: StateSource.External); + + var agentId = input.AgentId ?? Guid.Empty.ToString(); + var textCompletion = CompletionProvider.GetTextCompletion(_services); + var response = await textCompletion.GetCompletion(input.Text, agentId, Guid.NewGuid().ToString()); + + await HookEmitter.Emit(_services, async hook => + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = agentId, + Provider = textCompletion.Provider, + Model = textCompletion.Model, + TemplateName = input.Template, + UserMessage = input.Text, + CompletionText = response + }), agentId); + + return response; + } + + #region Chat + [HttpPost("/instruct/chat-completion")] + public async Task ChatCompletion([FromBody] IncomingInstructRequest input) + { + var state = _services.GetRequiredService(); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + state.SetState("provider", input.Provider, source: StateSource.External) + .SetState("model", input.Model, source: StateSource.External) + .SetState("model_id", input.ModelId, source: StateSource.External); + + var agentId = input.AgentId ?? Guid.Empty.ToString(); + var completion = CompletionProvider.GetChatCompletion(_services); + var message = await completion.GetChatCompletions(new Agent() + { + Id = agentId, + Instruction = input.Instruction + }, new List + { + new RoleDialogModel(AgentRole.User, input.Text) + { + Files = input.Files?.Select(x => new BotSharpFile + { + FileUrl = x.FileUrl, + FileData = x.FileData, + ContentType = x.ContentType + }).ToList() ?? [] + } + }); + + await HookEmitter.Emit(_services, async hook => + await hook.OnResponseGenerated(new InstructResponseModel + { + AgentId = agentId, + Provider = completion.Provider, + Model = completion.Model, + TemplateName = input.Template, + UserMessage = input.Text, + SystemInstruction = message.RenderedInstruction, + CompletionText = message.Content + }), agentId); + + return message.Content; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBase/KnowledgeBaseController.cs similarity index 94% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBase/KnowledgeBaseController.cs index 27588318d..3a9a9e732 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBase/KnowledgeBaseController.cs @@ -68,14 +68,15 @@ public async Task> SearchVectorKnowledge([ { var options = new VectorSearchOptions { - Fields = request.Fields, - FilterGroups = request.FilterGroups, - Limit = request.Limit ?? 5, - Confidence = request.Confidence ?? 0.5f, - WithVector = request.WithVector + Fields = request?.Fields, + FilterGroups = request?.FilterGroups, + Limit = request?.Limit ?? 5, + Confidence = request?.Confidence ?? 0.5f, + WithVector = request?.WithVector ?? false, + SearchParam = request?.SearchParam }; - var results = await _knowledgeService.SearchVectorKnowledge(request.Text, collection, options); + var results = await _knowledgeService.SearchVectorKnowledge(request?.Text ?? string.Empty, collection, options); return results.Select(x => VectorKnowledgeViewModel.From(x)).ToList(); } @@ -206,13 +207,15 @@ public async Task DeleteVectorCollectionSnapshots([FromRoute] string colle [HttpPost("/knowledge/document/{collection}/upload")] public async Task UploadKnowledgeDocuments([FromRoute] string collection, [FromBody] VectorKnowledgeUploadRequest request) { - var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, request.Files, request.ChunkOption); + var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, request.Files, request.Options); return response; } - [HttpPost("/knowledge/document/{collection}/form-upload")] - public async Task UploadKnowledgeDocuments([FromRoute] string collection, - [FromForm] IEnumerable files, [FromForm] ChunkOption? option = null) + [HttpPost("/knowledge/document/{collection}/form")] + public async Task UploadKnowledgeDocuments( + [FromRoute] string collection, + [FromForm] IEnumerable files, + [FromForm] KnowledgeDocOptions? options = null) { if (files.IsNullOrEmpty()) { @@ -230,7 +233,7 @@ public async Task UploadKnowledgeDocuments([FromRoute] }); } - var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, docs, option); + var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, docs, options); return response; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Logging/LoggerController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Logging/LoggerController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Mcp/McpController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Mcp/McpController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RealtimeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Realtime/RealtimeController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/RealtimeController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Realtime/RealtimeController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RulesController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/RulesController.cs deleted file mode 100644 index 0f58d9e56..000000000 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RulesController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Rules; - -namespace BotSharp.OpenAPI.Controllers; - -[Authorize] -[ApiController] -public class RulesController -{ - private readonly IServiceProvider _services; - - public RulesController( - IServiceProvider services) - { - _services = services; - } - - [HttpGet("/rule/triggers")] - public IEnumerable GetRuleTriggers() - { - var triggers = _services.GetServices(); - return triggers.Select(x => new AgentRule - { - TriggerName = x.GetType().Name - }).OrderBy(x => x.TriggerName).ToList(); - } - - [HttpGet("/rule/formalization")] - public async Task GetFormalizedRuleDefinition([FromBody] AgentRule rule) - { - return "{}"; - } -} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LlmProviderController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/LlmProviderController.cs similarity index 78% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/LlmProviderController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/LlmProviderController.cs index 6b87bec2f..6c2b335c1 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LlmProviderController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/LlmProviderController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.MLTasks.Filters; using BotSharp.Abstraction.MLTasks.Settings; namespace BotSharp.OpenAPI.Controllers; @@ -9,6 +10,7 @@ public class LlmProviderController : ControllerBase { private readonly IServiceProvider _services; private readonly ILlmProviderService _llmProvider; + public LlmProviderController(IServiceProvider services, ILlmProviderService llmProvider) { _services = services; @@ -22,16 +24,16 @@ public IEnumerable GetLlmProviders() } [HttpGet("/llm-provider/{provider}/models")] - public IEnumerable GetLlmProviderModels([FromRoute] string provider) + public IEnumerable GetLlmProviderModels([FromRoute] string provider, [FromQuery] LlmModelType modelType = LlmModelType.Chat) { var list = _llmProvider.GetProviderModels(provider); - return list.Where(x => x.Type == LlmModelType.Chat); + return list.Where(x => x.Type == modelType); } [HttpGet("/llm-configs")] - public List GetLlmConfigs([FromQuery] LlmConfigOptions options) + public List GetLlmConfigs([FromQuery] LlmConfigFilter filter) { - var configs = _llmProvider.GetLlmConfigs(options); + var configs = _llmProvider.GetLlmConfigs(filter); return configs; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/PluginController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/PluginController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RouterController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/RouterController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/RouterController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/RouterController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/SettingController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/SettingController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/SettingController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Setting/SettingController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/User/RoleController.cs similarity index 100% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/User/RoleController.cs diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/User/UserController.cs similarity index 90% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/User/UserController.cs index 24b34cbf2..472752cc1 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/User/UserController.cs @@ -34,7 +34,7 @@ public async Task> GetToken([FromHeader(Name = "Authorizatio { if (authcode.Contains(' ')) { - authcode = authcode.Split(' ')[1]; + authcode = authcode.Split(' ', StringSplitOptions.RemoveEmptyEntries).Last(); } var token = await _userService.GetToken(authcode); @@ -46,6 +46,28 @@ public async Task> GetToken([FromHeader(Name = "Authorizatio return Ok(token); } + [AllowAnonymous] + [HttpPost("/renew-token")] + public async Task> RenewToken([FromBody] RenewTokenModel request) + { + request ??= new(); + if (request.RefreshToken?.Contains(" ") == true) + { + request.RefreshToken = request.RefreshToken?.Split(' ', StringSplitOptions.RemoveEmptyEntries)?.LastOrDefault() ?? string.Empty; + } + if (request.AccessToken?.Contains(" ") == true) + { + request.AccessToken = request.AccessToken?.Split(' ', StringSplitOptions.RemoveEmptyEntries)?.LastOrDefault(); + } + + var newToken = await _userService.RenewToken(request.RefreshToken, request.AccessToken); + if (newToken == null) + { + return Unauthorized(); + } + return Ok(newToken); + } + [AllowAnonymous] [HttpGet("/sso/{provider}")] public async Task Authorize([FromRoute] string provider, string redirectUrl) diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptGenerationRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptGenerationRequest.cs new file mode 100644 index 000000000..3c68ac897 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptGenerationRequest.cs @@ -0,0 +1,14 @@ +using BotSharp.Abstraction.Coding.Options; +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Agents; + +public class AgentCodeScriptGenerationRequest +{ + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public CodeGenHandleOptions? Options { get; set; } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptUpdateModel.cs new file mode 100644 index 000000000..0b2120462 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptUpdateModel.cs @@ -0,0 +1,23 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Agents.Options; +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Agents; + +public class AgentCodeScriptUpdateModel +{ + [JsonPropertyName("code_scripts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? CodeScripts { get; set; } + + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AgentCodeScriptUpdateOptions? Options { get; set; } +} + +public class AgentCodeScriptDeleteModel +{ + [JsonPropertyName("code_scripts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? CodeScripts { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentDeleteRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentDeleteRequest.cs new file mode 100644 index 000000000..cb962c9a5 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentDeleteRequest.cs @@ -0,0 +1,11 @@ +using BotSharp.Abstraction.Agents.Options; +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Agents; + +public class AgentDeleteRequest +{ + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AgentDeleteOptions? Options { get; set; } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentCodeScriptViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentCodeScriptViewModel.cs new file mode 100644 index 000000000..dca5b41c4 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentCodeScriptViewModel.cs @@ -0,0 +1,52 @@ +using BotSharp.Abstraction.Agents.Models; +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Agents; + +public class AgentCodeScriptViewModel +{ + + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + [JsonPropertyName("content")] + public string Content { get; set; } = null!; + + [JsonPropertyName("script_type")] + public string ScriptType { get; set; } = null!; + + public AgentCodeScriptViewModel() + { + + } + + public static AgentCodeScriptViewModel From(AgentCodeScript model) + { + if (model == null) + { + return null; + } + + return new AgentCodeScriptViewModel + { + Name = model.Name, + Content = model.Content, + ScriptType = model.ScriptType + }; + } + + public static AgentCodeScript To(AgentCodeScriptViewModel model) + { + if (model == null) + { + return null; + } + + return new AgentCodeScript + { + Name = model.Name, + Content = model.Content, + ScriptType = model.ScriptType + }; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentRuleViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentRuleViewModel.cs new file mode 100644 index 000000000..23386c13d --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentRuleViewModel.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Agents; + +public class AgentRuleViewModel +{ + [JsonPropertyName("trigger_name")] + public string TriggerName { get; set; } = string.Empty; + + [JsonPropertyName("channel")] + public string Channel { get; set; } = string.Empty; + + [JsonPropertyName("statement")] + public string Statement { get; set; } = string.Empty; + + [JsonPropertyName("output_args")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonDocument? OutputArgs { get; set; } + + [JsonPropertyName("json_args")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? JsonArgs + { + get + { + if (OutputArgs == null) + { + return null; + } + + var json = JsonSerializer.Serialize(OutputArgs.RootElement, new JsonSerializerOptions { WriteIndented = true }); + return $"```json\r\n{json}\r\n```"; + } + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs index 197c11746..67ab1a0f3 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ConversationChartDataResponse.cs @@ -16,25 +16,4 @@ public class ConversationChartDataResponse Data = result.Data }; } -} - - -public class ConversationChartCodeResponse -{ - public string Code { get; set; } - public string Language { get; set; } - - public static ConversationChartCodeResponse? From(ChartCodeResult? result) - { - if (result == null) - { - return null; - } - - return new() - { - Code = result.Code, - Language = result.Language - }; - } -} +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs index 96c831cb0..d1f44c34d 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs @@ -44,8 +44,8 @@ public class ImageGenerationRequest : InstructBaseRequest public class ImageVariationRequest : InstructBaseRequest { - [JsonPropertyName("image_convert_provider")] - public string? ImageConvertProvider { get; set; } + [JsonPropertyName("image_converter")] + public string? ImageConverter { get; set; } } public class ImageVariationFileRequest : ImageVariationRequest @@ -60,8 +60,8 @@ public class ImageEditRequest : InstructBaseRequest [JsonPropertyName("text")] public string Text { get; set; } = string.Empty; - [JsonPropertyName("image_convert_provider")] - public string? ImageConvertProvider { get; set; } + [JsonPropertyName("image_converter")] + public string? ImageConverter { get; set; } } public class ImageEditFileRequest : ImageEditRequest @@ -81,8 +81,8 @@ public class ImageMaskEditRequest : InstructBaseRequest [JsonPropertyName("text")] public string Text { get; set; } = string.Empty; - [JsonPropertyName("image_convert_provider")] - public string? ImageConvertProvider { get; set; } + [JsonPropertyName("image_converter")] + public string? ImageConverter { get; set; } } public class ImageMaskEditFileRequest : ImageMaskEditRequest @@ -100,8 +100,8 @@ public class PdfReadRequest : InstructBaseRequest [JsonPropertyName("text")] public string Text { get; set; } = string.Empty; - [JsonPropertyName("image_convert_provider")] - public string? ImageConvertProvider { get; set; } + [JsonPropertyName("image_converter")] + public string? ImageConverter { get; set; } } public class PdfReadFileRequest : PdfReadRequest diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs index 0b20fca36..140b7be75 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs @@ -2,12 +2,8 @@ namespace BotSharp.OpenAPI.ViewModels.Instructs; -public class InstructBaseViewModel +public class InstructBaseViewModel : ResponseBase { [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; - - [JsonPropertyName("message")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Message { get; set; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs index 50a2fc5c2..93b48dc2a 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs @@ -22,4 +22,7 @@ public class SearchVectorKnowledgeRequest [JsonPropertyName("with_vector")] public bool WithVector { get; set; } + + [JsonPropertyName("search_param")] + public VectorSearchParamModel? SearchParam { get; set; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs index 760519668..2a97733e4 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs @@ -8,6 +8,6 @@ public class VectorKnowledgeUploadRequest [JsonPropertyName("files")] public IEnumerable Files { get; set; } = new List(); - [JsonPropertyName("chunk_option")] - public ChunkOption? ChunkOption { get; set; } + [JsonPropertyName("options")] + public KnowledgeDocOptions? Options { get; set; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/RenewTokenModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/RenewTokenModel.cs new file mode 100644 index 000000000..6c60aba9f --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/RenewTokenModel.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Users; + +public class RenewTokenModel +{ + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } = string.Empty; + + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs index fcb639101..d6b4c4107 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs @@ -1,7 +1,9 @@ using Anthropic.SDK.Common; using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.Files.Models; +using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.MLTasks.Settings; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -39,10 +41,10 @@ public async Task GetChatCompletions(Agent agent, List(); - var settings = settingsService.GetSetting(Provider, _model ?? agent.LlmConfig?.Model ?? "claude-3-haiku"); + var settings = settingsService.GetSetting(Provider, _model ?? agent.LlmConfig?.Model ?? "claude-haiku-4-5-20251001"); var client = new AnthropicClient(new APIAuthentication(settings.ApiKey)); - var (prompt, parameters) = PrepareOptions(agent, conversations, settings); + var (prompt, parameters) = PrepareOptions(agent, conversations); var response = await client.Messages.GetClaudeMessageAsync(parameters); @@ -74,6 +76,8 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, List< throw new NotImplementedException(); } - private (string, MessageParameters) PrepareOptions(Agent agent, List conversations, - LlmModelSetting settings) + private (string, MessageParameters) PrepareOptions(Agent agent, List conversations) { var agentService = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + var settingsService = _services.GetRequiredService(); + var settings = settingsService.GetSetting(Provider, _model); + var allowMultiModal = settings != null && settings.MultiModal; renderedInstructions = []; // Prepare instruction and functions @@ -140,7 +147,17 @@ public Task GetChatCompletionsStreamingAsync(Agent agent, List< { if (message.Role == AgentRole.User) { - messages.Add(new Message(RoleType.User, message.LlmContent)); + var contentParts = new List(); + if (allowMultiModal && !message.Files.IsNullOrEmpty()) + { + CollectMessageContentParts(contentParts, message.Files); + } + contentParts.Add(new TextContent() { Text = message.LlmContent }); + messages.Add(new Message + { + Role = RoleType.User, + Content = contentParts + }); } else if (message.Role == AgentRole.Assistant) { @@ -177,7 +194,6 @@ public Task GetChatCompletionsStreamingAsync(Agent agent, List< } } - var state = _services.GetRequiredService(); var temperature = decimal.Parse(state.GetState("temperature", "0.0")); var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens) ? tokens @@ -201,8 +217,6 @@ public Task GetChatCompletionsStreamingAsync(Agent agent, List< }; } - ; - JsonSerializerOptions? jsonSerializationOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, @@ -299,4 +313,53 @@ public void SetModelName(string model) { _model = model; } + + private void CollectMessageContentParts(List contentParts, List files) + { + foreach (var file in files) + { + if (!string.IsNullOrEmpty(file.FileData)) + { + var (contentType, binary) = FileUtility.GetFileInfoFromData(file.FileData); + var contentPart = new ImageContent + { + Source = new ImageSource + { + MediaType = contentType, + Data = Convert.ToBase64String(binary.ToArray()) + } + }; + contentParts.Add(contentPart); + } + else if (!string.IsNullOrEmpty(file.FileStorageUrl)) + { + var fileStorage = _services.GetRequiredService(); + var binary = fileStorage.GetFileBytes(file.FileStorageUrl); + var contentType = FileUtility.GetFileContentType(file.FileStorageUrl); + var contentPart = new ImageContent + { + Source = new ImageSource + { + MediaType = contentType, + Data = Convert.ToBase64String(binary) + } + }; + contentParts.Add(contentPart); + } + else if (!string.IsNullOrEmpty(file.FileUrl)) + { + var contentType = FileUtility.GetFileContentType(file.FileUrl); + + var contentPart = new ImageContent + { + Source = new ImageSource + { + MediaType = contentType, + Url = file.FileUrl + } + }; + contentParts.Add(contentPart); + } + } + } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs b/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs index 71efda50d..d311aac61 100644 --- a/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs +++ b/src/Plugins/BotSharp.Plugin.ChartHandler/Functions/PlotChartFn.cs @@ -38,10 +38,10 @@ public async Task Execute(RoleDialogModel message) Name = agent?.Name ?? "AI Programmer", Instruction = inst, LlmConfig = GetLlmConfig(), - TemplateDict = new Dictionary + TemplateDict = new() { - { "plotting_requirement", args?.PlottingRequirement ?? string.Empty }, - { "chart_element_id", $"chart-{message.MessageId}" } + ["plotting_requirement"] = args?.PlottingRequirement ?? string.Empty, + ["chart_element_id"] = $"chart-{message.MessageId}" } }; diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs b/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs index 514666926..4ba2e30c0 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs @@ -1,11 +1,7 @@ -using BotSharp.Abstraction.Crontab; using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Abstraction.MessageHub.Observers; -using BotSharp.Core.MessageHub; -using BotSharp.Core.MessageHub.Observers; using BotSharp.Plugin.ChatHub.Hooks; using BotSharp.Plugin.ChatHub.Observers; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; namespace BotSharp.Plugin.ChatHub; @@ -34,6 +30,5 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs deleted file mode 100644 index dd584f172..000000000 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs +++ /dev/null @@ -1,62 +0,0 @@ -using BotSharp.Abstraction.Conversations.Dtos; -using BotSharp.Abstraction.Conversations.Enums; -using BotSharp.Abstraction.Crontab; -using BotSharp.Abstraction.Crontab.Models; -using Microsoft.AspNetCore.SignalR; - -namespace BotSharp.Plugin.ChatHub.Hooks; - -public class ChatHubCrontabHook : ICrontabHook -{ - private readonly IServiceProvider _services; - private readonly IHubContext _chatHub; - private readonly ILogger _logger; - private readonly IUserIdentity _user; - private readonly BotSharpOptions _options; - private readonly ChatHubSettings _settings; - - public ChatHubCrontabHook( - IServiceProvider services, - IHubContext chatHub, - ILogger logger, - IUserIdentity user, - BotSharpOptions options, - ChatHubSettings settings) - { - _services = services; - _chatHub = chatHub; - _logger = logger; - _user = user; - _options = options; - _settings = settings; - } - - public async Task OnCronTriggered(CrontabItem item) - { - var data = new ChatResponseDto() - { - ConversationId = item.ConversationId, - MessageId = Guid.NewGuid().ToString(), - Text = item.ExecutionResult, - Function = "", - Sender = new() - { - FirstName = "Crontab", - LastName = "AI", - Role = AgentRole.Assistant - } - }; - - await SendEvent(item, data); - } - - private async Task SendEvent(CrontabItem item, ChatResponseDto data) - { - try - { - var json = JsonSerializer.Serialize(data, _options.JsonSerializerOptions); - await _chatHub.Clients.User(item.UserId).SendAsync(ChatEvent.OnNotificationGenerated, json); - } - catch { } - } -} diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 7156f29f9..321deed56 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Conversations.Enums; -using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Models; using Microsoft.AspNetCore.SignalR; using System.Runtime.CompilerServices; diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs index fcd2be3a4..957bce442 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs @@ -68,7 +68,7 @@ public async Task Execute(RoleDialogModel message) var llmProviderService = _services.GetRequiredService(); var provider = llmProviderService.GetProviders().FirstOrDefault(x => x == "openai"); var model = llmProviderService.GetProviderModel(provider: provider ?? "openai", id: "gpt-4o"); - var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, model: model.Name); + var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, model: model?.Name); var convService = _services.GetRequiredService(); var conversationId = convService.ConversationId; var dialogs = convService.GetDialogHistory(fromBreakpoint: false); diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/BotSharp.Plugin.FuzzySharp.csproj b/src/Plugins/BotSharp.Plugin.FuzzySharp/BotSharp.Plugin.FuzzySharp.csproj new file mode 100644 index 000000000..8561dc204 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/BotSharp.Plugin.FuzzySharp.csproj @@ -0,0 +1,21 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + + + + + + + \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Constants/MatchReason.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Constants/MatchReason.cs new file mode 100644 index 000000000..f46b3abf7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Constants/MatchReason.cs @@ -0,0 +1,20 @@ + +namespace BotSharp.Plugin.FuzzySharp.Constants; + +public static class MatchReason +{ + /// + /// Token matched a synonym term (e.g., HVAC -> Air Conditioning/Heating) + /// + public const string SynonymMatch = "synonym_match"; + + /// + /// Token exactly matched a vocabulary entry + /// + public const string ExactMatch = "exact_match"; + + /// + /// Token was flagged as a potential typo and a correction was suggested + /// + public const string TypoCorrection = "typo_correction"; +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Constants/TextConstants.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Constants/TextConstants.cs new file mode 100644 index 000000000..a8c749d13 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Constants/TextConstants.cs @@ -0,0 +1,29 @@ + +namespace BotSharp.Plugin.FuzzySharp.Constants; + +public static class TextConstants +{ + /// + /// Characters that need to be separated during tokenization (by adding spaces before and after) + /// Includes: parentheses, brackets, braces, punctuation marks, special symbols, etc. + /// This ensures "(IH)" is split into "(", "IH", ")" + /// + public static readonly char[] SeparatorChars = + { + // Parentheses and brackets + '(', ')', '[', ']', '{', '}', + // Punctuation marks + ',', '.', ';', ':', '!', '?', + // Special symbols + '=', '@', '#', '$', '%', '^', '&', '*', '+', '-', '\\', '|', '<', '>', '~', '`' + }; + + /// + /// Whitespace characters used as token separators during tokenization. + /// Includes: space, tab, newline, and carriage return. + /// + public static readonly char[] TokenSeparators = + { + ' ', '\t', '\n', '\r' + }; +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Controllers/FuzzySharpController.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Controllers/FuzzySharpController.cs new file mode 100644 index 000000000..bd1288b92 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Controllers/FuzzySharpController.cs @@ -0,0 +1,59 @@ +using BotSharp.Abstraction.Knowledges; +using BotSharp.Abstraction.Knowledges.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.FuzzySharp.Controllers; + +[ApiController] +public class FuzzySharpController : ControllerBase +{ + private readonly IPhraseService _phraseService; + private readonly ILogger _logger; + + public FuzzySharpController( + IPhraseService phraseService, + ILogger logger) + { + _phraseService = phraseService; + _logger = logger; + } + + /// + /// Analyze text for typos and entities using vocabulary. + /// + /// Returns: + /// - `original`: Original input text + /// - `tokens`: Tokenized text (only included if `include_tokens=true`) + /// - `flagged`: List of flagged items (each with `match_type`): + /// - `synonym_match` - Business abbreviations (confidence=1.0) + /// - `exact_match` - Exact vocabulary matches (confidence=1.0) + /// - `typo_correction` - Spelling corrections (confidence less than 1.0) + /// - `processing_time_ms`: Processing time in milliseconds + /// + /// Text analysis request + /// Text analysis response + [HttpPost("fuzzy-sharp/analyze-text")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task AnalyzeText([FromBody] string text) + { + try + { + if (string.IsNullOrWhiteSpace(text)) + { + return BadRequest(new { error = "Text is required" }); + } + + var result = await _phraseService.SearchPhrasesAsync(text); + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing and searching entities"); + return StatusCode(500, new { error = $"Error analyzing and searching entities: {ex.Message}" }); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Arguments/TextAnalysisRequest.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Arguments/TextAnalysisRequest.cs new file mode 100644 index 000000000..92bfad905 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Arguments/TextAnalysisRequest.cs @@ -0,0 +1,13 @@ + +namespace BotSharp.Plugin.FuzzySharp.FuzzSharp.Arguments; + +public class TextAnalysisRequest +{ + public string Text { get; set; } = string.Empty; + public string? VocabularyFolderName { get; set; } + public string? SynonymMappingFile { get; set; } + public double Cutoff { get; set; } = 0.82; + public int TopK { get; set; } = 5; + public int MaxNgram { get; set; } = 5; + public bool IncludeTokens { get; set; } = false; +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/INgramProcessor.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/INgramProcessor.cs new file mode 100644 index 000000000..90a9a06f1 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/INgramProcessor.cs @@ -0,0 +1,26 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp.Models; + +namespace BotSharp.Plugin.FuzzySharp.FuzzSharp; + +public interface INgramProcessor +{ + /// + /// Process tokens and generate all possible n-gram match results + /// + /// List of tokens to process + /// Vocabulary (source -> vocabulary set) + /// Synonym term Mapping + /// Lookup table (lowercase vocabulary -> (canonical form, source list)) + /// Maximum n-gram length + /// Minimum confidence threshold for fuzzy matching + /// Maximum number of matches to return + /// List of flagged items + List ProcessNgrams( + List tokens, + Dictionary> vocabulary, + Dictionary synonymMapping, + Dictionary Sources)> lookup, + int maxNgram, + double cutoff, + int topK); +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/IResultProcessor.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/IResultProcessor.cs new file mode 100644 index 000000000..c900877bf --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/IResultProcessor.cs @@ -0,0 +1,17 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp.Models; + +namespace BotSharp.Plugin.FuzzySharp.FuzzSharp; + +/// +/// Result processor interface +/// Responsible for processing match results, including deduplication and sorting +/// +public interface IResultProcessor +{ + /// + /// Process a list of flagged items, removing overlapping duplicates and sorting + /// + /// List of flagged items to process + /// Processed list of flagged items (deduplicated and sorted) + List ProcessResults(List flagged); +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/ITokenMatcher.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/ITokenMatcher.cs new file mode 100644 index 000000000..c715a8255 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/ITokenMatcher.cs @@ -0,0 +1,39 @@ +namespace BotSharp.Plugin.FuzzySharp.FuzzSharp; + +public interface ITokenMatcher +{ + /// + /// Try to match a content span and return a match result + /// + /// The matching context containing all necessary information + /// Match result if found, null otherwise + MatchResult? TryMatch(MatchContext context); + + /// + /// Priority of this matcher (higher priority matchers are tried first) + /// + int Priority { get; } +} + +/// +/// Context information for token matching +/// +public record MatchContext( + string ContentSpan, + string ContentLow, + int StartIndex, + int NgramLength, + Dictionary> Vocabulary, + Dictionary SynonymMapping, + Dictionary Sources)> Lookup, + double Cutoff, + int TopK); + +/// +/// Result of a token match +/// +public record MatchResult( + string CanonicalForm, + List Sources, + string MatchType, + double Confidence); diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Models/FlaggedItem.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Models/FlaggedItem.cs new file mode 100644 index 000000000..67bbd2802 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Models/FlaggedItem.cs @@ -0,0 +1,13 @@ + +namespace BotSharp.Plugin.FuzzySharp.FuzzSharp.Models; + +public class FlaggedItem +{ + public int Index { get; set; } + public string Token { get; set; } = string.Empty; + public List Sources { get; set; } = new(); + public string MatchType { get; set; } = string.Empty; + public string CanonicalForm { get; set; } = string.Empty; + public double Confidence { get; set; } + public int NgramLength { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Models/TextAnalysisResponse.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Models/TextAnalysisResponse.cs new file mode 100644 index 000000000..0a05d9cd1 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzSharp/Models/TextAnalysisResponse.cs @@ -0,0 +1,10 @@ + +namespace BotSharp.Plugin.FuzzySharp.FuzzSharp.Models; + +public class TextAnalysisResponse +{ + public string Original { get; set; } = string.Empty; + public List? Tokens { get; set; } + public List Flagged { get; set; } = new(); + public double ProcessingTimeMs { get; set; } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzySharpPlugin.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzySharpPlugin.cs new file mode 100644 index 000000000..1a125ea08 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/FuzzySharpPlugin.cs @@ -0,0 +1,29 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp; +using BotSharp.Abstraction.Knowledges; +using BotSharp.Abstraction.Plugins; +using BotSharp.Plugin.FuzzySharp.Services; +using BotSharp.Plugin.FuzzySharp.Services.Matching; +using BotSharp.Plugin.FuzzySharp.Services.Processors; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BotSharp.Plugin.FuzzySharp; + +public class FuzzySharpPlugin : IBotSharpPlugin +{ + public string Id => "379e6f7b-c58c-458b-b8cd-0374e5830711"; + public string Name => "Fuzzy Sharp"; + public string Description => "Analyze text for typos and entities using domain-specific vocabulary."; + public string IconUrl => "https://cdn-icons-png.flaticon.com/512/9592/9592995.png"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/CsvPhraseCollectionLoader.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/CsvPhraseCollectionLoader.cs new file mode 100644 index 000000000..af471bfee --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/CsvPhraseCollectionLoader.cs @@ -0,0 +1,187 @@ +using BotSharp.Abstraction.Knowledges; +using BotSharp.Core.Infrastructures; +using CsvHelper; +using CsvHelper.Configuration; +using Microsoft.Extensions.Logging; +using System.Globalization; +using System.IO; + +namespace BotSharp.Plugin.FuzzySharp.Services; + +public class CsvPhraseCollectionLoader : IPhraseCollection +{ + private readonly ILogger _logger; + + public CsvPhraseCollectionLoader(ILogger logger) + { + _logger = logger; + } + + [SharpCache(60)] + public async Task>> LoadVocabularyAsync() + { + string foldername = ""; + var vocabulary = new Dictionary>(); + + if (string.IsNullOrEmpty(foldername)) + { + return vocabulary; + } + + // Load CSV files from the folder + var csvFileDict = await LoadCsvFilesFromFolderAsync(foldername); + if (csvFileDict.Count == 0) + { + return vocabulary; + } + + // Load each CSV file + foreach (var (source, filePath) in csvFileDict) + { + try + { + var terms = await LoadCsvFileAsync(filePath); + vocabulary[source] = terms; + _logger.LogInformation($"Loaded {terms.Count} terms for source '{source}' from {filePath}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error loading CSV file for source '{source}': {filePath}"); + } + } + + return vocabulary; + } + + [SharpCache(60)] + public async Task> LoadSynonymMappingAsync() + { + string filename = ""; + var result = new Dictionary(); + if (string.IsNullOrWhiteSpace(filename)) + { + return result; + } + + var searchFolder = Path.Combine(AppContext.BaseDirectory, "data", "plugins", "fuzzySharp"); + var filePath = Path.Combine(searchFolder, filename); + + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + { + return result; + } + + try + { + using var reader = new StreamReader(filePath); + using var csv = new CsvReader(reader, CreateCsvConfig()); + + await csv.ReadAsync(); + csv.ReadHeader(); + + if (!HasRequiredColumns(csv)) + { + _logger.LogWarning("Synonym mapping file missing required columns: {FilePath}", filePath); + return result; + } + + while (await csv.ReadAsync()) + { + var term = csv.GetField("term") ?? string.Empty; + var dbPath = csv.GetField("dbPath") ?? string.Empty; + var canonicalForm = csv.GetField("canonical_form") ?? string.Empty; + + if (term.Length == 0 || dbPath.Length == 0 || canonicalForm.Length == 0) + { + _logger.LogWarning( + "Missing column(s) in CSV at row {Row}: term={Term}, dbPath={DbPath}, canonical_form={CanonicalForm}", + csv.Parser.RawRow, + term ?? "", + dbPath ?? "", + canonicalForm ?? ""); + continue; + } + + var key = term.ToLowerInvariant(); + result[key] = (dbPath, canonicalForm); + } + + _logger.LogInformation("Loaded synonym mapping from {FilePath}: {Count} terms", filePath, result.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading synonym mapping file: {FilePath}", filePath); + } + + return result; + } + + private async Task> LoadCsvFileAsync(string filePath) + { + var terms = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (!File.Exists(filePath)) + { + _logger.LogWarning($"CSV file does not exist: {filePath}"); + return terms; + } + + using var reader = new StreamReader(filePath); + using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false // No header in the CSV files + }); + + while (await csv.ReadAsync()) + { + // Read the first column (assuming it contains the terms) + var term = csv.GetField(0); + if (!string.IsNullOrWhiteSpace(term)) + { + terms.Add(term.Trim()); + } + } + + _logger.LogInformation($"Loaded {terms.Count} terms from {Path.GetFileName(filePath)}"); + return terms; + } + + private async Task> LoadCsvFilesFromFolderAsync(string folderName) + { + var csvFileDict = new Dictionary(); + var searchFolder = Path.Combine(AppContext.BaseDirectory, "data", "plugins", "fuzzySharp", folderName); + if (!Directory.Exists(searchFolder)) + { + _logger.LogWarning($"Folder does not exist: {searchFolder}"); + return csvFileDict; + } + + var csvFiles = Directory.GetFiles(searchFolder, "*.csv"); + foreach (var file in csvFiles) + { + var fileName = Path.GetFileNameWithoutExtension(file); + csvFileDict[fileName] = file; + } + + _logger.LogInformation($"Loaded {csvFileDict.Count} CSV files from {searchFolder}"); + return await Task.FromResult(csvFileDict); + } + + private static CsvConfiguration CreateCsvConfig() + { + return new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = true, + DetectColumnCountChanges = true, + MissingFieldFound = null + }; + } + + private static bool HasRequiredColumns(CsvReader csv) + { + return csv.HeaderRecord is { Length: > 0 } headers + && headers.Contains("term") + && headers.Contains("dbPath") + && headers.Contains("canonical_form"); + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/ExactMatcher.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/ExactMatcher.cs new file mode 100644 index 000000000..38e562eff --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/ExactMatcher.cs @@ -0,0 +1,23 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp; +using BotSharp.Plugin.FuzzySharp.Constants; + +namespace BotSharp.Plugin.FuzzySharp.Services.Matching; + +public class ExactMatcher : ITokenMatcher +{ + public int Priority => 2; // Second highest priority + + public MatchResult? TryMatch(MatchContext context) + { + if (context.Lookup.TryGetValue(context.ContentLow, out var match)) + { + return new MatchResult( + CanonicalForm: match.CanonicalForm, + Sources: match.Sources, + MatchType: MatchReason.ExactMatch, + Confidence: 1.0); + } + + return null; + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/FuzzyMatcher.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/FuzzyMatcher.cs new file mode 100644 index 000000000..193e28bc6 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/FuzzyMatcher.cs @@ -0,0 +1,81 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp; +using System.Text.RegularExpressions; +using FuzzySharp; +using FuzzySharp.SimilarityRatio; +using FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; +using BotSharp.Plugin.FuzzySharp.Constants; + +namespace BotSharp.Plugin.FuzzySharp.Services.Matching; + +public class FuzzyMatcher : ITokenMatcher +{ + public int Priority => 1; // Lowest priority + + public MatchResult? TryMatch(MatchContext context) + { + var match = CheckTypoCorrection(context.ContentSpan, context.Lookup, context.Cutoff); + if (match == null) + { + return null; + } + + var (canonicalForm, sources, confidence) = match.Value; + return new MatchResult( + CanonicalForm: canonicalForm, + Sources: sources, + MatchType: MatchReason.TypoCorrection, + Confidence: confidence); + } + + /// + /// Check typo correction using fuzzy matching + /// + private (string CanonicalForm, List Sources, double Confidence)? CheckTypoCorrection( + string contentSpan, + Dictionary Sources)> lookup, + double cutoff) + { + // Convert cutoff to 0-100 scale for FuzzySharp + var scoreCutoff = (int)(cutoff * 100); + + // Get all candidates from lookup + var candidates = lookup.Keys.ToList(); + + // Find best match using ExtractOne + var scorer = ScorerCache.Get(); + var result = Process.ExtractOne( + contentSpan, + candidates, + candidate => Normalize(candidate), // Preprocessor function + scorer, + scoreCutoff // Score cutoff + ); + + if (result == null) + { + return null; + } + + // Get the canonical form and sources from lookup + var match = lookup[result.Value]; + return (match.CanonicalForm, match.Sources, Math.Round(result.Score / 100.0, 3)); + } + + /// + /// Normalize text for fuzzy matching comparison + /// - Replaces all non-word characters (except apostrophes) with spaces + /// - Converts to lowercase + /// - Collapses multiple spaces into single space + /// - Trims leading/trailing whitespace + /// Example: "Test-Value (123)" → "test value 123" + /// + /// Text to normalize + /// Normalized text suitable for fuzzy matching + private string Normalize(string text) + { + // Replace non-word characters (except apostrophes) with spaces + var normalized = Regex.Replace(text, @"[^\w']+", " ", RegexOptions.IgnoreCase); + // Convert to lowercase, collapse multiple spaces, and trim + return Regex.Replace(normalized.ToLowerInvariant(), @"\s+", " ").Trim(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/SynonymMatcher.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/SynonymMatcher.cs new file mode 100644 index 000000000..9f6d8f97d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Matching/SynonymMatcher.cs @@ -0,0 +1,23 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp; +using BotSharp.Plugin.FuzzySharp.Constants; + +namespace BotSharp.Plugin.FuzzySharp.Services.Matching; + +public class SynonymMatcher : ITokenMatcher +{ + public int Priority => 3; // Highest priority + + public MatchResult? TryMatch(MatchContext context) + { + if (context.SynonymMapping.TryGetValue(context.ContentLow, out var match)) + { + return new MatchResult( + CanonicalForm: match.CanonicalForm, + Sources: new List { match.DbPath }, + MatchType: MatchReason.SynonymMatch, + Confidence: 1.0); + } + + return null; + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/PhraseService.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/PhraseService.cs new file mode 100644 index 000000000..cd05ca6a6 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/PhraseService.cs @@ -0,0 +1,199 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp; +using BotSharp.Plugin.FuzzySharp.FuzzSharp.Arguments; +using BotSharp.Plugin.FuzzySharp.FuzzSharp.Models; +using BotSharp.Abstraction.Knowledges; +using BotSharp.Abstraction.Knowledges.Models; +using BotSharp.Plugin.FuzzySharp.Utils; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace BotSharp.Plugin.FuzzySharp.Services; + +public class PhraseService : IPhraseService +{ + private readonly ILogger _logger; + private readonly IEnumerable _phraseLoaderServices; + private readonly INgramProcessor _ngramProcessor; + private readonly IResultProcessor _resultProcessor; + + public PhraseService( + ILogger logger, + IEnumerable phraseLoaderServices, + INgramProcessor ngramProcessor, + IResultProcessor resultProcessor) + { + _logger = logger; + _phraseLoaderServices = phraseLoaderServices; + _ngramProcessor = ngramProcessor; + _resultProcessor = resultProcessor; + } + + public Task> SearchPhrasesAsync(string term) + { + var request = BuildTextAnalysisRequest(term); + var response = AnalyzeTextAsync(request); + return response.ContinueWith(t => + { + var results = t.Result.Flagged.Select(f => new SearchPhrasesResult + { + Token = f.Token, + Sources = f.Sources, + CanonicalForm = f.CanonicalForm, + MatchType = f.MatchType, + Confidence = f.Confidence + }).ToList(); + return results; + }); + } + + private TextAnalysisRequest BuildTextAnalysisRequest(string inputText) + { + return new TextAnalysisRequest + { + Text = inputText + }; + } + + /// + /// Analyze text for typos and entities using domain-specific vocabulary + /// + private async Task AnalyzeTextAsync(TextAnalysisRequest request) + { + var stopwatch = Stopwatch.StartNew(); + try + { + // Tokenize the text + var tokens = TextTokenizer.Tokenize(request.Text); + + // Load vocabulary + var vocabulary = await LoadAllVocabularyAsync(); + + // Load synonym mapping + var synonymMapping = await LoadAllSynonymMappingAsync(); + + // Analyze text + var flagged = AnalyzeTokens(tokens, vocabulary, synonymMapping, request); + + stopwatch.Stop(); + + var response = new TextAnalysisResponse + { + Original = request.Text, + Flagged = flagged, + ProcessingTimeMs = Math.Round(stopwatch.Elapsed.TotalMilliseconds, 2) + }; + + if (request.IncludeTokens) + { + response.Tokens = tokens; + } + + _logger.LogInformation( + $"Text analysis completed in {response.ProcessingTimeMs}ms | " + + $"Text length: {request.Text.Length} chars | " + + $"Flagged items: {flagged.Count}"); + + return response; + } + catch (Exception) + { + stopwatch.Stop(); + throw; + } + } + + public async Task>> LoadAllVocabularyAsync() + { + var results = await Task.WhenAll(_phraseLoaderServices.Select(c => c.LoadVocabularyAsync())); + var merged = new Dictionary>(); + + foreach (var dict in results) + { + foreach (var kvp in dict) + { + if (!merged.TryGetValue(kvp.Key, out var set)) + merged[kvp.Key] = new HashSet(kvp.Value); + else + set.UnionWith(kvp.Value); + } + } + + return merged; + } + + public async Task> LoadAllSynonymMappingAsync() + { + var results = await Task.WhenAll(_phraseLoaderServices.Select(c => c.LoadSynonymMappingAsync())); + var merged = new Dictionary(); + + foreach (var dict in results) + { + foreach (var kvp in dict) + merged[kvp.Key] = kvp.Value; // later entries override earlier ones + } + + return merged; + } + + /// + /// Analyze tokens for typos and entities + /// + private List AnalyzeTokens( + List tokens, + Dictionary> vocabulary, + Dictionary synonymMapping, + TextAnalysisRequest request) + { + // Build lookup table for O(1) exact match lookups (matching Python's build_lookup) + var lookup = BuildLookup(vocabulary); + + // Process n-grams and find matches + var flagged = _ngramProcessor.ProcessNgrams( + tokens, + vocabulary, + synonymMapping, + lookup, + request.MaxNgram, + request.Cutoff, + request.TopK); + + // Process results: deduplicate and sort + return _resultProcessor.ProcessResults(flagged); + } + + /// + /// Build a lookup dictionary mapping lowercase terms to their canonical form and sources. + /// This is a performance optimization - instead of iterating through all sources for each lookup, + /// we build a flat dictionary once at the start. + /// + /// Matches Python's build_lookup() function. + /// + private Dictionary Sources)> BuildLookup( + Dictionary> vocabulary) + { + var lookup = new Dictionary Sources)>(); + + foreach (var (source, terms) in vocabulary) + { + foreach (var term in terms) + { + var key = term.ToLowerInvariant(); + if (lookup.TryGetValue(key, out var existing)) + { + // Term already exists - add this source to the list if not already there + if (!existing.Sources.Contains(source)) + { + existing.Sources.Add(source); + } + } + else + { + // New term - create entry with single source in list + lookup[key] = (term, new List { source }); + } + } + } + + return lookup; + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Processors/NgramProcessor.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Processors/NgramProcessor.cs new file mode 100644 index 000000000..86e584067 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Processors/NgramProcessor.cs @@ -0,0 +1,131 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp; +using BotSharp.Plugin.FuzzySharp.FuzzSharp.Models; + +namespace BotSharp.Plugin.FuzzySharp.Services.Processors; + +public class NgramProcessor : INgramProcessor +{ + private readonly List _matchers; + + public NgramProcessor(IEnumerable matchers) + { + // Sort matchers by priority (highest first) + _matchers = matchers.OrderByDescending(m => m.Priority).ToList(); + } + + public List ProcessNgrams( + List tokens, + Dictionary> vocabulary, + Dictionary synonymMapping, + Dictionary Sources)> lookup, + int maxNgram, + double cutoff, + int topK) + { + var flagged = new List(); + + // Process n-grams from largest to smallest + for (int n = maxNgram; n >= 1; n--) + { + for (int i = 0; i <= tokens.Count - n; i++) + { + var item = ProcessSingleNgram( + tokens, + i, + n, + vocabulary, + synonymMapping, + lookup, + cutoff, + topK); + + if (item != null) + { + flagged.Add(item); + } + } + } + + return flagged; + } + + /// + /// Process a single n-gram at the specified position + /// + private FlaggedItem? ProcessSingleNgram( + List tokens, + int startIdx, + int n, + Dictionary> vocabulary, + Dictionary synonymMapping, + Dictionary Sources)> lookup, + double cutoff, + int topK) + { + // Extract content span + var (contentSpan, spanTokens, contentIndices) = ExtractContentSpan(tokens, startIdx, n); + if (string.IsNullOrWhiteSpace(contentSpan)) + { + return null; + } + + var contentLow = contentSpan.ToLowerInvariant(); + + // Try matching in priority order using matchers + var context = new MatchContext( + contentSpan, + contentLow, + startIdx, + n, + vocabulary, + synonymMapping, + lookup, + cutoff, + topK); + + foreach (var matcher in _matchers) + { + var matchResult = matcher.TryMatch(context); + if (matchResult != null) + { + return CreateFlaggedItem(matchResult, startIdx, contentSpan, n); + } + } + + return null; + } + + /// + /// Create a FlaggedItem from a MatchResult + /// + private FlaggedItem CreateFlaggedItem( + MatchResult matchResult, + int startIndex, + string contentSpan, + int ngramLength) + { + return new FlaggedItem + { + Index = startIndex, + Token = contentSpan, + Sources = matchResult.Sources, + MatchType = matchResult.MatchType, + CanonicalForm = matchResult.CanonicalForm, + Confidence = matchResult.Confidence, + NgramLength = ngramLength + }; + } + + /// + /// Extract content span + /// + private (string ContentSpan, List Tokens, List ContentIndices) ExtractContentSpan( + List tokens, + int startIdx, + int n) + { + var span = tokens.Skip(startIdx).Take(n).ToList(); + var indices = Enumerable.Range(startIdx, n).ToList(); + return (string.Join(" ", span), span, indices); + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Processors/ResultProcessor.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Processors/ResultProcessor.cs new file mode 100644 index 000000000..ea402804d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Services/Processors/ResultProcessor.cs @@ -0,0 +1,102 @@ +using BotSharp.Plugin.FuzzySharp.FuzzSharp; +using BotSharp.Plugin.FuzzySharp.FuzzSharp.Models; +using BotSharp.Plugin.FuzzySharp.Constants; + +namespace BotSharp.Plugin.FuzzySharp.Services.Processors; + +public class ResultProcessor : IResultProcessor +{ + public List ProcessResults(List flagged) + { + // Remove overlapping duplicates + var deduped = RemoveOverlappingDuplicates(flagged); + + // Sort by confidence (descending), then match_type (alphabetically) + // This matches Python's _sort_and_format_results function + return deduped + .OrderByDescending(f => f.Confidence) + .ThenBy(f => f.MatchType) + .ToList(); + } + + /// + /// Remove overlapping detections with the same canonical form. + /// When multiple detections overlap and have the same canonical_form, + /// keep only the best one based on: + /// 1. Prefer synonym_match over exact_match over typo_correction (matches matcher priority) + /// 2. Highest confidence + /// 3. Shortest n-gram length + /// + private List RemoveOverlappingDuplicates(List flagged) + { + var deduped = new List(); + var skipIndices = new HashSet(); + + for (int i = 0; i < flagged.Count; i++) + { + if (skipIndices.Contains(i)) + { + continue; + } + + var item = flagged[i]; + var itemRange = (item.Index, item.Index + item.NgramLength); + + // Find all overlapping items with same canonical_form (regardless of match_type) + var overlappingGroup = new List { item }; + for (int j = i + 1; j < flagged.Count; j++) + { + if (skipIndices.Contains(j)) + { + continue; + } + + var other = flagged[j]; + if (item.CanonicalForm == other.CanonicalForm) + { + var otherRange = (other.Index, other.Index + other.NgramLength); + if (RangesOverlap(itemRange, otherRange)) + { + overlappingGroup.Add(other); + skipIndices.Add(j); + } + } + } + + // Keep the best item from the overlapping group + // Priority: synonym_match (3) > exact_match (2) > typo_correction (1) + // Then highest confidence, then shortest ngram + var bestItem = overlappingGroup + .OrderByDescending(x => GetMatchTypePriority(x.MatchType)) + .ThenByDescending(x => x.Confidence) + .ThenBy(x => x.NgramLength) + .First(); + deduped.Add(bestItem); + } + + return deduped; + } + + /// + /// Get priority value for match type (higher is better) + /// Matches the priority order in matchers: synonym > exact > fuzzy + /// + private int GetMatchTypePriority(string matchType) + { + return matchType switch + { + MatchReason.SynonymMatch => 3, // Highest priority + MatchReason.ExactMatch => 2, // Second priority + MatchReason.TypoCorrection => 1, // Lowest priority + _ => 0 // Unknown types get lowest priority + }; + } + + /// + /// Check if two token ranges overlap. + /// + private bool RangesOverlap((int start, int end) range1, (int start, int end) range2) + { + return range1.start < range2.end && range2.start < range1.end; + } +} diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Using.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Using.cs new file mode 100644 index 000000000..1a0fe1eab --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Using.cs @@ -0,0 +1,5 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text; +global using System.Threading.Tasks; diff --git a/src/Plugins/BotSharp.Plugin.FuzzySharp/Utils/TextTokenizer.cs b/src/Plugins/BotSharp.Plugin.FuzzySharp/Utils/TextTokenizer.cs new file mode 100644 index 000000000..8853733a2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FuzzySharp/Utils/TextTokenizer.cs @@ -0,0 +1,63 @@ +using BotSharp.Plugin.FuzzySharp.Constants; + +namespace BotSharp.Plugin.FuzzySharp.Utils; + +public static class TextTokenizer +{ + /// + /// Preprocess text: add spaces before and after characters that need to be separated + /// This allows subsequent simple whitespace tokenization to correctly separate these characters + /// Example: "(IH)" -> " ( IH ) " -> ["(", "IH", ")"] + /// + /// Text to preprocess + /// Preprocessed text + public static string PreprocessText(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return text; + } + + var result = new StringBuilder(text.Length * 2); + + foreach (var ch in text) + { + // If it's a character that needs to be separated, add spaces before and after + if (TextConstants.SeparatorChars.Contains(ch)) + { + result.Append(' '); + result.Append(ch); + result.Append(' '); + } + else + { + result.Append(ch); + } + } + + return result.ToString(); + } + + /// + /// Simple whitespace tokenization + /// Should be called after preprocessing text with PreprocessText + /// + /// Text to tokenize + /// List of tokens + public static List SimpleTokenize(string text) + { + return text.Split(TextConstants.TokenSeparators, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + + /// + /// Complete tokenization flow: preprocessing + tokenization + /// This is the recommended usage + /// + /// Text to tokenize + /// List of tokens + public static List Tokenize(string text) + { + var preprocessed = PreprocessText(text); + return SimpleTokenize(preprocessed); + } +} diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj b/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj index ac68607cc..0e0f57843 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/GoogleAiPlugin.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/GoogleAiPlugin.cs index 7b3118867..3382f3504 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/GoogleAiPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/GoogleAiPlugin.cs @@ -10,7 +10,7 @@ public class GoogleAiPlugin : IBotSharpPlugin { public string Id => "962ff441-2b40-4db4-b530-49efb1688a75"; public string Name => "Google AI"; - public string Description => "Making AI helpful for everyone (PaLM 2, Gemini)"; + public string Description => "Making AI helpful for everyone"; public string IconUrl => "https://vectorseek.com/wp-content/uploads/2021/12/Google-AI-Logo-Vector.png"; public void RegisterDI(IServiceCollection services, IConfiguration config) { @@ -20,10 +20,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) return settingService.Bind("GoogleAi"); }); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs similarity index 98% rename from src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs rename to src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs index 60e0ef025..d224fb122 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs @@ -5,14 +5,13 @@ using GenerativeAI; using GenerativeAI.Core; using GenerativeAI.Types; -using Google.Ai.Generativelanguage.V1Beta2; namespace BotSharp.Plugin.GoogleAi.Providers.Chat; -public class GeminiChatCompletionProvider : IChatCompletion +public class ChatCompletionProvider : IChatCompletion { private readonly IServiceProvider _services; - private readonly ILogger _logger; + private readonly ILogger _logger; private List renderedInstructions = []; private string _model; @@ -21,10 +20,10 @@ public class GeminiChatCompletionProvider : IChatCompletion public string Model => _model; private GoogleAiSettings _settings; - public GeminiChatCompletionProvider( + public ChatCompletionProvider( IServiceProvider services, GoogleAiSettings googleSettings, - ILogger logger) + ILogger logger) { _settings = googleSettings; _services = services; @@ -104,7 +103,7 @@ public async Task GetChatCompletionsAsync(Agent agent, List _logger; - private List renderedInstructions = []; - - private string _model; - - public string Provider => "google-palm"; - public string Model => _model; - - public PalmChatCompletionProvider( - IServiceProvider services, - ILogger logger) - { - _services = services; - _logger = logger; - } - - public async Task GetChatCompletions(Agent agent, List conversations) - { - var contentHooks = _services.GetHooks(agent.Id); - - // Before chat completion hook - foreach (var hook in contentHooks) - { - await hook.BeforeGenerating(agent, conversations); - } - - var client = ProviderHelper.GetPalmClient(Provider, _model, _services); - var (prompt, messages, hasFunctions) = PrepareOptions(agent, conversations); - - RoleDialogModel msg; - - if (hasFunctions) - { - // use text completion - // var response = client.GenerateTextAsync(prompt, null).Result; - var response = await client.ChatAsync(new PalmChatCompletionRequest - { - Context = prompt, - Messages = messages, - Temperature = 0.1f - }); - - var message = response.Candidates.First(); - - // check if returns function calling - var llmResponse = message.Content.JsonContent(); - - msg = new RoleDialogModel(llmResponse.Role, llmResponse.Content) - { - CurrentAgentId = agent.Id, - FunctionName = llmResponse.FunctionName, - FunctionArgs = JsonSerializer.Serialize(llmResponse.Args), - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - else - { - var response = await client.ChatAsync(messages, context: prompt, examples: null, options: null); - - var message = response.Candidates.First(); - - // check if returns function calling - var llmResponse = message.Content.JsonContent(); - - msg = new RoleDialogModel(llmResponse.Role, llmResponse.Content ?? message.Content) - { - CurrentAgentId = agent.Id, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(msg, new TokenStatsModel - { - Prompt = prompt, - Provider = Provider, - Model = _model - }); - } - - return msg; - } - - private (string, List, bool) PrepareOptions(Agent agent, List conversations) - { - var agentService = _services.GetRequiredService(); - var routing = _services.GetRequiredService(); - var router = routing.Router; - - // Prepare instruction and functions - var renderData = agentService.CollectRenderData(agent); - var (prompt, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData); - if (!string.IsNullOrWhiteSpace(prompt)) - { - renderedInstructions.Add(prompt); - } - - var messages = conversations.Select(c => new PalmChatMessage(c.LlmContent, c.Role == AgentRole.User ? "user" : "AI")) - .ToList(); - - if (!functions.IsNullOrEmpty()) - { - prompt += "\r\n\r\n[Functions] defined in JSON Schema:\r\n"; - prompt += JsonSerializer.Serialize(functions, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }); - - prompt += "\r\n\r\n[Conversations]\r\n"; - foreach (var dialog in conversations) - { - prompt += dialog.Role == AgentRole.Function ? - $"{dialog.Role}: {dialog.FunctionName} => {dialog.LlmContent}\r\n" : - $"{dialog.Role}: {dialog.LlmContent}\r\n"; - } - - prompt += "\r\n\r\n" + router.Templates.FirstOrDefault(x => x.Name == "response_with_function").Content; - - return (prompt, new List - { - new PalmChatMessage("Which function should be used for the next step based on latest user or function response, output your response in JSON:", AgentRole.User), - }, true); - } - - return (prompt, messages, false); - } - - public Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) - { - throw new NotImplementedException(); - } - - public Task GetChatCompletionsStreamingAsync(Agent agent, List conversations) - { - throw new NotImplementedException(); - } - - public void SetModelName(string model) - { - _model = model; - } -} diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Image/ImageCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Image/ImageCompletionProvider.cs new file mode 100644 index 000000000..1ddd71f55 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Image/ImageCompletionProvider.cs @@ -0,0 +1,58 @@ +using System.IO; + +namespace BotSharp.Plugin.GoogleAI.Providers.Image; + +public partial class ImageCompletionProvider : IImageCompletion +{ + protected readonly GoogleAiSettings _settings; + protected readonly IServiceProvider _services; + protected readonly ILogger _logger; + + private const int DEFAULT_IMAGE_COUNT = 1; + private const int IMAGE_COUNT_LIMIT = 5; + + protected string _model; + + public virtual string Provider => "google-ai"; + public string Model => _model; + + public ImageCompletionProvider( + GoogleAiSettings settings, + ILogger logger, + IServiceProvider services) + { + _settings = settings; + _services = services; + _logger = logger; + } + + public void SetModelName(string model) + { + _model = model; + } + + public Task GetImageGeneration(Agent agent, RoleDialogModel message) + { + throw new NotImplementedException(); + } + + public Task GetImageVariation(Agent agent, RoleDialogModel message, Stream image, string imageFileName) + { + throw new NotImplementedException(); + } + + public Task GetImageEdits(Agent agent, RoleDialogModel message, Stream image, string imageFileName) + { + throw new NotImplementedException(); + } + + public Task GetImageEdits(Agent agent, RoleDialogModel message, Stream image, string imageFileName, Stream mask, string maskFileName) + { + throw new NotImplementedException(); + } + + public Task GetImageComposition(Agent agent, RoleDialogModel message, Stream[] images, string[] imageFileNames) + { + throw new NotImplementedException(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/ProviderHelper.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/ProviderHelper.cs index 7b6c25ea0..1f907c2bd 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/ProviderHelper.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/ProviderHelper.cs @@ -1,5 +1,3 @@ -using LLMSharp.Google.Palm; - namespace BotSharp.Plugin.GoogleAi.Providers; public static class ProviderHelper @@ -19,12 +17,4 @@ public static GenerativeAI.GoogleAi GetGeminiClient(string provider, string mode return new GenerativeAI.GoogleAi(aiSettings.Gemini.ApiKey); } } - - public static GooglePalmClient GetPalmClient(string provider, string model, IServiceProvider services) - { - var settingsService = services.GetRequiredService(); - var settings = settingsService.GetSetting(provider, model); - var client = new GooglePalmClient(settings.ApiKey); - return client; - } } diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/PalmTextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/PalmTextCompletionProvider.cs deleted file mode 100644 index 14d3ebab9..000000000 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/PalmTextCompletionProvider.cs +++ /dev/null @@ -1,65 +0,0 @@ -using BotSharp.Abstraction.Hooks; - -namespace BotSharp.Plugin.GoogleAi.Providers.Text; - -[Obsolete] -public class PalmTextCompletionProvider : ITextCompletion -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - private readonly ITokenStatistics _tokenStatistics; - - private string _model; - - public string Provider => "google-palm"; - public string Model => _model; - - public PalmTextCompletionProvider( - IServiceProvider services, - ILogger logger, - ITokenStatistics tokenStatistics) - { - _services = services; - _logger = logger; - _tokenStatistics = tokenStatistics; - } - - public async Task GetCompletion(string text, string agentId, string messageId) - { - var contentHooks = _services.GetHooks(agentId); - - // Before completion hook - var agent = new Agent() { Id = agentId }; - var userMessage = new RoleDialogModel(AgentRole.User, text) { MessageId = messageId }; - - foreach (var hook in contentHooks) - { - await hook.BeforeGenerating(agent, new List { userMessage }); - } - - var client = ProviderHelper.GetPalmClient(Provider, _model, _services); - _tokenStatistics.StartTimer(); - var response = await client.GenerateTextAsync(text, null); - _tokenStatistics.StopTimer(); - - var message = response.Candidates.First(); - var completion = message.Output.Trim(); - - // After completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(new RoleDialogModel(AgentRole.Assistant, completion), new TokenStatsModel - { - Prompt = text, - Provider = Provider - }); - } - - return completion; - } - - public void SetModelName(string model) - { - _model = model; - } -} diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/GeminiTextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/TextCompletionProvider.cs similarity index 88% rename from src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/GeminiTextCompletionProvider.cs rename to src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/TextCompletionProvider.cs index abc12c3e3..6a838d2f6 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/GeminiTextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Text/TextCompletionProvider.cs @@ -1,16 +1,13 @@ -using BotSharp.Abstraction.Agents.Enums; -using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Loggers; using GenerativeAI; using GenerativeAI.Core; namespace BotSharp.Plugin.GoogleAi.Providers.Text; -public class GeminiTextCompletionProvider : ITextCompletion +public class TextCompletionProvider : ITextCompletion { private readonly IServiceProvider _services; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ITokenStatistics _tokenStatistics; private string _model; @@ -18,10 +15,10 @@ public class GeminiTextCompletionProvider : ITextCompletion public string Model => _model; private GoogleAiSettings _settings; - public GeminiTextCompletionProvider( + public TextCompletionProvider( IServiceProvider services, GoogleAiSettings googleSettings, - ILogger logger, + ILogger logger, ITokenStatistics tokenStatistics) { _settings = googleSettings; diff --git a/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/ComposeImageFn.cs b/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/ComposeImageFn.cs index 4c1544e09..0097801c5 100644 --- a/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/ComposeImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/ComposeImageFn.cs @@ -11,7 +11,6 @@ public class ComposeImageFn : IFunctionCallback private readonly ILogger _logger; private readonly ImageHandlerSettings _settings; - private Agent _agent; private string _conversationId; private string _messageId; @@ -29,22 +28,23 @@ public async Task Execute(RoleDialogModel message) { var args = JsonSerializer.Deserialize(message.FunctionArgs); var descrpition = args?.UserRequest ?? string.Empty; - await Init(message); + + var agentService = _services.GetRequiredService(); + var agent = await agentService.GetAgent(message.CurrentAgentId); + + Init(message); SetImageOptions(); var image = await SelectImage(descrpition); - var response = await GetImageEditGeneration(message, descrpition, image); + var response = await GetImageEditGeneration(agent, message, descrpition, image); message.Content = response; message.StopCompletion = true; return true; } - private async Task Init(RoleDialogModel message) + private void Init(RoleDialogModel message) { - var agentService = _services.GetRequiredService(); var convService = _services.GetRequiredService(); - - _agent = await agentService.GetAgent(message.CurrentAgentId); _conversationId = convService.ConversationId; _messageId = message.MessageId; } @@ -74,7 +74,7 @@ private void SetImageOptions() return selecteds?.FirstOrDefault(); } - private async Task GetImageEditGeneration(RoleDialogModel message, string description, MessageFileModel? image) + private async Task GetImageEditGeneration(Agent agent, RoleDialogModel message, string description, MessageFileModel? image) { if (image == null) { @@ -83,15 +83,10 @@ private async Task GetImageEditGeneration(RoleDialogModel message, strin try { - var (provider, model) = GetLlmProviderModel(); + var (provider, model) = GetLlmProviderModel(agent); var completion = CompletionProvider.GetImageCompletion(_services, provider: provider, model: model); var text = !string.IsNullOrWhiteSpace(description) ? description : message.Content; var dialog = RoleDialogModel.From(message, AgentRole.User, text); - var agent = new Agent - { - Id = _agent?.Id ?? BuiltInAgentId.UtilityAssistant, - Name = _agent?.Name ?? "Utility Assistant" - }; var fileStorage = _services.GetRequiredService(); var fileBinary = fileStorage.GetFileBytes(image.FileStorageUrl); @@ -110,7 +105,7 @@ private async Task GetImageEditGeneration(RoleDialogModel message, strin return response.Content; } - return await GetImageEditResponse(description, defaultContent: null); + return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description, savedFiles); } catch (Exception ex) { @@ -120,43 +115,19 @@ private async Task GetImageEditGeneration(RoleDialogModel message, strin } } - private async Task GetImageEditResponse(string description, string? defaultContent) - { - if (defaultContent != null) - { - return defaultContent; - } - - var llmConfig = _agent.LlmConfig; - var agent = new Agent - { - Id = _agent?.Id ?? BuiltInAgentId.UtilityAssistant, - Name = _agent?.Name ?? "Utility Assistant", - LlmConfig = new AgentLlmConfig - { - Provider = llmConfig?.Provider ?? "openai", - Model = llmConfig?.Model ?? "gpt-5-mini", - MaxOutputTokens = llmConfig?.MaxOutputTokens, - ReasoningEffortLevel = llmConfig?.ReasoningEffortLevel - } - }; - - return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description); - } - - private (string, string) GetLlmProviderModel() + private (string, string) GetLlmProviderModel(Agent agent) { - var state = _services.GetRequiredService(); - var llmProviderService = _services.GetRequiredService(); - - var provider = state.GetState("image_edit_llm_provider"); - var model = state.GetState("image_edit_llm_provider"); + var provider = agent?.LlmConfig?.ImageComposition?.Provider; + var model = agent?.LlmConfig?.ImageComposition?.Model; if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) { return (provider, model); } + provider = _settings?.Composition?.Provider; + model = _settings?.Composition?.Model; + if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) { return (provider, model); @@ -191,7 +162,7 @@ private IEnumerable SaveGeneratedImage(ImageGeneration? image) private async Task ConvertImageToPngWithRgba(BinaryData binaryFile) { - var provider = _settings?.Edit?.ImageConverter?.Provider; + var provider = _settings?.Composition?.ImageConverter?.Provider; var converter = _services.GetServices().FirstOrDefault(x => x.Provider == provider); if (converter == null) { diff --git a/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/EditImageFn.cs b/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/EditImageFn.cs index 8e73d639c..59e7de286 100644 --- a/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/EditImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/EditImageFn.cs @@ -32,7 +32,7 @@ public async Task Execute(RoleDialogModel message) var agentService = _services.GetRequiredService(); var agent = await agentService.GetAgent(message.CurrentAgentId); - await Init(message); + Init(message); SetImageOptions(); var image = await SelectImage(descrpition); @@ -42,10 +42,9 @@ public async Task Execute(RoleDialogModel message) return true; } - private async Task Init(RoleDialogModel message) + private void Init(RoleDialogModel message) { var convService = _services.GetRequiredService(); - _conversationId = convService.ConversationId; _messageId = message.MessageId; } @@ -108,7 +107,7 @@ private async Task GetImageEdit(Agent agent, RoleDialogModel message, st return response.Content; } - return await GetImageEditResponse(agent, description); + return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description, savedFiles); } catch (Exception ex) { @@ -118,23 +117,18 @@ private async Task GetImageEdit(Agent agent, RoleDialogModel message, st } } - private async Task GetImageEditResponse(Agent agent, string description) - { - return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description); - } - private (string, string) GetLlmProviderModel(Agent agent) { - var provider = agent?.LlmConfig?.ImageEdit?.Provider; - var model = agent?.LlmConfig?.ImageEdit?.Model; + var provider = agent?.LlmConfig?.ImageComposition?.Provider; + var model = agent?.LlmConfig?.ImageComposition?.Model; if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) { return (provider, model); } - provider = _settings?.Edit?.Provider; - model = _settings?.Edit?.Model; + provider = _settings?.Composition?.Provider; + model = _settings?.Composition?.Model; if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) { @@ -170,7 +164,7 @@ private IEnumerable SaveGeneratedImage(ImageGeneration? image) private async Task ConvertImageToPngWithRgba(BinaryData binaryFile) { - var provider = _settings?.Edit?.ImageConverter?.Provider; + var provider = _settings?.Composition?.ImageConverter?.Provider; var converter = _services.GetServices().FirstOrDefault(x => x.Provider == provider); if (converter == null) { diff --git a/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/GenerateImageFn.cs b/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/GenerateImageFn.cs index 12fa23006..14a463d85 100644 --- a/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/GenerateImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.ImageHandler/Functions/GenerateImageFn.cs @@ -68,7 +68,7 @@ private async Task GetImageGeneration(Agent agent, RoleDialogModel messa return result.Content; } - return await GetImageGenerationResponse(agent, description); + return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description, savedFiles); } catch (Exception ex) { @@ -78,23 +78,18 @@ private async Task GetImageGeneration(Agent agent, RoleDialogModel messa } } - private async Task GetImageGenerationResponse(Agent agent, string description) - { - return await AiResponseHelper.GetImageGenerationResponse(_services, agent, description); - } - private (string, string) GetLlmProviderModel(Agent agent) { - var provider = agent?.LlmConfig?.ImageGeneration?.Provider; - var model = agent?.LlmConfig?.ImageGeneration?.Model; + var provider = agent?.LlmConfig?.ImageComposition?.Provider; + var model = agent?.LlmConfig?.ImageComposition?.Model; if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) { return (provider, model); } - provider = _settings?.Generation?.Provider; - model = _settings?.Generation?.Model; + provider = _settings?.Composition?.Provider; + model = _settings?.Composition?.Model; if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) { diff --git a/src/Plugins/BotSharp.Plugin.ImageHandler/Helpers/AiResponseHelper.cs b/src/Plugins/BotSharp.Plugin.ImageHandler/Helpers/AiResponseHelper.cs index 3c49743c7..ce999e753 100644 --- a/src/Plugins/BotSharp.Plugin.ImageHandler/Helpers/AiResponseHelper.cs +++ b/src/Plugins/BotSharp.Plugin.ImageHandler/Helpers/AiResponseHelper.cs @@ -2,7 +2,19 @@ namespace BotSharp.Plugin.ImageHandler.Helpers; internal static class AiResponseHelper { - internal static string GetDefaultResponse(IEnumerable files) + internal static async Task GetImageGenerationResponse(IServiceProvider services, Agent agent, string description, IEnumerable? files = null) + { + var text = $"Please generate a user-friendly response from the following description to " + + $"inform user that you have completed the required image: {description}"; + + var provider = agent?.LlmConfig?.Provider ?? "openai"; + var model = agent?.LlmConfig?.Model ?? "gpt-4o-mini"; + var completion = CompletionProvider.GetChatCompletion(services, provider: provider, model: model); + var response = await completion.GetChatCompletions(agent, [new RoleDialogModel(AgentRole.User, text)]); + return response.Content.IfNullOrEmptyAs(GetDefaultResponse(files)) ?? string.Empty; + } + + internal static string GetDefaultResponse(IEnumerable? files) { if (files.IsNullOrEmpty()) { @@ -16,16 +28,4 @@ internal static string GetDefaultResponse(IEnumerable files) return $"Here is the image you asked for: {string.Join(", ", files)}"; } - - internal static async Task GetImageGenerationResponse(IServiceProvider services, Agent agent, string description) - { - var text = $"Please generate a user-friendly response from the following description to " + - $"inform user that you have completed the required image: {description}"; - - var provider = agent?.LlmConfig?.Provider ?? "openai"; - var model = agent?.LlmConfig?.Model ?? "gpt-4o-mini"; - var completion = CompletionProvider.GetChatCompletion(services, provider: provider, model: model); - var response = await completion.GetChatCompletions(agent, [new RoleDialogModel(AgentRole.User, text)]); - return response.Content; - } } diff --git a/src/Plugins/BotSharp.Plugin.ImageHandler/Settings/ImageHandlerSettings.cs b/src/Plugins/BotSharp.Plugin.ImageHandler/Settings/ImageHandlerSettings.cs index 81a11c8d5..bdf188d6f 100644 --- a/src/Plugins/BotSharp.Plugin.ImageHandler/Settings/ImageHandlerSettings.cs +++ b/src/Plugins/BotSharp.Plugin.ImageHandler/Settings/ImageHandlerSettings.cs @@ -5,8 +5,7 @@ namespace BotSharp.Plugin.ImageHandler.Settings; public class ImageHandlerSettings { public ImageReadSettings? Reading { get; set; } - public ImageGenerationSettings? Generation { get; set; } - public ImageEditSettings? Edit { get; set; } + public ImageCompositionSettings? Composition { get; set; } } public class ImageReadSettings : LlmProviderModel @@ -14,12 +13,7 @@ public class ImageReadSettings : LlmProviderModel public string? ImageDetailLevel { get; set; } } -public class ImageGenerationSettings : LlmProviderModel -{ - -} - -public class ImageEditSettings : LlmProviderModel +public class ImageCompositionSettings : LlmProviderModel { public SettingBase? ImageConverter { get; set; } -} \ No newline at end of file +} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs index e1bebc582..2384d9ebb 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs @@ -30,7 +30,6 @@ public static ITextEmbedding GetTextEmbeddingSetting(IServiceProvider services, // Set up text embedding var embedding = services.GetServices().FirstOrDefault(x => x.Provider == provider); - if (dimension <= 0) { dimension = GetLlmTextEmbeddingDimension(services, provider, model); diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs index 207879311..24fc5546b 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files.Models; +using BotSharp.Abstraction.Files.Proccessors; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Knowledges.Filters; using BotSharp.Abstraction.Knowledges.Helpers; @@ -7,19 +8,20 @@ using BotSharp.Abstraction.Knowledges.Responses; using BotSharp.Abstraction.VectorStorage.Enums; using System.Net.Http; -using System.Net.Mime; namespace BotSharp.Plugin.KnowledgeBase.Services; public partial class KnowledgeService { - public async Task UploadDocumentsToKnowledge(string collectionName, - IEnumerable files, ChunkOption? option = null) + public async Task UploadDocumentsToKnowledge( + string collectionName, + IEnumerable files, + KnowledgeDocOptions? options = null) { var res = new UploadKnowledgeResponse { Success = [], - Failed = files?.Select(x => x.FileName) ?? new List() + Failed = files?.Select(x => x.FileName) ?? [] }; if (string.IsNullOrWhiteSpace(collectionName) || files.IsNullOrEmpty()) @@ -33,14 +35,13 @@ public async Task UploadDocumentsToKnowledge(string col return res; } - var db = _services.GetRequiredService(); var fileStoreage = _services.GetRequiredService(); - var userId = await GetUserId(); var vectorStoreProvider = _settings.VectorDb.Provider; + var knowledgeFiles = new List(); var successFiles = new List(); var failedFiles = new List(); - foreach (var file in files) + foreach (var file in files!) { if (string.IsNullOrWhiteSpace(file.FileData) && string.IsNullOrWhiteSpace(file.FileUrl)) @@ -52,52 +53,50 @@ public async Task UploadDocumentsToKnowledge(string col { // Get document info var (contentType, binary) = await GetFileInfo(file); - var contents = await GetFileContent(contentType, binary, option ?? ChunkOption.Default()); - - // Save document - var fileId = Guid.NewGuid(); - var saved = SaveDocument(collectionName, vectorStoreProvider, fileId, file.FileName, binary); - if (!saved) + var fileData = new FileBinaryDataModel + { + FileName = file.FileName, + ContentType = contentType, + FileBinaryData = binary + }; + var knowledges = await GetFileKnowledge(fileData, options); + if (knowledges.IsNullOrEmpty()) { failedFiles.Add(file.FileName); continue; } - // Save to vector db + var fileId = Guid.NewGuid(); var payload = new Dictionary() { - { KnowledgePayloadName.DataSource, VectorPayloadValue.BuildStringValue(VectorDataSource.File) }, - { KnowledgePayloadName.FileId, VectorPayloadValue.BuildStringValue(fileId.ToString()) }, - { KnowledgePayloadName.FileName, VectorPayloadValue.BuildStringValue(file.FileName) }, - { KnowledgePayloadName.FileSource, VectorPayloadValue.BuildStringValue(file.FileSource) } + { KnowledgePayloadName.DataSource, (VectorPayloadValue)VectorDataSource.File }, + { KnowledgePayloadName.FileId, (VectorPayloadValue)fileId.ToString() }, + { KnowledgePayloadName.FileName, (VectorPayloadValue)file.FileName }, + { KnowledgePayloadName.FileSource, (VectorPayloadValue)file.FileSource } }; if (!string.IsNullOrWhiteSpace(file.FileUrl)) { - payload[KnowledgePayloadName.FileUrl] = VectorPayloadValue.BuildStringValue(file.FileUrl); + payload[KnowledgePayloadName.FileUrl] = (VectorPayloadValue)file.FileUrl; } - var dataIds = await SaveToVectorDb(collectionName, contents, payload); - if (!dataIds.IsNullOrEmpty()) + foreach (var kg in knowledges) { - db.SaveKnolwedgeBaseFileMeta(new KnowledgeDocMetaData + var kgPayload = new Dictionary(kg.Payload ?? new Dictionary()); + foreach (var pair in payload) { - Collection = collectionName, - FileId = fileId, - FileName = file.FileName, - FileSource = file.FileSource, - ContentType = contentType, - VectorStoreProvider = vectorStoreProvider, - VectorDataIds = dataIds, - CreateDate = DateTime.UtcNow, - CreateUserId = userId - }); - successFiles.Add(file.FileName); + kgPayload[pair.Key] = pair.Value; + } + kg.Payload = kgPayload; } - else + + knowledgeFiles.Add(new() { - failedFiles.Add(file.FileName); - } + FileId = fileId, + FileData = fileData, + FileSource = VectorDataSource.File, + FileKnowledges = knowledges + }); } catch (Exception ex) { @@ -107,10 +106,11 @@ public async Task UploadDocumentsToKnowledge(string col } } + var response = await HandleKnowledgeFiles(collectionName, vectorStoreProvider, knowledgeFiles, saveFile: true); return new UploadKnowledgeResponse { - Success = successFiles, - Failed = failedFiles + Success = successFiles.Concat(response.Success).Distinct(), + Failed = failedFiles.Concat(response.Failed).Distinct() }; } @@ -136,39 +136,37 @@ public async Task ImportDocumentContentToKnowledge(string collectionName, var fileId = Guid.NewGuid(); var contentType = FileUtility.GetFileContentType(fileName); - var innerPayload = new Dictionary(); - if (payload != null) - { - foreach (var item in payload) - { - innerPayload[item.Key] = item.Value; - } - } - - innerPayload[KnowledgePayloadName.DataSource] = VectorPayloadValue.BuildStringValue(VectorDataSource.File); - innerPayload[KnowledgePayloadName.FileId] = VectorPayloadValue.BuildStringValue(fileId.ToString()); - innerPayload[KnowledgePayloadName.FileName] = VectorPayloadValue.BuildStringValue(fileName); - innerPayload[KnowledgePayloadName.FileSource] = VectorPayloadValue.BuildStringValue(fileSource); + var innerPayload = new Dictionary(payload ?? []); + innerPayload[KnowledgePayloadName.DataSource] = (VectorPayloadValue)VectorDataSource.File; + innerPayload[KnowledgePayloadName.FileId] = (VectorPayloadValue)fileId.ToString(); + innerPayload[KnowledgePayloadName.FileName] = (VectorPayloadValue)fileName; + innerPayload[KnowledgePayloadName.FileSource] = (VectorPayloadValue)fileSource; if (!string.IsNullOrWhiteSpace(refData?.Url)) { - innerPayload[KnowledgePayloadName.FileUrl] = VectorPayloadValue.BuildStringValue(refData.Url); + innerPayload[KnowledgePayloadName.FileUrl] = (VectorPayloadValue)refData.Url; } - var dataIds = await SaveToVectorDb(collectionName, contents, innerPayload); - db.SaveKnolwedgeBaseFileMeta(new KnowledgeDocMetaData + var kgFile = new FileKnowledgeWrapper { - Collection = collectionName, FileId = fileId, - FileName = fileName, FileSource = fileSource, - ContentType = contentType, - VectorStoreProvider = vectorStoreProvider, - VectorDataIds = dataIds, - RefData = refData, - CreateDate = DateTime.UtcNow, - CreateUserId = userId - }); + FileData = new() + { + FileName = fileName, + ContentType = contentType, + FileBinaryData = BinaryData.Empty + }, + FileKnowledges = new List + { + new() + { + Contents = contents, + Payload = innerPayload + } + } + }; + await HandleKnowledgeFiles(collectionName, vectorStoreProvider, [kgFile], saveFile: false); return true; } catch (Exception ex) @@ -338,7 +336,6 @@ public async Task GetKnowledgeDocumentBinaryData(string col } - #region Private methods /// /// Get file content type and file bytes @@ -370,20 +367,16 @@ public async Task GetKnowledgeDocumentBinaryData(string col } #region Read doc content - private async Task> GetFileContent(string contentType, BinaryData binary, ChunkOption option) + private async Task> GetFileKnowledge(FileBinaryDataModel file, KnowledgeDocOptions? options) { - IEnumerable results = new List(); - - if (contentType.IsEqualTo(MediaTypeNames.Text.Plain)) + var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(options?.Processor)); + if (processor == null) { - results = await ReadTxt(binary, option); + return Enumerable.Empty(); } - else if (contentType.IsEqualTo(MediaTypeNames.Application.Pdf)) - { - results = await ReadPdf(binary); - } - - return results; + + var response = await processor.GetFileKnowledgeAsync(file, options: options); + return response?.Knowledges ?? []; } private async Task> ReadTxt(BinaryData binary, ChunkOption option) @@ -398,11 +391,6 @@ private async Task> ReadTxt(BinaryData binary, ChunkOption o var lines = TextChopper.Chop(content, option); return lines; } - - private async Task> ReadPdf(BinaryData binary) - { - return Enumerable.Empty(); - } #endregion @@ -427,16 +415,96 @@ private async Task> SaveToVectorDb(string collectionName, IE for (int i = 0; i < contents.Count(); i++) { var content = contents.ElementAt(i); - var vector = await textEmbedding.GetVectorAsync(content); - var dataId = Guid.NewGuid(); - var saved = await vectorDb.Upsert(collectionName, dataId, vector, content, payload ?? []); - if (!saved) continue; + try + { + var vector = await textEmbedding.GetVectorAsync(content); + var dataId = Guid.NewGuid(); + var saved = await vectorDb.Upsert(collectionName, dataId, vector, content, payload ?? []); + + if (!saved) + { + continue; + } - dataIds.Add(dataId.ToString()); + dataIds.Add(dataId.ToString()); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when saving file knowledge to vector db collection {collectionName}. (Content: {content.SubstringMax(20)})"); + } } return dataIds; } + + private async Task HandleKnowledgeFiles( + string collectionName, + string vectorStore, + IEnumerable knowledgeFiles, + bool saveFile = false) + { + if (knowledgeFiles.IsNullOrEmpty()) + { + return new(); + } + + var successFiles = new List(); + var failedFiles = new List(); + var db = _services.GetRequiredService(); + + var userId = await GetUserId(); + foreach (var item in knowledgeFiles) + { + var file = item.FileData; + + // Save document + if (saveFile) + { + var saved = SaveDocument(collectionName, vectorStore, item.FileId, file.FileName, file.FileBinaryData); + if (!saved) + { + _logger.LogWarning($"Failed to save knowledge file: {file.FileName} to collection {collectionName}."); + failedFiles.Add(file.FileName); + continue; + } + } + + // Save to vector db + var dataIds = new List(); + foreach (var kg in item.FileKnowledges) + { + var ids = await SaveToVectorDb(collectionName, kg.Contents, kg.Payload?.ToDictionary()); + dataIds.AddRange(ids); + } + + if (!dataIds.IsNullOrEmpty()) + { + db.SaveKnolwedgeBaseFileMeta(new KnowledgeDocMetaData + { + Collection = collectionName, + FileId = item.FileId, + FileName = file.FileName, + FileSource = item.FileSource ?? VectorDataSource.File, + ContentType = file.ContentType, + VectorStoreProvider = vectorStore, + VectorDataIds = dataIds, + CreateDate = DateTime.UtcNow, + CreateUserId = userId + }); + successFiles.Add(file.FileName); + } + else + { + failedFiles.Add(file.FileName); + } + } + + return new UploadKnowledgeResponse + { + Success = successFiles, + Failed = failedFiles + }; + } #endregion } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs index 5d638f731..d0817c802 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs @@ -2,7 +2,6 @@ using BotSharp.Abstraction.VectorStorage.Enums; using BotSharp.Abstraction.VectorStorage.Filters; using BotSharp.Abstraction.VectorStorage.Options; -using Microsoft.AspNetCore.Http.HttpResults; namespace BotSharp.Plugin.KnowledgeBase.Services; @@ -82,7 +81,10 @@ public async Task> GetVectorCollections(stri }).ToList(); var vectorDb = GetVectorDb(); - if (vectorDb == null) return []; + if (vectorDb == null) + { + return []; + } var dbCollections = await vectorDb.GetCollections(); return configs.Where(x => dbCollections.Contains(x.Name)); } diff --git a/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Embeddings.cs b/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Embeddings.cs index 9d7f357fe..5522e6191 100644 --- a/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Embeddings.cs +++ b/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Embeddings.cs @@ -35,8 +35,8 @@ private IEnumerable ProcessBase(EmbeddingRequestBase requestBody) }; - var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result; - var stream = response.Content.ReadAsStreamAsync().Result; + var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false).GetAwaiter().GetResult(); + var stream = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); byte[] buffer = new byte[8192]; int bytesRead; diff --git a/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Images.cs b/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Images.cs index 22fd81ad2..ee0a1b76e 100644 --- a/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Images.cs +++ b/src/Plugins/BotSharp.Plugin.MetaGLM/Modules/Images.cs @@ -31,8 +31,8 @@ private IEnumerable GenerateBase(ImageRequestBase requestBody) }; - var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result; - var stream = response.Content.ReadAsStreamAsync().Result; + var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false).GetAwaiter().GetResult(); + var stream = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); byte[] buffer = new byte[8192]; int bytesRead; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentCodeScriptDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentCodeScriptDocument.cs index 9a39fb58c..52f4570a1 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentCodeScriptDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentCodeScriptDocument.cs @@ -31,7 +31,9 @@ public static AgentCodeScript ToDomainModel(AgentCodeScriptDocument script) AgentId = script.AgentId, Name = script.Name, Content = script.Content, - ScriptType = script.ScriptType + ScriptType = script.ScriptType, + CreatedTime = script.CreatedTime, + UpdatedTime = script.UpdatedTime }; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoModel.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoModel.cs index 165a590a1..cdd76cb54 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoModel.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentLlmConfigMongoModel.cs @@ -12,8 +12,7 @@ public class AgentLlmConfigMongoModel public int? MaxOutputTokens { get; set; } public string? ReasoningEffortLevel { get; set; } - public LlmImageGenerationConfigMongoModel? ImageGeneration { get; set; } - public LlmImageEditConfigMongoModel? ImageEdit { get; set; } + public LlmImageCompositionConfigMongoModel? ImageComposition { get; set; } public LlmAudioTranscriptionConfigMongoModel? AudioTranscription { get; set; } public LlmRealtimeConfigMongoModel? Realtime { get; set; } @@ -34,8 +33,7 @@ public class AgentLlmConfigMongoModel MaxRecursionDepth = config.MaxRecursionDepth, MaxOutputTokens = config.MaxOutputTokens, ReasoningEffortLevel = config.ReasoningEffortLevel, - ImageGeneration = LlmImageGenerationConfigMongoModel.ToMongoModel(config.ImageGeneration), - ImageEdit = LlmImageEditConfigMongoModel.ToMongoModel(config.ImageEdit), + ImageComposition = LlmImageCompositionConfigMongoModel.ToMongoModel(config.ImageComposition), AudioTranscription = LlmAudioTranscriptionConfigMongoModel.ToMongoModel(config.AudioTranscription), Realtime = LlmRealtimeConfigMongoModel.ToMongoModel(config.Realtime) }; @@ -56,8 +54,7 @@ public class AgentLlmConfigMongoModel MaxRecursionDepth = config.MaxRecursionDepth, MaxOutputTokens = config.MaxOutputTokens, ReasoningEffortLevel = config.ReasoningEffortLevel, - ImageGeneration = LlmImageGenerationConfigMongoModel.ToDomainModel(config.ImageGeneration), - ImageEdit = LlmImageEditConfigMongoModel.ToDomainModel(config.ImageEdit), + ImageComposition = LlmImageCompositionConfigMongoModel.ToDomainModel(config.ImageComposition), AudioTranscription = LlmAudioTranscriptionConfigMongoModel.ToDomainModel(config.AudioTranscription), Realtime = LlmRealtimeConfigMongoModel.ToDomainModel(config.Realtime) }; @@ -65,65 +62,33 @@ public class AgentLlmConfigMongoModel } [BsonIgnoreExtraElements(Inherited = true)] -public class LlmImageGenerationConfigMongoModel : LlmProviderModelMongoModel +public class LlmImageCompositionConfigMongoModel : LlmProviderModelMongoModel { - public static LlmImageGenerationConfig? ToDomainModel(LlmImageGenerationConfigMongoModel? config) + public static LlmImageCompositionConfig? ToDomainModel(LlmImageCompositionConfigMongoModel? config) { if (config == null) { return null; } - return new LlmImageGenerationConfig + return new LlmImageCompositionConfig { Provider = config.Provider, - Model = config.Model, + Model = config.Model }; } - public static LlmImageGenerationConfigMongoModel? ToMongoModel(LlmImageGenerationConfig? config) + public static LlmImageCompositionConfigMongoModel? ToMongoModel(LlmImageCompositionConfig? config) { if (config == null) { return null; } - return new LlmImageGenerationConfigMongoModel + return new LlmImageCompositionConfigMongoModel { Provider = config.Provider, - Model = config.Model, - }; - } -} - -[BsonIgnoreExtraElements(Inherited = true)] -public class LlmImageEditConfigMongoModel : LlmProviderModelMongoModel -{ - public static LlmImageEditConfig? ToDomainModel(LlmImageEditConfigMongoModel? config) - { - if (config == null) - { - return null; - } - - return new LlmImageEditConfig - { - Provider = config.Provider, - Model = config.Model, - }; - } - - public static LlmImageEditConfigMongoModel? ToMongoModel(LlmImageEditConfig? config) - { - if (config == null) - { - return null; - } - - return new LlmImageEditConfigMongoModel - { - Provider = config.Provider, - Model = config.Model, + Model = config.Model }; } } @@ -141,7 +106,7 @@ public class LlmAudioTranscriptionConfigMongoModel : LlmProviderModelMongoModel return new LlmAudioTranscriptionConfig { Provider = config.Provider, - Model = config.Model, + Model = config.Model }; } @@ -155,7 +120,7 @@ public class LlmAudioTranscriptionConfigMongoModel : LlmProviderModelMongoModel return new LlmAudioTranscriptionConfigMongoModel { Provider = config.Provider, - Model = config.Model, + Model = config.Model }; } } @@ -173,7 +138,7 @@ public class LlmRealtimeConfigMongoModel : LlmProviderModelMongoModel return new LlmRealtimeConfig { Provider = config.Provider, - Model = config.Model, + Model = config.Model }; } @@ -187,7 +152,7 @@ public class LlmRealtimeConfigMongoModel : LlmProviderModelMongoModel return new LlmRealtimeConfigMongoModel { Provider = config.Provider, - Model = config.Model, + Model = config.Model }; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index a0d0ccff6..05fed34d7 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -15,7 +15,7 @@ public MongoDbContext(BotSharpDatabaseSettings dbSettings) var mongoDbConnectionString = dbSettings.BotSharpMongoDb; _mongoClient = new MongoClient(mongoDbConnectionString); _mongoDbDatabaseName = GetDatabaseName(mongoDbConnectionString); - _collectionPrefix = dbSettings.TablePrefix.IfNullOrEmptyAs("BotSharp"); + _collectionPrefix = dbSettings.TablePrefix.IfNullOrEmptyAs("BotSharp")!; } private string GetDatabaseName(string mongoDbConnectionString) @@ -61,7 +61,9 @@ private bool CollectionExists(IMongoDatabase database, string collectionName) private IMongoCollection GetCollectionOrCreate(string name) { if (string.IsNullOrWhiteSpace(name)) + { throw new ArgumentException($"The collection {name} cannot be empty."); + } var collectionName = $"{_collectionPrefix}_{name}"; if (!CollectionExists(Database, collectionName)) @@ -74,6 +76,29 @@ private IMongoCollection GetCollectionOrCreate(string name } #region Indexes + private IMongoCollection CreateAgentCodeScriptIndex() + { + var collection = GetCollectionOrCreate("AgentCodeScripts"); + var curIndexes = collection.Indexes.List().ToList().Where(x => x.Contains("name")).Select(x => x["name"].AsString); + + if (!curIndexes.Any(x => x.StartsWith("AgentId"))) + { + CreateIndex(collection, Builders.IndexKeys.Ascending(x => x.AgentId)); + } + + if (!curIndexes.Any(x => x.StartsWith("Name"))) + { + CreateIndex(collection, Builders.IndexKeys.Ascending(x => x.Name)); + } + + if (!curIndexes.Any(x => x.StartsWith("ScriptType"))) + { + CreateIndex(collection, Builders.IndexKeys.Ascending(x => x.ScriptType)); + } + + return collection; + } + private IMongoCollection CreateConversationIndex() { var collection = GetCollectionOrCreate("Conversations"); @@ -81,8 +106,7 @@ private IMongoCollection CreateConversationIndex() var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreatedTime")); if (createTimeIndex == null) { - var indexDef = Builders.IndexKeys.Descending(x => x.CreatedTime); - collection.Indexes.CreateOne(new CreateIndexModel(indexDef)); + CreateIndex(collection, Builders.IndexKeys.Descending(x => x.CreatedTime)); } return collection; } @@ -94,8 +118,7 @@ private IMongoCollection CreateConversationStateIndex var stateIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("States.Key")); if (stateIndex == null) { - var indexDef = Builders.IndexKeys.Ascending("States.Key"); - collection.Indexes.CreateOne(new CreateIndexModel(indexDef)); + CreateIndex(collection, Builders.IndexKeys.Ascending("States.Key")); } return collection; } @@ -107,8 +130,7 @@ private IMongoCollection CreateAgentTaskIndex() var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreatedTime")); if (createTimeIndex == null) { - var indexDef = Builders.IndexKeys.Descending(x => x.CreatedTime); - collection.Indexes.CreateOne(new CreateIndexModel(indexDef)); + CreateIndex(collection, Builders.IndexKeys.Descending(x => x.CreatedTime)); } return collection; } @@ -120,8 +142,7 @@ private IMongoCollection CreateContentLogIndex() var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreatedTime")); if (createTimeIndex == null) { - var indexDef = Builders.IndexKeys.Ascending(x => x.CreatedTime); - collection.Indexes.CreateOne(new CreateIndexModel(indexDef)); + CreateIndex(collection, Builders.IndexKeys.Ascending(x => x.CreatedTime)); } return collection; } @@ -133,8 +154,7 @@ private IMongoCollection CreateStateLogIndex() var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreatedTime")); if (createTimeIndex == null) { - var indexDef = Builders.IndexKeys.Ascending(x => x.CreatedTime); - collection.Indexes.CreateOne(new CreateIndexModel(indexDef)); + CreateIndex(collection, Builders.IndexKeys.Ascending(x => x.CreatedTime)); } return collection; } @@ -146,11 +166,15 @@ private IMongoCollection CreateInstructionLogIndex() var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreatedTime")); if (createTimeIndex == null) { - var indexDef = Builders.IndexKeys.Descending(x => x.CreatedTime); - collection.Indexes.CreateOne(new CreateIndexModel(indexDef)); + CreateIndex(collection, Builders.IndexKeys.Descending(x => x.CreatedTime)); } return collection; } + + private void CreateIndex(IMongoCollection collection, IndexKeysDefinition indexKeyDef, CreateIndexOptions? options = null) where T : MongoBase + { + collection.Indexes.CreateOne(new CreateIndexModel(indexKeyDef, options)); + } #endregion #endregion @@ -161,7 +185,7 @@ public IMongoCollection AgentTasks => CreateAgentTaskIndex(); public IMongoCollection AgentCodeScripts - => GetCollectionOrCreate("AgentCodeScripts"); + => CreateAgentCodeScriptIndex(); public IMongoCollection Conversations => CreateConversationIndex(); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index 044a52b13..5e3a0bb35 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Agents.Options; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Routing.Models; @@ -9,7 +10,10 @@ public partial class MongoRepository { public void UpdateAgent(Agent agent, AgentField field) { - if (agent == null || string.IsNullOrWhiteSpace(agent.Id)) return; + if (agent == null || string.IsNullOrWhiteSpace(agent.Id)) + { + return; + } switch (field) { @@ -90,7 +94,10 @@ public void UpdateAgent(Agent agent, AgentField field) #region Update Agent Fields private void UpdateAgentName(string agentId, string name) { - if (string.IsNullOrWhiteSpace(name)) return; + if (string.IsNullOrWhiteSpace(name)) + { + return; + } var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update @@ -102,7 +109,10 @@ private void UpdateAgentName(string agentId, string name) private void UpdateAgentDescription(string agentId, string description) { - if (string.IsNullOrWhiteSpace(description)) return; + if (string.IsNullOrWhiteSpace(description)) + { + return; + } var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update @@ -174,7 +184,10 @@ private void UpdateAgentInheritAgentId(string agentId, string? inheritAgentId) private void UpdateAgentProfiles(string agentId, List profiles) { - if (profiles == null) return; + if (profiles == null) + { + return; + } var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update @@ -186,7 +199,10 @@ private void UpdateAgentProfiles(string agentId, List profiles) public bool UpdateAgentLabels(string agentId, List labels) { - if (labels == null) return false; + if (labels == null) + { + return false; + } var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update @@ -199,7 +215,10 @@ public bool UpdateAgentLabels(string agentId, List labels) private void UpdateAgentRoutingRules(string agentId, List rules) { - if (rules == null) return; + if (rules == null) + { + return; + } var ruleElements = rules.Select(x => RoutingRuleMongoElement.ToMongoElement(x)).ToList(); var filter = Builders.Filter.Eq(x => x.Id, agentId); @@ -226,7 +245,10 @@ private void UpdateAgentInstructions(string agentId, string instruction, List functions) { - if (functions == null) return; + if (functions == null) + { + return; + } var functionsToUpdate = functions.Select(f => FunctionDefMongoElement.ToMongoElement(f)).ToList(); var filter = Builders.Filter.Eq(x => x.Id, agentId); @@ -239,7 +261,10 @@ private void UpdateAgentFunctions(string agentId, List functions) private void UpdateAgentTemplates(string agentId, List templates) { - if (templates == null) return; + if (templates == null) + { + return; + } var templatesToUpdate = templates.Select(t => AgentTemplateMongoElement.ToMongoElement(t)).ToList(); var filter = Builders.Filter.Eq(x => x.Id, agentId); @@ -252,7 +277,10 @@ private void UpdateAgentTemplates(string agentId, List templates) private void UpdateAgentResponses(string agentId, List responses) { - if (responses == null || string.IsNullOrWhiteSpace(agentId)) return; + if (responses == null || string.IsNullOrWhiteSpace(agentId)) + { + return; + } var responsesToUpdate = responses.Select(r => AgentResponseMongoElement.ToMongoElement(r)).ToList(); var filter = Builders.Filter.Eq(x => x.Id, agentId); @@ -265,7 +293,10 @@ private void UpdateAgentResponses(string agentId, List responses) private void UpdateAgentSamples(string agentId, List samples) { - if (samples == null) return; + if (samples == null) + { + return; + } var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update @@ -277,7 +308,10 @@ private void UpdateAgentSamples(string agentId, List samples) private void UpdateAgentUtilities(string agentId, bool mergeUtility, List utilities) { - if (utilities == null) return; + if (utilities == null) + { + return; + } var elements = utilities?.Select(x => AgentUtilityMongoElement.ToMongoElement(x))?.ToList() ?? []; @@ -292,7 +326,10 @@ private void UpdateAgentUtilities(string agentId, bool mergeUtility, List mcps) { - if (mcps == null) return; + if (mcps == null) + { + return; + } var elements = mcps?.Select(x => AgentMcpToolMongoElement.ToMongoElement(x))?.ToList() ?? []; @@ -305,7 +342,10 @@ private void UpdateAgentMcpTools(string agentId, List mcps) } private void UpdateAgentKnowledgeBases(string agentId, List knowledgeBases) { - if (knowledgeBases == null) return; + if (knowledgeBases == null) + { + return; + } var elements = knowledgeBases?.Select(x => AgentKnowledgeBaseMongoElement.ToMongoElement(x))?.ToList() ?? []; @@ -319,7 +359,10 @@ private void UpdateAgentKnowledgeBases(string agentId, List private void UpdateAgentRules(string agentId, List rules) { - if (rules == null) return; + if (rules == null) + { + return; + } var elements = rules?.Select(x => AgentRuleMongoElement.ToMongoElement(x))?.ToList() ?? []; @@ -389,7 +432,10 @@ private void UpdateAgentAllFields(Agent agent) public Agent? GetAgent(string agentId, bool basicsOnly = false) { var agent = _dc.Agents.AsQueryable().FirstOrDefault(x => x.Id == agentId); - if (agent == null) return null; + if (agent == null) + { + return null; + } return TransformAgentDocument(agent); } @@ -451,7 +497,10 @@ join u in _dc.Users.AsQueryable() on ua.UserId equals u.Id where ua.UserId == userId || u.ExternalId == userId select ua).ToList(); - if (found.IsNullOrEmpty()) return []; + if (found.IsNullOrEmpty()) + { + return []; + } var res = found.Select(x => new UserAgent { @@ -468,7 +517,10 @@ join u in _dc.Users.AsQueryable() on ua.UserId equals u.Id foreach (var item in res) { var agent = agents.FirstOrDefault(x => x.Id == item.AgentId); - if (agent == null) continue; + if (agent == null) + { + continue; + } item.Agent = agent; } @@ -480,7 +532,10 @@ public List GetAgentResponses(string agentId, string prefix, string inte { var responses = new List(); var agent = _dc.Agents.AsQueryable().FirstOrDefault(x => x.Id == agentId); - if (agent == null) return responses; + if (agent == null) + { + return responses; + } return agent.Responses.Where(x => x.Prefix == prefix && x.Intent == intent).Select(x => x.Content).ToList(); } @@ -488,21 +543,33 @@ public List GetAgentResponses(string agentId, string prefix, string inte public string GetAgentTemplate(string agentId, string templateName) { var agent = _dc.Agents.AsQueryable().FirstOrDefault(x => x.Id == agentId); - if (agent == null) return string.Empty; + if (agent == null) + { + return string.Empty; + } return agent.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(templateName))?.Content ?? string.Empty; } public bool PatchAgentTemplate(string agentId, AgentTemplate template) { - if (string.IsNullOrEmpty(agentId) || template == null) return false; + if (string.IsNullOrEmpty(agentId) || template == null) + { + return false; + } var filter = Builders.Filter.Eq(x => x.Id, agentId); var agent = _dc.Agents.Find(filter).FirstOrDefault(); - if (agent == null || agent.Templates.IsNullOrEmpty()) return false; + if (agent == null || agent.Templates.IsNullOrEmpty()) + { + return false; + } var foundTemplate = agent.Templates.FirstOrDefault(x => x.Name.IsEqualTo(template.Name)); - if (foundTemplate == null) return false; + if (foundTemplate == null) + { + return false; + } foundTemplate.Content = template.Content; var update = Builders.Update.Set(x => x.Templates, agent.Templates); @@ -512,11 +579,17 @@ public bool PatchAgentTemplate(string agentId, AgentTemplate template) public bool AppendAgentLabels(string agentId, List labels) { - if (labels.IsNullOrEmpty()) return false; + if (labels.IsNullOrEmpty()) + { + return false; + } var filter = Builders.Filter.Eq(x => x.Id, agentId); var agent = _dc.Agents.Find(filter).FirstOrDefault(); - if (agent == null) return false; + if (agent == null) + { + return false; + } var prevLabels = agent.Labels ?? []; var curLabels = prevLabels.Concat(labels).Distinct().ToList(); @@ -530,7 +603,10 @@ public bool AppendAgentLabels(string agentId, List labels) public void BulkInsertAgents(List agents) { - if (agents.IsNullOrEmpty()) return; + if (agents.IsNullOrEmpty()) + { + return; + } var agentDocs = agents.Select(x => new AgentDocument { @@ -569,10 +645,16 @@ public void BulkInsertAgents(List agents) public void BulkInsertUserAgents(List userAgents) { - if (userAgents.IsNullOrEmpty()) return; + if (userAgents.IsNullOrEmpty()) + { + return; + } var filtered = userAgents.Where(x => !string.IsNullOrEmpty(x.UserId) && !string.IsNullOrEmpty(x.AgentId)).ToList(); - if (filtered.IsNullOrEmpty()) return; + if (filtered.IsNullOrEmpty()) + { + return; + } var userAgentDocs = filtered.Select(x => new UserAgentDocument { @@ -604,22 +686,33 @@ public bool DeleteAgents() } } - public bool DeleteAgent(string agentId) + public bool DeleteAgent(string agentId, AgentDeleteOptions? options = null) { try { - if (string.IsNullOrEmpty(agentId)) return false; + if (string.IsNullOrEmpty(agentId)) + { + return false; + } + + if (options == null || options.DeleteUserAgents) + { + var userAgentFilter = Builders.Filter.Eq(x => x.AgentId, agentId); + _dc.UserAgents.DeleteMany(userAgentFilter); + } + + if (options == null || options.DeleteRoleAgents) + { + var roleAgentFilter = Builders.Filter.Eq(x => x.AgentId, agentId); + _dc.RoleAgents.DeleteMany(roleAgentFilter); + } + + DeleteAgentCodeScripts(agentId, options?.ToDeleteCodeScripts); - var agentFilter = Builders.Filter.Eq(x => x.Id, agentId); - var userAgentFilter = Builders.Filter.Eq(x => x.AgentId, agentId); - var roleAgentFilter = Builders.Filter.Eq(x => x.AgentId, agentId); var agentTaskFilter = Builders.Filter.Eq(x => x.AgentId, agentId); - var agentCodeFilter = Builders.Filter.Eq(x => x.AgentId, agentId); - - _dc.UserAgents.DeleteMany(userAgentFilter); - _dc.RoleAgents.DeleteMany(roleAgentFilter); _dc.AgentTasks.DeleteMany(agentTaskFilter); - _dc.AgentCodeScripts.DeleteMany(agentCodeFilter); + + var agentFilter = Builders.Filter.Eq(x => x.Id, agentId); _dc.Agents.DeleteOne(agentFilter); return true; } @@ -631,7 +724,10 @@ public bool DeleteAgent(string agentId) private Agent TransformAgentDocument(AgentDocument? agentDoc) { - if (agentDoc == null) return new Agent(); + if (agentDoc == null) + { + return new Agent(); + } return new Agent { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentCodeScript.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentCodeScript.cs index fa2830f2a..cc876e465 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentCodeScript.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentCodeScript.cs @@ -1,6 +1,5 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Repositories.Models; using BotSharp.Abstraction.Repositories.Options; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -36,7 +35,7 @@ public List GetAgentCodeScripts(string agentId, AgentCodeScript return found.Select(x => AgentCodeScriptDocument.ToDomainModel(x)).ToList(); } - public string? GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) + public AgentCodeScript? GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) { if (string.IsNullOrWhiteSpace(agentId) || string.IsNullOrWhiteSpace(scriptName) @@ -54,7 +53,7 @@ public List GetAgentCodeScripts(string agentId, AgentCodeScript }; var found = _dc.AgentCodeScripts.Find(builder.And(filters)).FirstOrDefault(); - return found?.Content; + return found != null ? AgentCodeScriptDocument.ToDomainModel(found) : null; } public bool UpdateAgentCodeScripts(string agentId, List scripts, AgentCodeScriptDbUpdateOptions? options = null) @@ -66,20 +65,31 @@ public bool UpdateAgentCodeScripts(string agentId, List scripts var builder = Builders.Filter; var ops = scripts.Where(x => !string.IsNullOrWhiteSpace(x.Name)) - .Select(x => new UpdateOneModel( - builder.And(new List> - { - builder.Eq(y => y.AgentId, agentId), - builder.Eq(y => y.Name, x.Name), - builder.Eq(y => y.ScriptType, x.ScriptType) - }), - Builders.Update.Set(y => y.Content, x.Content) - .Set(x => x.UpdatedTime, DateTime.UtcNow) - ) { IsUpsert = options?.IsUpsert ?? false }) + .Select(x => + { + var updateBuilder = Builders.Update + .Set(y => y.Content, x.Content) + .Set(y => y.UpdatedTime, DateTime.UtcNow) + .SetOnInsert(y => y.Id, Guid.NewGuid().ToString()) + .SetOnInsert(y => y.AgentId, agentId) + .SetOnInsert(y => y.Name, x.Name) + .SetOnInsert(y => y.ScriptType, x.ScriptType) + .SetOnInsert(y => y.CreatedTime, DateTime.UtcNow); + + return new UpdateOneModel( + builder.And(new List> + { + builder.Eq(y => y.AgentId, agentId), + builder.Eq(y => y.Name, x.Name), + builder.Eq(y => y.ScriptType, x.ScriptType) + }), + updateBuilder + ) { IsUpsert = options?.IsUpsert ?? false }; + }) .ToList(); var result = _dc.AgentCodeScripts.BulkWrite(ops, new BulkWriteOptions { IsOrdered = false }); - return result.ModifiedCount > 0 || result.MatchedCount > 0; + return true; } public bool BulkInsertAgentCodeScripts(string agentId, List scripts) @@ -93,7 +103,7 @@ public bool BulkInsertAgentCodeScripts(string agentId, List scr { var script = AgentCodeScriptDocument.ToMongoModel(x); script.AgentId = agentId; - script.Id = x.Id.IfNullOrEmptyAs(Guid.NewGuid().ToString()); + script.Id = x.Id.IfNullOrEmptyAs(Guid.NewGuid().ToString())!; script.CreatedTime = DateTime.UtcNow; script.UpdatedTime = DateTime.UtcNow; return script; @@ -111,6 +121,8 @@ public bool DeleteAgentCodeScripts(string agentId, List? script } DeleteResult deleted; + var builder = Builders.Filter; + if (scripts != null) { var scriptPaths = scripts.Select(x => x.CodePath); @@ -119,13 +131,16 @@ public bool DeleteAgentCodeScripts(string agentId, List? script new BsonDocument("$concat", new BsonArray { "$ScriptType", "/", "$Name" }), new BsonArray(scriptPaths) })); - - var filterDef = new BsonDocumentFilterDefinition(exprFilter); + + var filterDef = builder.And( + builder.Eq(x => x.AgentId, agentId), + new BsonDocumentFilterDefinition(exprFilter) + ); deleted = _dc.AgentCodeScripts.DeleteMany(filterDef); } else { - deleted = _dc.AgentCodeScripts.DeleteMany(Builders.Filter.Empty); + deleted = _dc.AgentCodeScripts.DeleteMany(builder.Eq(x => x.AgentId, agentId)); } return deleted.DeletedCount > 0; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs index 060994d44..f4f4fb656 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs @@ -1,6 +1,5 @@ using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Tasks.Models; -using MongoDB.Driver; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -67,10 +66,16 @@ public async ValueTask> GetAgentTasks(AgentTaskFilter filt public AgentTask? GetAgentTask(string agentId, string taskId) { - if (string.IsNullOrEmpty(taskId)) return null; + if (string.IsNullOrEmpty(taskId)) + { + return null; + } var taskDoc = _dc.AgentTasks.AsQueryable().FirstOrDefault(x => x.Id == taskId); - if (taskDoc == null) return null; + if (taskDoc == null) + { + return null; + } var agentDoc = _dc.Agents.AsQueryable().FirstOrDefault(x => x.Id == taskDoc.AgentId); var agent = TransformAgentDocument(agentDoc); @@ -107,11 +112,17 @@ public void BulkInsertAgentTasks(string agentId, List tasks) public void UpdateAgentTask(AgentTask task, AgentTaskField field) { - if (task == null || string.IsNullOrEmpty(task.Id)) return; + if (task == null || string.IsNullOrEmpty(task.Id)) + { + return; + } var filter = Builders.Filter.Eq(x => x.Id, task.Id); var taskDoc = _dc.AgentTasks.Find(filter).FirstOrDefault(); - if (taskDoc == null) return; + if (taskDoc == null) + { + return; + } switch (field) { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs index 3eedc0050..99d46123f 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs @@ -224,6 +224,10 @@ public async ValueTask> GetInstructionLogs(Instr { logFilters.Add(logBuilder.In(x => x.TemplateName, filter.TemplateNames)); } + if (!string.IsNullOrEmpty(filter.SimilarTemplateName)) + { + logFilters.Add(logBuilder.Regex(x => x.TemplateName, new BsonRegularExpression(filter.SimilarTemplateName, "i"))); + } if (filter.StartTime.HasValue) { logFilters.Add(logBuilder.Gte(x => x.CreatedTime, filter.StartTime.Value)); diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageClientExtensions.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageClientExtensions.cs index 2fb14e9ef..9f2555042 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageClientExtensions.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageClientExtensions.cs @@ -2,6 +2,8 @@ using OpenAI.Images; using System.ClientModel; using System.ClientModel.Primitives; +using System.Net.Http; +using System.Net.Http.Headers; using System.Reflection; namespace BotSharp.Plugin.OpenAI.Providers.Image; @@ -15,6 +17,7 @@ public static class ImageClientExtensions /// Generates image edits with multiple input images for composition /// /// The ImageClient instance + /// The LLM model /// Array of image streams to compose /// Array of corresponding file names for the images /// The prompt describing the desired composition @@ -23,52 +26,62 @@ public static class ImageClientExtensions /// ClientResult containing the generated image collection public static ClientResult GenerateImageEdits( this ImageClient client, + string model, Stream[] images, string[] imageFileNames, string prompt, int? imageCount = null, - ImageEditOptions options = null) + ImageEditOptions? options = null) { if (client == null) + { throw new ArgumentNullException(nameof(client)); + } - if (images == null || images.Length == 0) + if (images.IsNullOrEmpty()) + { throw new ArgumentException("At least one image is required", nameof(images)); + } if (imageFileNames == null || imageFileNames.Length != images.Length) + { throw new ArgumentException("Image file names array must match images array length", nameof(imageFileNames)); + } if (string.IsNullOrWhiteSpace(prompt)) + { throw new ArgumentException("Prompt cannot be null or empty", nameof(prompt)); + } // Get the pipeline from the client var pipeline = client.Pipeline; using var message = pipeline.CreateMessage(); // Build the request - BuildMultipartRequest(message, images, imageFileNames, prompt, imageCount, options); + BuildMultipartRequest(message, model, images, imageFileNames, prompt, imageCount, options); // Send the request pipeline.Send(message); - if (message.Response.IsError) + if (message.Response == null || message.Response.IsError) { - throw new InvalidOperationException($"API request failed with status {message.Response.Status}: {message.Response.ReasonPhrase} \r\n{message.Response.Content}"); + throw new InvalidOperationException($"API request failed with status {message.Response?.Status}: {message.Response?.ReasonPhrase} \r\n{message.Response?.Content}"); } // Parse the response - var generatedImages = ParseResponse(message.Response, options?.ResponseFormat); + var generatedImages = ParseResponse(message.Response); return ClientResult.FromValue(generatedImages, message.Response); } private static void BuildMultipartRequest( PipelineMessage message, + string model, Stream[] images, string[] imageFileNames, string prompt, int? imageCount, - ImageEditOptions options) + ImageEditOptions? options) { message.Request.Method = "POST"; @@ -76,102 +89,82 @@ private static void BuildMultipartRequest( var endpoint = "https://api.openai.com"; message.Request.Uri = new Uri($"{endpoint.TrimEnd('/')}/v1/images/edits"); - // Create multipart form data var boundary = $"----WebKitFormBoundary{Guid.NewGuid():N}"; - var contentBuilder = new MemoryStream(); - - // Add prompt - WriteFormField(contentBuilder, boundary, "prompt", prompt); - - WriteFormField(contentBuilder, boundary, "model", "gpt-image-1-mini"); - - // Add image count - WriteFormField(contentBuilder, boundary, "n", imageCount.Value.ToString() ?? "1"); - - for (var i = 0; i < images.Length; i++) - { - WriteFormField(contentBuilder, boundary, "image[]", imageFileNames[i], images[i], "image/png"); - } - - // Add optional parameters supported by OpenAI image edits API - if (options.Quality.HasValue) + using var form = new MultipartFormDataContent(boundary) { - WriteFormField(contentBuilder, boundary, "quality", options.Quality.ToString() ?? "auto"); - } + { new StringContent(prompt), "prompt" }, + { new StringContent(model ?? "gpt-image-1-mini"), "model" } + }; - if (options.Size.HasValue) + if (imageCount.HasValue) { - WriteFormField(contentBuilder, boundary, "size", ConvertImageSizeToString(options.Size.Value)); + form.Add(new StringContent(imageCount.Value.ToString()), "n"); } - - if (options.Background.HasValue) + else { - WriteFormField(contentBuilder, boundary, "background", options.Background.ToString() ?? "auto"); + form.Add(new StringContent("1"), "n"); } - WriteFormField(contentBuilder, boundary, "output_format", "png"); - - if (!string.IsNullOrEmpty(options.EndUserId)) + if (options != null) { - WriteFormField(contentBuilder, boundary, "user", options.EndUserId); - } - - WriteFormField(contentBuilder, boundary, "moderation", "auto"); - - // Write closing boundary - var closingBoundary = Encoding.UTF8.GetBytes($"--{boundary}--\r\n"); - contentBuilder.Write(closingBoundary, 0, closingBoundary.Length); - - // Set the content - contentBuilder.Position = 0; - message.Request.Content = BinaryContent.Create(BinaryData.FromStream(contentBuilder)); + if (options.Quality.HasValue) + { + form.Add(new StringContent(options.Quality.ToString()), "quality"); + } - // Set content type header - message.Request.Headers.Set("Content-Type", $"multipart/form-data; boundary={boundary}"); - } + if (options.Size.HasValue) + { + form.Add(new StringContent(ConvertImageSizeToString(options.Size.Value)), "size"); + } - private static void WriteFormField(MemoryStream stream, string boundary, string name, string value) - { - var header = $"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n"; - var body = $"{header}\r\n{value}\r\n"; - var bytes = Encoding.UTF8.GetBytes(body); - stream.Write(bytes, 0, bytes.Length); - } + if (options.Background.HasValue) + { + form.Add(new StringContent(options.Background.ToString()), "background"); + } - private static void WriteFormField(MemoryStream stream, string boundary, string name, string fileName, Stream fileStream, string contentType) - { - var header = $"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"; filename=\"{fileName}\"\r\nContent-Type: {contentType}\r\n\r\n"; - var headerBytes = Encoding.UTF8.GetBytes(header); - stream.Write(headerBytes, 0, headerBytes.Length); + if (options.ResponseFormat.HasValue) + { + form.Add(new StringContent(options.ResponseFormat.ToString()), "response_format"); + } + } - // Copy file stream - if (fileStream.CanSeek) + for (var i = 0; i < images.Length; i++) { - fileStream.Position = 0; - } - fileStream.CopyTo(stream); + if (images[i].CanSeek) + { + images[i].Position = 0; + } + var fileContent = new StreamContent(images[i]); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var newLine = Encoding.UTF8.GetBytes("\r\n"); - stream.Write(newLine, 0, newLine.Length); - } + if (images.Length > 1) + { + form.Add(fileContent, name: "image[]", fileName: imageFileNames[i]); + } + else + { + form.Add(fileContent, name: "image", fileName: imageFileNames[i]); + } + } - #region Helper Methods + using var ms = new MemoryStream(); + form.CopyTo(ms, null, CancellationToken.None); + ms.Position = 0; - private static string GetEndpoint(PipelineMessage message) - { - // Try to get the endpoint from the request URI if already set - return message.Request.Uri?.GetLeftPart(UriPartial.Authority); + message.Request.Headers.Set("Content-Type", form.Headers.ContentType.ToString()); + message.Request.Content = BinaryContent.Create(BinaryData.FromStream(ms)); } - private static GeneratedImageCollection ParseResponse(PipelineResponse response, GeneratedImageFormat? format) + #region Private Methods + private static GeneratedImageCollection ParseResponse(PipelineResponse response) { try { // Try to use ModelReaderWriter to deserialize the response - var modelReaderWriter = ModelReaderWriter.Read(response.Content); - if (modelReaderWriter != null) + var result = ModelReaderWriter.Read(response.Content); + if (result != null) { - return modelReaderWriter; + return result; } } catch (Exception ex) @@ -196,7 +189,7 @@ private static GeneratedImageCollection ParseResponse(PipelineResponse response, var result = fromResponseMethod.Invoke(null, new object[] { response }); if (result != null) { - return (GeneratedImageCollection)result; + return result as GeneratedImageCollection; } } @@ -211,7 +204,7 @@ private static GeneratedImageCollection ParseResponse(PipelineResponse response, var result = deserializeMethod.Invoke(null, new object[] { jsonDocument.RootElement }); if (result != null) { - return (GeneratedImageCollection)result; + return result as GeneratedImageCollection; } } } diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Compose.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Compose.cs index d07c4b7b0..6215ba1af 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Compose.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Compose.cs @@ -1,6 +1,4 @@ #pragma warning disable OPENAI001 -using OpenAI.Images; - namespace BotSharp.Plugin.OpenAI.Providers.Image; public partial class ImageCompletionProvider @@ -20,11 +18,7 @@ public async Task GetImageComposition(Agent agent, RoleDialogMo var imageClient = client.GetImageClient(_model); // Use the new extension method to support multiple images - options.ResponseFormat = "b64_json"; - options.Quality = "medium"; - options.Background = "auto"; - options.Size = GeneratedImageSize.Auto; - var response = imageClient.GenerateImageEdits(images, imageFileNames, prompt, imageCount, options); + var response = imageClient.GenerateImageEdits(_model, images, imageFileNames, prompt, imageCount, options); var generatedImageCollection = response.Value; var generatedImages = GetImageGenerations(generatedImageCollection, options.ResponseFormat); diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Edit.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Edit.cs index 732cc7a96..7a8d44725 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Edit.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Image/ImageCompletionProvider.Edit.cs @@ -56,12 +56,14 @@ public async Task GetImageEdits(Agent agent, RoleDialogModel me var state = _services.GetRequiredService(); var size = state.GetState("image_size"); + var quality = state.GetState("image_quality"); var responseFormat = state.GetState("image_response_format"); var background = state.GetState("image_background"); var settings = settingsService.GetSetting(Provider, _model)?.Image?.Edit; size = settings?.Size != null ? LlmUtility.VerifyModelParameter(size, settings.Size.Default, settings.Size.Options) : null; + quality = settings?.Quality != null ? LlmUtility.VerifyModelParameter(quality, settings.Quality.Default, settings.Quality.Options) : null; responseFormat = settings?.ResponseFormat != null ? LlmUtility.VerifyModelParameter(responseFormat, settings.ResponseFormat.Default, settings.ResponseFormat.Options) : null; background = settings?.Background != null ? LlmUtility.VerifyModelParameter(background, settings.Background.Default, settings.Background.Options) : null; @@ -70,6 +72,10 @@ public async Task GetImageEdits(Agent agent, RoleDialogModel me { options.Size = GetImageSize(size); } + if (!string.IsNullOrEmpty(quality)) + { + options.Quality = GetImageQuality(quality); + } if (!string.IsNullOrEmpty(responseFormat)) { options.ResponseFormat = GetImageResponseFormat(responseFormat); diff --git a/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs index 94b4a6873..a3c7e9464 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs @@ -171,7 +171,7 @@ public async Task GetDecomposedStepAsync(Agent router, string me // chat completion var completion = CompletionProvider.GetChatCompletion(_services, provider: "openai", - model: model.Name); + model: model?.Name); int retryCount = 0; while (retryCount < 2) diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlPrimaryStageFn.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlPrimaryStageFn.cs index a8f2e3627..eb5c2686f 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlPrimaryStageFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlPrimaryStageFn.cs @@ -52,7 +52,6 @@ public async Task Execute(RoleDialogModel message) Id = message.CurrentAgentId, Name = Name, Instruction = prompt, - TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig }; var response = await GetAiResponse(plannerAgent); diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlSecondaryStageFn.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlSecondaryStageFn.cs index 4ab6a7663..44e27da46 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlSecondaryStageFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlSecondaryStageFn.cs @@ -56,7 +56,6 @@ public async Task Execute(RoleDialogModel message) Id = PlannerAgentId.SqlPlanner, Name = Name, Instruction = prompt, - TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig }; diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Hooks/SqlPlannerAgentHook.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Hooks/SqlPlannerAgentHook.cs index 64645eac8..2b9c1389b 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Hooks/SqlPlannerAgentHook.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Hooks/SqlPlannerAgentHook.cs @@ -9,21 +9,21 @@ public SqlPlannerAgentHook(IServiceProvider services, AgentSettings settings) { } - public override bool OnInstructionLoaded(string template, Dictionary dict) + public override bool OnInstructionLoaded(string template, IDictionary dict) { var knowledgeHooks = _services.GetServices(); // Get global knowledges - var Knowledges = new List(); + var knowledges = new List(); foreach (var hook in knowledgeHooks) { var k = hook.GetGlobalKnowledges(new RoleDialogModel(AgentRole.User, template) { CurrentAgentId = PlannerAgentId.SqlPlanner - }).Result; - Knowledges.AddRange(k); + }).ConfigureAwait(false).GetAwaiter().GetResult(); + knowledges.AddRange(k); } - dict["global_knowledges"] = Knowledges; + dict["global_knowledges"] = knowledges; return true; } diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/PrimaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/PrimaryStagePlanFn.cs index 561b99770..e03f57c5e 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/PrimaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/PrimaryStagePlanFn.cs @@ -52,7 +52,6 @@ public async Task Execute(RoleDialogModel message) Id = message.CurrentAgentId, Name = Name, Instruction = prompt, - TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig }; var response = await GetAiResponse(plannerAgent); diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/SecondaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/SecondaryStagePlanFn.cs index 208a15d9d..23daedf50 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/SecondaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Functions/SecondaryStagePlanFn.cs @@ -56,7 +56,6 @@ public async Task Execute(RoleDialogModel message) Id = PlannerAgentId.TwoStagePlanner, Name = Name, Instruction = prompt, - TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig }; diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs index 8dc9e7d50..642336aaf 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs @@ -1,6 +1,8 @@ +using BotSharp.Abstraction.Coding.Settings; using Microsoft.Extensions.Logging; using Python.Runtime; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace BotSharp.Plugin.PythonInterpreter.Functions; @@ -12,16 +14,19 @@ public class PyProgrammerFn : IFunctionCallback private readonly IServiceProvider _services; private readonly ILogger _logger; - private readonly PythonInterpreterSettings _settings; + private readonly CodingSettings _codingSettings; + private readonly PythonInterpreterSettings _pySettings; public PyProgrammerFn( IServiceProvider services, ILogger logger, - PythonInterpreterSettings settings) + CodingSettings codingSettings, + PythonInterpreterSettings pySettings) { _services = services; _logger = logger; - _settings = settings; + _codingSettings = codingSettings; + _pySettings = pySettings; } public async Task Execute(RoleDialogModel message) @@ -40,10 +45,10 @@ public async Task Execute(RoleDialogModel message) Name = agent.Name, Instruction = inst, LlmConfig = GetLlmConfig(), - TemplateDict = new Dictionary + TemplateDict = new() { - { "python_version", _settings.PythonVersion ?? "3.11" }, - { "user_requirement", args?.UserRquirement ?? string.Empty } + ["python_version"] = _pySettings.PythonVersion ?? "3.11", + ["user_requirement"] = args?.UserRquirement ?? string.Empty } }; @@ -53,7 +58,7 @@ public async Task Execute(RoleDialogModel message) dialogs = convService.GetDialogHistory(); } - var messageLimit = _settings.CodeGeneration?.MessageLimit > 0 ? _settings.CodeGeneration.MessageLimit.Value : 50; + var messageLimit = _codingSettings.CodeGeneration?.MessageLimit > 0 ? _codingSettings.CodeGeneration.MessageLimit.Value : 50; dialogs = dialogs.TakeLast(messageLimit).ToList(); dialogs.Add(new RoleDialogModel(AgentRole.User, "Please follow the instruction and chat context to generate valid python code.") { @@ -104,48 +109,31 @@ public async Task Execute(RoleDialogModel message) /// private (bool, string) InnerRunCode(string codeScript) { - using (Py.GIL()) + var codeProvider = _codingSettings.CodeExecution?.Processor; + codeProvider = !string.IsNullOrEmpty(codeProvider) ? codeProvider : BuiltInCodeProcessor.PyInterpreter; + var processor = _services.GetServices() + .FirstOrDefault(x => x.Provider.IsEqualTo(codeProvider)); + + if (processor == null) { - // Import necessary Python modules - dynamic sys = Py.Import("sys"); - dynamic io = Py.Import("io"); + return (false, "Unable to execute python code script."); + } - try - { - // Redirect standard output/error to capture it - dynamic stringIO = io.StringIO(); - sys.stdout = stringIO; - sys.stderr = stringIO; - sys.argv = new PyList(); - - // Set global items - using var globals = new PyDict(); - if (codeScript?.Contains("__main__") == true) - { - globals.SetItem("__name__", new PyString("__main__")); - } + var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - // Execute Python script - PythonEngine.Exec(codeScript, globals); + var response = processor.Run(codeScript, options: new() + { + UseLock = useLock, + UseProcess = useProcess + }, cancellationToken: cts.Token); - // Get result - var result = stringIO.getvalue()?.ToString() as string; - return (true, result?.TrimEnd('\r', '\n') ?? string.Empty); - } - catch (Exception ex) - { - var errorMsg = $"Error when executing inner python code. {ex.Message}"; - _logger.LogError(ex, errorMsg); - return (false, errorMsg); - } - finally - { - // Restore the original stdout/stderr/argv - sys.stdout = sys.__stdout__; - sys.stderr = sys.__stderr__; - sys.argv = new PyList(); - } + if (response == null || !response.Success) + { + return (false, !string.IsNullOrEmpty(response?.ErrorMsg) ? response.ErrorMsg : "Failed to execute python code script."); } + + return (true, response.Result); } private async Task GetChatCompletion(Agent agent, List dialogs) @@ -188,8 +176,8 @@ private string GetPyCodeInterpreterInstruction(string agentId) private (string, string) GetLlmProviderModel() { - var provider = _settings.CodeGeneration?.Provider; - var model = _settings.CodeGeneration?.Model; + var provider = _codingSettings.CodeGeneration?.Provider; + var model = _codingSettings.CodeGeneration?.Model; if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) { @@ -204,8 +192,8 @@ private string GetPyCodeInterpreterInstruction(string agentId) private AgentLlmConfig GetLlmConfig() { - var maxOutputTokens = _settings?.CodeGeneration?.MaxOutputTokens ?? 8192; - var reasoningEffortLevel = _settings?.CodeGeneration?.ReasoningEffortLevel ?? "minimal"; + var maxOutputTokens = _codingSettings?.CodeGeneration?.MaxOutputTokens ?? 8192; + var reasoningEffortLevel = _codingSettings?.CodeGeneration?.ReasoningEffortLevel ?? "minimal"; return new AgentLlmConfig { @@ -213,4 +201,16 @@ private AgentLlmConfig GetLlmConfig() ReasoningEffortLevel = reasoningEffortLevel }; } + + private (bool, bool, int) GetCodeExecutionConfig() + { + var codeExecution = _codingSettings.CodeExecution; + var defaultTimeoutSeconds = 10; + + var useLock = codeExecution?.UseLock ?? false; + var useProcess = codeExecution?.UseProcess ?? false; + var timeoutSeconds = codeExecution?.TimeoutSeconds > 0 ? codeExecution.TimeoutSeconds : defaultTimeoutSeconds; + + return (useLock, useProcess, timeoutSeconds); + } } diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs index b6fdb9941..a7b87cf64 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs @@ -1,7 +1,8 @@ +using BotSharp.Abstraction.Models; + namespace BotSharp.Plugin.PythonInterpreter.Models; -internal class PackageInstallResult +internal class PackageInstallResult : ResponseBase { - internal bool Success { get; set; } - internal string ErrorMsg { get; set; } + } diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs index a56a9e447..0ca84c3ff 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs @@ -40,6 +40,7 @@ public void Configure(IApplicationBuilder app) Runtime.PythonDLL = pyLoc; PythonEngine.Initialize(); _pyState = PythonEngine.BeginAllowThreads(); + PythonEngine.DebugGIL = true; lifetime.ApplicationStopping.Register(() => { try diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs index 76bd6fe60..42bc1480c 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs @@ -1,63 +1,171 @@ using Microsoft.Extensions.Logging; using Python.Runtime; +using System.Diagnostics; +using System.Text; using System.Threading; using System.Threading.Tasks; + namespace BotSharp.Plugin.PythonInterpreter.Services; public class PyCodeInterpreter : ICodeProcessor { private readonly IServiceProvider _services; private readonly ILogger _logger; - private readonly CodeScriptExecutor _executor; + private readonly CodingSettings _settings; + private static readonly SemaphoreSlim _semLock = new(initialCount: 1, maxCount: 1); public PyCodeInterpreter( IServiceProvider services, ILogger logger, - CodeScriptExecutor executor) + CodingSettings settings) { _services = services; _logger = logger; - _executor = executor; + _settings = settings; } - public string Provider => "botsharp-py-interpreter"; + public string Provider => BuiltInCodeProcessor.PyInterpreter; - public async Task RunAsync(string codeScript, CodeInterpretOptions? options = null) + public CodeInterpretResponse Run(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { - if (options?.UseMutex == true) + if (options?.UseLock == true) { - return await _executor.ExecuteAsync(async () => - { - return InnerRunCode(codeScript, options); - }, cancellationToken: options?.CancellationToken ?? CancellationToken.None); + return InnerRunWithLock(codeScript, options, cancellationToken); + } + + return InnerRunCode(codeScript, options, cancellationToken); + } + + public async Task GenerateCodeScriptAsync(string text, CodeGenerationOptions? options = null) + { + Agent? agent = null; + + var agentId = options?.AgentId; + var templateName = options?.TemplateName; + + if (!string.IsNullOrEmpty(agentId)) + { + var agentService = _services.GetRequiredService(); + agent = await agentService.GetAgent(agentId); } - return InnerRunCode(codeScript, options); + + var instruction = string.Empty; + if (agent != null && !string.IsNullOrEmpty(templateName)) + { + instruction = agent.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(templateName))?.Content; + } + + var (provider, model) = GetLlmProviderModel(); + var innerAgent = new Agent + { + Id = agent?.Id ?? BuiltInAgentId.AIProgrammer, + Name = agent?.Name ?? "AI Programmer", + Instruction = instruction, + LlmConfig = new AgentLlmConfig + { + Provider = options?.Provider ?? provider, + Model = options?.Model ?? model, + MaxOutputTokens = options?.MaxOutputTokens ?? _settings?.CodeGeneration?.MaxOutputTokens, + ReasoningEffortLevel = options?.ReasoningEffortLevel ?? _settings?.CodeGeneration?.ReasoningEffortLevel + }, + TemplateDict = options?.Data ?? new() + }; + + text = text.IfNullOrEmptyAs("Please follow the instruction to generate code script.")!; + var completion = CompletionProvider.GetChatCompletion(_services, provider: innerAgent.LlmConfig.Provider, model: innerAgent.LlmConfig.Model); + var response = await completion.GetChatCompletions(innerAgent, new List + { + new RoleDialogModel(AgentRole.User, text) + { + CurrentAgentId = innerAgent.Id + } + }); + + return new CodeGenerationResult + { + Success = true, + Content = response.Content, + Language = options?.ProgrammingLanguage ?? "python" + }; } - private CodeInterpretResponse InnerRunCode(string codeScript, CodeInterpretOptions? options = null) + + #region Private methods + private CodeInterpretResponse InnerRunWithLock(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { + var lockAcquired = false; + try { - return CoreRun(codeScript, options); + _semLock.Wait(cancellationToken); + lockAcquired = true; + return InnerRunCode(codeScript, options, cancellationToken); } catch (Exception ex) { - var errorMsg = $"Error when executing inner python code in {nameof(PyCodeInterpreter)}: {Provider}."; - _logger.LogError(ex, errorMsg); + _logger.LogError(ex, $"Error in {nameof(InnerRunWithLock)} in {Provider}"); + return new() { ErrorMsg = ex.Message }; + } + finally + { + if (lockAcquired) + { + _semLock.Release(); + } + } + } - return new CodeInterpretResponse + private CodeInterpretResponse InnerRunCode(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + { + var response = new CodeInterpretResponse(); + var scriptName = options?.ScriptName ?? codeScript.SubstringMax(30); + + try + { + _logger.LogWarning($"Begin running python code script in {Provider}: {scriptName}"); + + if (options?.UseProcess == true) { - Success = false, - ErrorMsg = errorMsg - }; + response = CoreRunProcess(codeScript, options, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + response = CoreRunScript(codeScript, options, cancellationToken); + } + + _logger.LogWarning($"End running python code script in {Provider}: {scriptName}"); + + return response; + } + catch (OperationCanceledException oce) + { + _logger.LogError(oce, $"Operation cancelled in {nameof(InnerRunCode)} in {Provider}."); + response.ErrorMsg = oce.Message; + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when executing code script ({scriptName}) in {nameof(InnerRunCode)} in {Provider}."); + response.ErrorMsg = ex.Message; + return response; } } - private CodeInterpretResponse CoreRun(string codeScript, CodeInterpretOptions? options = null) + private CodeInterpretResponse CoreRunScript(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { - using (Py.GIL()) + cancellationToken.ThrowIfCancellationRequested(); + + var execTask = Task.Factory.StartNew(() => { + Thread.Sleep(100); + + _logger.LogWarning($"Before acquiring Py.GIL (Python engine initialized: {PythonEngine.IsInitialized}). Thread {Thread.CurrentThread.ManagedThreadId}."); + + var gil = Py.GIL(); + + _logger.LogWarning($"After acquiring Py.GIL ({gil != null}). Thread {Thread.CurrentThread.ManagedThreadId}."); + // Import necessary Python modules dynamic sys = Py.Import("sys"); dynamic io = Py.Import("io"); @@ -65,9 +173,10 @@ private CodeInterpretResponse CoreRun(string codeScript, CodeInterpretOptions? o try { // Redirect standard output/error to capture it - dynamic stringIO = io.StringIO(); - sys.stdout = stringIO; - sys.stderr = stringIO; + dynamic outIO = io.StringIO(); + dynamic errIO = io.StringIO(); + sys.stdout = outIO; + sys.stderr = errIO; // Set global items using var globals = new PyDict(); @@ -80,9 +189,9 @@ private CodeInterpretResponse CoreRun(string codeScript, CodeInterpretOptions? o var list = new PyList(); if (options?.Arguments?.Any() == true) { - list.Append(new PyString(options?.ScriptName ?? "script.py")); + list.Append(new PyString(options?.ScriptName ?? $"{Guid.NewGuid()}.py")); - foreach (var arg in options.Arguments) + foreach (var arg in options!.Arguments) { if (!string.IsNullOrWhiteSpace(arg.Key) && !string.IsNullOrWhiteSpace(arg.Value)) { @@ -93,28 +202,29 @@ private CodeInterpretResponse CoreRun(string codeScript, CodeInterpretOptions? o } sys.argv = list; + _logger.LogWarning($"Code script {options?.ScriptName} arguments: {string.Join("\r\n", options?.Arguments?.Select(x => x.ToString()) ?? [])}. Thread: {Thread.CurrentThread.ManagedThreadId}."); + + _logger.LogWarning($"Before executing code script {options?.ScriptName}. Thread: {Thread.CurrentThread.ManagedThreadId}."); + // Execute Python script PythonEngine.Exec(codeScript, globals); + _logger.LogWarning($"After executing code script {options?.ScriptName}. Thread: {Thread.CurrentThread.ManagedThreadId}."); + // Get result - var result = stringIO.getvalue()?.ToString() as string; + var stdout = outIO.getvalue()?.ToString() as string; + var stderr = errIO.getvalue()?.ToString() as string; return new CodeInterpretResponse { - Result = result?.TrimEnd('\r', '\n'), + Result = stdout?.TrimEnd('\r', '\n') ?? string.Empty, Success = true }; } catch (Exception ex) { - var errorMsg = $"Error when executing core python code in {nameof(PyCodeInterpreter)}: {Provider}. {ex.Message}"; - _logger.LogError(ex, errorMsg); - - return new CodeInterpretResponse - { - Success = false, - ErrorMsg = errorMsg - }; + _logger.LogError(ex, $"Error in {nameof(CoreRunScript)} in {Provider}."); + return new() { ErrorMsg = ex.Message }; } finally { @@ -122,7 +232,114 @@ private CodeInterpretResponse CoreRun(string codeScript, CodeInterpretOptions? o sys.stdout = sys.__stdout__; sys.stderr = sys.__stderr__; sys.argv = new PyList(); + + _logger.LogWarning($"Before disposing Py.GIL ({gil != null}). Thread {Thread.CurrentThread.ManagedThreadId}."); + gil?.Dispose(); + _logger.LogWarning($"After disposing Py.GIL. Thread {Thread.CurrentThread.ManagedThreadId}."); + } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + return execTask.WaitAsync(cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + + private async Task CoreRunProcess(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) + { + var psi = new ProcessStartInfo + { + FileName = "python", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + + // Add raw code script + psi.ArgumentList.Add("-c"); + psi.ArgumentList.Add(codeScript); + + // Add arguments (safe—no shared state) + if (options?.Arguments?.Any() == true) + { + foreach (var arg in options.Arguments!) + { + if (!string.IsNullOrWhiteSpace(arg.Key) && !string.IsNullOrWhiteSpace(arg.Value)) + { + psi.ArgumentList.Add($"--{arg.Key}"); + psi.ArgumentList.Add($"{arg.Value}"); + } } } + + using var proc = new Process { StartInfo = psi, EnableRaisingEvents = true }; + if (!proc.Start()) + { + throw new InvalidOperationException($"Failed to start Python process in {Provider}."); + } + + try + { + using var reg = cancellationToken.Register(() => + { + try + { + if (!proc.HasExited) + { + proc.Kill(entireProcessTree: true); + } + } + catch { } + }); + + var stdoutTask = proc.StandardOutput.ReadToEndAsync(cancellationToken); + var stderrTask = proc.StandardError.ReadToEndAsync(cancellationToken); + + await Task.WhenAll([proc.WaitForExitAsync(cancellationToken), stdoutTask, stderrTask]); + + cancellationToken.ThrowIfCancellationRequested(); + + return new CodeInterpretResponse + { + Success = proc.ExitCode == 0, + Result = stdoutTask.Result?.TrimEnd('\r', '\n') ?? string.Empty, + ErrorMsg = stderrTask.Result + }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error in {nameof(CoreRunProcess)} in {Provider}."); + throw; + } + finally + { + try + { + if (!proc.HasExited) + { + proc.Kill(entireProcessTree: true); + proc.WaitForExit(); + } + } + catch { } + } + } + + private (string, string) GetLlmProviderModel() + { + var provider = _settings.CodeGeneration?.Provider; + var model = _settings.CodeGeneration?.Model; + + if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(model)) + { + return (provider, model); + } + + provider = "openai"; + model = "gpt-5-mini"; + + return (provider, model); } + #endregion } diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Settings/PythonInterpreterSettings.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Settings/PythonInterpreterSettings.cs index 8fa79dc8a..0c86d9daa 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Settings/PythonInterpreterSettings.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Settings/PythonInterpreterSettings.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Models; - namespace BotSharp.Plugin.PythonInterpreter.Settings; public class PythonInterpreterSettings @@ -9,10 +7,4 @@ public class PythonInterpreterSettings /// public string InstallLocation { get; set; } public string PythonVersion { get; set; } - public CodeGenerationSetting? CodeGeneration { get; set; } -} - -public class CodeGenerationSetting : LlmConfigBase -{ - public int? MessageLimit { get; set; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs index bd2c943b7..9296406a3 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs @@ -21,6 +21,9 @@ global using BotSharp.Abstraction.Messaging.Models.RichContent.Template; global using BotSharp.Abstraction.Routing; global using BotSharp.Abstraction.Coding; +global using BotSharp.Abstraction.Coding.Enums; +global using BotSharp.Abstraction.Coding.Models; +global using BotSharp.Abstraction.Coding.Settings; global using BotSharp.Abstraction.Coding.Options; global using BotSharp.Abstraction.Coding.Responses; global using BotSharp.Core.Coding; diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs index 32543f83e..cbca32ae5 100644 --- a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs +++ b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs @@ -103,12 +103,18 @@ public async Task> GetCollections() { var exist = await DoesCollectionExist(collectionName); - if (!exist) return null; + if (!exist) + { + return null; + } var client = GetClient(); var details = await client.GetCollectionInfoAsync(collectionName); - if (details == null) return null; + if (details == null) + { + return null; + } var payloadSchema = details.PayloadSchema?.Select(x => new PayloadSchemaDetail { @@ -287,11 +293,13 @@ public async Task> Search(string collectionNam options ??= VectorSearchOptions.Default(); Filter? queryFilter = BuildQueryFilter(options.FilterGroups); WithPayloadSelector? payloadSelector = BuildPayloadSelector(options.Fields); + SearchParams? param = BuildSearchParam(options?.SearchParam); var client = GetClient(); var points = await client.SearchAsync(collectionName, vector, limit: (ulong)options.Limit.GetValueOrDefault(), + searchParams: param, scoreThreshold: options.Confidence, filter: queryFilter, payloadSelector: payloadSelector, @@ -824,6 +832,21 @@ public async Task DeleteCollectionShapshot(string collectionName, string s }; } + private SearchParams? BuildSearchParam(VectorSearchParamModel? param) + { + if (param == null + || param.ExactSearch == null) + { + return null; + } + + var search = new SearchParams + { + Exact = param.ExactSearch.Value + }; + return search; + } + private PayloadSchemaType ConvertPayloadSchemaType(string schemaType) { PayloadSchemaType res; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs index c2f0682b3..ca518ce19 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs @@ -136,7 +136,6 @@ private async Task RefineSqlStatement(RoleDialogModel message, Id = message.CurrentAgentId ?? string.Empty, Name = "sqlDriver_ExecuteQuery", Instruction = dictionarySqlPrompt, - TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/VerifyDictionaryTerm.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/VerifyDictionaryTerm.cs index ac2889bdf..93ff21a40 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/VerifyDictionaryTerm.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/VerifyDictionaryTerm.cs @@ -38,7 +38,6 @@ public async Task Execute(RoleDialogModel message) Id = message.CurrentAgentId ?? string.Empty, Name = "sqlDriver_DictionarySearch", Instruction = dictionarySqlPrompt, - TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartProcessor.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartProcessor.cs new file mode 100644 index 000000000..858cf11f0 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartProcessor.cs @@ -0,0 +1,47 @@ +using BotSharp.Abstraction.Chart.Options; +using BotSharp.Abstraction.Repositories; + +namespace BotSharp.Plugin.SqlDriver.Services; + +public class SqlChartProcessor : IChartProcessor +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public SqlChartProcessor( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Provider => "sql_driver"; + + public async Task GetConversationChartDataAsync(string conversationId, string messageId, ChartDataOptions? options = null) + { + if (string.IsNullOrWhiteSpace(conversationId)) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(options?.TargetStateName)) + { + var db = _services.GetRequiredService(); + var states = db.GetConversationStates(conversationId); + var value = states?.GetValueOrDefault(options?.TargetStateName)?.Values?.LastOrDefault()?.Data; + + // To do + //return new ChartDataResult(); + } + + // Dummy data for testing + var data = new + { + categories = new string[] { "A", "B", "C", "D", "E" }, + values = new int[] { 42, 67, 29, 85, 53 } + }; + + return new ChartDataResult { Data = data }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartService.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartService.cs deleted file mode 100644 index 5efef68f4..000000000 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Services/SqlChartService.cs +++ /dev/null @@ -1,156 +0,0 @@ -using BotSharp.Abstraction.Chart.Options; -using BotSharp.Abstraction.Options; -using BotSharp.Abstraction.Repositories; -using BotSharp.Core.Infrastructures; -using BotSharp.Plugin.SqlDriver.LlmContext; - -namespace BotSharp.Plugin.SqlDriver.Services; - -public class SqlChartService : IBotSharpChartService -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - private readonly BotSharpOptions _botSharpOptions; - - public SqlChartService( - IServiceProvider services, - ILogger logger, - BotSharpOptions botSharpOptions) - { - _services = services; - _logger = logger; - _botSharpOptions = botSharpOptions; - } - - public string Provider => "sql_driver"; - - public async Task GetConversationChartData(string conversationId, string messageId, ChartDataOptions options) - { - if (string.IsNullOrWhiteSpace(conversationId)) - { - return null; - } - - if (!string.IsNullOrWhiteSpace(options?.TargetStateName)) - { - var db = _services.GetRequiredService(); - var states = db.GetConversationStates(conversationId); - var value = states?.GetValueOrDefault(options?.TargetStateName)?.Values?.LastOrDefault()?.Data; - - // To do - //return new ChartDataResult(); - } - - // Dummy data for testing - var data = new - { - categories = new string[] { "A", "B", "C", "D", "E" }, - values = new int[] { 42, 67, 29, 85, 53 } - }; - - return new ChartDataResult { Data = data }; - } - - public async Task GetConversationChartCode(string conversationId, string messageId, ChartCodeOptions options) - { - if (string.IsNullOrWhiteSpace(conversationId)) - { - return null; - } - - var agentService = _services.GetRequiredService(); - - var agentId = options.AgentId.IfNullOrEmptyAs(BuiltInAgentId.UtilityAssistant); - var templateName = options.TemplateName.IfNullOrEmptyAs("util-chart-plot_instruction"); - var inst = GetChartCodeInstruction(agentId, templateName); - - var agent = await agentService.GetAgent(agentId); - agent = new Agent - { - Id = agent.Id, - Name = agent.Name, - Instruction = inst, - LlmConfig = new AgentLlmConfig - { - MaxOutputTokens = options.Llm?.MaxOutputTokens ?? 8192, - ReasoningEffortLevel = options.Llm?.ReasoningEffortLevel - }, - TemplateDict = BuildChartStates(options) - }; - - var dialogs = new List - { - new RoleDialogModel - { - Role = AgentRole.User, - MessageId = messageId, - Content = options.Text.IfNullOrEmptyAs("Please follow the instruction to generate response.") - } - }; - var response = await GetChatCompletion(agent, dialogs, options); - var obj = response.JsonContent(); - - return new ChartCodeResult - { - Code = obj?.JsCode, - Language = "javascript" - }; - } - - - private Dictionary BuildChartStates(ChartCodeOptions options) - { - var states = new Dictionary(); - - if (!options.States.IsNullOrEmpty()) - { - foreach (var item in options.States) - { - if (item.Value == null) - { - continue; - } - states[item.Key] = item.Value; - } - } - return states; - } - - private string GetChartCodeInstruction(string agentId, string templateName) - { - var db = _services.GetRequiredService(); - var templateContent = db.GetAgentTemplate(agentId, templateName); - return templateContent; - } - - private async Task GetChatCompletion(Agent agent, List dialogs, ChartCodeOptions options) - { - try - { - var (provider, model) = GetLlmProviderModel(options); - var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, model: model); - var response = await completion.GetChatCompletions(agent, dialogs); - return response.Content; - } - catch (Exception ex) - { - var error = $"Error when generating chart code. {ex.Message}"; - _logger.LogWarning(ex, error); - return error; - } - } - - private (string, string) GetLlmProviderModel(ChartCodeOptions options) - { - var provider = "openai"; - var model = "gpt-5"; - - if (options?.Llm != null) - { - provider = options.Llm.Provider.IfNullOrEmptyAs(provider); - model = options.Llm.Model.IfNullOrEmptyAs(model); - } - - return (provider, model); - } -} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs index 1dd27ad79..7d079e57e 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs @@ -30,6 +30,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/VerifyDictionaryTerm.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/VerifyDictionaryTerm.cs index 2e6dbe72c..ce6fa547d 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/VerifyDictionaryTerm.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/VerifyDictionaryTerm.cs @@ -35,7 +35,6 @@ public async Task Execute(RoleDialogModel message) Id = message.CurrentAgentId ?? string.Empty, Name = "sqlDriver_DictionarySearch", Instruction = dictionarySqlPrompt, - TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig }; diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs index 28360518e..6b06f147b 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs @@ -161,7 +161,7 @@ private static async Task GetReplySpeechFileName(string conversationId, private static string GetHints(string agentId, AssistantMessage reply, IServiceProvider sp) { var agentService = sp.GetRequiredService(); - var agent = agentService.GetAgent(agentId).Result; + var agent = agentService.GetAgent(agentId).ConfigureAwait(false).GetAwaiter().GetResult(); var extraWords = new List(); HookEmitter.Emit(sp, hook => extraWords.AddRange(hook.OnModelTranscriptPrompt(agent)), agentId); diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs index 3a4d62f2b..37a0a0a21 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.cs @@ -44,7 +44,7 @@ public void SetAgent(Agent agent) _ => AriaRole.Generic }; element = _instance.GetPage(contextId).Locator($"[name='{context.ElementName}']"); - var count = element.CountAsync().Result; + var count = element.CountAsync().ConfigureAwait(false).GetAwaiter().GetResult(); if (count == 0) { _logger.LogError($"Can't locate element {role} {context.ElementName}"); diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 3e6cb68f0..ed8d2bd04 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -38,6 +38,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index c85c9cef2..a7cef6872 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -6,7 +6,6 @@ } }, "AllowedHosts": "*", - //"OTEL_EXPORTER_OTLP_ENDPOINT": "https://us.cloud.langfuse.com", "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", "OTEL_SERVICE_NAME": "apiservice", "AllowedOrigins": [ @@ -53,7 +52,11 @@ "Name": "gpt-35-turbo", "Version": "1106", "ApiKey": "", - "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/" + "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/", + "Type": "chat", + "Capabilities": [ + "Chat" + ] }, { "Name": "gpt-35-turbo-instruct", @@ -61,6 +64,9 @@ "ApiKey": "", "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/", "Type": "text", + "Capabilities": [ + "Text" + ], "Cost": { "TextInputCost": 0.0015, "CachedTextInputCost": 0, @@ -77,7 +83,10 @@ "Models": [ { "Name": "llama-2-7b-guanaco-qlora.Q2_K.gguf", - "Type": "chat" + "Type": "chat", + "Capabilities": [ + "Chat" + ] } ] }, @@ -86,11 +95,17 @@ "Models": [ { "Name": "mistralai/Mistral-7B-v0.1", - "Type": "text" + "Type": "text", + "Capabilities": [ + "Text" + ] }, { "Name": "TinyLlama/TinyLlama-1.1B-Chat-v1.0", - "Type": "text" + "Type": "text", + "Capabilities": [ + "Text" + ] } ] }, @@ -100,6 +115,9 @@ { "Name": "gpt-35-turbo", "Type": "chat", + "Capabilities": [ + "Chat" + ], "Cost": { "TextInputCost": 0.0015, "CachedTextInputCost": 0, @@ -117,6 +135,9 @@ { "Name": "chatglm3_6b", "Type": "chat", + "Capabilities": [ + "Chat" + ], "Cost": { "TextInputCost": 0.0015, "CachedTextInputCost": 0, @@ -138,6 +159,10 @@ "ApiKey": "", "Type": "chat", "MultiModal": true, + "Capabilities": [ + "Chat", + "ImageReading" + ], "Cost": { "TextInputCost": 0.00015, "CachedTextInputCost": 0, @@ -154,6 +179,10 @@ "ApiKey": "", "Type": "chat", "MultiModal": true, + "Capabilities": [ + "Chat", + "ImageReading" + ], "Cost": { "TextInputCost": 0.0025, "CachedTextInputCost": 0, @@ -168,9 +197,11 @@ "Name": "gpt-4o-mini-realtime-preview-2024-12-17", "Version": "2024-12-17", "ApiKey": "", - "Type": "chat", + "Type": "realtime", "MultiModal": true, - "RealTime": true, + "Capabilities": [ + "Realtime" + ], "Cost": { "TextInputCost": 0.0025, "CachedTextInputCost": 0, @@ -186,9 +217,218 @@ "Version": "3-small", "ApiKey": "", "Type": "embedding", + "Capabilities": [ + "Embedding" + ], "Embedding": { "Dimension": 1536 } + }, + { + "Id": "dall-e", + "Name": "dall-e-2", + "Version": "dall-e-2", + "ApiKey": "", + "Endpoint": "", + "Type": "image", + "Capabilities": [ + "ImageGeneration", + "ImageEdit", + "ImageVariation" + ], + "Image": { + "Generation": { + "Size": { + "Default": "1024x1024", + "Options": [ "256x256", "512x512", "1024x1024" ] + }, + "Quality": { + "Default": "standard", + "Options": [ "standard" ] + }, + "ResponseFormat": { + "Default": "bytes", + "Options": [ "url", "bytes" ] + } + }, + "Edit": { + "Size": { + "Default": "1024x1024", + "Options": [ "256x256", "512x512", "1024x1024" ] + }, + "ResponseFormat": { + "Default": "bytes", + "Options": [ "url", "bytes" ] + } + }, + "Variation": { + "Size": { + "Default": "1024x1024", + "Options": [ "256x256", "512x512", "1024x1024" ] + }, + "ResponseFormat": { + "Default": "bytes", + "Options": [ "url", "bytes" ] + } + } + }, + "Cost": { + "TextInputCost": 0.01, + "CachedTextInputCost": 0, + "AudioInputCost": 0, + "CachedAudioInputCost": 0, + "TextOutputCost": 0.03, + "AudioOutputCost": 0 + } + }, + { + "Id": "dall-e", + "Name": "dall-e-3", + "Version": "dall-e-3", + "ApiKey": "", + "Type": "image", + "Capabilities": [ + "ImageGeneration" + ], + "Image": { + "Generation": { + "Size": { + "Default": "1024x1024", + "Options": [ "1024x1024", "1792x1024", "1024x1792" ] + }, + "Quality": { + "Default": "standard", + "Options": [ "standard", "hd", "auto" ] + }, + "Style": { + "Default": "natural", + "Options": [ "natural", "vivid" ] + }, + "ResponseFormat": { + "Default": "bytes", + "Options": [ "url", "bytes" ] + } + } + }, + "Cost": { + "TextInputCost": 0.01, + "CachedTextInputCost": 0, + "AudioInputCost": 0, + "CachedAudioInputCost": 0, + "TextOutputCost": 0.03, + "AudioOutputCost": 0 + } + }, + { + "Id": "gpt-image", + "Name": "gpt-image-1", + "Version": "gpt-image-1", + "ApiKey": "", + "Type": "image", + "Capabilities": [ + "ImageComposition" + ], + "Image": { + "Edit": { + "Size": { + "Default": "1024x1024", + "Options": [ "1024x1024", "1536x1024", "1024x1536", "auto" ] + }, + "Quality": { + "Default": "medium", + "Options": [ "low", "medium", "high", "auto" ] + }, + "Background": { + "Default": "auto", + "Options": [ "auto", "transparent", "opaque" ] + } + } + }, + "Cost": { + "TextInputCost": 0.01, + "CachedTextInputCost": 0, + "AudioInputCost": 0, + "CachedAudioInputCost": 0, + "TextOutputCost": 0.03, + "AudioOutputCost": 0 + } + }, + { + "Id": "gpt-image", + "Name": "gpt-image-1-mini", + "Version": "gpt-image-1-mini", + "ApiKey": "", + "Type": "image", + "Capabilities": [ + "ImageComposition" + ], + "Image": { + "Edit": { + "Size": { + "Default": "1024x1024", + "Options": [ "1024x1024", "1536x1024", "1024x1536", "auto" ] + }, + "Quality": { + "Default": "medium", + "Options": [ "low", "medium", "high", "auto" ] + }, + "Background": { + "Default": "auto", + "Options": [ "auto", "transparent", "opaque" ] + } + } + }, + "Cost": { + "TextInputCost": 0.01, + "CachedTextInputCost": 0, + "AudioInputCost": 0, + "CachedAudioInputCost": 0, + "TextOutputCost": 0.03, + "AudioOutputCost": 0 + } + }, + { + "Id": "gpt-4o-web", + "Name": "gpt-4o-search-preview", + "Version": "gpt-4o-search-preview", + "ApiKey": "", + "Type": "chat", + "Capabilities": [ + "WebSearch" + ], + "WebSearch": { + "SearchContextSize": "low" + }, + "Cost": { + "TextInputCost": 0.005, + "CachedTextInputCost": 0.0025, + "AudioInputCost": 0.04, + "CachedAudioInputCost": 0.0025, + "TextOutputCost": 0.02, + "AudioOutputCost": 0.08 + } + }, + { + "Id": "gpt-4o-web", + "Name": "gpt-4o-mini-search-preview", + "Version": "gpt-4o-mini-search-preview", + "ApiKey": "", + "Type": "chat", + "Capabilities": [ + "WebSearch" + ], + "WebSearch": { + "IsDefault": true, + "SearchContextSize": "low" + }, + "Cost": { + "TextInputCost": 0.005, + "CachedTextInputCost": 0.0025, + "AudioInputCost": 0.04, + "CachedAudioInputCost": 0.0025, + "TextOutputCost": 0.02, + "AudioOutputCost": 0.08 + } } ] }, @@ -200,6 +440,10 @@ "ApiKey": "", "Endpoint": "https://api.deepseek.com/v1/", "Type": "chat", + "Capabilities": [ + "Chat", + "ImageReading" + ], "Cost": { "TextInputCost": 0.0015, "CachedTextInputCost": 0, @@ -219,6 +463,11 @@ "ApiKey": "", "Type": "chat", "MultiModal": true, + "Capabilities": [ + "Chat", + "ImageReading", + "PdfReading" + ], "Cost": { "TextInputCost": 0.0015, "CachedTextInputCost": 0, @@ -233,6 +482,9 @@ "ApiKey": "", "Type": "realtime", "MultiModal": true, + "Capabilities": [ + "Realtime" + ], "Cost": { "TextInputCost": 0.0015, "CachedTextInputCost": 0, @@ -301,6 +553,23 @@ } }, + "Coding": { + "CodeGeneration": { + "Provider": "openai", + "Model": "gpt-5-mini", + "MaxOutputTokens": 8192, + "ReasoningEffortLevel": "minimal", + "Processor": null, + "MessageLimit": 50 + }, + "CodeExecution": { + "Processor": null, + "UseLock": false, + "UseProcess": false, + "TimeoutSeconds": 3 + } + }, + "MCP": { "Enabled": false, "McpClientOptions": { @@ -490,15 +759,11 @@ "Model": "gpt-5-mini", "ImageDetailLevel": "auto" }, - "Generation": { - "Provider": "openai", - "Model": "gpt-image-1" - }, - "Edit": { + "Composition": { "Provider": "openai", "Model": "gpt-image-1", "ImageConverter": { - "Provider": "image-handler" + "Provider": null } } }, @@ -585,7 +850,7 @@ }, "MetaGLM": { - "ApiKey": "6b6c8b3fca3e5da21d633e350980744d.938gruOqrK4BDqW8", + "ApiKey": "", "BaseAddress": "http://localhost:8100/v1/", "ModelId": "chatglm3_6b", "Temperature": 0.7, @@ -608,14 +873,7 @@ "PythonInterpreter": { "InstallLocation": "C:/Users/xxx/AppData/Local/Programs/Python/Python313/python313.dll", - "PythonVersion": "3.13.3", - "CodeGeneration": { - "Provider": "openai", - "Model": "gpt-5", - "MaxOutputTokens": 8192, - "ReasoningEffortLevel": "minimal", - "MessageLimit": 50 - } + "PythonVersion": "3.13.3" }, "RealtimeModel": { @@ -671,9 +929,11 @@ "BotSharp.Plugin.EmailHandler", "BotSharp.Plugin.AudioHandler", "BotSharp.Plugin.ChartHandler", - "BotSharp.Plugin.AudioHandler", "BotSharp.Plugin.ExcelHandler", "BotSharp.Plugin.SqlDriver", - "BotSharp.Plugin.TencentCos" + "BotSharp.Plugin.TencentCos", + "BotSharp.Plugin.PythonInterpreter", + "BotSharp.Plugin.FuzzySharp" ] - } + } +} diff --git a/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs b/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs index 45d6dc6d5..add520c70 100644 --- a/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs +++ b/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Agents; using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Agents.Options; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Plugins.Models; @@ -41,32 +42,32 @@ public Task InheritAgent(Agent agent) return Task.CompletedTask; } - public string RenderInstruction(Agent agent, Dictionary? renderData = null) + public string RenderInstruction(Agent agent, IDictionary? renderData = null) { return "Fake Instruction"; } - public string RenderTemplate(Agent agent, string templateName, Dictionary? renderData = null) + public string RenderTemplate(Agent agent, string templateName, IDictionary? renderData = null) { return $"Rendered template for {templateName}"; } - public bool RenderFunction(Agent agent, FunctionDef def, Dictionary? renderData = null) + public bool RenderFunction(Agent agent, FunctionDef def, IDictionary? renderData = null) { return true; } - public (string, IEnumerable) PrepareInstructionAndFunctions(Agent agent, Dictionary? renderData = null, StringComparer? comparer = null) + public (string, IEnumerable) PrepareInstructionAndFunctions(Agent agent, IDictionary? renderData = null, StringComparer? comparer = null) { return (string.Empty, []); } - public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, Dictionary? renderData = null) + public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, IDictionary? renderData = null) { return def.Parameters; } - public bool RenderVisibility(string? visibilityExpression, Dictionary dict) + public bool RenderVisibility(string? visibilityExpression, IDictionary dict) { return true; } @@ -76,7 +77,7 @@ public Task GetAgent(string id) return Task.FromResult(new Agent()); } - public Task DeleteAgent(string id) + public Task DeleteAgent(string id, AgentDeleteOptions? options = null) { return Task.FromResult(true); } @@ -121,9 +122,9 @@ public Task> GetAgentUtilityOptions() return Task.FromResult(Enumerable.Empty()); } - public Dictionary CollectRenderData(Agent agent) + public IDictionary CollectRenderData(Agent agent) { - return []; + return new Dictionary(); } } } \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/Hooks/CommonAgentHook.cs b/tests/BotSharp.Plugin.PizzaBot/Hooks/CommonAgentHook.cs index 95f9e6ef0..bc70387dc 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Hooks/CommonAgentHook.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Hooks/CommonAgentHook.cs @@ -11,7 +11,7 @@ public CommonAgentHook(IServiceProvider services, AgentSettings settings) { } - public override bool OnInstructionLoaded(string template, Dictionary dict) + public override bool OnInstructionLoaded(string template, IDictionary dict) { dict["current_date"] = DateTime.Now.ToString("MM/dd/yyyy"); dict["current_time"] = DateTime.Now.ToString("hh:mm tt"); diff --git a/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaBotAgentHook.cs b/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaBotAgentHook.cs index ff2eb8f6f..cbd26eb86 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaBotAgentHook.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaBotAgentHook.cs @@ -12,7 +12,7 @@ public PizzaBotAgentHook(IServiceProvider services, AgentSettings settings) { } - public override bool OnInstructionLoaded(string template, Dictionary dict) + public override bool OnInstructionLoaded(string template, IDictionary dict) { return base.OnInstructionLoaded(template, dict); }