""" Jarvis LangGraph Agent 主图定义 """ from langgraph.graph import StateGraph, END from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage from app.agents.state import AgentState, AgentRole from app.agents.prompts import ( MASTER_SYSTEM_PROMPT, PLANNER_SYSTEM_PROMPT, EXECUTOR_SYSTEM_PROMPT, LIBRARIAN_SYSTEM_PROMPT, ANALYST_SYSTEM_PROMPT, PLANNER_SCOPE_PROMPT, PLANNER_STEPS_PROMPT, EXECUTOR_TASKS_PROMPT, EXECUTOR_FORUM_PROMPT, LIBRARIAN_RETRIEVAL_PROMPT, LIBRARIAN_GRAPH_PROMPT, ANALYST_PROGRESS_PROMPT, ANALYST_INSIGHTS_PROMPT, ) from app.agents.tools import ALL_TOOLS, SUB_COMMANDER_TOOLSETS from app.agents.skill_registry import build_skill_context from app.services.llm_service import get_llm from langchain_openai import ChatOpenAI from langchain_anthropic import ChatAnthropic from langchain_ollama import ChatOllama import httpx SUB_COMMANDER_PROMPTS = { "planner_scope": PLANNER_SCOPE_PROMPT, "planner_steps": PLANNER_STEPS_PROMPT, "executor_tasks": EXECUTOR_TASKS_PROMPT, "executor_forum": EXECUTOR_FORUM_PROMPT, "librarian_retrieval": LIBRARIAN_RETRIEVAL_PROMPT, "librarian_graph": LIBRARIAN_GRAPH_PROMPT, "analyst_progress": ANALYST_PROGRESS_PROMPT, "analyst_insights": ANALYST_INSIGHTS_PROMPT, } ROLE_SUB_COMMANDERS = { AgentRole.PLANNER: ["planner_scope", "planner_steps"], AgentRole.EXECUTOR: ["executor_tasks", "executor_forum"], AgentRole.LIBRARIAN: ["librarian_retrieval", "librarian_graph"], AgentRole.ANALYST: ["analyst_progress", "analyst_insights"], } ROLE_SKILL_CONTEXT = { AgentRole.PLANNER: "planner", AgentRole.EXECUTOR: "executor", AgentRole.LIBRARIAN: "librarian", AgentRole.ANALYST: "analyst", } def _create_llm_from_config(config: dict): """根据用户模型配置创建 LLM 实例""" provider = config.get("provider", "openai") model = config.get("model", "") api_key = config.get("api_key", "") base_url = config.get("base_url", "") if provider == "openai" or provider == "deepseek" or provider == "custom": return ChatOpenAI( api_key=api_key, model=model, base_url=base_url or None, timeout=httpx.Timeout(60.0, connect=10.0), ) elif provider == "claude": return ChatAnthropic( api_key=api_key, model=model, timeout=httpx.Timeout(60.0, connect=10.0), ) elif provider == "ollama": return ChatOllama( base_url=base_url or "http://localhost:11434", model=model, timeout=httpx.Timeout(120.0, connect=10.0), ) else: return ChatOpenAI( api_key=api_key, model=model, base_url=base_url or None, timeout=httpx.Timeout(60.0, connect=10.0), ) def _get_llm_for_state(state: AgentState): """从 state 获取 LLM 实例,优先使用用户配置的模型""" user_llm_config = state.get("user_llm_config") if user_llm_config: return _create_llm_from_config(user_llm_config) return get_llm() async def _ainvoke(llm, messages: list[BaseMessage]): ainvoke = getattr(llm, "ainvoke", None) if callable(ainvoke): return await ainvoke(messages) return await llm.invoke(messages) async def _ainvoke_with_tools(llm, messages: list[BaseMessage], tools=None): toolset = tools if tools is not None else ALL_TOOLS bound_llm = llm.bind_tools(toolset) if hasattr(bound_llm, "ainvoke"): return await bound_llm.ainvoke(messages) return await bound_llm.invoke(messages) def _compile_graph(graph: StateGraph, callbacks: list | None = None): if callbacks: try: return graph.compile(callbacks=callbacks) except TypeError as exc: if "callbacks" not in str(exc): raise return graph.compile() def _msg_type(msg: BaseMessage) -> str: """Get message type, handles both .type (new) and .role (old) attribute names.""" return getattr(msg, "type", None) or getattr(msg, "role", "human") def _filter_user_messages(messages: list) -> list[BaseMessage]: return [m for m in messages if _msg_type(m) in ("human", "user")] def _normalize_user_text(text: str) -> str: return (text or "").strip().lower() def _is_simple_greeting(text: str) -> bool: normalized = _normalize_user_text(text) return normalized in {"你好", "您好", "早", "早上好", "在吗", "嗨", "hi", "hello"} def _is_identity_question(text: str) -> bool: normalized = _normalize_user_text(text) return normalized in {"你是谁", "你是誰"} def _is_capability_question(text: str) -> bool: normalized = _normalize_user_text(text) return normalized in {"你能做什么", "你可以做什么", "你会做什么"} def _choose_sub_commander(role: AgentRole, user_query: str) -> str: text = _normalize_user_text(user_query) if role == AgentRole.PLANNER: if any(keyword in text for keyword in ["步骤", "计划", "拆解", "排期", "优先级", "路线"]): return "planner_steps" return "planner_scope" if role == AgentRole.EXECUTOR: if any(keyword in text for keyword in ["论坛", "帖子", "发帖", "指令", "discussion", "instruction"]): return "executor_forum" return "executor_tasks" if role == AgentRole.LIBRARIAN: if any(keyword in text for keyword in ["图谱", "关系", "构建", "沉淀", "节点", "graph"]): return "librarian_graph" return "librarian_retrieval" if role == AgentRole.ANALYST: if any(keyword in text for keyword in ["趋势", "风险", "洞察", "建议", "机会", "insight"]): return "analyst_insights" return "analyst_progress" return ROLE_SUB_COMMANDERS[role][0] def _record_sub_commander(state: AgentState, sub_commander: str, user_query: str): state["current_sub_commander"] = sub_commander state["active_sub_commanders"] = state.get("active_sub_commanders", []) + [sub_commander] state["sub_commander_trace"] = state.get("sub_commander_trace", []) + [{ "agent": state.get("current_agent", AgentRole.MASTER).value, "sub_commander": sub_commander, "query": user_query, }] def _build_system_messages(state: AgentState, system_prompt: str, role: AgentRole): system_msgs: list[BaseMessage] = [SystemMessage(content=system_prompt)] skill_ctx = build_skill_context(ROLE_SKILL_CONTEXT[role]) if skill_ctx: system_msgs.append(SystemMessage(content=skill_ctx)) return system_msgs async def _run_sub_commander( state: AgentState, role: AgentRole, manager_prompt: str, user_query: str, *, use_tools: bool, summary_target: str | None = None, ): llm = _get_llm_for_state(state) sub_commander = _choose_sub_commander(role, user_query) _record_sub_commander(state, sub_commander, user_query) toolset = SUB_COMMANDER_TOOLSETS.get(sub_commander, []) system_msgs = _build_system_messages(state, manager_prompt, role) system_msgs.append(SystemMessage(content=f"本次应由子指挥官 `{sub_commander}` 接手。请严格按该角色职责输出。")) system_msgs.append(SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander])) if use_tools and toolset: response = await _ainvoke_with_tools( llm, system_msgs + [HumanMessage(content=f"用户请求: {user_query}")], toolset, ) tool_calls = getattr(response, "tool_calls", None) or [] if tool_calls: results = [] for tc in tool_calls: tool_name = tc.get("name") args = tc.get("args", {}) for tool in toolset: if tool.name == tool_name: try: result = tool.invoke(args) results.append(f"[{tool_name}] {result}") except Exception as e: results.append(f"[{tool_name}] 执行失败: {e}") break state["tool_calls"] = tool_calls state["last_tool_result"] = "\n".join(results) follow_up = await _ainvoke( llm, [ SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander]), HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}") ] ) state["final_response"] = follow_up.content else: state["final_response"] = response.content else: response = await _ainvoke( llm, system_msgs + [HumanMessage(content=f"用户请求: {user_query}")], ) state["final_response"] = response.content if summary_target: state[summary_target] = state.get("final_response", "") state["should_respond"] = True return state # ===================== 节点定义 (async) ===================== async def master_node(state: AgentState) -> AgentState: """主Agent节点: 理解用户意图,决定调用哪个子Agent""" messages: list[BaseMessage] = state["messages"] user_msgs = _filter_user_messages(messages) user_query = user_msgs[-1].content.strip() if user_msgs else "" if _is_simple_greeting(user_query): state["final_response"] = "您好。我在。\n\n您把问题给我,我先帮您收束重点,再往下推。" state["should_respond"] = True return state if _is_identity_question(user_query): state["final_response"] = "我是 Jarvis。\n\n比起做一个泛泛的助手,我更像您的判断型协作伙伴:帮您看清问题、压缩路径、把事情往前推进。" state["should_respond"] = True return state if _is_capability_question(user_query): state["final_response"] = "主要做三件事。\n- 帮您判断:看问题本质、梳理取舍、给出方向\n- 帮您收束:把复杂内容理顺,把重点拎出来\n- 帮您推进:拆任务、定步骤、把下一步变清楚\n\n如果您现在有具体目标,我可以直接进入处理。" state["should_respond"] = True return state llm = _get_llm_for_state(state) system_msgs: list[BaseMessage] = [SystemMessage(content=MASTER_SYSTEM_PROMPT)] memory_ctx = state.get("memory_context") if memory_ctx: system_msgs.append( SystemMessage(content=f"\n\n【记忆上下文】\n{memory_ctx}\n\n---\n") ) response: AIMessage = await _ainvoke(llm, system_msgs + messages) content = response.content.strip().lower() if any(kw in content for kw in ["搜索", "查找", "知识", "检索"]): next_agent = AgentRole.LIBRARIAN elif any(kw in content for kw in ["计划", "安排", "拆解", "规划"]): next_agent = AgentRole.PLANNER elif any(kw in content for kw in ["执行", "做", "操作", "创建", "更新"]): next_agent = AgentRole.EXECUTOR elif any(kw in content for kw in ["分析", "报告", "统计", "总结"]): next_agent = AgentRole.ANALYST else: state["final_response"] = response.content state["should_respond"] = True return state state["current_agent"] = next_agent state["current_sub_commander"] = None state["active_agents"] = state.get("active_agents", [AgentRole.MASTER]) + [next_agent] state["should_respond"] = True return state async def planner_node(state: AgentState) -> AgentState: """规划Agent节点: 制定计划,拆解任务步骤""" user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" return await _run_sub_commander(state, AgentRole.PLANNER, PLANNER_SYSTEM_PROMPT, user_query, use_tools=False) async def executor_node(state: AgentState) -> AgentState: """执行Agent节点: 调用工具执行具体任务""" user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" return await _run_sub_commander(state, AgentRole.EXECUTOR, EXECUTOR_SYSTEM_PROMPT, user_query, use_tools=True) async def librarian_node(state: AgentState) -> AgentState: """知识管理员节点: 管理知识库和知识图谱""" user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" return await _run_sub_commander(state, AgentRole.LIBRARIAN, LIBRARIAN_SYSTEM_PROMPT, user_query, use_tools=True, summary_target="knowledge_context") async def analyst_node(state: AgentState) -> AgentState: """分析师节点: 分析工作数据,生成报告""" user_msgs = _filter_user_messages(state["messages"]) user_query = user_msgs[-1].content if user_msgs else "" return await _run_sub_commander(state, AgentRole.ANALYST, ANALYST_SYSTEM_PROMPT, user_query, use_tools=True, summary_target="analysis_report") def route_agent(state: AgentState) -> str: """路由函数: 决定下一个节点""" if state.get("final_response"): return END return state.get("current_agent", AgentRole.MASTER).value # ===================== 构建图 ===================== def create_agent_graph(callbacks: list | None = None): graph = StateGraph(AgentState) graph.add_node(AgentRole.MASTER.value, master_node) graph.add_node(AgentRole.PLANNER.value, planner_node) graph.add_node(AgentRole.EXECUTOR.value, executor_node) graph.add_node(AgentRole.LIBRARIAN.value, librarian_node) graph.add_node(AgentRole.ANALYST.value, analyst_node) graph.set_entry_point(AgentRole.MASTER.value) graph.add_conditional_edges( AgentRole.MASTER.value, route_agent, { AgentRole.PLANNER.value: AgentRole.PLANNER.value, AgentRole.EXECUTOR.value: AgentRole.EXECUTOR.value, AgentRole.LIBRARIAN.value: AgentRole.LIBRARIAN.value, AgentRole.ANALYST.value: AgentRole.ANALYST.value, END: END, } ) for role in [AgentRole.PLANNER, AgentRole.EXECUTOR, AgentRole.LIBRARIAN, AgentRole.ANALYST]: graph.add_edge(role.value, END) return _compile_graph(graph, callbacks=callbacks) _agent_graph = None def get_agent_graph(callbacks: list | None = None): """ 获取编译好的 Agent 图(单例缓存)。 Callbacks 在首次编译时固定注入,后续调用忽略 callbacks 参数。 如需变更 Callbacks(如修改 LANGCHAIN_PROJECT),需重启服务。 Args: callbacks: 可选的额外 Callbacks,会与全局 LangSmith Callbacks 合并 """ global _agent_graph if _agent_graph is None: from app.config_tracing import get_langsmith_callbacks langsmith_callbacks = get_langsmith_callbacks() all_callbacks = (callbacks or []) + langsmith_callbacks _agent_graph = create_agent_graph(callbacks=all_callbacks or None) return _agent_graph