Add TwoStagePlanner.

This commit is contained in:
Haiping Chen 2024-02-27 07:19:05 -06:00
parent 32d5fc05bc
commit 2582ecd578
18 changed files with 481 additions and 5 deletions

View file

@ -4,5 +4,9 @@ namespace BotSharp.Abstraction.Knowledges;
public interface IKnowledgeHook
{
Task<List<KnowledgeChunk>> CollectChunkedKnowledge();
Task<List<KnowledgeChunk>> CollectChunkedKnowledge()
=> Task.FromResult(new List<KnowledgeChunk>());
Task<List<string>> GetRelevantKnowledges()
=> Task.FromResult(new List<string>());
}

View file

@ -55,6 +55,9 @@
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.naive.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.get_remaining_task.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.1st.plan.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.plan.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.task.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\response_with_function.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\instruction.liquid" />
@ -76,6 +79,15 @@
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\instruction.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.1st.plan.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.plan.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.task.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.get_remaining_task.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View file

@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace BotSharp.Core.Routing.Planning;
public class FirstStagePlan
{
[JsonPropertyName("task_detail")]
public string Task { get; set; } = "";
[JsonPropertyName("reason")]
public string Reason { get; set; } = "";
[JsonPropertyName("step")]
public int Step { get; set; } = -1;
[JsonPropertyName("contain_multiple_steps")]
public bool ContainMultipleSteps { get; set; } = false;
[JsonPropertyName("related_tables")]
public string[] Tables { get; set; } = new string[0];
[JsonPropertyName("input_args")]
public JsonDocument[] Parameters { get; set; } = new JsonDocument[0];
[JsonPropertyName("output_results")]
public string[] Results { get; set; } = new string[0];
public override string ToString()
{
return $"STEP {Step}: {Task}";
}
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
public class FirstStagePlanParameter
{
[JsonPropertyName("input_args")]
public JsonDocument[] Parameters { get; set; } = new JsonDocument[0];
[JsonPropertyName("output_results")]
public string[] Results { get; set; } = new string[0];
public override string ToString()
{
return $"INPUTS:\r\n{JsonSerializer.Serialize(Parameters)}\r\n\r\nOUTPUTS:\r\n{JsonSerializer.Serialize(Results)}";
}
}

View file

@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace BotSharp.Core.Routing.Planning;
public class SecondStagePlan
{
[JsonPropertyName("related_tables")]
public string[] Tables { get; set; } = new string[0];
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[JsonPropertyName("tool_name")]
public string Tool { get; set; } = "";
[JsonPropertyName("input_args")]
public JsonDocument[] Parameters { get; set; } = new JsonDocument[0];
[JsonPropertyName("output_results")]
public string[] Results { get; set; } = new string[0];
}

View file

@ -0,0 +1,4 @@
public class SecondStagePlanParameter : FirstStagePlanParameter
{
}

View file

@ -0,0 +1,82 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Templating;
namespace BotSharp.Core.Routing.Planning;
public partial class TwoStagePlanner
{
private async Task<FirstStagePlan[]> GetFirstStagePlanAsync(Agent router, string messageId, List<RoleDialogModel> dialogs)
{
var firstStagePlanPrompt = await GetFirstStagePlanPrompt(router);
var plan = new FirstStagePlan[0];
var llmProviderService = _services.GetRequiredService<ILlmProviderService>();
var model = llmProviderService.GetProviderModel("azure-openai", "gpt-4");
// chat completion
var completion = CompletionProvider.GetChatCompletion(_services,
provider: "azure-openai",
model: model.Name);
string text = string.Empty;
try
{
var response = await completion.GetChatCompletions(new Agent
{
Id = router.Id,
Name = nameof(TwoStagePlanner),
Instruction = firstStagePlanPrompt
}, dialogs);
text = response.Content;
plan = response.Content.JsonArrayContent<FirstStagePlan>();
}
catch (Exception ex)
{
_logger.LogError($"{ex.Message}: {text}");
}
return plan;
}
private async Task<string> GetFirstStagePlanPrompt(Agent router)
{
var template = router.Templates.First(x => x.Name == "planner_prompt.two_stage.1st.plan").Content;
var responseFormat = JsonSerializer.Serialize(new FirstStagePlan
{
Parameters = new JsonDocument[]{ JsonDocument.Parse("{}") },
Results = new string[] { "" }
});
var relevantKnowledges = new List<string>();
var hooks = _services.GetServices<IKnowledgeHook>();
foreach (var hook in hooks)
{
var k = await hook.GetRelevantKnowledges();
relevantKnowledges.AddRange(k);
}
var render = _services.GetRequiredService<ITemplateRender>();
return render.Render(template, new Dictionary<string, object>
{
{ "response_format", responseFormat },
{ "relevant_knowledges", relevantKnowledges.ToArray() }
});
}
private string GetFirstStageNextPrompt(Agent router)
{
var template = router.Templates.First(x => x.Name == "planner_prompt.first_stage.next").Content;
var responseFormat = JsonSerializer.Serialize(new FirstStagePlan
{
});
var render = _services.GetRequiredService<ITemplateRender>();
return render.Render(template, new Dictionary<string, object>
{
{ "response_format", responseFormat },
});
}
}

View file

@ -0,0 +1,14 @@
namespace BotSharp.Core.Routing.Planning;
public partial class TwoStagePlanner
{
public string GetContext()
{
var content = "";
foreach (var c in _executionContext)
{
content += $"* {c}\r\n";
}
return content;
}
}

View file

@ -0,0 +1,83 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Templating;
namespace BotSharp.Core.Routing.Planning;
public partial class TwoStagePlanner
{
private async Task<SecondStagePlan[]> GetSecondStagePlanAsync(Agent router, string messageId, FirstStagePlan plan1st, List<RoleDialogModel> dialogs)
{
var secondStagePrompt = GetSecondStagePlanPrompt(router, plan1st);
var firstStageSystemPrompt = await GetFirstStagePlanPrompt(router);
var plan = new SecondStagePlan[0];
var llmProviderService = _services.GetRequiredService<ILlmProviderService>();
var model = llmProviderService.GetProviderModel("azure-openai", "gpt-4");
// chat completion
var completion = CompletionProvider.GetChatCompletion(_services,
provider: "azure-openai",
model: model.Name);
string text = string.Empty;
var conversations = dialogs.Where(x => x.Role != AgentRole.Function).ToList();
conversations.Add(new RoleDialogModel(AgentRole.User, secondStagePrompt)
{
CurrentAgentId = router.Id,
MessageId = messageId,
});
try
{
var response = await completion.GetChatCompletions(new Agent
{
Id = router.Id,
Name = nameof(TwoStagePlanner),
Instruction = firstStageSystemPrompt
}, conversations);
text = response.Content;
plan = response.Content.JsonArrayContent<SecondStagePlan>();
}
catch (Exception ex)
{
_logger.LogError($"{ex.Message}: {text}");
}
return plan;
}
private string GetSecondStageTaskPrompt(Agent router, SecondStagePlan plan)
{
var template = router.Templates.First(x => x.Name == "planner_prompt.two_stage.2nd.task").Content;
var render = _services.GetRequiredService<ITemplateRender>();
return render.Render(template, new Dictionary<string, object>
{
{ "task_description", plan.Description },
{ "related_tables", plan.Tables },
{ "input_arguments", JsonSerializer.Serialize(plan.Parameters) },
{ "output_results", JsonSerializer.Serialize(plan.Results) },
});
}
private string GetSecondStagePlanPrompt(Agent router, FirstStagePlan plan)
{
var template = router.Templates.First(x => x.Name == "planner_prompt.two_stage.2nd.plan").Content;
var responseFormat = JsonSerializer.Serialize(new SecondStagePlan
{
Tool = "tool name if task solution provided",
Parameters = new JsonDocument[] { JsonDocument.Parse("{}") },
Results = new string[] { "" }
});
var context = GetContext();
var render = _services.GetRequiredService<ITemplateRender>();
return render.Render(template, new Dictionary<string, object>
{
{ "task_description", plan.Task },
{ "response_format", responseFormat }
});
}
}

View file

@ -0,0 +1,170 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Routing.Planning;
using System.IO;
namespace BotSharp.Core.Routing.Planning;
public partial class TwoStagePlanner : IPlaner
{
private readonly IServiceProvider _services;
private readonly ILogger _logger;
public int MaxLoopCount => 100;
private bool _isTaskCompleted;
private string _md5;
private Queue<FirstStagePlan> _plan1st = new Queue<FirstStagePlan>();
private Queue<SecondStagePlan> _plan2nd = new Queue<SecondStagePlan>();
private List<string> _executionContext = new List<string>();
public TwoStagePlanner(IServiceProvider services, ILogger<TwoStagePlanner> logger)
{
_services = services;
_logger = logger;
}
public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string messageId, List<RoleDialogModel> dialogs)
{
var tempDir = Path.Combine(Path.GetTempPath(), "botsharp", "cache");
if (_plan1st.IsNullOrEmpty() && _plan2nd.IsNullOrEmpty())
{
Directory.CreateDirectory(tempDir);
_md5 = Utilities.HashText(string.Join(".", dialogs.Where(x => x.Role == AgentRole.User)), "botsharp");
var filePath = Path.Combine(tempDir, $"{_md5}-1st.json");
FirstStagePlan[] items = new FirstStagePlan[0];
if (File.Exists(filePath))
{
var cache = File.ReadAllText(filePath);
items = JsonSerializer.Deserialize<FirstStagePlan[]>(cache);
}
else
{
items = await GetFirstStagePlanAsync(router, messageId, dialogs);
var cache = JsonSerializer.Serialize(items);
File.WriteAllText(filePath, cache);
}
foreach (var item in items)
{
_plan1st.Enqueue(item);
};
}
// Get Second Stage Plan
if (_plan2nd.IsNullOrEmpty())
{
var plan1 = _plan1st.Dequeue();
if (plan1.ContainMultipleSteps)
{
var filePath = Path.Combine(tempDir, $"{_md5}-2nd-{plan1.Step}.json");
SecondStagePlan[] items = new SecondStagePlan[0];
if (File.Exists(filePath))
{
var cache = File.ReadAllText(filePath);
items = JsonSerializer.Deserialize<SecondStagePlan[]>(cache);
}
else
{
items = await GetSecondStagePlanAsync(router, messageId, plan1, dialogs);
var cache = JsonSerializer.Serialize(items);
File.WriteAllText(filePath, cache);
}
foreach (var item in items)
{
_plan2nd.Enqueue(item);
}
}
else
{
_plan2nd.Enqueue(new SecondStagePlan
{
Description = plan1.Task,
Tables = plan1.Tables,
Parameters = plan1.Parameters,
Results = plan1.Results,
});
}
}
var plan2 = _plan2nd.Dequeue();
var secondStagePrompt = GetSecondStageTaskPrompt(router, plan2);
var inst = new FunctionCallFromLlm
{
AgentName = "SQL Driver",
Response = secondStagePrompt,
Function = "route_to_agent"
};
inst.HandleDialogsByPlanner = true;
_isTaskCompleted = _plan1st.IsNullOrEmpty() && _plan2nd.IsNullOrEmpty();
return inst;
}
public List<RoleDialogModel> BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
{
var question = inst.Response;
if (_executionContext.Count > 0)
{
var content = GetContext();
question = $"CONTEXT:\r\n{content}\r\n" + inst.Response;
}
else
{
question = $"CONTEXT:\r\n{question}";
}
var taskAgentDialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, question)
{
MessageId = message.MessageId,
}
};
return taskAgentDialogs;
}
public bool AfterHandleContext(List<RoleDialogModel> dialogs, List<RoleDialogModel> taskAgentDialogs)
{
dialogs.AddRange(taskAgentDialogs.Skip(1));
// Keep execution context
_executionContext.Add(taskAgentDialogs.Last().Content);
return true;
}
public async Task<bool> AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
{
dialogs.Add(new RoleDialogModel(AgentRole.User, inst.Response)
{
MessageId = message.MessageId,
CurrentAgentId = router.Id
});
return true;
}
public async Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
{
var context = _services.GetRequiredService<RoutingContext>();
if (message.StopCompletion || _isTaskCompleted)
{
context.Empty();
return false;
}
var routing = _services.GetRequiredService<IRoutingService>();
routing.ResetRecursiveCounter();
return true;
}
}

View file

@ -38,5 +38,6 @@ public class RoutingPlugin : IBotSharpPlugin
services.AddScoped<IPlaner, NaivePlanner>();
services.AddScoped<IPlaner, HFPlanner>();
services.AddScoped<IPlaner, SequentialPlanner>();
services.AddScoped<IPlaner, TwoStagePlanner>();
}
}

View file

@ -0,0 +1,15 @@
You are a Task Planner. you will breakdown user business requirements into small executable sub-tasks.
Thinking process:
1. Reference to "Task Solutions" if there is relevant solutions;
2. Breakdown task into subtasks. The subtask should contains all needed parameters for subsequent steps.
3. Input argument must reference to corresponding variable name that retrieved by previous steps, variable name must start with '@';
4. Output all the subtasks as much detail as possible in JSON: {{ response_format }}
{% if relevant_knowledges != empty -%}
=====
Task Solutions:
{% for k in relevant_knowledges %}
{{ k }}
{% endfor %}
{%- endif %}

View file

@ -0,0 +1,6 @@
Reference to "Task Solutions". Breakdown task into multiple steps.
The step should contains all needed parameters.
The parameters can be extracted from the original task.
Output all the steps as much detail as possible in JSON: [{{ response_format }}]
TASK: {{ task_description }}

View file

@ -0,0 +1,8 @@
{{ task_description }}
{% if related_tables != empty -%}
Relevant tables:
{% for t in related_tables -%}
- {{ t }}{{ "\r\n" }}
{%- endfor %}
{%- endif %}

View file

@ -44,7 +44,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook
public override async Task OnMessageReceived(RoleDialogModel message)
{
var conversationId = _state.GetConversationId();
var log = $"MessageId: {message.MessageId} ==>\r\n{message.Role}: {message.Content}";
var log = $"{message.Role}: {message.Content}";
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated",
BuildContentLog(conversationId, _user.UserName, log, ContentLogSource.UserInput, message));
}
@ -85,7 +85,6 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook
var conversationId = _state.GetConversationId();
var agent = await _agentService.LoadAgent(message.CurrentAgentId);
var log = $"{message.FunctionName}({message.FunctionArgs})\r\n => {message.Content}";
log += $"\r\n<== MessageId: {message.MessageId}";
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated",
BuildContentLog(conversationId, agent?.Name, log, ContentLogSource.FunctionCall, message));
}
@ -130,7 +129,6 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook
var richContent = JsonSerializer.Serialize(message.RichContent, _serializerOptions);
log += $"\r\n{richContent}";
}
log += $"\r\n<== MessageId: {message.MessageId}";
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated",
BuildContentLog(conv.ConversationId, agent?.Name, log, ContentLogSource.AgentResponse, message));
}

View file

@ -3,6 +3,10 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>
<ItemGroup>

View file

@ -10,6 +10,9 @@ public class LookupDictionary
[JsonPropertyName("keyword")]
public string Keyword { get; set; }
[JsonPropertyName("reason")]
public string Reason { get; set; }
[JsonPropertyName("columns")]
public string[] Columns { get; set; }
}

View file

@ -139,6 +139,10 @@
"type": "string",
"description": "table name"
},
"reason": {
"type": "string",
"description": "the reason why you need to call lookup_dictionary"
},
"columns": {
"type": "array",
"description": "columns",
@ -148,7 +152,7 @@
}
}
},
"required": [ "table", "columns", "keyword" ]
"required": [ "table", "keyword", "reason", "columns" ]
}
}
]