humanus.cpp/flow/flow_planning.cpp

407 lines
15 KiB
C++

#include "flow_planning.h"
namespace humanus {
// Get an appropriate executor agent for the current step.
// Can be extended to select agents based on step type/requirements.
std::shared_ptr<BaseAgent> PlanningFlow::get_executor(const std::string& step_type = "") const {
// If step type is provided and matches an agent key, use that agent
if (!step_type.empty() && agents.find(step_type) != agents.end()) {
return agents[step_type];
}
// Otherwise use the first available executor or fall back to primary agent
for (const auto& key : executor_keys) {
if (agents.find(key) != agents.end()) {
return agents[key];
}
}
// 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
if (planning_tool.plans.find(active_plan_id) == planning_tool.plans.end()) {
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
mcp::json step_info;
_get_current_step_info(current_step_index, step_info);
// Exit if no more steps or plan completed
if (current_step_index < 0) {
result += _finalize_plan();
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);
result += step_result + "\n";
// Check if agent wants to terminate
if (executor->state == AgentState::TERMINATED) {
break;
}
}
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
Message system_message = Message::system_message(
"You are a planning assistant. Your task is to create a detailed plan with clear steps."
);
// Create a user message with the request
Message user_message = Message::user_message(
"Create a detailed plan to accomplish this task: " + request
);
// Call LLM with PlanningTool
auto response = llm->ask_tool(
{user_message},
{system_message},
{planning_tool.to_param()},
"required"
);
// Process tool calls if present
if (response.contains("tool_calls") && !response["tool_calls"].empty()) {
tool_calls = ToolCall::from_json_list(response["tool_calls"]);
for (const auto& tool_call : tool_calls) {
// Parse the arguments
auto args = tool_call.function.arguments;
if (args.is_string()) {
try {
args = json::parse(args);
} 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
auto result = planning_tool.execute(tool_call.function.name, args);
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
planning_tool.execute({
{"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.
// Returns (None, None) if no active step is found.
void PlanningFlow::_get_current_step_info(int& current_step_index, mcp::json& step_info) {
if (active_plan_id.empty() || planning_tool.plans.find(active_plan_id) == planning_tool.plans.end()) {
logger->error("Plan with ID " + active_plan_id + " not found");
current_step_index = -1;
step_info = mcp::json::object();
return;
}
try {
// Direct access to plan data from planning tool storage
mcp::json& plan_data = planning_tool.plans[active_plan_id];
mcp::json steps = plan_data.value("steps", mcp::json::array());
mcp::json step_status = plan_data.value("status", mcp::json::array());
// Find first non-completed step
for (size_t i = 0; i < steps.size(); ++i) {
const auto& step = steps[i];
std::string step_status;
if (i >= step_status.size()) {
step_status = "not_started";
} else {
step_status = step_status[i];
}
if (step_status == "not_started" || step_status == "in_progress") {
// Extract step type/category if available
step_info = {
{"type", step},
}
}
// 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 {
planning_tool.execute({
{"command", "mark_step"},
{"plan_id", active_plan_id},
{"step_index", i},
{"status", "in_progress"}
});
} catch (const std::exception& e) {
logger->error("Error marking step as in_progress: " + std::string(e.what()));
// Update step status directly if needed
if (i < step_status.size()) {
step_status[i] = "in_progress";
} else {
while (i > step_status.size()) {
step_status.push_back("not_started");
}
step_status.push_back("in_progress");
}
plan_data["step_statuses"] = step_statuses;
}
current_step_index = i;
step_info = step;
return;
}
current_step_index = -1;
step_info = mcp::json::object(); // No active step found
} catch (const std::exception& e) {
logger->error("Error finding current step index: " + std::string(e.what()));
current_step_index = -1;
step_info = mcp::json::object();
}
}
// Execute the current step with the specified agent using agent.run().
std::string PlanningFlow::_execute_step(const std::shared_ptr<BaseAgent>& executor, const mcp::json& step_info) {
// Prepare context for the agent with current plan status
mcp::json plan_status = _get_plan_status();
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;
step_prompt += "\n\nCURRENT PLAN STATUS:\n";
step_prompt += plan_status.dump(4);
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.";
// Use agent.run() to execute the step
try {
std::string result = executor->run(step_prompt);
// Mark the step as completed after successful execution
_mark_step_completed();
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
planning_tool.execute({
{"command", "mark_step"},
{"plan_id", active_plan_id},
{"step_index", current_step_index},
{"status", "completed"}
});
logger->info(
"Marked step " + std::to_string(current_step_index) + " as completed in plan " + active_plan_id
);
} catch (const std::exception& e) {
LOG_WARN("Failed to update plan status: " + std::string(e.what()));
// Update step status directly in planning tool storage
if (planning_tool.plans.find(active_plan_id) != planning_tool.plans.end()) {
mcp::json& plan_data = planning_tool.plans[active_plan_id];
mcp::json step_statuses = plan_data.value("step_statuses", mcp::json::array());
// 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";
plan_data["step_statuses"] = step_statuses;
}
}
}
// Get the current plan as formatted text.
std::string PlanningFlow::_get_plan_text() {
try {
auto result = planning_tool.execute({
{"command", "get"},
{"plan_id", active_plan_id}
});
return result.to_string();
} 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 {
if (planning_tool.plans.find(active_plan_id) == planning_tool.plans.end()) {
return "Error: Plan with ID " + active_plan_id + " not found";
}
mcp::json& plan_data = planning_tool.plans[active_plan_id];
auto title = plan_data.value("title", "");
auto steps = plan_data.value("steps", mcp::json::array());
auto step_statuses = plan_data.value("step_statuses", mcp::json::array());
auto step_notes = plan_data.value("step_notes", mcp::json::array());
// 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
mcp::json status_counts = {
{"completed", 0},
{"in_progress", 0},
{"blocked", 0},
{"not_started", 0}
};
for (const auto& status : step_statuses) {
if (status_counts.contains(status)) {
status_counts[status]++;
}
}
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";
plan_text_ss << "Status: " << status_counts["completed"].get<int>() << " completed, " << status_counts["in_progress"].get<int>() << " in progress, "
<< status_counts["blocked"].get<int>() << " blocked, " << status_counts["not_started"].get<int>() << " not started\n\n";
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 = "[ ]";
}
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;
}
}
// Finalize the plan and provide a summary using the flow's LLM directly
std::string PlanningFlow::_finalize_plan() {
std::string plan_text = _get_plan_text();
// Create a summary using the flow's LLM directly
try {
Message system_message = Message::system_message(
"You are a planning assistant. Your task is to summarize the completed plan."
);
Message user_message = Message::user_message(
"The plan has been completed. Here is the final plan status:\n\n" + plan_text + "\n\n" +
"Please provide a summary of what was accomplished and any final thoughts."
);
auto response = llm->ask(
{user_message},
{system_message}
);
return response.to_string();
} catch (const std::exception& e) {
LOG_ERROR("Error finalizing plan with LLM: " + std::string(e.what()));
// Fallback to using an agent for the summary
try {
auto agent = primary_agent();
std::string summary_prompt = "\nThe plan has been completed. Here is the final plan status:\n\n";
summary_prompt += plan_text + "\n\n";
summary_prompt += "Please provide a summary of what was accomplished and any final thoughts\n";
std::string summary = agent->run(summary_prompt);
return "Plan completed:\n\n" + summary;
} catch (const std::exception& e2) {
LOG_ERROR("Error finalizing plan with agent: " + std::string(e2.what()));
return "Plan completed. Error generating summary.";
}
}
}
}