Spaces:
Sleeping
Sleeping
File size: 6,501 Bytes
2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea 4db2d34 2eef9ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | """
backend/agents/planner.py
The Planner Agent — the strategic brain of the system.
Responsibilities:
1. Decompose complex tasks into ordered steps
2. Assign appropriate tools to each step
3. Define step dependencies (DAG structure)
4. Replan if Critic requests changes
5. Decide when task is complete
Uses Gemini function calling to produce structured JSON plan.
"""
from __future__ import annotations
import json
import uuid
from datetime import datetime, timezone
from langchain_core.messages import SystemMessage, HumanMessage
from ..core.llm import get_llm
from ..state.graph_state import (
WorkflowState, TaskStatus, AgentRole,
make_plan_step, make_agent_event,
)
from ..core.logger import get_logger
log = get_logger(__name__)
PLANNER_SYSTEM = """You are a task planner. Decompose the task into 2-4 steps MAX.
Tools: web_search, fetch_url, calculate, run_python, write_file, read_file, get_datetime, synthesize
Always end with a synthesize step.
Return ONLY JSON:
{"thinking":"one line","plan":[{"step_id":"step_1","title":"short","description":"what to do","tool":"tool_name","depends_on":[]}],"estimated_complexity":"low|medium|high"}
Rules: minimal steps, no redundancy, synthesize last, depends_on lists prerequisite step_ids."""
REPLAN_ADDITION = """
Replanning — fix these issues: {critique}
Already done: {completed_steps}
Only plan remaining steps."""
PLANNING_TOOL = {
"type": "function",
"function": {
"name": "submit_plan",
"description": "Submit the structured task plan",
"parameters": {
"type": "object",
"required": ["thinking", "plan"],
"properties": {
"thinking": {"type": "string"},
"plan": {
"type": "array",
"items": {
"type": "object",
"required": ["step_id", "title", "description", "tool"],
"properties": {
"step_id": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
"tool": {"type": "string"},
"depends_on": {"type": "array", "items": {"type": "string"}},
},
},
},
"estimated_complexity": {"type": "string", "enum": ["low", "medium", "high"]},
},
},
},
}
def planner_node(state: WorkflowState) -> WorkflowState:
"""
LangGraph node — runs the Planner agent.
Called at start and whenever Critic requests replanning.
"""
from ..core.config import get_settings
settings = get_settings()
llm = get_llm("planner", temperature=0.2)
log.info("Planner running", task_id=state["task_id"], iteration=state["iteration"])
# Build prompt — include critique context if replanning
system_content = PLANNER_SYSTEM
if state.get("needs_replanning") and state.get("critique"):
completed = [
s["title"] for s in state["plan"]
if s["status"] == "done"
]
system_content += REPLAN_ADDITION.format(
critique=state["critique"],
completed_steps="\n".join(f"- {t}" for t in completed) or "None",
)
# Memory context
memory_context = ""
if state.get("memories"):
mem_texts = [m["content"] for m in state["memories"][:3]]
memory_context = "\n\nRelevant past experience:\n" + "\n".join(f"• {t}" for t in mem_texts)
user_msg = f"Task: {state['task']}{memory_context}"
if state.get("step_results"):
results_summary = json.dumps(
{k: str(v)[:200] for k, v in list(state["step_results"].items())[-3:]},
indent=2
)
user_msg += f"\n\nRecent results:\n{results_summary}"
try:
response = llm.invoke(
[SystemMessage(content=system_content), HumanMessage(content=user_msg)],
tools=[PLANNING_TOOL],
tool_choice={"type": "function", "function": {"name": "submit_plan"}},
)
tool_call = response.tool_calls[0] if response.tool_calls else None
if not tool_call:
raise ValueError("No tool call in planner response")
plan_data = tool_call["args"]
thinking = plan_data.get("thinking", "")
# Convert to plan steps (hard cap)
new_plan = []
raw_steps = plan_data.get("plan", [])[:settings.max_plan_steps]
for step in raw_steps:
new_plan.append(make_plan_step(
step_id=step.get("step_id", f"step_{len(new_plan)+1}"),
title=step.get("title", ""),
description=step.get("description", ""),
tool=step.get("tool"),
depends_on=step.get("depends_on", []),
))
log.info("Plan created", steps=len(new_plan), thinking=thinking[:100])
# Preserve completed steps if replanning
if state.get("needs_replanning"):
completed = [s for s in state["plan"] if s["status"] == "done"]
final_plan = completed + new_plan
else:
final_plan = new_plan
tokens = response.usage_metadata.get("total_tokens", 0) if response.usage_metadata else 0
return {
**state,
"status": TaskStatus.EXECUTING,
"plan": final_plan,
"current_step_index": len([s for s in final_plan if s["status"] == "done"]),
"needs_replanning": False,
"iteration": state["iteration"] + 1,
"total_tokens": state["total_tokens"] + tokens,
"updated_at": datetime.now(timezone.utc).isoformat(),
"events": state["events"] + [
make_agent_event(
AgentRole.PLANNER, "plan_created",
f"Created {len(new_plan)}-step plan: {thinking[:200]}",
{"steps": [s["title"] for s in new_plan], "complexity": plan_data.get("estimated_complexity")},
)
],
}
except Exception as e:
log.error("Planner failed", error=str(e))
return {
**state,
"status": TaskStatus.FAILED,
"error_message": f"Planner failed: {e}",
"events": state["events"] + [
make_agent_event(AgentRole.PLANNER, "error", f"Planning failed: {e}")
],
}
|