clean code of sql planner

This commit is contained in:
Haiping Chen 2025-01-18 04:06:51 +00:00
parent fe381066a5
commit 122f18fe1a
26 changed files with 265 additions and 103 deletions

View file

@ -72,10 +72,10 @@
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\functions\plan_primary_stage.json">
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\functions\sql_primary_stage.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\functions\plan_secondary_stage.json">
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\functions\sql_secondary_stage.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\functions\sql_review.json">
@ -87,16 +87,16 @@
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\instructions\instruction.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\two_stage.1st.plan.liquid">
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\sql.primary.plan.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\two_stage.2nd.plan.liquid">
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\sql.secondary.plan.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\two_stage.next.liquid">
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\sql.next.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\two_stage.summarize.liquid">
<Content Include="data\agents\da7aad2c-8112-48a2-ab7b-1f87da524741\templates\sql.generation.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

View file

@ -1,6 +1,8 @@
using BotSharp.Plugin.Planner.Sequential;
using BotSharp.Plugin.Planner.SqlGeneration;
using BotSharp.Plugin.Planner.SqlGeneration.Hooks;
using BotSharp.Plugin.Planner.TwoStaging;
using BotSharp.Plugin.Planner.TwoStaging.Hooks;
namespace BotSharp.Plugin.Planner;
@ -26,7 +28,7 @@ public class PlannerPlugin : IBotSharpPlugin
services.AddScoped<ITaskPlanner, SequentialPlanner>();
services.AddScoped<ITaskPlanner, TwoStageTaskPlanner>();
services.AddScoped<ITaskPlanner, SqlGenerationPlanner>();
services.AddScoped<IAgentHook, PlannerAgentHook>();
services.AddScoped<IAgentUtilityHook, PlannerUtilityHook>();
services.AddScoped<IAgentHook, SqlPlannerAgentHook>();
services.AddScoped<IAgentUtilityHook, TwoStagingPlannerUtilityHook>();
}
}

View file

@ -1,3 +1,4 @@
using BotSharp.Plugin.Planner.SqlGeneration;
using BotSharp.Plugin.Planner.TwoStaging;
using BotSharp.Plugin.Planner.TwoStaging.Models;
@ -54,12 +55,12 @@ public class SqlGenerationFn : IFunctionCallback
states.SetState("table_ddls", ddlStatements);
// Summarize and generate query
var prompt = await GetSummaryPlanPrompt(msgCopy, taskRequirement, domainKnowledge, dictionaryItems, ddlStatements, excelImportResult);
_logger.LogInformation($"Summary plan prompt:\r\n{prompt}");
var prompt = await GetSqlGenerationPrompt(msgCopy, taskRequirement, domainKnowledge, dictionaryItems, ddlStatements, excelImportResult);
_logger.LogInformation($"SQL Generation plan prompt:\r\n{prompt}");
var plannerAgent = new Agent
{
Id = PlannerAgentId.TwoStagePlanner,
Id = PlannerAgentId.SqlPlanner,
Name = Name,
Instruction = prompt,
LlmConfig = currentAgent.LlmConfig
@ -75,19 +76,19 @@ public class SqlGenerationFn : IFunctionCallback
return true;
}
private async Task<string> GetSummaryPlanPrompt(RoleDialogModel message, string taskDescription, string domainKnowledge, string dictionaryItems, string ddlStatement, string excelImportResult)
private async Task<string> GetSqlGenerationPrompt(RoleDialogModel message, string taskDescription, string domainKnowledge, string dictionaryItems, string ddlStatement, string excelImportResult)
{
var agentService = _services.GetRequiredService<IAgentService>();
var render = _services.GetRequiredService<ITemplateRender>();
var knowledgeHooks = _services.GetServices<IKnowledgeHook>();
var agent = await agentService.GetAgent(PlannerAgentId.TwoStagePlanner);
var template = agent.Templates.FirstOrDefault(x => x.Name == "two_stage.summarize")?.Content ?? string.Empty;
var agent = await agentService.GetAgent(PlannerAgentId.SqlPlanner);
var template = agent.Templates.FirstOrDefault(x => x.Name == "sql.generation")?.Content ?? string.Empty;
var additionalRequirements = new List<string>();
await HookEmitter.Emit<IPlanningHook>(_services, async x =>
{
var requirement = await x.GetSummaryAdditionalRequirements(nameof(TwoStageTaskPlanner), message);
var requirement = await x.GetSummaryAdditionalRequirements(nameof(SqlGenerationPlanner), message);
additionalRequirements.Add(requirement);
});

View file

@ -0,0 +1,105 @@
using BotSharp.Plugin.Planner.TwoStaging.Models;
namespace BotSharp.Plugin.Planner.SqlGeneration.Functions;
public class SqlPrimaryStageFn : IFunctionCallback
{
public string Name => "sql_primary_stage";
public string Indication => "Currently analyzing and breaking down user requirements.";
private readonly IServiceProvider _services;
private readonly ILogger<SqlPrimaryStageFn> _logger;
public SqlPrimaryStageFn(
IServiceProvider services,
ILogger<SqlPrimaryStageFn> logger)
{
_services = services;
_logger = logger;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var agentService = _services.GetRequiredService<IAgentService>();
var state = _services.GetRequiredService<IConversationStateService>();
state.SetState("max_tokens", "4096");
var task = JsonSerializer.Deserialize<PrimaryRequirementRequest>(message.FunctionArgs);
var searchQuestions = new List<string>(task.Questions);
searchQuestions.AddRange(task.NormQuestions);
searchQuestions = searchQuestions.Distinct().ToList();
// Get knowledge from vectordb
var hooks = _services.GetServices<IKnowledgeHook>();
var knowledges = new List<string>();
foreach (var question in searchQuestions)
{
foreach (var hook in hooks)
{
var k = await hook.GetDomainKnowledges(message, question);
knowledges.AddRange(k);
}
}
knowledges = knowledges.Distinct().ToList();
var knowledgeState = string.Join("\r\n", knowledges);
state.SetState("domain_knowledges", knowledgeState);
// Get first stage planning prompt
var currentAgent = await agentService.LoadAgent(message.CurrentAgentId);
var prompt = await GetPrimaryStagePlanPrompt(message, task.Requirements, knowledges);
var plannerAgent = new Agent
{
Id = message.CurrentAgentId,
Name = Name,
Instruction = prompt,
TemplateDict = new Dictionary<string, object>(),
LlmConfig = currentAgent.LlmConfig
};
var response = await GetAiResponse(plannerAgent);
message.Content = response.Content;
var states = _services.GetRequiredService<IConversationStateService>();
states.SetState("planning_result", response.Content);
return true;
}
private async Task<string> GetPrimaryStagePlanPrompt(RoleDialogModel message, string taskDescription, List<string> domainKnowledges)
{
var agentService = _services.GetRequiredService<IAgentService>();
var render = _services.GetRequiredService<ITemplateRender>();
var knowledgeHooks = _services.GetServices<IKnowledgeHook>();
var agent = await agentService.GetAgent(PlannerAgentId.SqlPlanner);
var template = agent.Templates.FirstOrDefault(x => x.Name == "sql.primary.plan")?.Content ?? string.Empty;
var responseFormat = JsonSerializer.Serialize(new FirstStagePlan { });
// Get global knowledges
var globalKnowledges = new List<string>();
foreach (var hook in knowledgeHooks)
{
var k = await hook.GetGlobalKnowledges(message);
globalKnowledges.AddRange(k);
}
return render.Render(template, new Dictionary<string, object>
{
{ "task_description", taskDescription },
{ "global_knowledges", globalKnowledges },
{ "domain_knowledges", domainKnowledges },
{ "response_format", responseFormat }
});
}
private async Task<RoleDialogModel> GetAiResponse(Agent plannerAgent)
{
var conv = _services.GetRequiredService<IConversationService>();
var wholeDialogs = conv.GetDialogHistory();
var completion = CompletionProvider.GetChatCompletion(_services,
provider: plannerAgent.LlmConfig.Provider,
model: plannerAgent.LlmConfig.Model);
return await completion.GetChatCompletions(plannerAgent, wholeDialogs);
}
}

View file

@ -1,6 +1,4 @@
using BotSharp.Plugin.Planner.SqlGeneration.Models;
using BotSharp.Plugin.Planner.TwoStaging;
using BotSharp.Plugin.Planner.TwoStaging.Models;
namespace BotSharp.Plugin.Planner.SqlGeneration.Functions;
@ -23,14 +21,16 @@ public class SqlReviewFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<SqlReviewArgs>(message.FunctionArgs);
if (!message.Content.StartsWith("```sql"))
{
message.Content = $"```sql\r\n{args.SqlStatement}\r\n```";
}
if (args != null && !args.IsSqlTemplate && args.ContainsSqlStatements)
{
await HookEmitter.Emit<IPlanningHook>(_services, async hook =>
await hook.OnSourceCodeGenerated(nameof(TwoStageTaskPlanner), message, "sql")
await hook.OnSourceCodeGenerated(nameof(SqlGenerationPlanner), message, "sql")
);
}
return true;

View file

@ -0,0 +1,106 @@
using BotSharp.Plugin.Planner.TwoStaging.Models;
namespace BotSharp.Plugin.Planner.SqlGeneration.Functions;
public class SqlSecondaryStageFn : IFunctionCallback
{
public string Name => "sql_secondary_stage";
public string Indication => "Further analyzing and breaking down user sub-needs.";
private readonly IServiceProvider _services;
private readonly ILogger<SqlSecondaryStageFn> _logger;
public SqlSecondaryStageFn(
IServiceProvider services,
ILogger<SqlSecondaryStageFn> logger)
{
_services = services;
_logger = logger;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var agentService = _services.GetRequiredService<IAgentService>();
var knowledgeService = _services.GetRequiredService<IKnowledgeService>();
var knowledgeSettings = _services.GetRequiredService<KnowledgeBaseSettings>();
var states = _services.GetRequiredService<IConversationStateService>();
var msgSecondary = RoleDialogModel.From(message);
var collectionName = knowledgeSettings.Default.CollectionName;
var planResult = states.GetState("planning_result");
var taskSecondary = JsonSerializer.Deserialize<SecondaryBreakdownTask>(msgSecondary.FunctionArgs);
// Search knowledgebase
var hooks = _services.GetServices<IKnowledgeHook>();
var knowledges = new List<string>();
foreach (var hook in hooks)
{
var k = await hook.GetDomainKnowledges(message, taskSecondary.SolutionQuestion);
knowledges.AddRange(k);
}
knowledges = knowledges.Distinct().ToList();
var knowledgeResults = string.Join("\r\n\r\n=====\r\n", knowledges);
var knowledgeState = states.GetState("domain_knowledges");
knowledgeState += string.Join("\r\n", knowledges);
states.SetState("domain_knowledges", knowledgeState);
// Get second stage planning prompt
var currentAgent = await agentService.LoadAgent(message.CurrentAgentId);
var prompt = await GetSecondStagePlanPrompt(taskSecondary.TaskDescription, planResult, knowledgeResults, message);
_logger.LogInformation(prompt);
var plannerAgent = new Agent
{
Id = PlannerAgentId.SqlPlanner,
Name = Name,
Instruction = prompt,
TemplateDict = new Dictionary<string, object>(),
LlmConfig = currentAgent.LlmConfig
};
var response = await GetAiResponse(plannerAgent);
message.Content = response.Content;
_logger.LogInformation(response.Content);
states.SetState("planning_result", response.Content);
return true;
}
private async Task<string> GetSecondStagePlanPrompt(string taskDescription, string planResult, string knowledgeResults, RoleDialogModel message)
{
var agentService = _services.GetRequiredService<IAgentService>();
var render = _services.GetRequiredService<ITemplateRender>();
var agent = await agentService.GetAgent(message.CurrentAgentId);
var template = agent.Templates.FirstOrDefault(x => x.Name == "sql.secondary.plan")?.Content ?? string.Empty;
var responseFormat = JsonSerializer.Serialize(new SecondStagePlan
{
Parameters = [JsonDocument.Parse("{}")],
Results = [string.Empty]
});
return render.Render(template, new Dictionary<string, object>
{
{ "task_description", taskDescription },
{ "primary_plan", planResult },
{ "additional_knowledge", knowledgeResults },
{ "response_format", responseFormat }
});
}
private async Task<RoleDialogModel> GetAiResponse(Agent plannerAgent)
{
var conv = _services.GetRequiredService<IConversationService>();
var wholeDialogs = conv.GetDialogHistory();
wholeDialogs.Last().Content += "\r\nOutput in JSON format.";
var completion = CompletionProvider.GetChatCompletion(_services,
provider: plannerAgent.LlmConfig.Provider,
model: plannerAgent.LlmConfig.Model);
return await completion.GetChatCompletions(plannerAgent, wholeDialogs);
}
}

View file

@ -1,10 +1,10 @@
namespace BotSharp.Plugin.Planner.Hooks;
namespace BotSharp.Plugin.Planner.SqlGeneration.Hooks;
public class PlannerAgentHook : AgentHookBase
public class SqlPlannerAgentHook : AgentHookBase
{
public override string SelfId => PlannerAgentId.TwoStagePlanner;
public override string SelfId => PlannerAgentId.SqlPlanner;
public PlannerAgentHook(IServiceProvider services, AgentSettings settings)
public SqlPlannerAgentHook(IServiceProvider services, AgentSettings settings)
: base(services, settings)
{
}
@ -19,7 +19,7 @@ public class PlannerAgentHook : AgentHookBase
{
var k = hook.GetGlobalKnowledges(new RoleDialogModel(AgentRole.User, template)
{
CurrentAgentId = PlannerAgentId.TwoStagePlanner
CurrentAgentId = PlannerAgentId.SqlPlanner
}).Result;
Knowledges.AddRange(k);
}

View file

@ -1,13 +0,0 @@
namespace BotSharp.Plugin.Planner.SqlGeneration.Models;
public class PrimaryRequirementRequest
{
[JsonPropertyName("requirement_detail")]
public string Requirements { get; set; } = null!;
[JsonPropertyName("questions")]
public string[] Questions { get; set; } = [];
[JsonPropertyName("norm_questions")]
public string[] NormQuestions { get; set; } = [];
}

View file

@ -1,13 +0,0 @@
namespace BotSharp.Plugin.Planner.SqlGeneration.Models;
public class SecondaryBreakdownTask
{
[JsonPropertyName("task_description")]
public string TaskDescription { get; set; } = null!;
[JsonPropertyName("solution_search_question")]
public string SolutionQuestion { get; set; } = null!;
[JsonPropertyName("need_lookup_dictionary")]
public bool NeedLookupDictionary { get; set; }
}

View file

@ -1,13 +1,10 @@
namespace BotSharp.Plugin.Planner.SqlGeneration.Models;
public class FirstStagePlan
public class SqlPrimaryStagePlan
{
[JsonPropertyName("task_detail")]
public string Task { get; set; } = "";
//[JsonPropertyName("reason")]
//public string Reason { get; set; } = "";
[JsonPropertyName("step")]
public int Step { get; set; } = -1;
@ -23,15 +20,6 @@ public class FirstStagePlan
[JsonPropertyName("has_found_relevant_knowledge")]
public bool HasFoundRelevantKnowledge { get; set; } = false;
//[JsonPropertyName("related_urls")]
//public string[] Urls { get; set; } = [];
//[JsonPropertyName("input_args")]
//public JsonDocument[] Parameters { get; set; } = [];
//[JsonPropertyName("output_results")]
//public string[] Results { get; set; } = [];
public override string ToString()
{
return $"STEP {Step}: {Task}";

View file

@ -1,6 +1,6 @@
namespace BotSharp.Plugin.Planner.SqlGeneration.Models;
public class SecondStagePlan
public class SqlSecondStagePlan
{
[JsonPropertyName("related_tables")]
public string[] Tables { get; set; } = [];
@ -10,10 +10,4 @@ public class SecondStagePlan
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[JsonPropertyName("input_args")]
public JsonDocument[] Parameters { get; set; } = [];
[JsonPropertyName("output_results")]
public string[] Results { get; set; } = [];
}

View file

@ -94,8 +94,8 @@ public class SqlGenerationPlanner : ITaskPlanner
private async Task<string> GetNextStepPrompt(Agent router)
{
var agentService = _services.GetRequiredService<IAgentService>();
var planner = await agentService.LoadAgent(PlannerAgentId.TwoStagePlanner);
var template = planner.Templates.First(x => x.Name == "two_stage.next").Content;
var planner = await agentService.LoadAgent(PlannerAgentId.SqlPlanner);
var template = planner.Templates.First(x => x.Name == "sql.next").Content;
var states = _services.GetRequiredService<IConversationStateService>();
var render = _services.GetRequiredService<ITemplateRender>();
return render.Render(template, new Dictionary<string, object>

View file

@ -1,6 +1,6 @@
using BotSharp.Plugin.Planner.TwoStaging.Models;
namespace BotSharp.Plugin.Planner.Functions;
namespace BotSharp.Plugin.Planner.TwoStaging.Functions;
public class PrimaryStagePlanFn : IFunctionCallback
{

View file

@ -1,6 +1,6 @@
using BotSharp.Plugin.Planner.TwoStaging.Models;
namespace BotSharp.Plugin.Planner.Functions;
namespace BotSharp.Plugin.Planner.TwoStaging.Functions;
public class SecondaryStagePlanFn : IFunctionCallback
{

View file

@ -1,7 +1,6 @@
using BotSharp.Plugin.Planner.TwoStaging;
using BotSharp.Plugin.Planner.TwoStaging.Models;
namespace BotSharp.Plugin.Planner.Functions;
namespace BotSharp.Plugin.Planner.TwoStaging.Functions;
public class SummaryPlanFn : IFunctionCallback
{

View file

@ -1,6 +1,6 @@
namespace BotSharp.Plugin.Planner.Hooks;
namespace BotSharp.Plugin.Planner.TwoStaging.Hooks;
public class PlannerUtilityHook : IAgentUtilityHook
public class TwoStagingPlannerUtilityHook : IAgentUtilityHook
{
private const string PRIMARY_STAGE_FN = "plan_primary_stage";
private const string SECONDARY_STAGE_FN = "plan_secondary_stage";

View file

@ -32,5 +32,4 @@ global using BotSharp.Abstraction.Routing.Models;
global using BotSharp.Core.Infrastructures;
global using BotSharp.Core.Routing.Reasoning;
global using BotSharp.Plugin.Planner.Hooks;
global using BotSharp.Plugin.Planner.Enums;

View file

@ -1,5 +1,5 @@
{
"name": "plan_primary_stage",
"name": "sql_primary_stage",
"description": "Plan the high level steps to finish the task",
"parameters": {
"type": "object",

View file

@ -1,5 +1,5 @@
{
"name": "plan_secondary_stage",
"name": "sql_secondary_stage",
"description": "Based on the primary stage planning, make more detail steps of the second stage if the primary stage needs more information.",
"parameters": {
"type": "object",

View file

@ -3,12 +3,12 @@ You are going convert the user requirement into sql statements.
The user is dealing with a complex problem, and you need to break this complex problem into several small tasks to more easily solve the user's needs.
Follow these steps strictly and in order.
1. If user raised a new task, call plan_primary_stage to generate the primary plan.
1. If user raised a new task, call sql_primary_stage to generate the primary plan.
If the sql response can be generate directly based on the context, directly go to step 6 to call function sql_review.
2. If need_lookup_dictionary is True, call verify_dictionary_term to verify or get the enum/term/dictionary value. Pull id and name.
* If you no items retured, you can pull 100 records from the table and look for the match.
* If there is no any item found, you can pull up to 100 records from the table and look for the match.
* If need_lookup_dictionary is False, skip calling verify_dictionary_term.
3. If need_breakdown_task is true, call plan_secondary_stage for the specific primary stage.
3. If need_breakdown_task is true, call sql_secondary_stage for the specific primary stage.
4. Repeat step 3 until you processed all the primary steps.
5. Call sql_generation function to generate SQL statements.
6. Call sql_review function to review SQL statements. This is the step you must go through before reply to the user.

View file

@ -27,6 +27,7 @@ public class SqlValidateFn : IFunctionCallback
}
sql = Regex.Match(sql, pattern).Groups[1].Value;
message.Content = sql;
var dbHook = _services.GetRequiredService<ISqlDriverHook>();
var dbType = dbHook.GetDatabaseType(message);

View file

@ -43,26 +43,17 @@ public class SqlDriverPlanningHook : IPlanningHook
return;
}
var conv = _services.GetRequiredService<IConversationService>();
var wholeDialogs = conv.GetDialogHistory();
wholeDialogs.Add(RoleDialogModel.From(msg));
wholeDialogs.Add(RoleDialogModel.From(msg, AgentRole.User, $"call execute_sql to run query, set formatting_result as {settings.FormattingResult}"));
var agent = await _services.GetRequiredService<IAgentService>().LoadAgent(BuiltInAgentId.SqlDriver);
var completion = CompletionProvider.GetChatCompletion(_services,
provider: agent.LlmConfig.Provider,
model: agent.LlmConfig.Model);
var response = await completion.GetChatCompletions(agent, wholeDialogs);
// Invoke "execute_sql"
await routing.InvokeFunction(response.FunctionName, response);
msg.CurrentAgentId = agent.Id;
msg.FunctionName = response.FunctionName;
msg.FunctionArgs = response.FunctionArgs;
msg.Content = response.Content;
msg.StopCompletion = response.StopCompletion;
var executionMsg = new RoleDialogModel(AgentRole.Function, "execute sql and format the result")
{
FunctionArgs = JsonSerializer.Serialize(new ExecuteQueryArgs
{
SqlStatements = [msg.Content],
FormattingResult = settings.FormattingResult
})
};
await routing.InvokeFunction("execute_sql", executionMsg);
msg.Content = $"The SQL query has been reviewed and executed, the formatted result is: \r\n{executionMsg.Content}";
}
public async Task OnPlanningCompleted(string planner, RoleDialogModel msg)