Spaces:
Sleeping
Sleeping
| """LangGraph StateGraph assembly + streaming entry point. | |
| Pipeline: | |
| planner → skill_router → executor → reflector ─┐ | |
| ↑ ↑ ↓ │ | |
| │ │ └── reflects: enough? │ | |
| │ │ │ | |
| │ └──── hint: skip LLM, use plan step │ | |
| │ │ | |
| └────────── plan exhausted + need_more ──────┘ | |
| │ | |
| ↓ | |
| synthesizer → END | |
| The planner runs once at the start to pre-decompose the question | |
| into a sequence of plan steps. The skill router then walks through | |
| the plan without re-asking the LLM, with the reflector deciding | |
| when to stop or when to trigger a re-plan. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from typing import Any, AsyncIterator | |
| from langgraph.graph import END, StateGraph | |
| from app.agent.nodes.executor import executor_node | |
| from app.agent.nodes.planner import planner_node | |
| from app.agent.nodes.reflector import reflector_node | |
| from app.agent.nodes.skill_router import skill_router_node | |
| from app.agent.nodes.synthesizer import synthesize | |
| from app.agent.state import AgentState, EV_DONE, EV_ERROR | |
| from app.config import get_settings | |
| from app.skills.registry import REGISTRY | |
| logger = logging.getLogger(__name__) | |
| def _build_graph() -> Any: | |
| g = StateGraph(AgentState) | |
| g.add_node("planner", planner_node) | |
| g.add_node("skill_router", skill_router_node) | |
| g.add_node("executor", executor_node) | |
| g.add_node("reflector", reflector_node) | |
| g.add_node("synthesizer", lambda s: s) # placeholder; streaming handled outside | |
| g.set_entry_point("planner") | |
| g.add_edge("planner", "skill_router") # planner always feeds into the router | |
| # After router: if final_answer was set, go to synthesizer; else go to executor | |
| def _after_router(state: AgentState) -> str: | |
| if state.get("error"): | |
| return "synthesizer" | |
| if state.get("final_answer"): | |
| return "synthesizer" | |
| return "executor" | |
| g.add_conditional_edges("skill_router", _after_router, { | |
| "executor": "executor", | |
| "synthesizer": "synthesizer", | |
| }) | |
| # After executor: always go to reflector | |
| g.add_edge("executor", "reflector") | |
| # After reflector: three paths. | |
| # need_more + plan still has steps → skill_router (advance plan) | |
| # need_more + plan exhausted (cleared by reflector) → planner (re-plan) | |
| # sufficient / failed / rounds cap hit → synthesizer | |
| def _after_reflector(state: AgentState) -> str: | |
| verdict = state.get("reflection_verdict", "sufficient") | |
| rounds = state.get("rounds_used", 0) | |
| max_rounds = get_settings().agent_max_reflect_rounds | |
| if verdict == "need_more" and rounds < max_rounds: | |
| plan = state.get("plan") or [] | |
| if not plan: | |
| # Plan was cleared (exhausted) → re-plan | |
| return "planner" | |
| return "skill_router" | |
| return "synthesizer" | |
| g.add_conditional_edges("reflector", _after_reflector, { | |
| "planner": "planner", | |
| "skill_router": "skill_router", | |
| "synthesizer": "synthesizer", | |
| }) | |
| g.add_edge("synthesizer", END) | |
| # Default LangGraph recursion limit is 25. With the multi-step | |
| # plan + re-plan flow + anysearch being a slightly slower skill, | |
| # we hit it on complex questions. Pass the limit via the config | |
| # dict at astream time (LangGraph 0.x's .compile() doesn't accept | |
| # a recursion_limit kwarg; the config goes on astream/ainvoke). | |
| return g.compile() | |
| _GRAPH = None | |
| def get_graph() -> Any: | |
| global _GRAPH | |
| if _GRAPH is None: | |
| _GRAPH = _build_graph() | |
| return _GRAPH | |
| # ---------- Public streaming entry point ---------- | |
| async def run_agent_stream( | |
| user_query: str, | |
| history: list[dict[str, Any]], | |
| session_id: str, | |
| ) -> AsyncIterator[dict[str, Any]]: | |
| """Stream agent events for a single user turn.""" | |
| from app.agent.state import ( | |
| EV_MESSAGE_FINAL, | |
| EV_REFLECTION, | |
| EV_THINK, | |
| EV_TOOL_CALL, | |
| EV_TOOL_RESULT, | |
| ) | |
| from app.utils.trace import generate_trace_id | |
| if not REGISTRY.list_specs(): | |
| yield {"event": EV_ERROR, "data": {"message": "No skills registered"}} | |
| yield {"event": EV_DONE, "data": {}} | |
| return | |
| trace_id = generate_trace_id() | |
| init_state: AgentState = { | |
| "user_query": user_query, | |
| "session_id": session_id, | |
| "history": history, | |
| "tool_calls": [], | |
| "rounds_used": 0, | |
| "reflection_verdict": "need_more", | |
| "trace_id": trace_id, | |
| "plan": [], | |
| "pending_step_index": 0, | |
| "next_skill_hint": None, | |
| "next_args_hint": None, | |
| } | |
| yield {"event": EV_THINK, "data": {"step": "entry", "text": f"开始处理:{user_query}", "trace_id": trace_id}} | |
| graph = get_graph() | |
| final_state: AgentState = dict(init_state) | |
| try: | |
| # Bump LangGraph's default 25 recursion limit to 50 for | |
| # multi-step plan + re-plan flows. Most queries stay <10. | |
| async for event in graph.astream(init_state, config={"recursion_limit": 50}): | |
| # event is dict {node_name: node_output} | |
| for node_name, node_out in event.items(): | |
| if not isinstance(node_out, dict): | |
| continue | |
| final_state.update(node_out) | |
| # Stream per-node events | |
| if node_name == "planner": | |
| plan = node_out.get("plan") or [] | |
| steps = [ | |
| f"{s.get('target_skill') or 'final'} ({s.get('goal', '')[:40]})" | |
| for s in plan | |
| ] | |
| rationale = node_out.get("rationale", "") or "" | |
| yield { | |
| "event": EV_THINK, | |
| "data": { | |
| "step": "plan", | |
| "text": f"已规划 {len(plan)} 步:{' → '.join(steps)}" + ( | |
| f"\n理由:{rationale}" if rationale else "" | |
| ), | |
| }, | |
| } | |
| if node_name == "skill_router": | |
| tc = (node_out.get("tool_calls") or []) | |
| if tc and tc[-1].get("result") is None: | |
| last = tc[-1] | |
| yield { | |
| "event": EV_TOOL_CALL, | |
| "data": { | |
| "name": last["name"], | |
| "args": last.get("args", {}), | |
| "trace_id": last.get("trace_id", ""), | |
| }, | |
| } | |
| if node_out.get("final_answer"): | |
| yield { | |
| "event": EV_THINK, | |
| "data": {"step": "router_final", "text": "直接生成最终答案"}, | |
| } | |
| elif node_name == "executor": | |
| tc = (node_out.get("tool_calls") or []) | |
| if tc: | |
| last = tc[-1] | |
| yield { | |
| "event": EV_TOOL_RESULT, | |
| "data": { | |
| "name": last["name"], | |
| "ok": last.get("ok", False), | |
| "duration_ms": last.get("duration_ms", 0), | |
| "trace_id": last.get("trace_id", ""), | |
| "result": last.get("result"), | |
| "error": last.get("error"), | |
| }, | |
| } | |
| elif node_name == "reflector": | |
| yield { | |
| "event": EV_REFLECTION, | |
| "data": { | |
| "verdict": node_out.get("reflection_verdict", "sufficient"), | |
| "reason": node_out.get("reflection", ""), | |
| }, | |
| } | |
| except Exception as exc: # noqa: BLE001 | |
| logger.exception("agent graph execution failed") | |
| yield {"event": EV_ERROR, "data": {"message": f"Agent 执行失败: {exc}", "trace_id": trace_id}} | |
| # If the router produced a final answer directly (no synthesizer streaming) | |
| if final_state.get("final_answer") and not any( | |
| True for _ in [] | |
| ): # placeholder check | |
| yield { | |
| "event": EV_MESSAGE_FINAL, | |
| "data": { | |
| "content": final_state["final_answer"], | |
| "tool_calls": final_state.get("tool_calls", []), | |
| }, | |
| } | |
| else: | |
| # Stream synthesizer output | |
| async for ev in synthesize(final_state): | |
| yield ev | |
| yield {"event": EV_DONE, "data": {"trace_id": trace_id}} | |