borderless / ui /agent /graph /nodes /planner.py
spagestic's picture
MiniCPM5 ZeroGPU migration
147e220
Raw
History Blame Contribute Delete
3.63 kB
# ui/agent/graph/nodes/planner.py
from __future__ import annotations
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.config import get_stream_writer
from langgraph.types import Send
from ..llm import build_llm
from ..state import AgentState, CandidateCountry, TodoItem
from .config import MAX_TODOS, PLANNER_MAX_TOKENS, PLANNER_TEMPERATURE
from .helpers import (
extract_assistant_text,
extract_json,
heuristic_candidate_countries,
normalize_plan,
user_text,
)
from .prompts import PLANNER_SYSTEM_PROMPT
def _format_candidate_shortlist(candidates: list[CandidateCountry]) -> str:
lines = ["Discovery shortlist (preferred starting countries):"]
for item in candidates:
lines.append(
f"- {item['iso2']} {item['name']}: {item['pathway_hint']} ({item['label']})"
)
return "\n".join(lines)
def planner_node(state: AgentState, config: RunnableConfig) -> dict[str, Any]:
writer = get_stream_writer()
llm = build_llm(
config,
max_tokens=PLANNER_MAX_TOKENS,
temperature=PLANNER_TEMPERATURE,
)
profile_text = user_text(state["user_content"])
candidates = state.get("candidate_countries") or heuristic_candidate_countries(
profile_text
)
discovery_summary = str(state.get("discovery_summary") or "").strip()
profile_summary = str(state.get("profile_summary") or "").strip()
planner_context = "\n\n".join(
part
for part in [
_format_candidate_shortlist(candidates),
f"Discovery notes:\n{discovery_summary}" if discovery_summary else "",
f"Profile summary:\n{profile_summary}" if profile_summary else "",
]
if part
)
messages: list[Any] = [
{"role": "system", "content": PLANNER_SYSTEM_PROMPT},
*state.get("history_messages", []),
{
"role": "user",
"content": (
f"{planner_context}\n\n"
f"Original user request:\n{profile_text}\n\n"
"Produce the JSON research plan now."
),
},
]
raw_plan: dict[str, Any] | None = None
for _ in range(2):
response = llm.invoke(messages)
raw_plan = extract_json(extract_assistant_text(response))
if raw_plan and raw_plan.get("todos"):
break
raw_plan = None
plan = normalize_plan(raw_plan, profile_text, candidates)
thinking = str(plan.get("thinking") or "").strip()
if thinking:
writer({"type": "thinking", "text": thinking})
todos: list[TodoItem] = list(plan.get("todos") or [])
if not todos:
plan = normalize_plan(None, profile_text, candidates)
todos = plan["todos"]
writer({"type": "plan", "todos": todos})
countries = [str(code) for code in plan.get("countries") or [] if code]
if countries:
labels = [str(label) for label in plan.get("labels") or []]
writer(
{
"type": "globe",
"args": {"action": "show", "countries": countries, "labels": labels},
}
)
return {
"todos": todos,
"profile_summary": str(plan.get("profile_summary") or profile_summary),
}
def fan_out_research(state: AgentState) -> list[Send]:
return [
Send(
"researcher",
{"todo": todo, "profile_summary": state.get("profile_summary", "")},
)
for todo in state["todos"]
]