launch_paas.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. #!/usr/bin/env python3
  2. """
  3. research67/launch_paas.py — 通过 AgentPaaS API 启动科研智能体
  4. ================================================================
  5. 两种运行模式:
  6. 1. AgentPaaS API 模式: 启动 PaaS 服务,注册所有 agent,通过 REST API 执行
  7. 2. Claude Code CLI 桥接模式: 利用 Claude Max Plan 的 claude CLI 作为 LLM 后端
  8. 用法:
  9. # 模式 1: 启动 PaaS 服务 + 注册 + 执行
  10. python launch_paas.py serve # 启动服务 (端口 8000)
  11. python launch_paas.py register # 注册所有 agent 到 PaaS
  12. python launch_paas.py run # 通过 API 执行科研流程
  13. python launch_paas.py all # serve + register + run (一键启动)
  14. # 模式 2: 使用 Claude Code CLI (Max Plan)
  15. python launch_paas.py claude-code # 通过 claude CLI 执行
  16. """
  17. from __future__ import annotations
  18. import argparse
  19. import hashlib
  20. import json
  21. import os
  22. import secrets
  23. import subprocess
  24. import sys
  25. import time
  26. import urllib.request
  27. import urllib.error
  28. from pathlib import Path
  29. from typing import Dict, List, Optional
  30. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
  31. AGENT_DIR = Path(__file__).resolve().parent
  32. sys.path.insert(0, str(PROJECT_ROOT))
  33. PAAS_URL = os.environ.get("AGENTPAAS_URL", "http://127.0.0.1:8000")
  34. API_KEY = os.environ.get("AGENTPAAS_API_KEY", "")
  35. AGENT_CONFIGS = [
  36. ("research-67-orchestrator", "科研主编排器", AGENT_DIR / "orchestrator.yml"),
  37. ("research-67-idea-analyst", "创意分析师", AGENT_DIR / "agents" / "idea-analyst.yml"),
  38. ("research-67-lit-searcher", "文献检索员", AGENT_DIR / "agents" / "lit-searcher.yml"),
  39. ("research-67-exp-planner", "实验规划师", AGENT_DIR / "agents" / "exp-planner.yml"),
  40. ("research-67-exp-executor", "实验执行员", AGENT_DIR / "agents" / "exp-executor.yml"),
  41. ("research-67-result-analyzer", "结果分析师", AGENT_DIR / "agents" / "result-analyzer.yml"),
  42. ("research-67-paper-writer", "论文撰写员", AGENT_DIR / "agents" / "paper-writer.yml"),
  43. ("research-67-data-verifier", "数据核验员", AGENT_DIR / "agents" / "data-verifier.yml"),
  44. ("research-67-paper-reviewer", "论文评审员", AGENT_DIR / "agents" / "paper-reviewer.yml"),
  45. ("research-67-review-feedback", "评审反馈合成器", AGENT_DIR / "agents" / "review-feedback.yml"),
  46. ]
  47. # 迭代控制参数
  48. MAX_ROUNDS = 5 # 最大迭代轮数
  49. ACCEPTANCE_THRESHOLD = 0.50 # 目标接收概率 (50%)
  50. MAX_TURNS_PER_PHASE = 30 # 每个 agent 最大工具调用轮数
  51. PHASE_TIMEOUT = 3600 # 每个 phase 超时 (60分钟,Round1实测单阶段可达20min+)
  52. API_RETRY_MAX = 3 # API 失败最大重试次数
  53. API_RETRY_BACKOFF = 30 # 重试间隔基数 (秒)
  54. PHASE_COOLDOWN = 10 # 阶段间冷却 (秒,防 429/529)
  55. # ═══════════════════════════════════════════════════════════
  56. # 1. PaaS API Helpers
  57. # ═══════════════════════════════════════════════════════════
  58. def api_call(method: str, path: str, data: dict = None) -> dict:
  59. url = f"{PAAS_URL}/api/v1{path}"
  60. body = json.dumps(data).encode("utf-8") if data else None
  61. req = urllib.request.Request(url, data=body, method=method)
  62. req.add_header("Content-Type", "application/json")
  63. req.add_header("Authorization", f"Bearer {API_KEY}")
  64. try:
  65. with urllib.request.urlopen(req, timeout=600) as resp:
  66. return json.loads(resp.read())
  67. except urllib.error.HTTPError as e:
  68. body = e.read().decode()
  69. print(f" API Error {e.code}: {body[:500]}")
  70. return {"error": body}
  71. except urllib.error.URLError as e:
  72. print(f" Connection error: {e}")
  73. return {"error": str(e)}
  74. def wait_for_server(timeout: int = 30):
  75. """等待 PaaS 服务就绪"""
  76. for i in range(timeout):
  77. try:
  78. req = urllib.request.Request(f"{PAAS_URL}/health")
  79. with urllib.request.urlopen(req, timeout=2):
  80. return True
  81. except Exception:
  82. time.sleep(1)
  83. return False
  84. # ═══════════════════════════════════════════════════════════
  85. # 2. 命令: serve — 启动 AgentPaaS 服务
  86. # ═══════════════════════════════════════════════════════════
  87. def cmd_serve(args):
  88. """启动 AgentPaaS 并自动创建租户"""
  89. print("Starting AgentPaaS server...")
  90. # 启动服务(后台进程)
  91. proc = subprocess.Popen(
  92. [sys.executable, "-m", "agentpaas", "serve", "--port", str(args.port), "--dev"],
  93. cwd=str(PROJECT_ROOT),
  94. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  95. )
  96. print(f" PID: {proc.pid}")
  97. print(f" URL: http://127.0.0.1:{args.port}")
  98. print(" Waiting for server to be ready...")
  99. if not wait_for_server(30):
  100. print(" ERROR: Server failed to start within 30s")
  101. proc.kill()
  102. return None
  103. print(" ✓ Server ready")
  104. return proc
  105. def cmd_create_tenant(args):
  106. """创建租户并返回 API key"""
  107. print("Creating tenant...")
  108. result = subprocess.run(
  109. [sys.executable, "-m", "agentpaas", "create-tenant", "--name", "research-lab"],
  110. capture_output=True, text=True, cwd=str(PROJECT_ROOT),
  111. )
  112. print(result.stdout)
  113. # Extract API key from output
  114. for line in result.stdout.splitlines():
  115. if line.startswith("API Key:"):
  116. key = line.split(":", 1)[1].strip()
  117. return key
  118. return None
  119. # ═══════════════════════════════════════════════════════════
  120. # 3. 命令: register — 注册所有 agent
  121. # ═══════════════════════════════════════════════════════════
  122. def cmd_register(args):
  123. """注册所有 research67 agent 到 AgentPaaS"""
  124. import yaml
  125. print(f"\nRegistering {len(AGENT_CONFIGS)} agents to AgentPaaS...")
  126. agent_ids = {}
  127. for agent_id, name, yml_path in AGENT_CONFIGS:
  128. with open(yml_path, "r", encoding="utf-8") as f:
  129. config = yaml.safe_load(f)
  130. result = api_call("POST", "/agents", {
  131. "name": name,
  132. "description": config.get("description", ""),
  133. "config": config,
  134. "tags": ["research67", config.get("type", "react")],
  135. "environment": "production",
  136. })
  137. if "agent_id" in result:
  138. agent_ids[agent_id] = result["agent_id"]
  139. print(f" ✓ {name:20s} → {result['agent_id']} (v{result['version']})")
  140. else:
  141. print(f" ✗ {name:20s} → {result.get('error', 'Unknown error')}")
  142. # 保存 agent ID 映射
  143. mapping_path = AGENT_DIR / ".paas_agent_ids.json"
  144. mapping_path.write_text(json.dumps(agent_ids, indent=2), encoding="utf-8")
  145. print(f"\n Agent IDs saved to {mapping_path}")
  146. return agent_ids
  147. # ═══════════════════════════════════════════════════════════
  148. # 4. 命令: run — 通过 API 执行科研流程
  149. # ═══════════════════════════════════════════════════════════
  150. def cmd_run(args):
  151. """通过 AgentPaaS API 执行 orchestrator"""
  152. mapping_path = AGENT_DIR / ".paas_agent_ids.json"
  153. if not mapping_path.exists():
  154. print("ERROR: No agent ID mapping found. Run 'register' first.")
  155. return
  156. agent_ids = json.loads(mapping_path.read_text())
  157. orchestrator_id = agent_ids.get("research-67-orchestrator")
  158. if not orchestrator_id:
  159. print("ERROR: Orchestrator not registered.")
  160. return
  161. # 读取 IDEA
  162. idea_path = AGENT_DIR / "paper" / "paper01" / "IDEA.md"
  163. idea = idea_path.read_text(encoding="utf-8") if idea_path.exists() else "[IDEA.md not found]"
  164. print(f"\n{'═'*60}")
  165. print(f" Executing research67 orchestrator via AgentPaaS API")
  166. print(f" Agent ID: {orchestrator_id}")
  167. print(f" IDEA: {idea[:80]}...")
  168. print(f"{'═'*60}\n")
  169. result = api_call("POST", f"/agents/{orchestrator_id}/run", {
  170. "input": idea,
  171. "parameters": {},
  172. "context": {"sub_agent_ids": agent_ids},
  173. })
  174. if "error" not in result:
  175. print(f"\n ✓ Run completed")
  176. print(f" Run ID: {result.get('run_id')}")
  177. print(f" Status: {result.get('status')}")
  178. print(f" Tokens: {result.get('usage', {}).get('total_tokens', 0)}")
  179. print(f" Duration: {result.get('usage', {}).get('duration_ms', 0)}ms")
  180. print(f"\n Output (first 500 chars):")
  181. print(f" {result.get('output', '')[:500]}")
  182. else:
  183. print(f"\n ✗ Execution failed: {result['error'][:500]}")
  184. # ═══════════════════════════════════════════════════════════
  185. # 5. 命令: all — 一键启动全流程
  186. # ═══════════════════════════════════════════════════════════
  187. def cmd_all(args):
  188. """一键: 启动服务 + 创建租户 + 注册 agent + 执行"""
  189. global API_KEY
  190. proc = cmd_serve(args)
  191. if not proc:
  192. return
  193. try:
  194. # 创建租户
  195. key = cmd_create_tenant(args)
  196. if key:
  197. API_KEY = key
  198. os.environ["AGENTPAAS_API_KEY"] = key
  199. print(f" API Key: {key}")
  200. # 注册 agents
  201. cmd_register(args)
  202. # 执行
  203. cmd_run(args)
  204. finally:
  205. print("\nShutting down server...")
  206. proc.terminate()
  207. proc.wait(timeout=5)
  208. # ═══════════════════════════════════════════════════════════
  209. # 6. 命令: claude-code — 使用 Claude Code CLI 作为 LLM 后端
  210. # ═══════════════════════════════════════════════════════════
  211. def run_phase(phase_dir: Path, agent_name: str, desc: str,
  212. prompt_content: str, round_num: int) -> str:
  213. """
  214. 执行单个阶段,带自动重试和详细日志。
  215. 重试策略:
  216. - subprocess.TimeoutExpired → 不重试(已用完时间预算)
  217. - API 529/overloaded → 指数退避重试
  218. - "Request timed out" → 指数退避重试(通常是 API 网关超时)
  219. - 其他错误 → 重试 1 次
  220. """
  221. phase_dir.mkdir(parents=True, exist_ok=True)
  222. (phase_dir / "artifacts").mkdir(exist_ok=True)
  223. # 写入 prompt
  224. (phase_dir / "prompt.md").write_text(prompt_content, encoding="utf-8")
  225. print(f"\n{'━'*60}")
  226. print(f" Phase: {phase_dir.name} — {desc}")
  227. print(f" Agent: {agent_name} | Round: {round_num}")
  228. print(f" Max turns: {MAX_TURNS_PER_PHASE} | Timeout: {PHASE_TIMEOUT}s | Retries: {API_RETRY_MAX}")
  229. print(f"{'━'*60}")
  230. cmd = [
  231. "claude",
  232. "-p",
  233. "--model", "claude-opus-4-6",
  234. "--max-turns", str(MAX_TURNS_PER_PHASE),
  235. "--allowedTools", "Edit,Write,Read,Bash,Glob,Grep,WebSearch,WebFetch",
  236. ]
  237. last_error = ""
  238. for attempt in range(1, API_RETRY_MAX + 1):
  239. t0 = time.time()
  240. attempt_label = f"[attempt {attempt}/{API_RETRY_MAX}]"
  241. try:
  242. print(f" 🚀 {attempt_label} Running claude CLI ...")
  243. result = subprocess.run(
  244. cmd,
  245. input=prompt_content,
  246. capture_output=True, text=True,
  247. timeout=PHASE_TIMEOUT,
  248. cwd=str(phase_dir),
  249. )
  250. elapsed = time.time() - t0
  251. output = result.stdout or ""
  252. stderr = result.stderr or ""
  253. # --- 检测 API 层面错误(进程成功退出但内容是错误信息)---
  254. is_api_error = False
  255. for err_pattern in ["Request timed out", "overloaded_error", "529",
  256. "rate_limit", "500 Internal", "502 Bad Gateway",
  257. "API Error"]:
  258. if err_pattern in output or err_pattern in stderr:
  259. is_api_error = True
  260. last_error = (output + stderr)[:300]
  261. break
  262. if is_api_error and attempt < API_RETRY_MAX:
  263. wait = API_RETRY_BACKOFF * (2 ** (attempt - 1)) # 30s, 60s, 120s
  264. print(f" ⚠ {attempt_label} API error after {elapsed:.0f}s: {last_error[:100]}")
  265. print(f" ⏳ Retrying in {wait}s ...")
  266. time.sleep(wait)
  267. continue
  268. # --- 成功(或最后一次尝试,即使有 API 错误也保存)---
  269. if result.returncode != 0 and stderr:
  270. output = f"[STDERR] {stderr[:500]}\n{output}"
  271. (phase_dir / "claude_output.md").write_text(output, encoding="utf-8")
  272. # 产出检查
  273. report_path = phase_dir / "report.json"
  274. wp_path = phase_dir / "work_plan.md"
  275. artifacts = list((phase_dir / "artifacts").rglob("*"))
  276. artifacts_count = sum(1 for a in artifacts if a.is_file())
  277. status_parts = []
  278. if wp_path.exists():
  279. status_parts.append("✅ work_plan")
  280. if report_path.exists():
  281. status_parts.append("✅ report.json")
  282. else:
  283. status_parts.append("⚠ no report.json")
  284. status_parts.append(f"📎 {artifacts_count} artifacts")
  285. print(f" ✓ {attempt_label} Done in {elapsed:.0f}s | {len(output)} chars | {' | '.join(status_parts)}")
  286. return output
  287. except subprocess.TimeoutExpired:
  288. elapsed = time.time() - t0
  289. msg = f"[TIMEOUT] Phase {phase_dir.name} timed out after {elapsed:.0f}s (limit {PHASE_TIMEOUT}s)"
  290. print(f" ✗ {msg}")
  291. # 超时不重试(已用完整个时间窗口),但保存已有产出
  292. partial = ""
  293. if (phase_dir / "work_plan.md").exists():
  294. partial += " (work_plan exists, partial progress saved)"
  295. (phase_dir / "claude_output.md").write_text(msg + partial, encoding="utf-8")
  296. return msg
  297. except FileNotFoundError:
  298. print(" ✗ 'claude' CLI not found. Is Claude Code installed?")
  299. return "[ERROR] claude CLI not found"
  300. except Exception as e:
  301. last_error = str(e)
  302. if attempt < API_RETRY_MAX:
  303. wait = API_RETRY_BACKOFF * attempt
  304. print(f" ⚠ {attempt_label} Error: {last_error[:100]}. Retrying in {wait}s ...")
  305. time.sleep(wait)
  306. else:
  307. msg = f"[ERROR] {last_error}"
  308. print(f" ✗ {msg}")
  309. (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
  310. return msg
  311. # 所有重试都失败
  312. msg = f"[FAILED] All {API_RETRY_MAX} attempts failed. Last: {last_error[:200]}"
  313. (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
  314. return msg
  315. def build_prompt(workspace: Path, phase_dir: Path, desc: str,
  316. system_prompt: str, idea: str, round_num: int,
  317. prev_outputs: list, revision_context: str = "") -> str:
  318. """构建阶段 prompt"""
  319. input_parts = [
  320. f"workspace: {workspace}",
  321. f"phase_dir: {phase_dir}",
  322. f"round: {round_num}",
  323. f"\n## IDEA:\n{idea[:8000]}",
  324. ]
  325. if revision_context:
  326. input_parts.append(f"\n## 评审反馈与修改指令 (来自 Round {round_num - 1}):\n{revision_context[:6000]}")
  327. for prev in prev_outputs[-2:]:
  328. input_parts.append(f"\n## Previous phase output:\n{prev[:3000]}")
  329. full_input = "\n".join(input_parts)
  330. return f"""# {desc}
  331. ## System Instructions
  332. {system_prompt[:8000]}
  333. ## Input Context
  334. {full_input}
  335. ## Important
  336. - 将所有产出文件写入 {phase_dir}/
  337. - 先写 work_plan.md,再执行,最后写 report.json 和 report.md
  338. - report.json 必须包含 _meta 字段(含 phase, round, status)
  339. - 这是 Round {round_num}{',请根据评审反馈重点改进' if round_num > 1 else ''}
  340. """
  341. def read_review_score(workspace: Path, round_num: int) -> tuple:
  342. """读取评审报告的 acceptance_probability 和 overall_score
  343. 优先从 artifacts/review_report.json 读取(详细评审),
  344. 其次从 report.json 读取(元数据摘要)。
  345. """
  346. review_dir = workspace / f"round_{round_num}" / "08_paper_review"
  347. # 优先: artifacts/review_report.json (完整评审)
  348. for candidate in [
  349. review_dir / "artifacts" / "review_report.json",
  350. review_dir / "report.json",
  351. ]:
  352. if candidate.exists():
  353. try:
  354. data = json.loads(candidate.read_text(encoding="utf-8"))
  355. prob = data.get("estimated_acceptance_probability",
  356. data.get("acceptance_probability", 0))
  357. score = data.get("overall_score", 0)
  358. if score > 0 or prob > 0:
  359. return prob, score
  360. except Exception:
  361. continue
  362. return 0.0, 0
  363. def read_revision_context(workspace: Path, round_num: int) -> str:
  364. """读取上一轮的评审 + 反馈合成结果,作为下一轮的 revision_context"""
  365. parts = []
  366. # 评审报告摘要 (优先从 artifacts/review_report.json)
  367. review_dir = workspace / f"round_{round_num}" / "08_paper_review"
  368. review_report = review_dir / "artifacts" / "review_report.json"
  369. if not review_report.exists():
  370. review_report = review_dir / "report.json"
  371. if review_report.exists():
  372. try:
  373. data = json.loads(review_report.read_text(encoding="utf-8"))
  374. parts.append(f"### 上轮评审 (Round {round_num})")
  375. parts.append(f"- 总评分: {data.get('overall_score', '?')}/10")
  376. parts.append(f"- 接收概率: {data.get('estimated_acceptance_probability', data.get('acceptance_probability', '?'))}")
  377. parts.append(f"- 建议: {data.get('recommendation', '?')}")
  378. if "weaknesses" in data:
  379. parts.append("\n#### 弱点:")
  380. for w in data["weaknesses"][:6]:
  381. w_text = w if isinstance(w, str) else w.get("title", str(w)[:100])
  382. parts.append(f" - {w_text[:150]}")
  383. if "required_revisions" in data:
  384. parts.append("\n#### 必须修订:")
  385. for r in data["required_revisions"][:6]:
  386. r_text = r if isinstance(r, str) else r.get("title", str(r)[:100])
  387. parts.append(f" - {r_text[:150]}")
  388. except Exception:
  389. pass
  390. # 反馈合成的修改后 IDEA
  391. revised_idea = workspace / f"round_{round_num}" / "09_review_feedback" / "artifacts" / "revised_idea.md"
  392. if revised_idea.exists():
  393. parts.append(f"\n### 修改后的 IDEA (Round {round_num} 反馈合成):")
  394. parts.append(revised_idea.read_text(encoding="utf-8")[:4000])
  395. # 反馈合成的行动计划
  396. action_plan = workspace / f"round_{round_num}" / "09_review_feedback" / "artifacts" / "action_plan.json"
  397. if action_plan.exists():
  398. parts.append(f"\n### 各阶段行动计划:")
  399. parts.append(action_plan.read_text(encoding="utf-8")[:3000])
  400. return "\n".join(parts)
  401. def cmd_claude_code(args):
  402. """
  403. 通过 Claude Code CLI (Max Plan) 执行科研流程。
  404. 支持迭代: 评审 → 反馈合成 → 修改 IDEA → 重跑流程,直到 acceptance ≥ 50%。
  405. 可通过 --workspace 继续已有工作区的下一轮。
  406. """
  407. import yaml
  408. from datetime import datetime
  409. global MAX_ROUNDS, MAX_TURNS_PER_PHASE, PHASE_TIMEOUT, PHASE_COOLDOWN
  410. if hasattr(args, "max_rounds") and args.max_rounds:
  411. MAX_ROUNDS = args.max_rounds
  412. if hasattr(args, "max_turns") and args.max_turns:
  413. MAX_TURNS_PER_PHASE = args.max_turns
  414. if hasattr(args, "phase_timeout") and args.phase_timeout:
  415. PHASE_TIMEOUT = args.phase_timeout
  416. if hasattr(args, "cooldown") and args.cooldown is not None:
  417. PHASE_COOLDOWN = args.cooldown
  418. print(f"""
  419. ╔══════════════════════════════════════════════════════════════╗
  420. ║ Research-67 via Claude Code CLI (Max Plan) ║
  421. ║ Model: Claude Opus 4.6 (1M context) ║
  422. ║ Max turns/phase: {MAX_TURNS_PER_PHASE:<3d} | Threshold: {ACCEPTANCE_THRESHOLD:.0%} | Max rounds: {MAX_ROUNDS} ║
  423. ╚══════════════════════════════════════════════════════════════╝
  424. """)
  425. # 检查 claude CLI
  426. try:
  427. r = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=10)
  428. print(f" Claude CLI: {r.stdout.strip()}")
  429. except FileNotFoundError:
  430. print(" ERROR: 'claude' not found. Install Claude Code CLI.")
  431. return
  432. # 工作区: 复用已有 or 新建
  433. if hasattr(args, "workspace") and args.workspace:
  434. workspace = Path(args.workspace)
  435. if not workspace.exists():
  436. print(f" ERROR: Workspace not found: {workspace}")
  437. return
  438. print(f" Resuming workspace: {workspace}")
  439. else:
  440. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  441. workspace = AGENT_DIR / "workspace" / f"claude_run_{ts}"
  442. workspace.mkdir(parents=True, exist_ok=True)
  443. print(f" New workspace: {workspace}")
  444. # 读取 IDEA
  445. idea_path = AGENT_DIR / "paper" / "paper01" / "IDEA.md"
  446. idea_original = idea_path.read_text(encoding="utf-8") if idea_path.exists() else ""
  447. # 确定起始轮次
  448. start_round = 1
  449. for i in range(1, MAX_ROUNDS + 1):
  450. if (workspace / f"round_{i}" / "08_paper_review" / "report.json").exists():
  451. prob, score = read_review_score(workspace, i)
  452. print(f" Round {i} review found: score={score}/10, acceptance={prob:.0%}")
  453. if prob >= ACCEPTANCE_THRESHOLD:
  454. print(f" ✓ Already meets threshold! No further rounds needed.")
  455. return
  456. start_round = i + 1 # 从评审后的下一步开始
  457. else:
  458. break
  459. # 检查是否需要先跑 feedback (上一轮有评审但还没跑 feedback)
  460. need_feedback_first = False
  461. if start_round > 1:
  462. prev_round = start_round - 1
  463. feedback_dir = workspace / f"round_{prev_round}" / "09_review_feedback"
  464. if not (feedback_dir / "report.json").exists():
  465. need_feedback_first = True
  466. print(f" Round {prev_round} 评审已完成但 feedback 未执行,先执行反馈合成...")
  467. # ═══ 主循环 ═══
  468. MAIN_PHASES = [
  469. ("01_idea_analysis", "idea-analyst", "分析 IDEA 可行性"),
  470. ("02_literature_review", "lit-searcher", "检索相关文献"),
  471. ("03_experiment_plan", "exp-planner", "制定实验方案"),
  472. ("04_experiment_execution","exp-executor", "执行实验"),
  473. ("05_result_analysis", "result-analyzer", "分析实验结果"),
  474. ("06_paper_writing", "paper-writer", "撰写论文"),
  475. ("07_data_verification", "data-verifier", "核对数据"),
  476. ("08_paper_review", "paper-reviewer", "评审论文"),
  477. ]
  478. for round_num in range(start_round, MAX_ROUNDS + 1):
  479. print(f"\n{'═'*60}")
  480. print(f" 🔄 ROUND {round_num}/{MAX_ROUNDS}")
  481. print(f"{'═'*60}")
  482. # --- Phase 09: 评审反馈合成 (Round 2+ 之前,或 need_feedback_first) ---
  483. if round_num > 1 or need_feedback_first:
  484. feedback_round = round_num - 1 if round_num > 1 else start_round - 1
  485. feedback_dir = workspace / f"round_{feedback_round}" / "09_review_feedback"
  486. if not (feedback_dir / "report.json").exists():
  487. agent_yml = AGENT_DIR / "agents" / "review-feedback.yml"
  488. with open(agent_yml, "r", encoding="utf-8") as f:
  489. config = yaml.safe_load(f)
  490. review_context = read_revision_context(workspace, feedback_round)
  491. prompt = build_prompt(
  492. workspace, feedback_dir, "评审反馈合成 → 修改 IDEA",
  493. config.get("systemPrompt", ""), idea_original,
  494. feedback_round, [], revision_context=review_context
  495. )
  496. run_phase(feedback_dir, "review-feedback", "评审反馈合成", prompt, feedback_round)
  497. # 读取修改后的 IDEA(如果有)
  498. idea = idea_original
  499. revision_context = ""
  500. if round_num > 1:
  501. revision_context = read_revision_context(workspace, round_num - 1)
  502. revised_idea_path = workspace / f"round_{round_num - 1}" / "09_review_feedback" / "artifacts" / "revised_idea.md"
  503. if revised_idea_path.exists():
  504. idea = revised_idea_path.read_text(encoding="utf-8")
  505. print(f" 📝 Using revised IDEA from Round {round_num - 1}")
  506. # --- Main phases 01-08 ---
  507. prev_outputs = []
  508. failed_phases = []
  509. for phase_idx, (phase_name, agent_name, desc) in enumerate(MAIN_PHASES):
  510. phase_dir = workspace / f"round_{round_num}" / phase_name
  511. # 跳过已完成的阶段(支持断点续跑)
  512. if (phase_dir / "report.json").exists():
  513. print(f"\n ⏭ Skipping {phase_name} (report.json exists)")
  514. existing_output = (phase_dir / "claude_output.md").read_text(encoding="utf-8") \
  515. if (phase_dir / "claude_output.md").exists() else ""
  516. prev_outputs.append(existing_output[:5000])
  517. continue
  518. # 上游连续失败 ≥2 个阶段 → 后续阶段跳过(防级联空转)
  519. if len(failed_phases) >= 2:
  520. consecutive_fail = all(
  521. f_idx >= phase_idx - 2 for f_idx in failed_phases[-2:]
  522. )
  523. if consecutive_fail:
  524. print(f"\n ⏭ Skipping {phase_name} (upstream cascade failure)")
  525. prev_outputs.append("[SKIPPED] upstream failure")
  526. continue
  527. agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
  528. with open(agent_yml, "r", encoding="utf-8") as f:
  529. config = yaml.safe_load(f)
  530. prompt = build_prompt(
  531. workspace, phase_dir, desc,
  532. config.get("systemPrompt", ""), idea,
  533. round_num, prev_outputs,
  534. revision_context=revision_context if round_num > 1 else ""
  535. )
  536. output = run_phase(phase_dir, agent_name, desc, prompt, round_num)
  537. prev_outputs.append(output[:5000])
  538. # 检测失败
  539. if any(tag in output for tag in ["[TIMEOUT]", "[ERROR]", "[FAILED]",
  540. "Request timed out", "overloaded_error"]):
  541. failed_phases.append(phase_idx)
  542. # 阶段间冷却(防 API 限速)
  543. if phase_idx < len(MAIN_PHASES) - 1:
  544. print(f" ⏳ Cooldown {PHASE_COOLDOWN}s before next phase...")
  545. time.sleep(PHASE_COOLDOWN)
  546. # --- 检查评审结果 ---
  547. prob, score = read_review_score(workspace, round_num)
  548. print(f"\n{'─'*60}")
  549. print(f" 📊 Round {round_num} 评审结果: score={score}/10, acceptance={prob:.0%}")
  550. if prob >= ACCEPTANCE_THRESHOLD:
  551. print(f" ✅ 达到目标阈值 ({ACCEPTANCE_THRESHOLD:.0%})! 停止迭代。")
  552. break
  553. elif round_num < MAX_ROUNDS:
  554. print(f" ⚠ 未达阈值 ({prob:.0%} < {ACCEPTANCE_THRESHOLD:.0%}),进入 Round {round_num + 1}...")
  555. else:
  556. print(f" ❌ 已达最大轮数 ({MAX_ROUNDS}),停止。最终: score={score}, acceptance={prob:.0%}")
  557. # 最终统计
  558. print(f"\n{'═'*60}")
  559. print(f" Pipeline complete. Workspace: {workspace}")
  560. print(f" Total rounds: {round_num}")
  561. print(f" Final score: {score}/10, acceptance: {prob:.0%}")
  562. # 各轮得分追踪
  563. print(f"\n 📈 Score progression:")
  564. for r in range(1, round_num + 1):
  565. rp, rs = read_review_score(workspace, r)
  566. marker = " ✅" if rp >= ACCEPTANCE_THRESHOLD else ""
  567. print(f" Round {r}: score={rs}/10, acceptance={rp:.0%}{marker}")
  568. # 文件统计
  569. total_files = sum(1 for _ in workspace.rglob("*") if _.is_file())
  570. total_size = sum(f.stat().st_size for f in workspace.rglob("*") if f.is_file())
  571. print(f"\n 📦 Total files: {total_files}")
  572. print(f" 💾 Total size: {total_size / 1024 / 1024:.1f} MB")
  573. print(f"{'═'*60}\n")
  574. # ═══════════════════════════════════════════════════════════
  575. # CLI Entry Point
  576. # ═══════════════════════════════════════════════════════════
  577. def main():
  578. parser = argparse.ArgumentParser(description="Research-67 AgentPaaS Launcher")
  579. sub = parser.add_subparsers(dest="command")
  580. # serve
  581. serve_p = sub.add_parser("serve", help="Start AgentPaaS server")
  582. serve_p.add_argument("--port", type=int, default=8000)
  583. # register
  584. sub.add_parser("register", help="Register all agents to PaaS")
  585. # run
  586. sub.add_parser("run", help="Execute research pipeline via API")
  587. # all
  588. all_p = sub.add_parser("all", help="Serve + register + run (one-shot)")
  589. all_p.add_argument("--port", type=int, default=8000)
  590. # claude-code
  591. cc_p = sub.add_parser("claude-code", help="Execute via Claude Code CLI (Max Plan)")
  592. cc_p.add_argument("--workspace", type=str, default=None,
  593. help="Resume from existing workspace (skip completed rounds)")
  594. cc_p.add_argument("--max-rounds", type=int, default=None,
  595. help=f"Override max rounds (default: {MAX_ROUNDS})")
  596. cc_p.add_argument("--max-turns", type=int, default=None,
  597. help=f"Override max turns per phase (default: {MAX_TURNS_PER_PHASE})")
  598. cc_p.add_argument("--phase-timeout", type=int, default=None,
  599. help=f"Override phase timeout in seconds (default: {PHASE_TIMEOUT})")
  600. cc_p.add_argument("--cooldown", type=int, default=None,
  601. help=f"Override inter-phase cooldown in seconds (default: {PHASE_COOLDOWN})")
  602. args = parser.parse_args()
  603. commands = {
  604. "serve": cmd_serve,
  605. "register": cmd_register,
  606. "run": cmd_run,
  607. "all": cmd_all,
  608. "claude-code": cmd_claude_code,
  609. }
  610. if args.command in commands:
  611. commands[args.command](args)
  612. else:
  613. parser.print_help()
  614. print("\nExamples:")
  615. print(" python launch_paas.py all --port 8000 # 一键启动全流程 (需要 ANTHROPIC_API_KEY)")
  616. print(" python launch_paas.py claude-code # 使用 Claude Code CLI (Max Plan, 无需 API key)")
  617. if __name__ == "__main__":
  618. main()