humanus.cpp/flow/planning.cpp

426 lines
17 KiB
C++
Raw Normal View History

2025-03-16 22:56:03 +08:00
#include "planning.h"
2025-03-16 17:17:01 +08:00
namespace humanus {
// Get an appropriate executor agent for the current step.
// Can be extended to select agents based on step type/requirements.
2025-03-17 01:58:37 +08:00
std::shared_ptr<BaseAgent> PlanningFlow::get_executor(const std::string& step_type) const {
2025-03-16 17:17:01 +08:00
// If step type is provided and matches an agent key, use that agent
if (!step_type.empty() && agents.find(step_type) != agents.end()) {
2025-03-16 22:56:03 +08:00
return agents.at(step_type);
2025-03-16 17:17:01 +08:00
}
// Otherwise use the first available executor or fall back to primary agent
for (const auto& key : executor_keys) {
if (agents.find(key) != agents.end()) {
2025-03-16 22:56:03 +08:00
return agents.at(key);
2025-03-16 17:17:01 +08:00
}
}
// Fallback to primary agent
return primary_agent();
}
// Execute the planning flow with agents.
std::string PlanningFlow::execute(const std::string& input) {
try {
if (agents.find(primary_agent_key) == agents.end()) {
throw std::runtime_error("No primary agent available");
}
// Create initial plan if input provided
if (!input.empty()) {
_create_initial_plan(input);
// Verify plan was created successfully
2025-03-16 22:56:03 +08:00
if (planning_tool->plans.find(active_plan_id) == planning_tool->plans.end()) {
2025-03-16 17:17:01 +08:00
logger->error("Plan creation failed. Plan ID " + active_plan_id + " not found in planning tool.");
return "Failed to create plan for: " + input;
}
}
std::string result = "";
while (true) {
// Get current step to execute
2025-03-16 22:56:03 +08:00
json step_info;
2025-03-16 17:17:01 +08:00
_get_current_step_info(current_step_index, step_info);
// Exit if no more steps or plan completed
if (current_step_index < 0) {
break;
}
// Execute current step with appropriate agent
std::string step_type = step_info.value("type", "");
auto executor = get_executor(step_type);
std::string step_result = _execute_step(executor, step_info);
2025-03-20 01:12:15 +08:00
// result += step_result + "\n";
2025-03-16 17:17:01 +08:00
// Check if agent wants to terminate
2025-03-18 16:40:16 +08:00
if (executor->state == AgentState::FINISHED || executor->state == AgentState::ERR) {
2025-03-16 17:17:01 +08:00
break;
}
2025-03-19 18:44:54 +08:00
// Refactor memory
2025-03-26 19:28:02 +08:00
std::string prefix_sum = _summarize_plan(executor->memory->get_messages(step_result));
executor->reset(false);
2025-03-20 01:12:15 +08:00
executor->update_memory("assistant", prefix_sum);
if (!input.empty()) {
executor->update_memory("user", "Continue to accomplish the task: " + input);
}
result += "##" + step_info.value("type", "Step " + std::to_string(current_step_index)) + ":\n" + prefix_sum + "\n\n";
2025-03-16 17:17:01 +08:00
}
reset(true); // Clear short-termmemory and state for next plan
2025-03-20 01:12:15 +08:00
2025-03-16 17:17:01 +08:00
return result;
} catch (const std::exception& e) {
LOG_ERROR("Error executing planning flow: " + std::string(e.what()));
return "Execution failed: " + std::string(e.what());
}
}
// Create an initial plan based on the request using the flow's LLM and PlanningTool.
void PlanningFlow::_create_initial_plan(const std::string& request) {
logger->info("Creating initial plan with ID: " + active_plan_id);
// Create a system message for plan creation
2025-03-19 18:44:54 +08:00
std::string system_prompt = "You are a planning assistant. Your task is to create a detailed plan with clear steps.";
2025-03-16 17:17:01 +08:00
// Create a user message with the request
std::string user_prompt = "Please provide a detailed plan to accomplish this task: " + request + "\n\n";
user_prompt += "**Note**: The following executors will be used to accomplish the plan.\n\n";
for (const auto& [key, agent] : agents) {
auto tool_call_agent = std::dynamic_pointer_cast<ToolCallAgent>(agent);
if (tool_call_agent) {
user_prompt += "Available tools for executor `" + key + "`:\n";
user_prompt += tool_call_agent->available_tools.to_params().dump(2) + "\n\n";
}
}
2025-03-16 17:17:01 +08:00
// Call LLM with PlanningTool
auto response = llm->ask_tool(
{Message::user_message(user_prompt)},
2025-03-19 18:44:54 +08:00
system_prompt,
"", // No next_step_prompt for initial plan creation
json::array({planning_tool->to_param()}),
2025-03-16 17:17:01 +08:00
"required"
);
// Process tool calls if present
if (response.contains("tool_calls") && !response["tool_calls"].empty()) {
2025-03-16 22:56:03 +08:00
auto tool_calls = ToolCall::from_json_list(response["tool_calls"]);
2025-03-16 17:17:01 +08:00
for (const auto& tool_call : tool_calls) {
// Parse the arguments
auto args = tool_call.function.arguments;
if (args.is_string()) {
try {
2025-03-17 01:58:37 +08:00
std::string args_str = args.get<std::string>();
args = json::parse(args_str);
2025-03-16 17:17:01 +08:00
} catch (...) {
logger->error("Failed to parse tool arguments: " + args.dump());
continue;
}
}
// Ensure plan_id is set correctly and execute the tool
args["plan_id"] = active_plan_id;
// Execute the tool via ToolCollection instead of directly
2025-03-16 22:56:03 +08:00
auto result = planning_tool->execute(args);
2025-03-16 17:17:01 +08:00
logger->info("Plan creation result: " + result.to_string());
return;
}
}
// If execution reached here, create a default plan
logger->warn("Creating default plan");
// Create default plan using the ToolCollection
2025-03-16 22:56:03 +08:00
planning_tool->execute({
2025-03-16 17:17:01 +08:00
{"command", "create"},
{"plan_id", active_plan_id},
{"title", request.substr(0, std::min(50, static_cast<int>(request.size()))) + (request.size() > 50 ? "..." : "")},
{"steps", {"Analyze request", "Execute task", "Verify results"}}
});
}
// Parse the current plan to identify the first non-completed step's index and info.
2025-03-20 01:12:15 +08:00
// Returns (-1, None) if no active step is found.
2025-03-16 22:56:03 +08:00
void PlanningFlow::_get_current_step_info(int& current_step_index, json& step_info) {
if (active_plan_id.empty() || planning_tool->plans.find(active_plan_id) == planning_tool->plans.end()) {
2025-03-16 17:17:01 +08:00
logger->error("Plan with ID " + active_plan_id + " not found");
current_step_index = -1;
2025-03-16 22:56:03 +08:00
step_info = json::object();
2025-03-16 17:17:01 +08:00
return;
}
try {
// Direct access to plan data from planning tool storage
2025-03-20 01:12:15 +08:00
const json& plan_data = planning_tool->plans[active_plan_id];
2025-03-16 22:56:03 +08:00
json steps = plan_data.value("steps", json::array());
json step_statuses = plan_data.value("step_statuses", json::array());
2025-03-16 17:17:01 +08:00
// Find first non-completed step
for (size_t i = 0; i < steps.size(); ++i) {
2025-03-16 22:56:03 +08:00
const auto& step = steps[i].get<std::string>();
2025-03-16 17:17:01 +08:00
std::string step_status;
2025-03-20 01:12:15 +08:00
if (i >= step_statuses.size()) {
2025-03-16 17:17:01 +08:00
step_status = "not_started";
} else {
2025-03-20 01:12:15 +08:00
step_status = step_statuses[i].get<std::string>();
2025-03-16 17:17:01 +08:00
}
if (step_status == "not_started" || step_status == "in_progress") {
// Extract step type/category if available
step_info = {
2025-03-16 22:56:03 +08:00
{"type", step}
};
2025-03-20 01:12:15 +08:00
} else { // completed or skipped
continue;
2025-03-16 17:17:01 +08:00
}
// Try to extract step type from the text (e.g., [SEARCH] or [CODE])
std::regex step_regex("\\[([A-Z_]+)\\]");
std::smatch match;
if (std::regex_search(step, match, step_regex)) {
step_info["type"] = match[1].str(); // to_lower?
}
// Mark current step as in_progress
try {
2025-03-20 01:12:15 +08:00
ToolResult result = planning_tool->execute({
2025-03-16 17:17:01 +08:00
{"command", "mark_step"},
{"plan_id", active_plan_id},
{"step_index", i},
2025-03-20 01:12:15 +08:00
{"step_status", "in_progress"}
2025-03-16 17:17:01 +08:00
});
2025-03-20 01:12:15 +08:00
logger->info(
"Started executing step " + std::to_string(i) + " in plan " + active_plan_id
+ "\n\n" + result.to_string() + "\n\n"
);
2025-03-16 17:17:01 +08:00
} catch (const std::exception& e) {
logger->error("Error marking step as in_progress: " + std::string(e.what()));
// Update step status directly if needed
2025-03-16 22:56:03 +08:00
if (i < step_statuses.size()) {
step_statuses[i] = "in_progress";
2025-03-16 17:17:01 +08:00
} else {
2025-03-16 22:56:03 +08:00
while (i > step_statuses.size()) {
step_statuses.push_back("not_started");
2025-03-16 17:17:01 +08:00
}
2025-03-16 22:56:03 +08:00
step_statuses.push_back("in_progress");
2025-03-16 17:17:01 +08:00
}
2025-03-20 01:12:15 +08:00
planning_tool->plans[active_plan_id]["step_statuses"] = step_statuses;
2025-03-16 17:17:01 +08:00
}
current_step_index = i;
return;
}
current_step_index = -1;
2025-03-16 22:56:03 +08:00
step_info = json::object(); // No active step found
2025-03-16 17:17:01 +08:00
} catch (const std::exception& e) {
logger->error("Error finding current step index: " + std::string(e.what()));
current_step_index = -1;
2025-03-16 22:56:03 +08:00
step_info = json::object();
2025-03-16 17:17:01 +08:00
}
}
// Execute the current step with the specified agent using agent.run().
2025-03-16 22:56:03 +08:00
std::string PlanningFlow::_execute_step(const std::shared_ptr<BaseAgent>& executor, const json& step_info) {
2025-03-16 17:17:01 +08:00
// Prepare context for the agent with current plan status
2025-03-16 22:56:03 +08:00
json plan_status = _get_plan_text();
2025-03-16 17:17:01 +08:00
std::string step_text = step_info.value("text", "Step " + std::to_string(current_step_index));
// Create a prompt for the agent to execute the current step
std::string step_prompt;
2025-03-16 22:56:03 +08:00
step_prompt += "\nCURRENT PLAN STATUS:\n";
step_prompt += plan_status.dump(2);
2025-03-16 17:17:01 +08:00
step_prompt += "\n\nYOUR CURRENT TASK:\n";
step_prompt += "You are now working on step " + std::to_string(current_step_index) + ": \"" + step_text + "\"\n";
step_prompt += "Please execute this step using the appropriate tools. When you're done, provide a summary of what you accomplished and call `terminate` to trigger the next step.";
2025-03-16 17:17:01 +08:00
// Use agent.run() to execute the step
try {
2025-03-16 22:56:03 +08:00
std::string step_result = executor->run(step_prompt);
2025-03-16 17:17:01 +08:00
// Mark the step as completed after successful execution
2025-03-19 18:44:54 +08:00
if (executor->state != AgentState::ERR) {
_mark_step_completed();
}
2025-03-16 17:17:01 +08:00
return step_result;
} catch (const std::exception& e) {
LOG_ERROR("Error executing step " + std::to_string(current_step_index) + ": " + std::string(e.what()));
return "Error executing step " + std::to_string(current_step_index) + ": " + std::string(e.what());
}
}
// Mark the current step as completed.
void PlanningFlow::_mark_step_completed() {
if (current_step_index < 0) {
return;
}
try {
// Mark the step as completed
2025-03-17 14:07:41 +08:00
ToolResult result = planning_tool->execute({
2025-03-16 17:17:01 +08:00
{"command", "mark_step"},
{"plan_id", active_plan_id},
{"step_index", current_step_index},
2025-03-17 14:24:03 +08:00
{"step_status", "completed"}
2025-03-16 17:17:01 +08:00
});
logger->info(
"Marked step " + std::to_string(current_step_index) + " as completed in plan " + active_plan_id
2025-03-17 14:07:41 +08:00
+ "\n\n" + result.to_string() + "\n\n"
2025-03-16 17:17:01 +08:00
);
} catch (const std::exception& e) {
2025-03-16 22:56:03 +08:00
logger->warn("Failed to update plan status: " + std::string(e.what()));
2025-03-16 17:17:01 +08:00
// Update step status directly in planning tool storage
2025-03-16 22:56:03 +08:00
if (planning_tool->plans.find(active_plan_id) != planning_tool->plans.end()) {
2025-03-20 01:12:15 +08:00
const json& plan_data = planning_tool->plans[active_plan_id];
2025-03-16 22:56:03 +08:00
json step_statuses = plan_data.value("step_statuses", json::array());
2025-03-16 17:17:01 +08:00
// Ensure the step_statuses list is long enough
while (current_step_index >= step_statuses.size()) {
step_statuses.push_back("not_started");
}
// Update the status
step_statuses[current_step_index] = "completed";
2025-03-20 01:12:15 +08:00
planning_tool->plans[active_plan_id]["step_statuses"] = step_statuses;
2025-03-16 17:17:01 +08:00
}
}
}
// Get the current plan as formatted text.
std::string PlanningFlow::_get_plan_text() {
try {
2025-03-16 22:56:03 +08:00
auto result = planning_tool->execute({
2025-03-16 17:17:01 +08:00
{"command", "get"},
{"plan_id", active_plan_id}
});
2025-03-16 22:56:03 +08:00
return !result.output.empty() ? result.output.dump() : result.to_string();
2025-03-16 17:17:01 +08:00
} catch (const std::exception& e) {
LOG_ERROR("Error getting plan: " + std::string(e.what()));
return _generate_plan_text_from_storage();
}
}
// Generate plan text directly from storage if the planning tool fails.
std::string PlanningFlow::_generate_plan_text_from_storage() {
try {
2025-03-16 22:56:03 +08:00
if (planning_tool->plans.find(active_plan_id) == planning_tool->plans.end()) {
2025-03-16 17:17:01 +08:00
return "Error: Plan with ID " + active_plan_id + " not found";
}
2025-03-20 01:12:15 +08:00
const json& plan_data = planning_tool->plans[active_plan_id];
2025-03-16 22:56:03 +08:00
auto title = plan_data.value("title", "Untitled Plan");
auto steps = plan_data.value("steps", json::array());
auto step_statuses = plan_data.value("step_statuses", json::array());
auto step_notes = plan_data.value("step_notes", json::array());
2025-03-16 17:17:01 +08:00
// Ensure step_statuses and step_notes match the number of steps
while (step_statuses.size() < steps.size()) {
step_statuses.push_back("not_started");
}
while (step_notes.size() < steps.size()) {
step_notes.push_back("");
}
// Count steps by status
2025-03-16 22:56:03 +08:00
std::map<std::string, int> status_counts = {
2025-03-16 17:17:01 +08:00
{"completed", 0},
{"in_progress", 0},
{"blocked", 0},
{"not_started", 0}
};
for (const auto& status : step_statuses) {
2025-03-16 22:56:03 +08:00
if (status_counts.find(status) != status_counts.end()) {
status_counts[status] = status_counts[status] + 1;
2025-03-16 17:17:01 +08:00
}
}
int completed = status_counts["completed"];
int total = steps.size();
double progress = total > 0 ? (static_cast<double>(completed) / total) * 100.0 : 0.0;
std::stringstream plan_text_ss;
plan_text_ss << "Plan: " << title << "(ID: " << active_plan_id << ")\n";
plan_text_ss << std::string(plan_text_ss.str().size(), '=') << "\n\n";
plan_text_ss << "Total steps: " << completed << "/" << total << " steps completed (" << std::fixed << std::setprecision(1) << progress << "%)\n";
2025-03-16 22:56:03 +08:00
plan_text_ss << "Status: " << status_counts["completed"] << " completed, " << status_counts["in_progress"] << " in progress, "
<< status_counts["blocked"] << " blocked, " << status_counts["not_started"] << " not started\n\n";
2025-03-16 17:17:01 +08:00
plan_text_ss << "Steps:\n";
for (size_t i = 0; i < steps.size(); ++i) {
auto step = steps[i];
auto status = step_statuses[i];
auto notes = step_notes[i];
std::string status_mark;
if (status == "completed") {
status_mark = "[✓]";
} else if (status == "in_progress") {
status_mark = "[→]";
} else if (status == "blocked") {
status_mark = "[!]";
} else if (status == "not_started") {
status_mark = "[ ]";
2025-03-16 22:56:03 +08:00
} else { // unknown status
status_mark = "[?]";
2025-03-16 17:17:01 +08:00
}
plan_text_ss << i << ". " << status_mark << " " << step << "\n";
if (!notes.empty()) {
plan_text_ss << " Notes: " << notes << "\n";
}
}
return plan_text_ss.str();
} catch (const std::exception& e) {
logger->error("Error generating plan text from storage: " + std::string(e.what()));
return "Error: Unable to retrieve plan with ID " + active_plan_id;
}
}
2025-03-20 01:12:15 +08:00
// Summarize the plan using the flow's LLM directly
std::string PlanningFlow::_summarize_plan(const std::vector<Message> messages) {
2025-03-16 17:17:01 +08:00
std::string plan_text = _get_plan_text();
2025-03-20 01:12:15 +08:00
std::string system_prompt = "You are a planning assistant. Your task is to summarize the current plan.";
std::string next_step_prompt = "Above is the nearest finished step in the plan. Here is the current plan status:\n\n" + plan_text + "\n\n"
+ "Please provide a summary of what was accomplished and any thoughts for next steps (when the plan is not fully finished).";
2025-03-16 17:17:01 +08:00
// Create a summary using the flow's LLM directly
try {
auto response = llm->ask(
2025-03-20 01:12:15 +08:00
messages,
system_prompt,
next_step_prompt
2025-03-16 17:17:01 +08:00
);
2025-03-16 22:56:03 +08:00
return response;
2025-03-16 17:17:01 +08:00
} catch (const std::exception& e) {
2025-03-20 01:12:15 +08:00
LOG_ERROR("Error summarizing plan with LLM: " + std::string(e.what()));
2025-03-16 17:17:01 +08:00
// Fallback to using an agent for the summary
try {
auto agent = primary_agent();
2025-03-20 01:12:15 +08:00
std::string summary = agent->run(system_prompt + next_step_prompt);
return summary;
2025-03-16 17:17:01 +08:00
} catch (const std::exception& e2) {
2025-03-20 01:12:15 +08:00
LOG_ERROR("Error summarizing plan with agent: " + std::string(e2.what()));
return "Error generating summary.";
2025-03-16 17:17:01 +08:00
}
}
}
}