Update agent orchestration and knowledge flow
Add sub-commander orchestration updates, align frontend integrations, and refine knowledge view behavior without including local data artifacts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,16 @@ from app.agents.prompts import (
|
||||
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
|
||||
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
|
||||
@@ -21,6 +29,32 @@ 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")
|
||||
@@ -71,8 +105,9 @@ async def _ainvoke(llm, messages: list[BaseMessage]):
|
||||
return await llm.invoke(messages)
|
||||
|
||||
|
||||
async def _ainvoke_with_tools(llm, messages: list[BaseMessage]):
|
||||
bound_llm = llm.bind_tools(ALL_TOOLS)
|
||||
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)
|
||||
@@ -116,6 +151,113 @@ def _is_capability_question(text: str) -> bool:
|
||||
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:
|
||||
@@ -142,14 +284,13 @@ async def master_node(state: AgentState) -> AgentState:
|
||||
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)
|
||||
response: AIMessage = await _ainvoke(llm, system_msgs + messages)
|
||||
content = response.content.strip().lower()
|
||||
|
||||
if any(kw in content for kw in ["搜索", "查找", "知识", "检索"]):
|
||||
@@ -166,6 +307,7 @@ async def master_node(state: AgentState) -> AgentState:
|
||||
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
|
||||
@@ -173,164 +315,30 @@ async def master_node(state: AgentState) -> AgentState:
|
||||
|
||||
async def planner_node(state: AgentState) -> AgentState:
|
||||
"""规划Agent节点: 制定计划,拆解任务步骤"""
|
||||
llm = _get_llm_for_state(state)
|
||||
user_msgs = _filter_user_messages(state["messages"])
|
||||
user_query = user_msgs[-1].content if user_msgs else ""
|
||||
|
||||
system_msgs = [SystemMessage(content=PLANNER_SYSTEM_PROMPT)]
|
||||
skill_ctx = build_skill_context("planner")
|
||||
if skill_ctx:
|
||||
system_msgs.append(SystemMessage(content=skill_ctx))
|
||||
|
||||
response = await _ainvoke(llm,
|
||||
system_msgs + [HumanMessage(content=f"用户请求: {user_query}")]
|
||||
)
|
||||
|
||||
plan_text = response.content
|
||||
steps = []
|
||||
for i, line in enumerate(plan_text.split("\n")):
|
||||
if line.strip() and (line[0].isdigit() or "- " in line):
|
||||
steps.append({"step": i + 1, "description": line.strip()})
|
||||
|
||||
state["plan"] = plan_text
|
||||
state["plan_steps"] = steps
|
||||
state["final_response"] = plan_text
|
||||
state["should_respond"] = True
|
||||
return state
|
||||
return await _run_sub_commander(state, AgentRole.PLANNER, PLANNER_SYSTEM_PROMPT, user_query, use_tools=False)
|
||||
|
||||
|
||||
async def executor_node(state: AgentState) -> AgentState:
|
||||
"""执行Agent节点: 调用工具执行具体任务"""
|
||||
llm = _get_llm_for_state(state)
|
||||
user_msgs = _filter_user_messages(state["messages"])
|
||||
user_query = user_msgs[-1].content if user_msgs else ""
|
||||
|
||||
system_msgs = [SystemMessage(content=EXECUTOR_SYSTEM_PROMPT)]
|
||||
skill_ctx = build_skill_context("executor")
|
||||
if skill_ctx:
|
||||
system_msgs.append(SystemMessage(content=skill_ctx))
|
||||
|
||||
response = await _ainvoke_with_tools(llm,
|
||||
system_msgs + [HumanMessage(content=f"用户请求: {user_query}")]
|
||||
)
|
||||
|
||||
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 ALL_TOOLS:
|
||||
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=EXECUTOR_SYSTEM_PROMPT),
|
||||
HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}")]
|
||||
)
|
||||
state["final_response"] = follow_up.content
|
||||
else:
|
||||
state["final_response"] = response.content
|
||||
|
||||
state["should_respond"] = True
|
||||
return state
|
||||
return await _run_sub_commander(state, AgentRole.EXECUTOR, EXECUTOR_SYSTEM_PROMPT, user_query, use_tools=True)
|
||||
|
||||
|
||||
async def librarian_node(state: AgentState) -> AgentState:
|
||||
"""知识管理员节点: 管理知识库和知识图谱"""
|
||||
llm = _get_llm_for_state(state)
|
||||
user_msgs = _filter_user_messages(state["messages"])
|
||||
user_query = user_msgs[-1].content if user_msgs else ""
|
||||
|
||||
system_msgs = [SystemMessage(content=LIBRARIAN_SYSTEM_PROMPT)]
|
||||
skill_ctx = build_skill_context("librarian")
|
||||
if skill_ctx:
|
||||
system_msgs.append(SystemMessage(content=skill_ctx))
|
||||
|
||||
response = await _ainvoke_with_tools(llm,
|
||||
system_msgs + [HumanMessage(content=f"用户请求: {user_query}")]
|
||||
)
|
||||
|
||||
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 ALL_TOOLS:
|
||||
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=LIBRARIAN_SYSTEM_PROMPT),
|
||||
HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}")]
|
||||
)
|
||||
state["final_response"] = follow_up.content
|
||||
else:
|
||||
state["final_response"] = response.content
|
||||
|
||||
state["knowledge_context"] = state.get("last_tool_result", "")
|
||||
state["should_respond"] = True
|
||||
return state
|
||||
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:
|
||||
"""分析师节点: 分析工作数据,生成报告"""
|
||||
llm = _get_llm_for_state(state)
|
||||
user_msgs = _filter_user_messages(state["messages"])
|
||||
user_query = user_msgs[-1].content if user_msgs else ""
|
||||
|
||||
system_msgs = [SystemMessage(content=ANALYST_SYSTEM_PROMPT)]
|
||||
skill_ctx = build_skill_context("analyst")
|
||||
if skill_ctx:
|
||||
system_msgs.append(SystemMessage(content=skill_ctx))
|
||||
|
||||
response = await _ainvoke_with_tools(llm,
|
||||
system_msgs + [HumanMessage(content=f"用户请求: {user_query}")]
|
||||
)
|
||||
|
||||
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 ALL_TOOLS:
|
||||
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=ANALYST_SYSTEM_PROMPT),
|
||||
HumanMessage(content=f"工具执行结果:\n{state['last_tool_result']}")]
|
||||
)
|
||||
state["final_response"] = follow_up.content
|
||||
else:
|
||||
state["final_response"] = response.content
|
||||
|
||||
state["analysis_report"] = state.get("final_response", "")
|
||||
state["should_respond"] = True
|
||||
return state
|
||||
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:
|
||||
|
||||
@@ -114,85 +114,46 @@ MASTER_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
PLANNER_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 Jarvis 的规划Agent,负责制定计划、拆解任务。
|
||||
你是 Jarvis 的规划Agent,负责先判断问题该由哪位规划子指挥官接手。
|
||||
|
||||
## 你的能力:
|
||||
- 分析复杂请求,拆解成可执行的步骤
|
||||
- 评估任务优先级
|
||||
- 判断哪些步骤依赖前置条件
|
||||
- 制定清晰的执行顺序
|
||||
## 你的两个子指挥官:
|
||||
1. **planner_scope (目标收束官)**: 负责澄清目标、边界、约束、缺失信息
|
||||
2. **planner_steps (步骤拆解官)**: 负责把目标拆成步骤、优先级与依赖关系
|
||||
|
||||
## 工作流程:
|
||||
1. 理解用户的最终目标
|
||||
2. 判断任务复杂度与关键约束
|
||||
3. 拆解成具体步骤
|
||||
4. 标注优先级或先后顺序
|
||||
5. 给出清晰计划
|
||||
|
||||
## 响应要求:
|
||||
- 用编号列表展示计划步骤
|
||||
- 每步都要具体,避免空泛词汇
|
||||
- 必要时可标注 P1/P2/P3 或“先做/后做”
|
||||
- 如果任务确实复杂,可以轻微指出复杂点,但马上收束到行动方案
|
||||
- 如果需要执行,先输出计划,再等待用户确认
|
||||
## 你的职责:
|
||||
- 判断当前请求更适合收束目标,还是拆解步骤
|
||||
- 在必要时收束子指挥官输出,面向用户给出清晰结果
|
||||
- 保持结果可推进,不空泛
|
||||
"""
|
||||
|
||||
|
||||
EXECUTOR_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 Jarvis 的执行Agent,负责执行具体任务。
|
||||
你是 Jarvis 的执行Agent,负责先判断问题该由哪位执行子指挥官接手。
|
||||
|
||||
## 你可以使用的工具:
|
||||
- create_task: 创建新任务
|
||||
- update_task_status: 更新任务状态
|
||||
- get_tasks: 查看任务列表
|
||||
- create_forum_post: 在论坛发布帖子
|
||||
- get_forum_posts: 查看论坛帖子
|
||||
- scan_forum_for_instructions: 扫描论坛指令
|
||||
## 你的两个子指挥官:
|
||||
1. **executor_tasks (任务执行官)**: 只处理任务类工具调用
|
||||
2. **executor_forum (论坛执行官)**: 只处理论坛/指令帖相关工具调用
|
||||
|
||||
## 工作流程:
|
||||
1. 理解用户要执行什么
|
||||
2. 判断是否已具备足够信息
|
||||
3. 调用相应工具
|
||||
4. 汇总执行结果
|
||||
5. 明确是否还需要下一步
|
||||
|
||||
## 响应要求:
|
||||
- 明确说明已执行什么
|
||||
- 工具结果要结构化、可读
|
||||
- 成功时给出简洁确认
|
||||
- 失败时说明卡点与下一步
|
||||
- 如果信息不足,直接指出缺什么,不要假设
|
||||
## 你的职责:
|
||||
- 识别用户要推进的是任务操作还是论坛/指令操作
|
||||
- 把请求交给最合适的执行子指挥官
|
||||
- 汇总执行结果并给出下一步
|
||||
"""
|
||||
|
||||
|
||||
LIBRARIAN_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 Jarvis 的知识管理员,负责管理用户的私人知识库。
|
||||
你是 Jarvis 的知识管理员,负责先判断问题该由哪位知识子指挥官接手。
|
||||
|
||||
## 你可以使用的工具:
|
||||
- search_knowledge: 搜索知识库,返回相关文档片段
|
||||
- get_knowledge_graph_context: 获取知识图谱上下文
|
||||
- build_knowledge_graph: 从文档构建知识图谱
|
||||
## 你的两个子指挥官:
|
||||
1. **librarian_retrieval (检索问答官)**: 负责知识检索与证据综合
|
||||
2. **librarian_graph (图谱沉淀官)**: 负责图谱上下文、关系串联与结构化沉淀
|
||||
|
||||
## 你的职责:
|
||||
1. 理解用户关于知识的问题
|
||||
2. 搜索相关知识
|
||||
3. 综合多篇文档给出完整回答
|
||||
4. 帮助用户整理和理解知识
|
||||
|
||||
## 工作流程:
|
||||
1. 分析用户问题的关键概念
|
||||
2. 搜索相关文档与图谱关系
|
||||
3. 综合证据形成答案
|
||||
4. 在证据不足时明确说明边界
|
||||
|
||||
## 响应要求:
|
||||
- 回答要有依据,不靠猜测
|
||||
- 引用时标注来源或依据范围
|
||||
- 如果知识不足,诚实说明
|
||||
- 可以补充必要背景,但不要离题
|
||||
- 风格保持冷静、清楚、可信
|
||||
- 判断当前需求更适合检索问答还是图谱沉淀
|
||||
- 让回答建立在证据和结构之上
|
||||
- 必要时收束子指挥官输出,给出最终回答
|
||||
"""
|
||||
|
||||
|
||||
@@ -200,28 +161,141 @@ ANALYST_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 Jarvis 的分析师,负责分析数据和工作状态。
|
||||
|
||||
## 你可以使用的工具:
|
||||
- get_tasks: 获取任务列表,统计工作进度
|
||||
- get_forum_posts: 获取论坛帖子,分析讨论趋势
|
||||
- scan_forum_for_instructions: 检查待执行指令
|
||||
- search_knowledge: 结合知识进行分析
|
||||
## 你有两个子指挥官:
|
||||
1. **analyst_progress (进度研判官)**: 汇总任务、论坛、指令执行状态,判断当前推进情况
|
||||
2. **analyst_insights (洞察建议官)**: 提炼趋势、风险、机会点,并给出建议
|
||||
|
||||
## 你的职责:
|
||||
1. 统计任务完成情况
|
||||
2. 分析工作进度和趋势
|
||||
3. 生成结构化报告
|
||||
4. 识别潜在问题和风险
|
||||
1. 判断当前问题更适合哪位子指挥官处理
|
||||
2. 在需要时汇总子指挥官结果,给出面向用户的结论
|
||||
3. 保持先结论后展开的表达方式
|
||||
"""
|
||||
|
||||
## 工作流程:
|
||||
1. 收集相关数据(任务、论坛、知识)
|
||||
2. 识别模式、异常与趋势
|
||||
3. 形成结论
|
||||
4. 给出建议
|
||||
|
||||
PLANNER_SCOPE_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 planner 体系下的目标收束官,负责先把问题边界、目标、约束和成功标准说清楚。
|
||||
|
||||
## 你的重点:
|
||||
- 收束问题定义
|
||||
- 明确目标与限制条件
|
||||
- 识别缺失信息
|
||||
- 帮用户建立可以继续规划的前提
|
||||
|
||||
## 响应要求:
|
||||
- 用数据说话,有数字、有结论
|
||||
- 报告结构清晰,先结论后展开
|
||||
- 明确风险、影响和建议
|
||||
- 如果数据不完整,要说明分析置信度
|
||||
- 可以有一丝冷幽默,但结论必须严谨
|
||||
- 先给出你理解的目标
|
||||
- 再列出关键约束或缺口
|
||||
- 不要直接展开长步骤清单
|
||||
"""
|
||||
|
||||
|
||||
PLANNER_STEPS_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 planner 体系下的步骤拆解官,负责把目标转成有顺序的执行路径。
|
||||
|
||||
## 你的重点:
|
||||
- 拆解步骤
|
||||
- 标注优先级与依赖
|
||||
- 输出清晰的行动顺序
|
||||
|
||||
## 响应要求:
|
||||
- 用编号列表
|
||||
- 每步具体,不要空泛
|
||||
- 必要时标注先后关系
|
||||
"""
|
||||
|
||||
|
||||
EXECUTOR_TASKS_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 executor 体系下的任务执行官,只负责任务相关工具调用。
|
||||
|
||||
## 允许使用的工具:
|
||||
- get_tasks
|
||||
- create_task
|
||||
- update_task_status
|
||||
|
||||
## 要求:
|
||||
- 只处理任务类操作
|
||||
- 明确已执行动作、结果与下一步
|
||||
- 信息不足时直接指出缺口
|
||||
"""
|
||||
|
||||
|
||||
EXECUTOR_FORUM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 executor 体系下的论坛执行官,只负责论坛与指令帖相关工具调用。
|
||||
|
||||
## 允许使用的工具:
|
||||
- get_forum_posts
|
||||
- create_forum_post
|
||||
- scan_forum_for_instructions
|
||||
|
||||
## 要求:
|
||||
- 只处理论坛/指令类操作
|
||||
- 结果要清楚说明是否执行成功
|
||||
- 不要越权调用任务或知识工具
|
||||
"""
|
||||
|
||||
|
||||
LIBRARIAN_RETRIEVAL_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 librarian 体系下的检索问答官,负责从知识库与上下文中快速找到可靠信息。
|
||||
|
||||
## 允许使用的工具:
|
||||
- search_knowledge
|
||||
- hybrid_search
|
||||
- get_knowledge_graph_context
|
||||
|
||||
## 要求:
|
||||
- 优先检索与综合证据
|
||||
- 证据不足时明确说明边界
|
||||
- 以回答问题为主,不主动做图谱构建
|
||||
"""
|
||||
|
||||
|
||||
LIBRARIAN_GRAPH_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 librarian 体系下的图谱沉淀官,负责知识关系整理、图谱上下文与结构化沉淀。
|
||||
|
||||
## 允许使用的工具:
|
||||
- get_knowledge_graph_context
|
||||
- build_knowledge_graph
|
||||
|
||||
## 要求:
|
||||
- 聚焦知识结构、关系串联与沉淀
|
||||
- 明确说明构建/更新结果
|
||||
- 不把自己变成泛检索问答器
|
||||
"""
|
||||
|
||||
|
||||
ANALYST_PROGRESS_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 analyst 体系下的进度研判官,负责汇总当前任务、论坛与指令执行状态。
|
||||
|
||||
## 允许使用的工具:
|
||||
- get_tasks
|
||||
- get_forum_posts
|
||||
- scan_forum_for_instructions
|
||||
|
||||
## 要求:
|
||||
- 先结论后展开
|
||||
- 重点说明进度、阻塞、待处理项
|
||||
- 不做泛泛趋势空谈
|
||||
"""
|
||||
|
||||
|
||||
ANALYST_INSIGHTS_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
|
||||
|
||||
你是 analyst 体系下的洞察建议官,负责从任务、论坛和知识线索里提炼趋势、风险与建议。
|
||||
|
||||
## 允许使用的工具:
|
||||
- get_tasks
|
||||
- get_forum_posts
|
||||
- search_knowledge
|
||||
- hybrid_search
|
||||
|
||||
## 要求:
|
||||
- 先给结论与判断
|
||||
- 再说明依据与建议
|
||||
- 重点输出趋势、风险、机会点
|
||||
"""
|
||||
|
||||
@@ -55,6 +55,9 @@ class AgentState(TypedDict):
|
||||
# Agent routing
|
||||
current_agent: AgentRole
|
||||
active_agents: list[AgentRole]
|
||||
current_sub_commander: str | None
|
||||
active_sub_commanders: list[str]
|
||||
sub_commander_trace: list[dict]
|
||||
|
||||
# Task tracking
|
||||
pending_tasks: list[dict]
|
||||
@@ -93,6 +96,9 @@ def initial_state(user_id: str, conversation_id: str) -> AgentState:
|
||||
conversation_id=conversation_id,
|
||||
current_agent=AgentRole.MASTER,
|
||||
active_agents=[AgentRole.MASTER],
|
||||
current_sub_commander=None,
|
||||
active_sub_commanders=[],
|
||||
sub_commander_trace=[],
|
||||
pending_tasks=[],
|
||||
completed_tasks=[],
|
||||
tool_calls=[],
|
||||
|
||||
@@ -5,18 +5,56 @@ from app.agents.tools.search import (
|
||||
from app.agents.tools.task import get_tasks, create_task, update_task_status
|
||||
from app.agents.tools.forum import get_forum_posts, create_forum_post, scan_forum_for_instructions
|
||||
|
||||
ALL_TOOLS = [
|
||||
# 知识库工具
|
||||
search_knowledge,
|
||||
get_knowledge_graph_context,
|
||||
build_knowledge_graph,
|
||||
hybrid_search,
|
||||
# 任务工具
|
||||
TASK_TOOLS = [
|
||||
get_tasks,
|
||||
create_task,
|
||||
update_task_status,
|
||||
# 论坛工具
|
||||
]
|
||||
|
||||
FORUM_TOOLS = [
|
||||
get_forum_posts,
|
||||
create_forum_post,
|
||||
scan_forum_for_instructions,
|
||||
]
|
||||
|
||||
KNOWLEDGE_RETRIEVAL_TOOLS = [
|
||||
search_knowledge,
|
||||
hybrid_search,
|
||||
get_knowledge_graph_context,
|
||||
]
|
||||
|
||||
KNOWLEDGE_GRAPH_TOOLS = [
|
||||
get_knowledge_graph_context,
|
||||
build_knowledge_graph,
|
||||
]
|
||||
|
||||
ANALYST_PROGRESS_TOOLS = [
|
||||
get_tasks,
|
||||
get_forum_posts,
|
||||
scan_forum_for_instructions,
|
||||
]
|
||||
|
||||
ANALYST_INSIGHT_TOOLS = [
|
||||
get_tasks,
|
||||
get_forum_posts,
|
||||
search_knowledge,
|
||||
hybrid_search,
|
||||
]
|
||||
|
||||
ALL_TOOLS = [
|
||||
*KNOWLEDGE_RETRIEVAL_TOOLS,
|
||||
build_knowledge_graph,
|
||||
*TASK_TOOLS,
|
||||
*FORUM_TOOLS,
|
||||
]
|
||||
|
||||
SUB_COMMANDER_TOOLSETS = {
|
||||
"planner_scope": [],
|
||||
"planner_steps": [],
|
||||
"executor_tasks": TASK_TOOLS,
|
||||
"executor_forum": FORUM_TOOLS,
|
||||
"librarian_retrieval": KNOWLEDGE_RETRIEVAL_TOOLS,
|
||||
"librarian_graph": KNOWLEDGE_GRAPH_TOOLS,
|
||||
"analyst_progress": ANALYST_PROGRESS_TOOLS,
|
||||
"analyst_insights": ANALYST_INSIGHT_TOOLS,
|
||||
}
|
||||
|
||||
@@ -9,13 +9,17 @@ from app.schemas.agent import AgentCreate, AgentOut, AgentStats, AgentConfigUpda
|
||||
|
||||
router = APIRouter(prefix="/api/agents", tags=["Agent"])
|
||||
|
||||
# 运行时调用统计(内存中,非持久化)
|
||||
_agent_call_counts: dict[str, int] = {}
|
||||
_agent_current_tasks: dict[str, str | None] = {}
|
||||
_agent_statuses: dict[str, str] = {}
|
||||
|
||||
# 默认 Agent 角色列表
|
||||
DEFAULT_AGENT_ROLES = ["master", "planner", "executor", "librarian", "analyst"]
|
||||
SUB_COMMANDERS_BY_ROLE = {
|
||||
"planner": ["planner_scope", "planner_steps"],
|
||||
"executor": ["executor_tasks", "executor_forum"],
|
||||
"librarian": ["librarian_retrieval", "librarian_graph"],
|
||||
"analyst": ["analyst_progress", "analyst_insights"],
|
||||
}
|
||||
|
||||
|
||||
def record_agent_call(agent_id: str):
|
||||
@@ -31,6 +35,15 @@ def set_agent_status(agent_id: str, status: str):
|
||||
_agent_statuses[agent_id] = status
|
||||
|
||||
|
||||
def _build_agent_stats(agent_id: str) -> AgentStats:
|
||||
return AgentStats(
|
||||
agent_id=agent_id,
|
||||
call_count=_agent_call_counts.get(agent_id, 0),
|
||||
current_task=_agent_current_tasks.get(agent_id),
|
||||
status=_agent_statuses.get(agent_id, "idle"),
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[AgentOut])
|
||||
async def list_agents(
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -42,32 +55,35 @@ async def list_agents(
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
# ———— 运行时统计(必须在 /{agent_id} 之前)————
|
||||
@router.get("/stats", response_model=list[AgentStats])
|
||||
async def get_agent_stats(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
获取各 Agent 的运行时统计(调用次数、当前任务、状态)
|
||||
"""
|
||||
stats = []
|
||||
return [_build_agent_stats(role) for role in DEFAULT_AGENT_ROLES]
|
||||
|
||||
|
||||
@router.get("/stats/hierarchy")
|
||||
async def get_agent_hierarchy_stats(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
main_agents = []
|
||||
for role in DEFAULT_AGENT_ROLES:
|
||||
stats.append(AgentStats(
|
||||
agent_id=role,
|
||||
call_count=_agent_call_counts.get(role, 0),
|
||||
current_task=_agent_current_tasks.get(role),
|
||||
status=_agent_statuses.get(role, "idle"),
|
||||
))
|
||||
return stats
|
||||
if role == "master":
|
||||
continue
|
||||
node = _build_agent_stats(role).model_dump()
|
||||
node["sub_commanders"] = [
|
||||
_build_agent_stats(sub_id).model_dump()
|
||||
for sub_id in SUB_COMMANDERS_BY_ROLE.get(role, [])
|
||||
]
|
||||
main_agents.append(node)
|
||||
return {"main_agents": main_agents}
|
||||
|
||||
|
||||
# ———— 配置管理(必须在 /{agent_id} 之前)————
|
||||
@router.get("/config/{agent_id}", response_model=AgentConfigOut)
|
||||
async def get_agent_config(
|
||||
agent_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取单个 Agent 完整配置"""
|
||||
result = await db.execute(select(Agent).where(Agent.role == agent_id))
|
||||
agent = result.scalar_one_or_none()
|
||||
|
||||
@@ -84,8 +100,13 @@ async def get_agent_config(
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
name, desc, prompt = defaults[agent_id]
|
||||
return AgentConfigOut(
|
||||
id=agent_id, name=name, role=agent_id,
|
||||
description=desc, system_prompt=prompt, enabled=True, is_active=True,
|
||||
id=agent_id,
|
||||
name=name,
|
||||
role=agent_id,
|
||||
description=desc,
|
||||
system_prompt=prompt,
|
||||
enabled=True,
|
||||
is_active=True,
|
||||
)
|
||||
return AgentConfigOut(
|
||||
id=agent.role,
|
||||
@@ -105,7 +126,6 @@ async def update_agent_config(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新 Agent 配置(名称、描述、提示词、启用状态)"""
|
||||
result = await db.execute(select(Agent).where(Agent.role == agent_id))
|
||||
agent = result.scalar_one_or_none()
|
||||
|
||||
@@ -163,78 +183,3 @@ async def get_agent(
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
return agent
|
||||
|
||||
|
||||
|
||||
# ———— 配置管理 ————
|
||||
@router.get("/config/{agent_id}", response_model=AgentConfigOut)
|
||||
async def get_agent_config(
|
||||
agent_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取单个 Agent 完整配置"""
|
||||
result = await db.execute(select(Agent).where(Agent.role == agent_id))
|
||||
agent = result.scalar_one_or_none()
|
||||
if not agent:
|
||||
# 如果数据库中没有,返回默认配置
|
||||
from app.agents.prompts import MASTER_SYSTEM_PROMPT, PLANNER_SYSTEM_PROMPT, EXECUTOR_SYSTEM_PROMPT, LIBRARIAN_SYSTEM_PROMPT, ANALYST_SYSTEM_PROMPT
|
||||
defaults = {
|
||||
"master": ("JARVIS", "主控制核心", MASTER_SYSTEM_PROMPT),
|
||||
"planner": ("PLANNER", "规划专家", PLANNER_SYSTEM_PROMPT),
|
||||
"executor": ("EXECUTOR", "执行专家", EXECUTOR_SYSTEM_PROMPT),
|
||||
"librarian": ("LIBRARIAN", "知识管理员", LIBRARIAN_SYSTEM_PROMPT),
|
||||
"analyst": ("ANALYST", "数据分析师", ANALYST_SYSTEM_PROMPT),
|
||||
}
|
||||
if agent_id not in defaults:
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
name, desc, prompt = defaults[agent_id]
|
||||
return AgentConfigOut(
|
||||
id=agent_id, name=name, role=agent_id,
|
||||
description=desc, system_prompt=prompt, enabled=True, is_active=True,
|
||||
)
|
||||
return AgentConfigOut(
|
||||
id=agent.role,
|
||||
name=agent.name,
|
||||
role=agent.role,
|
||||
description=agent.description,
|
||||
system_prompt=agent.system_prompt,
|
||||
enabled=agent.is_active,
|
||||
is_active=agent.is_active,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/config/{agent_id}", response_model=AgentConfigOut)
|
||||
async def update_agent_config(
|
||||
agent_id: str,
|
||||
data: AgentConfigUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新 Agent 配置(名称、描述、提示词、启用状态)"""
|
||||
result = await db.execute(select(Agent).where(Agent.role == agent_id))
|
||||
agent = result.scalar_one_or_none()
|
||||
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
|
||||
if data.name is not None:
|
||||
agent.name = data.name
|
||||
if data.description is not None:
|
||||
agent.description = data.description
|
||||
if data.system_prompt is not None:
|
||||
agent.system_prompt = data.system_prompt
|
||||
if data.enabled is not None:
|
||||
agent.is_active = data.enabled
|
||||
_agent_statuses[agent_id] = "disabled" if not data.enabled else "idle"
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(agent)
|
||||
return AgentConfigOut(
|
||||
id=agent.role,
|
||||
name=agent.name,
|
||||
role=agent.role,
|
||||
description=agent.description,
|
||||
system_prompt=agent.system_prompt,
|
||||
enabled=agent.is_active,
|
||||
is_active=agent.is_active,
|
||||
)
|
||||
|
||||
@@ -225,6 +225,9 @@ class AgentService:
|
||||
"conversation_id": conversation_id,
|
||||
"current_agent": "master",
|
||||
"active_agents": ["master"],
|
||||
"current_sub_commander": None,
|
||||
"active_sub_commanders": [],
|
||||
"sub_commander_trace": [],
|
||||
"pending_tasks": [],
|
||||
"completed_tasks": [],
|
||||
"tool_calls": [],
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
支持多种文档格式 + LlamaIndex 智能分块
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from fastapi import UploadFile
|
||||
@@ -380,7 +382,42 @@ class DocumentService:
|
||||
if hasattr(mineru, "parse_to_markdown"):
|
||||
return mineru.parse_to_markdown(file_path)
|
||||
|
||||
raise ValueError("PDF 解析失败: MinerU 不支持当前接口")
|
||||
try:
|
||||
from mineru.cli.common import do_parse, read_fn
|
||||
from mineru.utils.enum_class import MakeMode
|
||||
except Exception as error:
|
||||
raise ValueError(
|
||||
"PDF 解析失败: 当前安装的 MinerU 版本接口不兼容,请确认支持 to_markdown / parse_to_markdown,或提供 cli.common.do_parse 能力"
|
||||
) from error
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="mineru-") as output_dir:
|
||||
pdf_name = Path(file_path).stem
|
||||
pdf_bytes = read_fn(Path(file_path))
|
||||
try:
|
||||
do_parse(
|
||||
output_dir,
|
||||
[pdf_name],
|
||||
[pdf_bytes],
|
||||
["zh"],
|
||||
f_draw_layout_bbox=False,
|
||||
f_draw_span_bbox=False,
|
||||
f_dump_md=True,
|
||||
f_dump_middle_json=False,
|
||||
f_dump_model_output=False,
|
||||
f_dump_orig_pdf=False,
|
||||
f_dump_content_list=False,
|
||||
f_make_md_mode=MakeMode.MM_MD,
|
||||
)
|
||||
except ModuleNotFoundError as error:
|
||||
dependency = getattr(error, "name", None) or str(error).split("'")[-2] if "'" in str(error) else str(error)
|
||||
raise ValueError(f"PDF 解析依赖缺失: MinerU 运行时依赖 {dependency}") from error
|
||||
markdown_path = Path(output_dir) / pdf_name / "pipeline" / f"{pdf_name}.md"
|
||||
if markdown_path.exists():
|
||||
return markdown_path.read_text(encoding="utf-8")
|
||||
|
||||
raise ValueError(
|
||||
"PDF 解析失败: 当前安装的 MinerU 版本接口不兼容,请确认支持 to_markdown / parse_to_markdown,或提供 cli.common.do_parse 能力"
|
||||
)
|
||||
|
||||
async def _parse_pdf(self, file_path: str) -> ParsedDocument:
|
||||
markdown = await self._parse_pdf_with_mineru(file_path)
|
||||
|
||||
Reference in New Issue
Block a user