diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index b45ff557a..d0284b224 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -72,8 +72,8 @@ 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); @@ -81,6 +81,6 @@ Task UpdateAgentCodeScripts(string agentId, List codeScri Task DeleteAgentCodeScripts(string agentId, List? codeScripts = null) => Task.FromResult(false); - Task GenerateCodeScript(string agentId, string text, CodeProcessOptions? options = null) + 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 3c71b3fbf..aa673e32b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentCodeScript.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentCodeScript.cs @@ -5,6 +5,9 @@ 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() { } 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/Models/CodeExecutionResponseModel.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeExecutionResponseModel.cs new file mode 100644 index 000000000..df85fb02b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Models/CodeExecutionResponseModel.cs @@ -0,0 +1,10 @@ +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 string ExecutionResult { get; set; } = default!; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeProcessOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenHandleOptions.cs similarity index 92% rename from src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeProcessOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenHandleOptions.cs index 343447b6b..963cb8a15 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeProcessOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenHandleOptions.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Coding.Options; -public class CodeProcessOptions : CodeGenerationOptions +public class CodeGenHandleOptions : CodeGenerationOptions { /// /// Code processor provider diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs index be6437041..de886f5ef 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs @@ -17,11 +17,10 @@ public class CodeGenerationOptions : LlmConfigBase public string? TemplateName { get; set; } /// - /// The programming language + /// Programming language /// - [JsonPropertyName("language")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Language { get; set; } = "python"; + [JsonPropertyName("programming_language")] + public string? ProgrammingLanguage { get; set; } /// /// Data that can be used to fill in the prompt diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs index 405028699..8d41f9853 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Responses/CodeInterpretResponse.cs @@ -6,6 +6,6 @@ public class CodeInterpretResponse : ResponseBase public override string ToString() { - return Result.IfNullOrEmptyAs(ErrorMsg ?? $"Success: {Success}")!; + 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 index 4b3d663ac..38f635222 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Settings/CodingSettings.cs @@ -18,6 +18,6 @@ public class CodeScriptExecutionSettings public string? Processor { get; set; } public bool UseLock { get; set; } public bool UseProcess { get; set; } - public int? TimeoutSeconds { get; set; } + public int TimeoutSeconds { get; set; } = 3; public int MaxConcurrency { get; set; } = 1; } \ 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/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/Instructs/Contexts/CodeInstructContext.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Contexts/CodeInstructContext.cs deleted file mode 100644 index 77e37efaa..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/Contexts/CodeInstructContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BotSharp.Abstraction.Instructs.Contexts; - -public class CodeInstructContext -{ - public string CodeScript { get; set; } - public string ScriptType { 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/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/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/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/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 52f43fb6b..374271b9f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -114,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/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/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 ac26508c5..ca0cbe7f7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Using.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Using.cs @@ -22,4 +22,5 @@ global using BotSharp.Abstraction.Crontab.Models; global using BotSharp.Abstraction.MCP.Models; 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.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 1b3006663..5442134cf 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 = await processor.RunAsync(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?.ToString() ?? string.Empty + }; + + 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/Services/AgentService.Coding.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Coding.cs index ccb9de56f..ea9d336f0 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Coding.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Coding.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Agents.Options; using BotSharp.Abstraction.Coding; +using BotSharp.Abstraction.Coding.Enums; using BotSharp.Abstraction.Coding.Options; namespace BotSharp.Core.Agents.Services; @@ -13,7 +14,7 @@ public async Task> GetAgentCodeScripts(string agentId, Age return await Task.FromResult(scripts); } - public async Task GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) + public async Task GetAgentCodeScript(string agentId, string scriptName, string scriptType = AgentCodeScriptType.Src) { var db = _services.GetRequiredService(); var script = db.GetAgentCodeScript(agentId, scriptName, scriptType); @@ -66,7 +67,7 @@ public async Task DeleteAgentCodeScripts(string agentId, List GenerateCodeScript(string agentId, string text, CodeProcessOptions? options = null) + public async Task GenerateCodeScript(string agentId, string text, CodeGenHandleOptions? options = null) { if (string.IsNullOrWhiteSpace(agentId)) { @@ -78,7 +79,7 @@ public async Task GenerateCodeScript(string agentId, strin var settings = _services.GetRequiredService(); var processor = options?.Processor ?? settings?.CodeGeneration?.Processor; - processor = !string.IsNullOrEmpty(processor) ? processor : "botsharp-py-interpreter"; + processor = !string.IsNullOrEmpty(processor) ? processor : BuiltInCodeProcessor.PyInterpreter; var codeProcessor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(processor)); if (codeProcessor == null) { 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 index a7aae26e9..a735c4dc3 100644 --- a/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Coding/CodeScriptExecutor.cs @@ -2,9 +2,11 @@ namespace BotSharp.Core.Coding; public class CodeScriptExecutor { + private const int DEFAULT_MAX_CONCURRENCY = 1; + private readonly CodingSettings _settings; private readonly ILogger _logger; - private readonly SemaphoreSlim _semLock = new(initialCount: 1, maxCount: 1); + private readonly SemaphoreSlim _semLock = new(initialCount: DEFAULT_MAX_CONCURRENCY, maxCount: DEFAULT_MAX_CONCURRENCY); public CodeScriptExecutor( CodingSettings settings, @@ -13,16 +15,15 @@ public CodeScriptExecutor( _settings = settings; _logger = logger; - var maxConcurrency = settings.CodeExecution?.MaxConcurrency > 0 ? settings.CodeExecution.MaxConcurrency : 1; + var maxConcurrency = settings.CodeExecution?.MaxConcurrency > 0 ? settings.CodeExecution.MaxConcurrency : DEFAULT_MAX_CONCURRENCY; _semLock = new(initialCount: maxConcurrency, maxCount: maxConcurrency); } public async Task ExecuteAsync(Func> func, CancellationToken cancellationToken = default) { - await _semLock.WaitAsync(cancellationToken); - try { + await _semLock.WaitAsync(cancellationToken); return await func(); } catch (Exception ex) 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/Instructs/Services/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs index bf19113e5..8e4d3ec6d 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs @@ -1,15 +1,13 @@ using BotSharp.Abstraction.Coding; -using BotSharp.Abstraction.Coding.Responses; +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; using BotSharp.Abstraction.Models; -using Microsoft.Extensions.Options; -using static Dapper.SqlMapper; namespace BotSharp.Core.Instructs; @@ -184,7 +182,7 @@ await hook.OnResponseGenerated(new InstructResponseModel var hooks = _services.GetHooks(agent.Id); var codeProvider = codeOptions?.Processor ?? codingSettings.CodeExecution?.Processor; - codeProvider = !string.IsNullOrEmpty(codeProvider) ? codeProvider : "botsharp-py-interpreter"; + codeProvider = !string.IsNullOrEmpty(codeProvider) ? codeProvider : BuiltInCodeProcessor.PyInterpreter; var codeProcessor = _services.GetServices() .FirstOrDefault(x => x.Provider.IsEqualTo(codeProvider)); @@ -219,7 +217,7 @@ await hook.OnResponseGenerated(new InstructResponseModel // Get code script var scriptType = codeOptions?.ScriptType ?? AgentCodeScriptType.Src; var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType); - if (string.IsNullOrWhiteSpace(codeScript)) + if (string.IsNullOrWhiteSpace(codeScript?.Content)) { #if DEBUG _logger.LogWarning($"Empty code script. (Agent: {agent.Id}, {scriptName})"); @@ -234,10 +232,9 @@ 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, - ScriptType = scriptType, Arguments = arguments }; @@ -245,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) @@ -261,46 +258,38 @@ await hook.OnResponseGenerated(new InstructResponseModel // Run code script var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(codingSettings); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - var codeResponse = await codeProcessor.RunAsync(context.CodeScript, options: new() + var codeResponse = await codeProcessor.RunAsync(context.CodeScript?.Content ?? string.Empty, options: new() { - ScriptName = scriptName, + ScriptName = context.CodeScript?.Name, Arguments = context.Arguments, UseLock = useLock, UseProcess = useProcess }, cancellationToken: cts.Token); - if (codeResponse == null || !codeResponse.Success) + if (codeResponse?.Success == true) { - return response; + response = new InstructResult + { + MessageId = message.MessageId, + Template = context.CodeScript?.Name, + Text = codeResponse.Result + }; } - response = new InstructResult + var codeExeResponse = new CodeExecutionResponseModel { - MessageId = message.MessageId, - Template = scriptName, - Text = codeResponse.Result + CodeProcessor = codeProcessor.Provider, + CodeScript = context.CodeScript, + ExecutionResult = codeResponse?.ToString() ?? string.Empty, + Text = message.Content, + Arguments = context.Arguments?.DistinctBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value ?? string.Empty) }; - if (context?.Arguments != null) - { - context.Arguments.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - } - // 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, response ?? new()); + await hook.AfterCodeExecution(agent, codeExeResponse); } return response; @@ -348,13 +337,12 @@ private async Task GetChatCompletion( /// private (bool, bool, int) GetCodeExecutionConfig(CodingSettings settings) { - var useLock = false; - var useProcess = false; - var timeoutSeconds = 3; + var codeExecution = settings.CodeExecution; + var defaultTimeoutSeconds = 3; - useLock = settings.CodeExecution?.UseLock ?? useLock; - useProcess = settings.CodeExecution?.UseProcess ?? useProcess; - timeoutSeconds = settings.CodeExecution?.TimeoutSeconds > 0 ? settings.CodeExecution.TimeoutSeconds.Value : timeoutSeconds; + 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/Repository/FileRepository/FileRepository.AgentCodeScript.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.AgentCodeScript.cs index 7ebfdc84f..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) @@ -66,11 +66,20 @@ public List GetAgentCodeScripts(string agentId, AgentCodeScript } var foundFile = Directory.EnumerateFiles(dir).FirstOrDefault(file => scriptName.IsEqualTo(Path.GetFileName(file))); - if (!string.IsNullOrEmpty(foundFile)) + 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) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs index 396e338f7..7c999b85f 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs @@ -1,5 +1,7 @@ using BotSharp.Abstraction.Loggers.Models; using System.IO; +using System.Text.RegularExpressions; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace BotSharp.Core.Repository { @@ -241,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); 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/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..1b2b43748 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, + 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 index 55c3381bb..40800d35c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs @@ -61,7 +61,8 @@ public async Task GenerateAgentCodeScript([FromRoute] stri var states = request.Options?.Data?.ToList(); var state = _services.GetRequiredService(); states?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - state.SetState("programming_language", request.Options?.Language, 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 index 4daefc717..52fd719fd 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -6,13 +6,16 @@ namespace BotSharp.OpenAPI.Controllers; public partial class AgentController { [HttpGet("/rule/triggers")] - public IEnumerable GetRuleTriggers() + public IEnumerable GetRuleTriggers() { var triggers = _services.GetServices(); - return triggers.Select(x => new AgentRule + return triggers.Select(x => new AgentRuleViewModel { - TriggerName = x.GetType().Name - }).OrderBy(x => x.TriggerName).ToList(); + TriggerName = x.Name, + Channel = x.Channel, + Statement = x.Statement, + OutputArgs = x.OutputArgs + }).OrderBy(x => x.TriggerName); } [HttpGet("/rule/formalization")] diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/GoogleController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Application/GoogleController.cs similarity index 93% rename from src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/GoogleController.cs rename to src/Infrastructure/BotSharp.OpenAPI/Controllers/Application/GoogleController.cs index 13faeb2d1..5497d6c2e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/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/Instruct/InstructModeController.File.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.File.cs index 031821433..9c348d8b6 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.File.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.File.cs @@ -24,7 +24,7 @@ public async Task PdfCompletion([FromBody] PdfReadFileRe Model = request.Model, AgentId = request.AgentId, TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); viewModel.Success = true; @@ -62,7 +62,7 @@ public async Task PdfCompletion([FromForm] IEnumerable ComposeImages([FromBody] ImageCompos Model = request.Model, AgentId = request.AgentId, TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); imageViewModel.Success = true; @@ -103,7 +103,7 @@ public async Task ImageVariation([FromBody] ImageVaria Provider = request.Provider, Model = request.Model, AgentId = request.AgentId, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); imageViewModel.Success = true; @@ -142,7 +142,7 @@ 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; @@ -182,7 +182,7 @@ public async Task ImageEdit([FromBody] ImageEditFileRe Model = request.Model, AgentId = request.AgentId, TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); imageViewModel.Success = true; @@ -222,7 +222,7 @@ 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; @@ -264,7 +264,7 @@ public async Task ImageMaskEdit([FromBody] ImageMaskEd Model = request.Model, AgentId = request.AgentId, TemplateName = request.TemplateName, - ImageConvertProvider = request.ImageConvertProvider + ImageConverter = request.ImageConverter }); imageViewModel.Success = true; @@ -312,7 +312,7 @@ 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; diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBase/KnowledgeBaseController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBase/KnowledgeBaseController.cs index 923470b81..3a9a9e732 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBase/KnowledgeBaseController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBase/KnowledgeBaseController.cs @@ -207,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()) { @@ -231,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/User/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/User/UserController.cs index 24b34cbf2..472752cc1 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/User/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 index 7b3b1607f..3c68ac897 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptGenerationRequest.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCodeScriptGenerationRequest.cs @@ -10,5 +10,5 @@ public class AgentCodeScriptGenerationRequest [JsonPropertyName("options")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public CodeProcessOptions? Options { get; set; } + public CodeGenHandleOptions? Options { get; set; } } 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/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/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.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.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.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/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.AgentCodeScript.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentCodeScript.cs index 86285583d..cc876e465 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentCodeScript.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentCodeScript.cs @@ -35,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) @@ -53,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) 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.PythonInterpreter/Functions/PyProgrammerFn.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs index 8e96a13ea..bdbebcab1 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs @@ -110,7 +110,7 @@ public async Task Execute(RoleDialogModel message) private async Task<(bool, string)> InnerRunCode(string codeScript) { var codeProvider = _codingSettings.CodeExecution?.Processor; - codeProvider = !string.IsNullOrEmpty(codeProvider) ? codeProvider : "botsharp-py-interpreter"; + codeProvider = !string.IsNullOrEmpty(codeProvider) ? codeProvider : BuiltInCodeProcessor.PyInterpreter; var processor = _services.GetServices() .FirstOrDefault(x => x.Provider.IsEqualTo(codeProvider)); @@ -121,6 +121,7 @@ public async Task Execute(RoleDialogModel message) var (useLock, useProcess, timeoutSeconds) = GetCodeExecutionConfig(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); + var response = await processor.RunAsync(codeScript, options: new() { UseLock = useLock, @@ -203,13 +204,12 @@ private AgentLlmConfig GetLlmConfig() private (bool, bool, int) GetCodeExecutionConfig() { - var useLock = false; - var useProcess = false; - var timeoutSeconds = 3; + var codeExecution = _codingSettings.CodeExecution; + var defaultTimeoutSeconds = 10; - useLock = _codingSettings.CodeExecution?.UseLock ?? useLock; - useProcess = _codingSettings.CodeExecution?.UseProcess ?? useProcess; - timeoutSeconds = _codingSettings.CodeExecution?.TimeoutSeconds > 0 ? _codingSettings.CodeExecution.TimeoutSeconds.Value : timeoutSeconds; + 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/Services/PyCodeInterpreter.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs index d88d3fb6c..815fab72d 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Coding.Models; -using BotSharp.Abstraction.Coding.Settings; using Microsoft.Extensions.Logging; using Python.Runtime; using System.Diagnostics; @@ -7,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; + namespace BotSharp.Plugin.PythonInterpreter.Services; public class PyCodeInterpreter : ICodeProcessor @@ -28,7 +27,7 @@ public PyCodeInterpreter( _settings = settings; } - public string Provider => "botsharp-py-interpreter"; + public string Provider => BuiltInCodeProcessor.PyInterpreter; public async Task RunAsync(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default) { @@ -47,7 +46,7 @@ public async Task RunAsync(string codeScript, CodeInterpr return new() { ErrorMsg = ex.Message }; } } - + return await InnerRunCode(codeScript, options, cancellationToken); } @@ -63,7 +62,7 @@ public async Task GenerateCodeScriptAsync(string text, Cod var agentService = _services.GetRequiredService(); agent = await agentService.GetAgent(agentId); } - + var instruction = string.Empty; if (agent != null && !string.IsNullOrEmpty(templateName)) { @@ -85,7 +84,7 @@ public async Task GenerateCodeScriptAsync(string text, Cod }, 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 @@ -100,7 +99,7 @@ public async Task GenerateCodeScriptAsync(string text, Cod { Success = true, Content = response.Content, - Language = options?.Language ?? "python" + Language = options?.ProgrammingLanguage ?? "python" }; } @@ -123,7 +122,7 @@ private async Task InnerRunCode(string codeScript, CodeIn { response = await CoreRunScript(codeScript, options, cancellationToken); } - + _logger.LogWarning($"End running python code script in {Provider}: {scriptName}"); return response; @@ -162,6 +161,21 @@ private async Task CoreRunScript(string codeScript, CodeI try { + // Capture the Python thread ID for the current thread executing under the GIL + var pythonThreadId = PythonEngine.GetPythonThreadID(); + using var reg = cancellationToken.Register(() => + { + try + { + PythonEngine.Interrupt(pythonThreadId); + _logger.LogWarning($"Cancellation requested: issued PythonEngine.Interrupt for thread {pythonThreadId} (request {requestId})"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to interrupt Python execution on cancellation."); + } + }); + // Redirect standard output/error to capture it dynamic outIO = io.StringIO(); dynamic errIO = io.StringIO(); 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/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index cf0abd413..be70e0d47 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -384,6 +384,49 @@ "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 + } } ] },