diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs index baa404d5b..afb42dac5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/MessageInfo.cs @@ -12,5 +12,5 @@ public class MessageInfo : ICacheKey public string StepId { get; set; } = Guid.NewGuid().ToString(); public string GetCacheKey() - => $"{nameof(MessageInfo)}-{ContextId}"; + => $"{nameof(MessageInfo)}"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Planning/IPlanningHook.cs b/src/Infrastructure/BotSharp.Abstraction/Planning/IPlanningHook.cs new file mode 100644 index 000000000..3f258c987 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Planning/IPlanningHook.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Planning; + +public interface IPlanningHook +{ + Task GetSummaryAdditionalRequirements(string planner) + => Task.FromResult(string.Empty); + Task OnPlanningCompleted(string planner, RoleDialogModel msg) + => Task.CompletedTask; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index b56dbfb64..ee7f225c5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -20,6 +20,7 @@ public interface IBotSharpRepository #region User User? GetUserByEmail(string email) => throw new NotImplementedException(); + User? GetUserByPhone(string phone) => throw new NotImplementedException(); User? GetUserById(string id) => throw new NotImplementedException(); User? GetUserByUserName(string userName) => throw new NotImplementedException(); void CreateUser(User user) => throw new NotImplementedException(); diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index ff10a5176..99f8b6ae9 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -180,6 +180,7 @@ + diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index 200889816..26a5ccb54 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -30,8 +30,10 @@ public static IServiceCollection AddBotSharpCore(this IServiceCollection service var cacheSettings = new SharpCacheSettings(); config.Bind("SharpCache", cacheSettings); services.AddSingleton(x => cacheSettings); - services.AddSingleton(); - + services.AddSingleton(); + + services.AddMemoryCache(); + RegisterPlugins(services, config); ConfigureBotSharpOptions(services, configOptions); diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/MemoryCacheService.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/MemoryCacheService.cs new file mode 100644 index 000000000..2d2fe13ba --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/MemoryCacheService.cs @@ -0,0 +1,35 @@ +using BotSharp.Abstraction.Infrastructures; +using Microsoft.Extensions.Caching.Memory; + +namespace BotSharp.Core.Infrastructures; + +public class MemoryCacheService : ICacheService +{ + private static IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions + { + }); + private readonly BotSharpDatabaseSettings _settings; + + public MemoryCacheService(BotSharpDatabaseSettings settings) + { + _settings = settings; + } + + public async Task GetAsync(string key) + { + return (T?)(_cache.Get(key) ?? default(T)); + } + + public async Task GetAsync(string key, Type type) + { + return _cache.Get(key) ?? default; + } + + public async Task SetAsync(string key, T value, TimeSpan? expiry) + { + _cache.Set(key, value, new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = expiry + }); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/CacheService.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/RedisCacheService.cs similarity index 87% rename from src/Infrastructure/BotSharp.Core/Infrastructures/CacheService.cs rename to src/Infrastructure/BotSharp.Core/Infrastructures/RedisCacheService.cs index 45767fc2b..279f90148 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/CacheService.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/RedisCacheService.cs @@ -4,12 +4,12 @@ namespace BotSharp.Core.Infrastructures; -public class CacheService : ICacheService +public class RedisCacheService : ICacheService { private readonly BotSharpDatabaseSettings _settings; private static ConnectionMultiplexer redis = null!; - public CacheService(BotSharpDatabaseSettings settings) + public RedisCacheService(BotSharpDatabaseSettings settings) { _settings = settings; } @@ -68,6 +68,11 @@ public async Task SetAsync(string key, T value, TimeSpan? expiry) return; } + if (redis == null) + { + redis = ConnectionMultiplexer.Connect(_settings.Redis); + } + var db = redis.GetDatabase(); await db.StringSetAsync(key, JsonConvert.SerializeObject(value), expiry); } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index 32a5127bf..447bb82cd 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -11,6 +11,11 @@ public partial class FileRepository return Users.FirstOrDefault(x => x.Email == email.ToLower()); } + public User? GetUserByPhone(string phone) + { + return Users.FirstOrDefault(x => x.Phone == phone); + } + public User? GetUserById(string id = null) { return Users.FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id)); diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 0e7f14ee4..42b708d6f 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -156,12 +156,10 @@ record = db.GetUserByUserName(id); return default; } -#if !DEBUG if (!isAuthenticatedByHook && Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password) { return default; } -#endif var accessToken = GenerateJwtToken(record); var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); @@ -325,8 +323,24 @@ public async Task VerifyEmailExisting(string email) public async Task SendVerificationCodeResetPassword(User user) { + if (!string.IsNullOrEmpty(user.Email) && !string.IsNullOrEmpty(user.Phone)) + { + return false; + } + var db = _services.GetRequiredService(); - var record = db.GetUserByEmail(user.Email); + + User? record = null; + + if (!string.IsNullOrEmpty(user.Email)) + { + record = db.GetUserByEmail(user.Email); + } + + if (!string.IsNullOrEmpty(user.Phone)) + { + record = db.GetUserByPhone(user.Phone); + } if (record == null) { return false; @@ -349,8 +363,23 @@ public async Task SendVerificationCodeResetPassword(User user) public async Task ResetUserPassword(User user) { + if (!string.IsNullOrEmpty(user.Email) && !string.IsNullOrEmpty(user.Phone)) + { + return false; + } var db = _services.GetRequiredService(); - var record = db.GetUserByEmail(user.Email); + + User? record = null; + + if (!string.IsNullOrEmpty(user.Email)) + { + record = db.GetUserByEmail(user.Email); + } + + if (!string.IsNullOrEmpty(user.Phone)) + { + record = db.GetUserByPhone(user.Phone); + } if (record == null) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index a7b481e28..ed9604acb 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -110,7 +110,7 @@ public async Task VerifyEmailExisting([FromQuery] string email) } [AllowAnonymous] [HttpPost("/user/verifycode")] - public async Task SendVerificationCodeResetPassword([FromQuery] UserCreationModel user) + public async Task SendVerificationCodeResetPassword([FromBody] UserCreationModel user) { return await _userService.SendVerificationCodeResetPassword(user.ToUser()); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index 17c9d0cd8..4f8da1564 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -10,6 +10,12 @@ public partial class MongoRepository return user != null ? user.ToUser() : null; } + public User? GetUserByPhone(string phone) + { + var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Phone == phone); + return user != null ? user.ToUser() : null; + } + public User? GetUserById(string id) { var user = _dc.Users.AsQueryable() diff --git a/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj b/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj index 5d06846f7..172c5eb96 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj +++ b/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj @@ -16,8 +16,10 @@ - + + + @@ -41,7 +43,13 @@ PreserveNewest - + + PreserveNewest + + + PreserveNewest + + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs index 71f2bf815..d2ef2e5c8 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs @@ -5,7 +5,7 @@ namespace BotSharp.Plugin.Planner.Functions; public class PrimaryStagePlanFn : IFunctionCallback { public string Name => "plan_primary_stage"; - + public string Indication => "Currently analyzing and breaking down user requirements."; private readonly IServiceProvider _services; private readonly ILogger _logger; diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs index 9ff3a002d..29a74364e 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs @@ -5,7 +5,7 @@ namespace BotSharp.Plugin.Planner.Functions; public class SecondaryStagePlanFn : IFunctionCallback { public string Name => "plan_secondary_stage"; - + public string Indication => "Further analyzing and breaking down user sub-needs."; private readonly IServiceProvider _services; private readonly ILogger _logger; diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs index c49889bc3..430caf19e 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Planning; +using BotSharp.Plugin.Planner.TwoStaging; using BotSharp.Plugin.Planner.TwoStaging.Models; namespace BotSharp.Plugin.Planner.Functions; @@ -5,7 +7,7 @@ namespace BotSharp.Plugin.Planner.Functions; public class SummaryPlanFn : IFunctionCallback { public string Name => "plan_summary"; - + public string Indication => "Organizing and summarizing the final output results."; private readonly IServiceProvider _services; private readonly ILogger _logger; @@ -62,7 +64,9 @@ public async Task Execute(RoleDialogModel message) var summary = await GetAiResponse(plannerAgent); message.Content = summary.Content; - message.StopCompletion = true; + + await HookEmitter.Emit(_services, x => + x.OnPlanningCompleted(nameof(TwoStageTaskPlanner), message)); return true; } @@ -74,18 +78,20 @@ private async Task GetSummaryPlanPrompt(string taskDescription, string r var agent = await agentService.GetAgent(BuiltInAgentId.Planner); var template = agent.Templates.FirstOrDefault(x => x.Name == "two_stage.summarize")?.Content ?? string.Empty; - var responseFormat = JsonSerializer.Serialize(new FirstStagePlan + + var additionalRequirements = new List(); + await HookEmitter.Emit(_services, async x => { - Parameters = [JsonDocument.Parse("{}")], - Results = [""] + var requirement = await x.GetSummaryAdditionalRequirements(nameof(TwoStageTaskPlanner)); + additionalRequirements.Add(requirement); }); return render.Render(template, new Dictionary { - { "table_structure", ddlStatement }, { "task_description", taskDescription }, + { "summary_requirements", string.Join("\r\n",additionalRequirements) }, { "relevant_knowledges", relevantKnowledge }, - { "response_format", responseFormat } + { "table_structure", ddlStatement }, }); } private async Task GetAiResponse(Agent plannerAgent) @@ -94,8 +100,8 @@ private async Task GetAiResponse(Agent plannerAgent) var wholeDialogs = conv.GetDialogHistory(); // Append text - wholeDialogs.Last().Content += "\n\nIf the table structure didn't mention auto incremental, the data field id needs to insert id manually and you need to use max(id) instead of LAST_INSERT_ID function.\nFor example, you should use SET @id = select max(id) from table;"; - wholeDialogs.Last().Content += "\n\nTry if you can generate a single query to fulfill the needs"; + wholeDialogs.Last().Content += "\n\nIf the table structure didn't mention auto incremental, the data field id needs to insert id manually and you need to use max(id).\nFor example, you should use SET @id = select max(id) from table;"; + wholeDialogs.Last().Content += "\n\nTry if you can generate a single query to fulfill the needs."; var completion = CompletionProvider.GetChatCompletion(_services, provider: plannerAgent.LlmConfig.Provider, diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SecondStagePlan.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SecondStagePlan.cs index f1292856d..d4f5dfcfe 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SecondStagePlan.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SecondStagePlan.cs @@ -3,7 +3,7 @@ namespace BotSharp.Plugin.Planner.TwoStaging.Models; public class SecondStagePlan { [JsonPropertyName("related_tables")] - public string[] Tables { get; set; } = new string[0]; + public string[] Tables { get; set; } = []; [JsonPropertyName("description")] public string Description { get; set; } = ""; @@ -12,8 +12,8 @@ public class SecondStagePlan public string Tool { get; set; } = ""; [JsonPropertyName("input_args")] - public JsonDocument[] Parameters { get; set; } = new JsonDocument[0]; + public JsonDocument[] Parameters { get; set; } = []; [JsonPropertyName("output_results")] - public string[] Results { get; set; } = new string[0]; + public string[] Results { get; set; } = []; } diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs index 5694a39f2..06050d868 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs @@ -18,8 +18,8 @@ public TwoStageTaskPlanner(IServiceProvider services, ILogger GetNextInstruction(Agent router, string messageId, List dialogs) { - var nextStepPrompt = await GetNextStepPrompt(router); var inst = new FunctionCallFromLlm(); + var nextStepPrompt = await GetNextStepPrompt(router); // chat completion var completion = CompletionProvider.GetChatCompletion(_services, @@ -125,7 +125,7 @@ private async Task GetNextStepPrompt(Agent router) { var agentService = _services.GetRequiredService(); var planner = await agentService.LoadAgent(BuiltInAgentId.Planner); - var template = planner.Templates.First(x => x.Name == "two_stage.1st.next").Content; + var template = planner.Templates.First(x => x.Name == "two_stage.next").Content; var states = _services.GetRequiredService(); var render = _services.GetRequiredService(); return render.Render(template, new Dictionary diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json index 5cb384f7d..d4e8bb778 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json @@ -11,8 +11,8 @@ "profiles": [ "planning" ], "utilities": [ "two-stage-planner" ], "llmConfig": { - "provider": "anthropic", - "model": "claude-3-5-sonnet-20240620", + "provider": "azure-openai", + "model": "gpt-4o", "max_recursion_depth": 10 } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid index 93adc53ea..0cf3094d5 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid @@ -3,6 +3,10 @@ Use the TwoStagePlanner approach to plan the overall implementation steps, follo 2. If need_additional_information is true, call plan_secondary_stage for the specific primary stage. 3. You must call plan_summary as the last planning step to summarize the final query. +*** IMPORTANT *** +Don't run the planning process repeatedly if you have already got the result of user's request. + + {% if global_knowledges != empty -%} ===== Global Knowledge: diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/database.summarize.mysql.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/database.summarize.mysql.liquid new file mode 100644 index 000000000..b756d6222 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/database.summarize.mysql.liquid @@ -0,0 +1,18 @@ +Try if you can generate a single query to fulfill the needs. The step should contains all needed parameters. +The parameters can be extracted from the original task. +If not, generate the query step by step based on the planning. + +The query must exactly based on the provided table structure. And carefully review the foreign keys to make sure you include all the accurate information. + +Note: Output should be only the sql query with sql comments that can be directly run in mysql database with version 8.0. + +Don't use the sql statement that specify target table for update in FROM clause. +For example, you CAN'T write query as below: +INSERT INTO data_Service (Id, Name) +VALUES ((SELECT MAX(Id) + 1 FROM data_Service), 'HVAC'); + +If the table structure didn't mention auto incremental, the data field id needs to insert id manually and you need to use max(id) instead of LAST_INSERT_ID function. +For example, you should use SET @id = select max(id) from table; + +* the alias of the table name in the sql query should be identical. +*** the generated sql query MUST be basedd on the provided table structure. *** diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/database.summarize.sqlserver.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/database.summarize.sqlserver.liquid new file mode 100644 index 000000000..16c989c4e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/database.summarize.sqlserver.liquid @@ -0,0 +1,12 @@ +Try if you can generate a SQL Server single query to fulfill the needs. The step should contains all needed parameters. +The parameters can be extracted from the original task. +If not, generate the query step by step based on the planning. + +The query must exactly based on the provided table structure. And carefully review the foreign keys to make sure you include all the accurate information. + +Note: Output should be only the sql query with sql comments that can be directly run in SQL Server. + +*** the alias of the table name in the sql query should be identical. *** +*** The generated sql query MUST be basedd on the provided table structure. *** +*** All queries return a maximum of 10 records. *** +*** Only select user friendly columns. *** diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.next.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.next.liquid similarity index 100% rename from src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.next.liquid rename to src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.next.liquid diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid index 5bfb2b6c4..9b7ced71d 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid @@ -1,25 +1,7 @@ -You are a planning summarizer and sql generator. You will convert the requirement into the excutable MySQL query statement based on the task description and related table structure and relationship. +You are a planning summarizer. You will generate the final output in JSON format based on the task description, knowledge and related table structure and relationship. -Try if you can generate a single query to fulfill the needs. The step should contains all needed parameters. -The parameters can be extracted from the original task. -If not, generate the query step by step based on the planning. - -The query must exactly based on the provided table structure. And carefully review the foreign keys to make sure you include all the accurate information. - - -Note: Output should be only the sql query with sql comments that can be directly run in mysql database with version 8.0. - -Don't use the sql statement that specify target table for update in FROM clause. -For example, you CAN'T write query as below: -INSERT INTO data_Service (Id, Name) -VALUES ((SELECT MAX(Id) + 1 FROM data_Service), 'HVAC'); - -If the table structure didn't mention auto incremental, the data field id needs to insert id manually and you need to use max(id) instead of LAST_INSERT_ID function. -For example, you should use SET @id = select max(id) from table; - -Additional Requirements: -* the alias of the table name in the sql query should be identical. -*** the generated sql query MUST be basedd on the provided table structure. *** +Requirements: +{{ summary_requirements }} ===== Task description: @@ -31,4 +13,4 @@ Relevant Knowledges: ===== Table Structure: -{{ table_structure }} \ No newline at end of file +{{ table_structure }} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj index 50da7a837..295fad70f 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj @@ -10,12 +10,19 @@ $(SolutionDir)packages + + + + + + + @@ -33,6 +40,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs index d6e2af4aa..3dcb1910f 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs @@ -1,25 +1,50 @@ +using BotSharp.Plugin.SqlDriver.Models; +using Dapper; +using Microsoft.Data.SqlClient; +using MySqlConnector; + namespace BotSharp.Plugin.SqlDriver.Functions; public class ExecuteQueryFn : IFunctionCallback { public string Name => "execute_sql"; - + public string Indication => "Performing data retrieval operation."; private readonly SqlDriverSetting _setting; + private readonly IServiceProvider _services; - public ExecuteQueryFn(SqlDriverSetting setting) + public ExecuteQueryFn(IServiceProvider services, SqlDriverSetting setting) { + _services = services; _setting = setting; } public async Task Execute(RoleDialogModel message) { - message.Content = "Executed"; - /*using var connection = new MySqlConnection(_setting.MySqlConnectionString); - message.Content = JsonSerializer.Serialize(connection.Query(args.SqlStatement), new JsonSerializerOptions + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var settings = _services.GetRequiredService(); + var results = settings.DatabaseType switch { - WriteIndented = true, - });*/ - // message.StopCompletion = true; + "MySql" => RunQueryInMySql(args.SqlStatements), + "SqlServer" => RunQueryInSqlServer(args.SqlStatements), + _ => throw new NotImplementedException($"Database type {settings.DatabaseType} is not supported.") + }; + + message.Content = JsonSerializer.Serialize(results); return true; } + + private IEnumerable RunQueryInMySql(string[] sqlTexts) + { + var settings = _services.GetRequiredService(); + using var connection = new MySqlConnection(settings.MySqlExecutionConnectionString); + return connection.Query(string.Join(";\r\n", sqlTexts)); + } + + private IEnumerable RunQueryInSqlServer(string[] sqlTexts) + { + var settings = _services.GetRequiredService(); + using var connection = new SqlConnection(settings.SqlServerExecutionConnectionString ?? settings.SqlServerConnectionString); + var dictionary = new Dictionary(); + return connection.Query(string.Join("\r\n", sqlTexts)); + } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs index f98b0da48..cf7d26c6d 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs @@ -1,4 +1,6 @@ using BotSharp.Plugin.SqlDriver.Models; +using Fluid.Ast.BinaryExpressions; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using MySqlConnector; @@ -7,6 +9,7 @@ namespace BotSharp.Plugin.SqlDriver.Functions; public class GetTableDefinitionFn : IFunctionCallback { public string Name => "get_table_definition"; + public string Indication => "Obtain the relevant data structure definitions."; private readonly IServiceProvider _services; private readonly ILogger _logger; @@ -23,11 +26,24 @@ public async Task Execute(RoleDialogModel message) var args = JsonSerializer.Deserialize(message.FunctionArgs); var tables = new string[] { args.Table }; var agentService = _services.GetRequiredService(); - var sqlDriver = _services.GetRequiredService(); var settings = _services.GetRequiredService(); // Get table DDL from database + var tableDdls = settings.DatabaseType switch + { + "MySql" => GetDdlFromMySql(tables), + "SqlServer" => GetDdlFromSqlServer(tables), + _ => throw new NotImplementedException($"Database type {settings.DatabaseType} is not supported.") + }; + + message.Content = string.Join("\r\n\r\n", tableDdls); + + return true; + } + private List GetDdlFromMySql(string[] tables) + { + var settings = _services.GetRequiredService(); var tableDdls = new List(); using var connection = new MySqlConnection(settings.MySqlExecutionConnectionString); connection.Open(); @@ -57,7 +73,60 @@ public async Task Execute(RoleDialogModel message) } connection.Close(); - message.Content = string.Join("\r\n\r\n", tableDdls); - return true; + + return tableDdls; + } + + private List GetDdlFromSqlServer(string[] tables) + { + var settings = _services.GetRequiredService(); + var tableDdls = new List(); + using var connection = new SqlConnection(settings.SqlServerExecutionConnectionString ?? settings.SqlServerConnectionString); + connection.Open(); + + foreach (var table in tables) + { + try + { + var sql = @$"DECLARE @TableName NVARCHAR(128) = '{table}'; +DECLARE @SQL NVARCHAR(MAX) = 'CREATE TABLE ' + @TableName + ' ('; + +SELECT @SQL = @SQL + ' + ' + COLUMN_NAME + ' ' + + DATA_TYPE + + CASE + WHEN CHARACTER_MAXIMUM_LENGTH IS NOT NULL AND DATA_TYPE LIKE '%char%' + THEN '(' + CAST(CHARACTER_MAXIMUM_LENGTH AS VARCHAR(10)) + ')' + WHEN DATA_TYPE IN ('decimal', 'numeric') + THEN '(' + CAST(NUMERIC_PRECISION AS VARCHAR(10)) + ',' + CAST(NUMERIC_SCALE AS VARCHAR(10)) + ')' + ELSE '' + END + ' ' + + CASE WHEN IS_NULLABLE = 'NO' THEN 'NOT NULL' ELSE 'NULL' END + ',' +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_NAME = @TableName +ORDER BY ORDINAL_POSITION; + +-- Remove the last comma and add closing parenthesis +SET @SQL = LEFT(@SQL, LEN(@SQL) - 1) + ');'; + +SELECT @SQL;"; + + using var command = new SqlCommand(sql, connection); + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + var result = reader.GetString(0); + tableDdls.Add(result); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Error when getting ddl statement of table {table}. {ex.Message}\r\n{ex.InnerException}"); + } + } + + connection.Close(); + + return tableDdls; } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs index 87731dd83..cfc9765f5 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs @@ -1,4 +1,5 @@ using BotSharp.Plugin.SqlDriver.Models; +using Microsoft.Data.SqlClient; using MySqlConnector; using static Dapper.SqlMapper; @@ -26,13 +27,12 @@ public async Task Execute(RoleDialogModel message) // check if need to instantely var settings = _services.GetRequiredService(); - using var connection = new MySqlConnection(settings.MySqlExecutionConnectionString); - var dictionary = new Dictionary(); - foreach(var p in args.Parameters) + var result = settings.DatabaseType switch { - dictionary["@" + p.Name] = p.Value; - } - var result = connection.Query(args.Statement, dictionary); + "MySql" => RunQueryInMySql(args), + "SqlServer" => RunQueryInSqlServer(args), + _ => throw new NotImplementedException($"Database type {settings.DatabaseType} is not supported.") + }; if (result == null) { @@ -46,4 +46,28 @@ public async Task Execute(RoleDialogModel message) return true; } + + private IEnumerable RunQueryInMySql(SqlStatement args) + { + var settings = _services.GetRequiredService(); + using var connection = new MySqlConnection(settings.MySqlExecutionConnectionString); + var dictionary = new Dictionary(); + foreach (var p in args.Parameters) + { + dictionary["@" + p.Name] = p.Value; + } + return connection.Query(args.Statement, dictionary); + } + + private IEnumerable RunQueryInSqlServer(SqlStatement args) + { + var settings = _services.GetRequiredService(); + using var connection = new SqlConnection(settings.SqlServerExecutionConnectionString ?? settings.SqlServerConnectionString); + var dictionary = new Dictionary(); + foreach (var p in args.Parameters) + { + dictionary["@" + p.Name] = p.Value; + } + return connection.Query(args.Statement, dictionary); + } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs new file mode 100644 index 000000000..c867bbe76 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs @@ -0,0 +1,53 @@ +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Planning; +using BotSharp.Abstraction.Routing; +using BotSharp.Core.Agents.Services; +using BotSharp.Core.Infrastructures; + +namespace BotSharp.Plugin.SqlDriver.Hooks; + +public class SqlDriverPlanningHook : IPlanningHook +{ + private readonly IServiceProvider _services; + + public SqlDriverPlanningHook(IServiceProvider services) + { + _services = services; + } + + public async Task GetSummaryAdditionalRequirements(string planner) + { + var settings = _services.GetRequiredService(); + var agentService = _services.GetRequiredService(); + var agent = await agentService.GetAgent(BuiltInAgentId.Planner); + return agent.Templates.FirstOrDefault(x => x.Name == $"database.summarize.{settings.DatabaseType.ToLower()}")?.Content ?? string.Empty; + } + + public async Task OnPlanningCompleted(string planner, RoleDialogModel msg) + { + var settings = _services.GetRequiredService(); + if (!settings.ExecuteSqlSelectAutonomous) + { + return; + } + + var conv = _services.GetRequiredService(); + var wholeDialogs = conv.GetDialogHistory(); + wholeDialogs.Add(RoleDialogModel.From(msg)); + wholeDialogs.Add(RoleDialogModel.From(msg, AgentRole.User, "use execute_sql to run query")); + + var agent = await _services.GetRequiredService().LoadAgent("beda4c12-e1ec-4b4b-b328-3df4a6687c4f"); + + var completion = CompletionProvider.GetChatCompletion(_services, + provider: agent.LlmConfig.Provider, + model: agent.LlmConfig.Model); + + var response = await completion.GetChatCompletions(agent, wholeDialogs); + var routing = _services.GetRequiredService(); + await routing.InvokeFunction(response.FunctionName, response); + msg.CurrentAgentId = agent.Id; + msg.FunctionName = response.FunctionName; + msg.FunctionArgs = response.FunctionArgs; + msg.Content = response.Content; + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Models/ExecuteQueryArgs.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Models/ExecuteQueryArgs.cs new file mode 100644 index 000000000..b1531e641 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Models/ExecuteQueryArgs.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.SqlDriver.Models; + +public class ExecuteQueryArgs +{ + [JsonPropertyName("sql_statements")] + public string[] SqlStatements { get; set; } = []; +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs index 8f4d4d953..b594e776c 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs @@ -2,8 +2,11 @@ namespace BotSharp.Plugin.SqlHero.Settings; public class SqlDriverSetting { + public string DatabaseType { get; set; } = "MySql"; public string MySqlConnectionString { get; set; } = null!; public string MySqlExecutionConnectionString { get; set; } = null!; public string SqlServerConnectionString { get; set; } = null!; + public string SqlServerExecutionConnectionString { get; set; } = null!; public string SqlLiteConnectionString { get; set; } = null!; + public bool ExecuteSqlSelectAutonomous { get; set; } = false; } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs index c059edfb0..f6aab3987 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Planning; + namespace BotSharp.Plugin.SqlDriver; public class SqlDriverPlugin : IBotSharpPlugin @@ -20,5 +22,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json index 60309dffa..2df8a124c 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json @@ -9,7 +9,7 @@ "isPublic": true, "profiles": [ "database" ], "llmConfig": { - "provider": "openai", + "provider": "azure-openai", "model": "gpt-4o-mini" }, "routingRules": [ diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/execute_sql.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/execute_sql.json new file mode 100644 index 000000000..15e6d281d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/execute_sql.json @@ -0,0 +1,18 @@ +{ + "name": "execute_sql", + "description": "Run the sql statements provided in the last converastion", + "parameters": { + "type": "object", + "properties": { + "sql_statements": { + "type": "array", + "description": "raw sql statements", + "items": { + "type": "string", + "description": "sql statement" + } + } + }, + "required": [ "sql_statement" ] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs index 035c1f4e4..4a67ffeb1 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs @@ -203,15 +203,18 @@ public async Task NewPage(MessageInfo message, /// /// seconds /// - public async Task Wait(string ctxId, int timeout = 60) + public async Task Wait(string ctxId, int timeout = 10, bool waitNetworkIdle = true) { foreach (var page in _pages[ctxId]) { await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions + if (waitNetworkIdle) { - Timeout = 1000 * timeout - }); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions + { + Timeout = 1000 * timeout + }); + } } await Task.Delay(100); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs index 67a46f03f..ab1a07314 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs @@ -4,7 +4,7 @@ public partial class PlaywrightWebDriver { public async Task ActionOnElement(MessageInfo message, ElementLocatingArgs location, ElementActionArgs action) { - await _instance.Wait(message.ContextId); + await _instance.Wait(message.ContextId, waitNetworkIdle: false); var result = await LocateElement(message, location); if (result.IsSuccess) {