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:
2026-03-24 21:44:04 +08:00
parent aafa05dc1c
commit 0d89325b09
14 changed files with 529 additions and 650 deletions

View File

@@ -11,8 +11,16 @@ from app.agents.prompts import (
EXECUTOR_SYSTEM_PROMPT, EXECUTOR_SYSTEM_PROMPT,
LIBRARIAN_SYSTEM_PROMPT, LIBRARIAN_SYSTEM_PROMPT,
ANALYST_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.agents.skill_registry import build_skill_context
from app.services.llm_service import get_llm from app.services.llm_service import get_llm
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
@@ -21,6 +29,32 @@ from langchain_ollama import ChatOllama
import httpx 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): def _create_llm_from_config(config: dict):
"""根据用户模型配置创建 LLM 实例""" """根据用户模型配置创建 LLM 实例"""
provider = config.get("provider", "openai") provider = config.get("provider", "openai")
@@ -71,8 +105,9 @@ async def _ainvoke(llm, messages: list[BaseMessage]):
return await llm.invoke(messages) return await llm.invoke(messages)
async def _ainvoke_with_tools(llm, messages: list[BaseMessage]): async def _ainvoke_with_tools(llm, messages: list[BaseMessage], tools=None):
bound_llm = llm.bind_tools(ALL_TOOLS) toolset = tools if tools is not None else ALL_TOOLS
bound_llm = llm.bind_tools(toolset)
if hasattr(bound_llm, "ainvoke"): if hasattr(bound_llm, "ainvoke"):
return await bound_llm.ainvoke(messages) return await bound_llm.ainvoke(messages)
return await bound_llm.invoke(messages) return await bound_llm.invoke(messages)
@@ -116,6 +151,113 @@ def _is_capability_question(text: str) -> bool:
return normalized in {"你能做什么", "你可以做什么", "你会做什么"} 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) =====================
async def master_node(state: AgentState) -> AgentState: async def master_node(state: AgentState) -> AgentState:
@@ -142,14 +284,13 @@ async def master_node(state: AgentState) -> AgentState:
llm = _get_llm_for_state(state) llm = _get_llm_for_state(state)
system_msgs: list[BaseMessage] = [SystemMessage(content=MASTER_SYSTEM_PROMPT)] system_msgs: list[BaseMessage] = [SystemMessage(content=MASTER_SYSTEM_PROMPT)]
# 注入记忆上下文
memory_ctx = state.get("memory_context") memory_ctx = state.get("memory_context")
if memory_ctx: if memory_ctx:
system_msgs.append( system_msgs.append(
SystemMessage(content=f"\n\n【记忆上下文】\n{memory_ctx}\n\n---\n") 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() content = response.content.strip().lower()
if any(kw in content for kw in ["搜索", "查找", "知识", "检索"]): if any(kw in content for kw in ["搜索", "查找", "知识", "检索"]):
@@ -166,6 +307,7 @@ async def master_node(state: AgentState) -> AgentState:
return state return state
state["current_agent"] = next_agent state["current_agent"] = next_agent
state["current_sub_commander"] = None
state["active_agents"] = state.get("active_agents", [AgentRole.MASTER]) + [next_agent] state["active_agents"] = state.get("active_agents", [AgentRole.MASTER]) + [next_agent]
state["should_respond"] = True state["should_respond"] = True
return state return state
@@ -173,164 +315,30 @@ async def master_node(state: AgentState) -> AgentState:
async def planner_node(state: AgentState) -> AgentState: async def planner_node(state: AgentState) -> AgentState:
"""规划Agent节点: 制定计划,拆解任务步骤""" """规划Agent节点: 制定计划,拆解任务步骤"""
llm = _get_llm_for_state(state)
user_msgs = _filter_user_messages(state["messages"]) user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else "" 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)
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
async def executor_node(state: AgentState) -> AgentState: async def executor_node(state: AgentState) -> AgentState:
"""执行Agent节点: 调用工具执行具体任务""" """执行Agent节点: 调用工具执行具体任务"""
llm = _get_llm_for_state(state)
user_msgs = _filter_user_messages(state["messages"]) user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else "" 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)
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
async def librarian_node(state: AgentState) -> AgentState: async def librarian_node(state: AgentState) -> AgentState:
"""知识管理员节点: 管理知识库和知识图谱""" """知识管理员节点: 管理知识库和知识图谱"""
llm = _get_llm_for_state(state)
user_msgs = _filter_user_messages(state["messages"]) user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else "" 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")
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
async def analyst_node(state: AgentState) -> AgentState: async def analyst_node(state: AgentState) -> AgentState:
"""分析师节点: 分析工作数据,生成报告""" """分析师节点: 分析工作数据,生成报告"""
llm = _get_llm_for_state(state)
user_msgs = _filter_user_messages(state["messages"]) user_msgs = _filter_user_messages(state["messages"])
user_query = user_msgs[-1].content if user_msgs else "" 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")
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
def route_agent(state: AgentState) -> str: def route_agent(state: AgentState) -> str:

View File

@@ -114,85 +114,46 @@ MASTER_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
PLANNER_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} EXECUTOR_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
你是 Jarvis 的执行Agent负责执行具体任务 你是 Jarvis 的执行Agent负责先判断问题该由哪位执行子指挥官接手
## 你可以使用的工具: ## 你的两个子指挥官:
- create_task: 创建新任务 1. **executor_tasks (任务执行官)**: 只处理任务类工具调用
- update_task_status: 更新任务状态 2. **executor_forum (论坛执行官)**: 只处理论坛/指令帖相关工具调用
- get_tasks: 查看任务列表
- create_forum_post: 在论坛发布帖子
- get_forum_posts: 查看论坛帖子
- scan_forum_for_instructions: 扫描论坛指令
## 工作流程: ## 你的职责:
1. 理解用户要执行什么 - 识别用户要推进的是任务操作还是论坛/指令操作
2. 判断是否已具备足够信息 - 把请求交给最合适的执行子指挥官
3. 调用相应工具 - 汇总执行结果并给出下一步
4. 汇总执行结果
5. 明确是否还需要下一步
## 响应要求:
- 明确说明已执行什么
- 工具结果要结构化、可读
- 成功时给出简洁确认
- 失败时说明卡点与下一步
- 如果信息不足,直接指出缺什么,不要假设
""" """
LIBRARIAN_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT} LIBRARIAN_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
你是 Jarvis 的知识管理员,负责管理用户的私人知识库 你是 Jarvis 的知识管理员,负责先判断问题该由哪位知识子指挥官接手
## 你可以使用的工具: ## 你的两个子指挥官:
- search_knowledge: 搜索知识库,返回相关文档片段 1. **librarian_retrieval (检索问答官)**: 负责知识检索与证据综合
- get_knowledge_graph_context: 获取知识图谱上下文 2. **librarian_graph (图谱沉淀官)**: 负责图谱上下文、关系串联与结构化沉淀
- build_knowledge_graph: 从文档构建知识图谱
## 你的职责: ## 你的职责:
1. 理解用户关于知识的问题 - 判断当前需求更适合检索问答还是图谱沉淀
2. 搜索相关知识 - 让回答建立在证据和结构之上
3. 综合多篇文档给出完整回答 - 必要时收束子指挥官输出,给出最终回答
4. 帮助用户整理和理解知识
## 工作流程:
1. 分析用户问题的关键概念
2. 搜索相关文档与图谱关系
3. 综合证据形成答案
4. 在证据不足时明确说明边界
## 响应要求:
- 回答要有依据,不靠猜测
- 引用时标注来源或依据范围
- 如果知识不足,诚实说明
- 可以补充必要背景,但不要离题
- 风格保持冷静、清楚、可信
""" """
@@ -200,28 +161,141 @@ ANALYST_SYSTEM_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
你是 Jarvis 的分析师,负责分析数据和工作状态。 你是 Jarvis 的分析师,负责分析数据和工作状态。
## 你可以使用的工具: ## 你有两个子指挥官:
- get_tasks: 获取任务列表,统计工作进度 1. **analyst_progress (进度研判官)**: 汇总任务、论坛、指令执行状态,判断当前推进情况
- get_forum_posts: 获取论坛帖子,分析讨论趋势 2. **analyst_insights (洞察建议官)**: 提炼趋势、风险、机会点,并给出建议
- scan_forum_for_instructions: 检查待执行指令
- search_knowledge: 结合知识进行分析
## 你的职责: ## 你的职责:
1. 统计任务完成情况 1. 判断当前问题更适合哪位子指挥官处理
2. 分析工作进度和趋势 2. 在需要时汇总子指挥官结果,给出面向用户的结论
3. 生成结构化报告 3. 保持先结论后展开的表达方式
4. 识别潜在问题和风险 """
## 工作流程:
1. 收集相关数据(任务、论坛、知识) PLANNER_SCOPE_PROMPT = f"""{JARVIS_PERSONA_PROMPT}
2. 识别模式、异常与趋势
3. 形成结论 你是 planner 体系下的目标收束官,负责先把问题边界、目标、约束和成功标准说清楚。
4. 给出建议
## 你的重点:
- 收束问题定义
- 明确目标与限制条件
- 识别缺失信息
- 帮用户建立可以继续规划的前提
## 响应要求: ## 响应要求:
- 用数据说话,有数字、有结论 - 先给出你理解的目标
- 报告结构清晰,先结论后展开 - 再列出关键约束或缺口
- 明确风险、影响和建议 - 不要直接展开长步骤清单
- 如果数据不完整,要说明分析置信度 """
- 可以有一丝冷幽默,但结论必须严谨
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
## 要求:
- 先给结论与判断
- 再说明依据与建议
- 重点输出趋势、风险、机会点
""" """

View File

@@ -55,6 +55,9 @@ class AgentState(TypedDict):
# Agent routing # Agent routing
current_agent: AgentRole current_agent: AgentRole
active_agents: list[AgentRole] active_agents: list[AgentRole]
current_sub_commander: str | None
active_sub_commanders: list[str]
sub_commander_trace: list[dict]
# Task tracking # Task tracking
pending_tasks: list[dict] pending_tasks: list[dict]
@@ -93,6 +96,9 @@ def initial_state(user_id: str, conversation_id: str) -> AgentState:
conversation_id=conversation_id, conversation_id=conversation_id,
current_agent=AgentRole.MASTER, current_agent=AgentRole.MASTER,
active_agents=[AgentRole.MASTER], active_agents=[AgentRole.MASTER],
current_sub_commander=None,
active_sub_commanders=[],
sub_commander_trace=[],
pending_tasks=[], pending_tasks=[],
completed_tasks=[], completed_tasks=[],
tool_calls=[], tool_calls=[],

View File

@@ -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.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 from app.agents.tools.forum import get_forum_posts, create_forum_post, scan_forum_for_instructions
ALL_TOOLS = [ TASK_TOOLS = [
# 知识库工具
search_knowledge,
get_knowledge_graph_context,
build_knowledge_graph,
hybrid_search,
# 任务工具
get_tasks, get_tasks,
create_task, create_task,
update_task_status, update_task_status,
# 论坛工具 ]
FORUM_TOOLS = [
get_forum_posts, get_forum_posts,
create_forum_post, create_forum_post,
scan_forum_for_instructions, 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,
}

View File

@@ -9,13 +9,17 @@ from app.schemas.agent import AgentCreate, AgentOut, AgentStats, AgentConfigUpda
router = APIRouter(prefix="/api/agents", tags=["Agent"]) router = APIRouter(prefix="/api/agents", tags=["Agent"])
# 运行时调用统计(内存中,非持久化)
_agent_call_counts: dict[str, int] = {} _agent_call_counts: dict[str, int] = {}
_agent_current_tasks: dict[str, str | None] = {} _agent_current_tasks: dict[str, str | None] = {}
_agent_statuses: dict[str, str] = {} _agent_statuses: dict[str, str] = {}
# 默认 Agent 角色列表
DEFAULT_AGENT_ROLES = ["master", "planner", "executor", "librarian", "analyst"] 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): 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 _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]) @router.get("", response_model=list[AgentOut])
async def list_agents( async def list_agents(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
@@ -42,32 +55,35 @@ async def list_agents(
return result.scalars().all() return result.scalars().all()
# ———— 运行时统计(必须在 /{agent_id} 之前)————
@router.get("/stats", response_model=list[AgentStats]) @router.get("/stats", response_model=list[AgentStats])
async def get_agent_stats( async def get_agent_stats(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
""" return [_build_agent_stats(role) for role in DEFAULT_AGENT_ROLES]
获取各 Agent 的运行时统计(调用次数、当前任务、状态)
"""
stats = [] @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: for role in DEFAULT_AGENT_ROLES:
stats.append(AgentStats( if role == "master":
agent_id=role, continue
call_count=_agent_call_counts.get(role, 0), node = _build_agent_stats(role).model_dump()
current_task=_agent_current_tasks.get(role), node["sub_commanders"] = [
status=_agent_statuses.get(role, "idle"), _build_agent_stats(sub_id).model_dump()
)) for sub_id in SUB_COMMANDERS_BY_ROLE.get(role, [])
return stats ]
main_agents.append(node)
return {"main_agents": main_agents}
# ———— 配置管理(必须在 /{agent_id} 之前)————
@router.get("/config/{agent_id}", response_model=AgentConfigOut) @router.get("/config/{agent_id}", response_model=AgentConfigOut)
async def get_agent_config( async def get_agent_config(
agent_id: str, agent_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""获取单个 Agent 完整配置"""
result = await db.execute(select(Agent).where(Agent.role == agent_id)) result = await db.execute(select(Agent).where(Agent.role == agent_id))
agent = result.scalar_one_or_none() agent = result.scalar_one_or_none()
@@ -84,8 +100,13 @@ async def get_agent_config(
raise HTTPException(status_code=404, detail="Agent 不存在") raise HTTPException(status_code=404, detail="Agent 不存在")
name, desc, prompt = defaults[agent_id] name, desc, prompt = defaults[agent_id]
return AgentConfigOut( return AgentConfigOut(
id=agent_id, name=name, role=agent_id, id=agent_id,
description=desc, system_prompt=prompt, enabled=True, is_active=True, name=name,
role=agent_id,
description=desc,
system_prompt=prompt,
enabled=True,
is_active=True,
) )
return AgentConfigOut( return AgentConfigOut(
id=agent.role, id=agent.role,
@@ -105,7 +126,6 @@ async def update_agent_config(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""更新 Agent 配置(名称、描述、提示词、启用状态)"""
result = await db.execute(select(Agent).where(Agent.role == agent_id)) result = await db.execute(select(Agent).where(Agent.role == agent_id))
agent = result.scalar_one_or_none() agent = result.scalar_one_or_none()
@@ -163,78 +183,3 @@ async def get_agent(
if not agent: if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在") raise HTTPException(status_code=404, detail="Agent 不存在")
return 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,
)

View File

@@ -225,6 +225,9 @@ class AgentService:
"conversation_id": conversation_id, "conversation_id": conversation_id,
"current_agent": "master", "current_agent": "master",
"active_agents": ["master"], "active_agents": ["master"],
"current_sub_commander": None,
"active_sub_commanders": [],
"sub_commander_trace": [],
"pending_tasks": [], "pending_tasks": [],
"completed_tasks": [], "completed_tasks": [],
"tool_calls": [], "tool_calls": [],

View File

@@ -3,6 +3,8 @@
支持多种文档格式 + LlamaIndex 智能分块 支持多种文档格式 + LlamaIndex 智能分块
""" """
from pathlib import Path
import tempfile
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from fastapi import UploadFile from fastapi import UploadFile
@@ -380,7 +382,42 @@ class DocumentService:
if hasattr(mineru, "parse_to_markdown"): if hasattr(mineru, "parse_to_markdown"):
return mineru.parse_to_markdown(file_path) 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: async def _parse_pdf(self, file_path: str) -> ParsedDocument:
markdown = await self._parse_pdf_with_mineru(file_path) markdown = await self._parse_pdf_with_mineru(file_path)

View File

@@ -1,6 +1,7 @@
import json import json
from io import BytesIO from io import BytesIO
import builtins import builtins
from pathlib import Path
import sys import sys
import types import types
@@ -351,6 +352,73 @@ async def test_get_document_content_returns_normalized_pdf_content(document_test
assert content == '# PDF Title\n\nNormalized pdf body.' assert content == '# PDF Title\n\nNormalized pdf body.'
@pytest.mark.asyncio
async def test_upload_document_uses_mineru_cli_do_parse_fallback_for_pdf(document_test_env, monkeypatch, tmp_path):
session, user = document_test_env
service = DocumentService(session)
fake_mineru = types.SimpleNamespace()
fake_common = types.SimpleNamespace()
def fake_do_parse(output_dir, pdf_file_names, pdf_bytes_list, p_lang_list, **kwargs):
output_path = Path(output_dir) / pdf_file_names[0] / 'pipeline'
output_path.mkdir(parents=True, exist_ok=True)
(output_path / f'{pdf_file_names[0]}.md').write_text('# PDF Title\n\nCLI fallback content.', encoding='utf-8')
fake_common.do_parse = fake_do_parse
monkeypatch.setitem(sys.modules, 'mineru', fake_mineru)
monkeypatch.setitem(sys.modules, 'mineru.cli', types.SimpleNamespace(common=fake_common))
monkeypatch.setitem(sys.modules, 'mineru.cli.common', fake_common)
monkeypatch.setattr('app.services.document_service.settings.UPLOAD_DIR', str(tmp_path / 'uploads'))
upload = UploadFile(filename='fallback.pdf', file=BytesIO(b'%PDF-1.4 fake'))
document = await service.upload_document(user.id, upload)
assert document.normalized_format == 'structured_markdown'
assert '# PDF Title' in document.normalized_content
assert 'CLI fallback content.' in document.normalized_content
@pytest.mark.asyncio
async def test_upload_document_raises_clear_error_when_mineru_cli_runtime_dependency_is_missing(document_test_env, monkeypatch):
session, user = document_test_env
service = DocumentService(session)
fake_mineru = types.SimpleNamespace()
fake_common = types.SimpleNamespace()
fake_enum_class = types.SimpleNamespace(MakeMode=types.SimpleNamespace(MM_MD='mm_markdown'))
def fake_do_parse(*args, **kwargs):
raise ModuleNotFoundError("No module named 'torch'")
fake_common.do_parse = fake_do_parse
fake_common.read_fn = lambda path: b'%PDF-1.4 fake'
monkeypatch.setitem(sys.modules, 'mineru', fake_mineru)
monkeypatch.setitem(sys.modules, 'mineru.cli', types.SimpleNamespace(common=fake_common))
monkeypatch.setitem(sys.modules, 'mineru.cli.common', fake_common)
monkeypatch.setitem(sys.modules, 'mineru.utils', types.SimpleNamespace(enum_class=fake_enum_class))
monkeypatch.setitem(sys.modules, 'mineru.utils.enum_class', fake_enum_class)
upload = UploadFile(filename='runtime-missing.pdf', file=BytesIO(b'%PDF-1.4 fake'))
with pytest.raises(ValueError, match="PDF 解析依赖缺失: MinerU 运行时依赖 torch"):
await service.upload_document(user.id, upload)
@pytest.mark.asyncio
async def test_upload_document_raises_clear_error_when_mineru_interface_is_unsupported(document_test_env, monkeypatch):
session, user = document_test_env
service = DocumentService(session)
fake_mineru = types.SimpleNamespace()
monkeypatch.setitem(sys.modules, 'mineru', fake_mineru)
upload = UploadFile(filename='unsupported.pdf', file=BytesIO(b'%PDF-1.4 fake'))
with pytest.raises(ValueError, match='PDF 解析失败: 当前安装的 MinerU 版本接口不兼容'):
await service.upload_document(user.id, upload)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_upload_document_raises_clear_error_when_pdf_dependency_is_missing(document_test_env, monkeypatch): async def test_upload_document_raises_clear_error_when_pdf_dependency_is_missing(document_test_env, monkeypatch):
session, user = document_test_env session, user = document_test_env

View File

@@ -7,6 +7,14 @@ export interface AgentStats {
status: 'active' | 'idle' | 'disabled' status: 'active' | 'idle' | 'disabled'
} }
export interface AgentHierarchyStatsNode extends AgentStats {
sub_commanders: AgentStats[]
}
export interface AgentHierarchyStats {
main_agents: AgentHierarchyStatsNode[]
}
export interface AgentConfig { export interface AgentConfig {
id: string id: string
name: string name: string
@@ -22,6 +30,11 @@ export const agentApi = {
return res.data return res.data
}, },
async getHierarchyStats(): Promise<AgentHierarchyStats> {
const res = await api.get('/api/agents/stats/hierarchy')
return res.data
},
async getConfig(id: string): Promise<AgentConfig> { async getConfig(id: string): Promise<AgentConfig> {
const res = await api.get(`/api/agents/config/${id}`) const res = await api.get(`/api/agents/config/${id}`)
return res.data return res.data

View File

@@ -21,7 +21,7 @@ export interface NavItem {
export const navItems: NavItem[] = [ export const navItems: NavItem[] = [
{ name: '沟通系统', path: '/chat', icon: MessageCircle }, { name: '沟通系统', path: '/chat', icon: MessageCircle },
{ name: '智能链路', path: '/agents', icon: Bot }, { name: '奥创中心', path: '/agents', icon: Bot },
{ name: '技能中心', path: '/skills', icon: Star }, { name: '技能中心', path: '/skills', icon: Star },
{ name: '资料中枢', path: '/knowledge', icon: BookOpen }, { name: '资料中枢', path: '/knowledge', icon: BookOpen },
{ name: '知识大脑', path: '/brain', icon: Network }, { name: '知识大脑', path: '/brain', icon: Network },

View File

@@ -181,7 +181,7 @@ describe('useKnowledgeView chunk editing', () => {
expect(view.documents.value[0].id).toBe('doc-3') expect(view.documents.value[0].id).toBe('doc-3')
}) })
it('enters a newly created folder so refresh keeps uploaded documents visible', async () => { it('stays on the current listing after creating a folder and shows the new folder', async () => {
const createdFolder = { const createdFolder = {
id: 'folder-new', id: 'folder-new',
name: '新文件夹', name: '新文件夹',
@@ -190,17 +190,6 @@ describe('useKnowledgeView chunk editing', () => {
created_at: '2026-03-22T00:00:00Z', created_at: '2026-03-22T00:00:00Z',
updated_at: '2026-03-22T00:00:00Z', updated_at: '2026-03-22T00:00:00Z',
} }
const uploadedDocument = {
id: 'doc-new',
title: 'Uploaded after create',
filename: 'uploaded.md',
file_type: 'md',
file_size: 256,
chunk_count: 1,
is_indexed: true,
folder_id: 'folder-new',
created_at: '2026-03-22T00:00:00Z',
}
mocks.folderCreate.mockResolvedValue({ data: createdFolder }) mocks.folderCreate.mockResolvedValue({ data: createdFolder })
mocks.folderGetTree.mockResolvedValue({ mocks.folderGetTree.mockResolvedValue({
@@ -213,36 +202,18 @@ describe('useKnowledgeView chunk editing', () => {
}, },
], ],
}) })
mocks.documentList
.mockResolvedValueOnce({ data: [] })
.mockResolvedValueOnce({ data: [uploadedDocument] })
.mockResolvedValue({ data: [uploadedDocument] })
mocks.documentUpload.mockResolvedValue({
data: {
id: 'doc-new',
title: 'Uploaded after create',
chunk_count: 1,
status: '上传成功,正在索引...',
ingestion_status: 'ready',
},
})
const view = useKnowledgeView() const view = useKnowledgeView()
view.showNewFolderDialog.value = true
view.newFolderName.value = '新文件夹' view.newFolderName.value = '新文件夹'
await view.createFolder() await view.createFolder()
expect(view.currentFolderId.value).toBe('folder-new') expect(view.currentFolderId.value).toBe(null)
expect(routeQuery.folder_id).toBe('folder-new') expect(routeQuery.folder_id).toBeUndefined()
expect(storage.get('knowledge.currentFolderId')).toBe('folder-new') expect(storage.get('knowledge.currentFolderId')).toBeUndefined()
expect(view.showNewFolderDialog.value).toBe(false)
const file = new File(['hello'], 'uploaded.md', { type: 'text/markdown' }) expect(view.visibleFolders.value).toHaveLength(1)
const event = { target: { files: [file], value: 'uploaded.md' } } as unknown as Event expect(view.visibleFolders.value[0].id).toBe('folder-new')
await view.handleUpload(event)
expect(mocks.documentUpload).toHaveBeenCalledWith(file, 'folder-new')
expect(view.documents.value).toHaveLength(1)
expect(view.documents.value[0].folder_id).toBe('folder-new')
}) })
it('loads documents at the root view instead of clearing the list', async () => { it('loads documents at the root view instead of clearing the list', async () => {

View File

@@ -298,13 +298,12 @@ export function useKnowledgeView() {
if (!newFolderName.value.trim()) return if (!newFolderName.value.trim()) return
try { try {
const response = await folderApi.create({ await folderApi.create({
name: newFolderName.value.trim(), name: newFolderName.value.trim(),
parent_id: newFolderParentId.value, parent_id: newFolderParentId.value,
}) })
await loadFolders() await loadFolders()
showNewFolderDialog.value = false showNewFolderDialog.value = false
await goToFolder(response.data.id)
} catch (error) { } catch (error) {
console.error('创建文件夹失败:', error) console.error('创建文件夹失败:', error)
} }

168
setup.bat
View File

@@ -1,168 +0,0 @@
@echo off
chcp 65001 >nul
title Jarvis 配置向导
echo ==========================================
echo Jarvis 个人 AI 助理 - 配置向导
echo ==========================================
echo.
REM --- 选择 LLM 提供商 ---
echo 请选择 LLM 提供商:
echo [1] OpenAI (GPT-4o) - 默认推荐
echo [2] DeepSeek (deepseek-chat) - 性价比高
echo [3] Claude (Anthropic) - 强大稳定
echo [4] Ollama (本地模型) - 完全免费
echo.
set /p PROVIDER_CHOICE=请选择 (1-4), 默认1:
if "%PROVIDER_CHOICE%"=="" set PROVIDER_CHOICE=1
if "%PROVIDER_CHOICE%"=="1" goto :openai
if "%PROVIDER_CHOICE%"=="2" goto :deepseek
if "%PROVIDER_CHOICE%"=="3" goto :claude
if "%PROVIDER_CHOICE%"=="4" goto :ollama
goto :openai
:openai
echo.
echo [选择] OpenAI (GPT-4o)
echo.
set /p OPENAI_API_KEY=请输入 OpenAI API Key (sk-...):
if "%OPENAI_API_KEY%"=="" (
echo [错误] API Key 不能为空
pause
exit /b 1
)
goto :write_env
:deepseek
echo.
echo [选择] DeepSeek
echo.
set /p OPENAI_API_KEY=请输入 DeepSeek API Key:
if "%OPENAI_API_KEY%"=="" (
echo [错误] API Key 不能为空
pause
exit /b 1
)
goto :write_env
:claude
echo.
echo [选择] Claude (Anthropic)
echo.
set /p ANTHROPIC_API_KEY=请输入 Anthropic API Key (sk-ant-...):
if "%ANTHROPIC_API_KEY%"=="" (
echo [错误] API Key 不能为空
pause
exit /b 1
)
goto :write_env
:ollama
echo.
echo [选择] Ollama (本地模型)
echo 请确保 Ollama 已启动 (ollama serve)
echo.
set /p OLLAMA_MODEL_IN=请输入模型名 (留空默认 llama3):
if "%OLLAMA_MODEL_IN%"=="" set OLLAMA_MODEL=llama3
goto :write_env
:write_env
echo.
echo [配置] 写入 backend\.env...
(
echo # =============================================
echo # Jarvis 后端配置
echo # =============================================
echo.
echo APP_NAME=Jarvis
echo APP_VERSION=0.1.0
echo DEBUG=true
echo SECRET_KEY=jarvis-secret-key-change-in-production
echo.
echo CORS_ORIGINS=http://localhost:5173,http://localhost:3000
echo.
echo # LLM 配置
) > backend\.env
if "%PROVIDER_CHOICE%"=="1" (
(
echo LLM_PROVIDER=openai
echo OPENAI_API_KEY=%OPENAI_API_KEY%
echo OPENAI_MODEL=gpt-4o
echo OPENAI_BASE_URL=https://api.openai.com/v1
) >> backend\.env
) else if "%PROVIDER_CHOICE%"=="2" (
(
echo LLM_PROVIDER=deepseek
echo OPENAI_API_KEY=%OPENAI_API_KEY%
echo OPENAI_BASE_URL=https://api.deepseek.com/v1
) >> backend\.env
) else if "%PROVIDER_CHOICE%"=="3" (
(
echo LLM_PROVIDER=claude
echo ANTHROPIC_API_KEY=%ANTHROPIC_API_KEY%
echo CLAUDE_MODEL=claude-sonnet-4-20250514
) >> backend\.env
) else (
(
echo LLM_PROVIDER=ollama
echo OLLAMA_BASE_URL=http://localhost:11434
echo OLLAMA_MODEL=%OLLAMA_MODEL%
) >> backend\.env
)
(
echo.
echo # 数据库
echo DATABASE_URL=sqlite+aiosqlite:///./data/jarvis.db
echo.
echo # ChromaDB
echo CHROMA_PERSIST_DIR=./data/chroma
echo.
echo # JWT
echo ACCESS_TOKEN_EXPIRE_MINUTES=1440
echo.
echo # 上传
echo UPLOAD_DIR=./data/uploads
echo MAX_UPLOAD_SIZE=52428800
echo.
echo # 定时任务
echo SCHEDULER_ENABLED=true
echo DAILY_PLAN_TIME=00:00
echo FORUM_SCAN_INTERVAL_MINUTES=30
echo.
echo DATA_DIR=./data
) >> backend\.env
echo [OK] 配置完成
REM --- 检查 uv ---
echo.
echo [检查] 验证环境...
where uv >nul 2>&1
if %errorlevel% neq 0 (
echo [警告] 未找到 uv请运行: pip install uv
echo 或访问: https://github.com/astral-sh/uv
) else (
echo [OK] uv 已安装
)
where npm >nul 2>&1
if %errorlevel% neq 0 (
echo [警告] 未找到 npm请安装 Node.js: https://nodejs.org
) else (
echo [OK] npm 已安装
)
echo.
echo ==========================================
echo 配置完成!
echo.
echo LLM: %PROVIDER_CHOICE%
echo.
echo 下一步: 双击 start.bat 启动服务
echo ==========================================
pause

115
start.bat
View File

@@ -1,115 +0,0 @@
@echo off
setlocal
chcp 65001 >nul
title Jarvis Start
set "ROOT=%~dp0"
set "BACKEND_DIR=%ROOT%backend"
set "FRONTEND_DIR=%ROOT%frontend"
set "LOG_DIR=%ROOT%logs"
set "PROJECT_ENV=%ROOT%.env"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
echo ==========================================
echo Jarvis - Quick Start
echo ==========================================
echo.
where uv >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] uv was not found. Install it first:
echo pip install uv
echo.
pause
exit /b 1
)
where npm >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] npm was not found. Install Node.js first.
echo.
pause
exit /b 1
)
if not exist "%PROJECT_ENV%" (
echo [INFO] .env was not found in the project root.
echo [INFO] Create it before first run.
echo.
)
echo [1/4] Install backend dependencies...
cd /d "%BACKEND_DIR%"
uv sync --quiet
if %errorlevel% neq 0 (
echo [ERROR] Failed to install backend dependencies.
pause
exit /b 1
)
echo [OK] Backend dependencies installed.
echo.
echo [2/4] Install frontend dependencies...
cd /d "%FRONTEND_DIR%"
call npm install >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Failed to install frontend dependencies.
pause
exit /b 1
)
echo [OK] Frontend dependencies installed.
set "BACKEND_HOST=127.0.0.1"
set "BACKEND_PORT="
if exist "%PROJECT_ENV%" (
for /f "usebackq tokens=1,* delims==" %%A in ("%PROJECT_ENV%") do (
if /I "%%A"=="HOST" set "BACKEND_HOST=%%B"
if /I "%%A"=="PORT" set "BACKEND_PORT=%%B"
)
)
if "%BACKEND_PORT%"=="" (
echo [ERROR] PORT was not found in .env.
echo [ERROR] Set PORT in the project root .env and run start.bat again.
pause
exit /b 1
)
set "FRONTEND_API_URL=http://%BACKEND_HOST%:%BACKEND_PORT%"
> "%FRONTEND_DIR%\.env.local" echo VITE_API_URL=%FRONTEND_API_URL%
set "BACKEND_LOG=%LOG_DIR%\backend-start.log"
set "FRONTEND_LOG=%LOG_DIR%\frontend-start.log"
echo.
echo [3/4] Start backend in background on %BACKEND_HOST%:%BACKEND_PORT%...
powershell -NoProfile -WindowStyle Hidden -Command "$wd='%BACKEND_DIR%'; $out='%BACKEND_LOG%'; Start-Process powershell -WindowStyle Hidden -WorkingDirectory $wd -ArgumentList '-NoProfile','-Command',('uv run uvicorn app.main:app --reload --host %BACKEND_HOST% --port %BACKEND_PORT% *>> ''{0}''' -f $out)"
if %errorlevel% neq 0 (
echo [ERROR] Failed to start backend.
pause
exit /b 1
)
echo Waiting for backend...
ping 127.0.0.1 -n 6 >nul
echo.
echo [4/4] Start frontend in background on port 5173...
powershell -NoProfile -WindowStyle Hidden -Command "$wd='%FRONTEND_DIR%'; $out='%FRONTEND_LOG%'; Start-Process powershell -WindowStyle Hidden -WorkingDirectory $wd -ArgumentList '-NoProfile','-Command',('npm run dev *>> ''{0}''' -f $out)"
if %errorlevel% neq 0 (
echo [ERROR] Failed to start frontend.
pause
exit /b 1
)
echo.
echo ==========================================
echo Started
echo.
echo Backend: http://%BACKEND_HOST%:%BACKEND_PORT%
echo Frontend: http://localhost:5173
echo API docs: http://%BACKEND_HOST%:%BACKEND_PORT%/docs
echo.
echo Logs:
echo - %BACKEND_LOG%
echo - %FRONTEND_LOG%
echo ==========================================
echo.
pause