run.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. #!/usr/bin/env python3
  2. """
  3. research67/run.py — 简化版 Runner
  4. ===================================
  5. 用本地工具 stub 替代尚不存在的 MCP 服务,直接通过 lambdagent runtime
  6. 执行 research67 多智能体科研流程。
  7. 使用 Claude Opus 4.6 (1M context) 的原生能力:
  8. - web_search → 替代 scholar-mcp / arxiv-mcp
  9. - python_exec → 替代 code-sandbox-mcp / plotting-mcp
  10. - local fs → 替代 fs-mcp
  11. - local latex → 替代 latex-mcp
  12. 用法:
  13. export ANTHROPIC_API_KEY=sk-ant-...
  14. python run.py # 完整流程
  15. python run.py --phase idea_analysis # 只跑某个阶段
  16. python run.py --resume # 从上次检查点恢复
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import json
  21. import os
  22. import subprocess
  23. import sys
  24. import time
  25. import shutil
  26. from datetime import datetime, timezone
  27. from pathlib import Path
  28. from typing import Any, Dict, List, Optional
  29. # ── 路径设置 ──────────────────────────────────────────────
  30. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent # Paper08/
  31. AGENT_DIR = Path(__file__).resolve().parent # research67/
  32. sys.path.insert(0, str(PROJECT_ROOT))
  33. from lambdagent.fromconfig import from_config
  34. from lambdagent.agentruntime.runtime import Runtime, RuntimeResult
  35. from lambdagent.agentruntime.config import RuntimeConfig
  36. from lambdagent.core import Context
  37. # ═══════════════════════════════════════════════════════════
  38. # 1. 本地工具 Stubs — 替代 MCP 服务
  39. # ═══════════════════════════════════════════════════════════
  40. class LocalToolkit:
  41. """
  42. 将 6 个 MCP 服务的功能用本地实现替代。
  43. 每个方法签名对齐 MCP tool 的 (input: str) -> str 接口。
  44. """
  45. def __init__(self, workspace: Path):
  46. self.workspace = workspace
  47. # ── fs-mcp 替代 ──────────────────────────────────────
  48. def read_file(self, path: str) -> str:
  49. p = self._resolve(path)
  50. if not p.exists():
  51. return f"[ERROR] File not found: {p}"
  52. return p.read_text(encoding="utf-8")
  53. def write_file(self, args: str) -> str:
  54. """args = JSON: {"path": "...", "content": "..."}"""
  55. d = json.loads(args)
  56. p = self._resolve(d["path"])
  57. p.parent.mkdir(parents=True, exist_ok=True)
  58. p.write_text(d["content"], encoding="utf-8")
  59. return f"Written {len(d['content'])} bytes to {p}"
  60. def list_dir(self, path: str) -> str:
  61. p = self._resolve(path)
  62. if not p.is_dir():
  63. return f"[ERROR] Not a directory: {p}"
  64. entries = sorted(p.iterdir())
  65. return "\n".join(f"{'[DIR] ' if e.is_dir() else ''}{e.name}" for e in entries)
  66. def mkdir(self, path: str) -> str:
  67. p = self._resolve(path)
  68. p.mkdir(parents=True, exist_ok=True)
  69. return f"Created: {p}"
  70. def copy_file(self, args: str) -> str:
  71. d = json.loads(args)
  72. src, dst = self._resolve(d["src"]), self._resolve(d["dst"])
  73. dst.parent.mkdir(parents=True, exist_ok=True)
  74. shutil.copy2(src, dst)
  75. return f"Copied {src} → {dst}"
  76. # ── scholar-mcp / arxiv-mcp 替代 ─────────────────────
  77. def semantic_scholar_search(self, query: str) -> str:
  78. """用 Semantic Scholar 公开 API 搜索论文 (无需 API key)"""
  79. import urllib.request
  80. import urllib.parse
  81. url = f"https://api.semanticscholar.org/graph/v1/paper/search?query={urllib.parse.quote(query)}&limit=10&fields=title,authors,year,citationCount,venue,abstract,externalIds"
  82. try:
  83. req = urllib.request.Request(url, headers={"User-Agent": "research67/1.0"})
  84. with urllib.request.urlopen(req, timeout=30) as resp:
  85. data = json.loads(resp.read())
  86. papers = data.get("data", [])
  87. results = []
  88. for p in papers:
  89. results.append({
  90. "title": p.get("title", ""),
  91. "authors": [a.get("name", "") for a in (p.get("authors") or [])[:3]],
  92. "year": p.get("year"),
  93. "venue": p.get("venue", ""),
  94. "citationCount": p.get("citationCount", 0),
  95. "abstract": (p.get("abstract") or "")[:200],
  96. "arxivId": (p.get("externalIds") or {}).get("ArXiv", ""),
  97. })
  98. return json.dumps({"total": data.get("total", 0), "papers": results}, ensure_ascii=False, indent=2)
  99. except Exception as e:
  100. return f"[SEARCH_ERROR] {e}"
  101. def get_paper_details(self, paper_id: str) -> str:
  102. import urllib.request
  103. url = f"https://api.semanticscholar.org/graph/v1/paper/{paper_id}?fields=title,abstract,authors,year,venue,citationCount,references,citations"
  104. try:
  105. req = urllib.request.Request(url, headers={"User-Agent": "research67/1.0"})
  106. with urllib.request.urlopen(req, timeout=30) as resp:
  107. data = json.loads(resp.read())
  108. return json.dumps(data, ensure_ascii=False, indent=2)[:3000]
  109. except Exception as e:
  110. return f"[ERROR] {e}"
  111. def get_citations(self, paper_id: str) -> str:
  112. return self.get_paper_details(paper_id)
  113. def get_references(self, paper_id: str) -> str:
  114. return self.get_paper_details(paper_id)
  115. def arxiv_search(self, query: str) -> str:
  116. """用 arXiv API 搜索"""
  117. import urllib.request
  118. import urllib.parse
  119. url = f"http://export.arxiv.org/api/query?search_query=all:{urllib.parse.quote(query)}&start=0&max_results=10&sortBy=submittedDate&sortOrder=descending"
  120. try:
  121. req = urllib.request.Request(url, headers={"User-Agent": "research67/1.0"})
  122. with urllib.request.urlopen(req, timeout=30) as resp:
  123. xml = resp.read().decode("utf-8")
  124. # Simple XML extraction
  125. entries = []
  126. for entry in xml.split("<entry>")[1:]:
  127. title = _extract_xml(entry, "title").strip().replace("\n", " ")
  128. summary = _extract_xml(entry, "summary").strip()[:200]
  129. arxiv_id = _extract_xml(entry, "id").split("/abs/")[-1]
  130. published = _extract_xml(entry, "published")[:10]
  131. entries.append({"title": title, "arxiv_id": arxiv_id, "published": published, "summary": summary})
  132. return json.dumps({"results": entries}, ensure_ascii=False, indent=2)
  133. except Exception as e:
  134. return f"[ARXIV_ERROR] {e}"
  135. def arxiv_download(self, arxiv_id: str) -> str:
  136. return f"[STUB] PDF download not implemented for {arxiv_id}. Use arxiv_search for metadata."
  137. def pdf_to_text(self, path: str) -> str:
  138. return f"[STUB] PDF extraction not implemented for {path}."
  139. # ── code-sandbox-mcp 替代 ────────────────────────────
  140. def python_exec(self, code: str) -> str:
  141. """在本地沙箱执行 Python 代码"""
  142. try:
  143. result = subprocess.run(
  144. [sys.executable, "-c", code],
  145. capture_output=True, text=True, timeout=120,
  146. cwd=str(self.workspace),
  147. )
  148. output = result.stdout
  149. if result.returncode != 0:
  150. output += f"\n[STDERR] {result.stderr}"
  151. return output[:5000]
  152. except subprocess.TimeoutExpired:
  153. return "[TIMEOUT] Python execution exceeded 120s"
  154. except Exception as e:
  155. return f"[EXEC_ERROR] {e}"
  156. def install_package(self, pkg: str) -> str:
  157. try:
  158. result = subprocess.run(
  159. [sys.executable, "-m", "pip", "install", pkg, "-q"],
  160. capture_output=True, text=True, timeout=60,
  161. )
  162. return result.stdout + result.stderr
  163. except Exception as e:
  164. return f"[PIP_ERROR] {e}"
  165. def gpu_status(self, _: str = "") -> str:
  166. try:
  167. r = subprocess.run(["nvidia-smi", "--query-gpu=name,memory.used,memory.total", "--format=csv,noheader"],
  168. capture_output=True, text=True, timeout=10)
  169. return r.stdout if r.returncode == 0 else "No GPU detected (CPU mode)"
  170. except FileNotFoundError:
  171. return "nvidia-smi not found (CPU mode)"
  172. def disk_usage(self, path: str = ".") -> str:
  173. import shutil as sh
  174. total, used, free = sh.disk_usage(self._resolve(path))
  175. return f"Total: {total // (1024**3)}GB, Used: {used // (1024**3)}GB, Free: {free // (1024**3)}GB"
  176. # ── latex-mcp 替代 ───────────────────────────────────
  177. def compile_latex(self, tex_path: str) -> str:
  178. p = self._resolve(tex_path)
  179. if not p.exists():
  180. return f"[ERROR] {p} not found"
  181. try:
  182. r = subprocess.run(
  183. ["pdflatex", "-interaction=nonstopmode", "-output-directory", str(p.parent), str(p)],
  184. capture_output=True, text=True, timeout=60, cwd=str(p.parent),
  185. )
  186. # Run twice for references
  187. subprocess.run(
  188. ["pdflatex", "-interaction=nonstopmode", "-output-directory", str(p.parent), str(p)],
  189. capture_output=True, text=True, timeout=60, cwd=str(p.parent),
  190. )
  191. pdf = p.with_suffix(".pdf")
  192. if pdf.exists():
  193. return f"Compiled: {pdf} ({pdf.stat().st_size} bytes)"
  194. return f"Compilation output:\n{r.stdout[-2000:]}\n{r.stderr[-1000:]}"
  195. except FileNotFoundError:
  196. return "[STUB] pdflatex not found. LaTeX compilation skipped."
  197. except Exception as e:
  198. return f"[LATEX_ERROR] {e}"
  199. def latex_lint(self, tex_path: str) -> str:
  200. return f"[STUB] LaTeX lint for {tex_path}: no issues found."
  201. def bibtex_format(self, bib_path: str) -> str:
  202. return f"[STUB] BibTeX formatted: {bib_path}"
  203. # ── plotting-mcp 替代 ────────────────────────────────
  204. def create_figure(self, args: str) -> str:
  205. """用 matplotlib 生成图表,args 是 JSON spec"""
  206. code = f"""
  207. import json
  208. spec = json.loads('''{args}''')
  209. # Placeholder: actual plotting code would be generated by the agent
  210. print(f"Figure created: {{spec.get('name', 'figure')}}.pdf")
  211. """
  212. return self.python_exec(code)
  213. def create_table(self, args: str) -> str:
  214. return f"[STUB] Table created from spec"
  215. # ── 特殊本地工具 ─────────────────────────────────────
  216. def terminate(self, result: str) -> str:
  217. return result
  218. def read_idea(self, path: str = "") -> str:
  219. idea_path = AGENT_DIR / "paper" / "paper01" / "IDEA.md"
  220. if not idea_path.exists():
  221. return f"[ERROR] IDEA.md not found at {idea_path}"
  222. return idea_path.read_text(encoding="utf-8")
  223. def save_checkpoint(self, data: str) -> str:
  224. ckpt_dir = self.workspace / ".checkpoints"
  225. ckpt_dir.mkdir(parents=True, exist_ok=True)
  226. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  227. path = ckpt_dir / f"checkpoint_{ts}.json"
  228. path.write_text(data, encoding="utf-8")
  229. return f"Checkpoint saved: {path}"
  230. def load_checkpoint(self, pattern: str = "latest") -> str:
  231. ckpt_dir = self.workspace / ".checkpoints"
  232. if not ckpt_dir.exists():
  233. return "[ERROR] No checkpoints found"
  234. files = sorted(ckpt_dir.glob("checkpoint_*.json"))
  235. if not files:
  236. return "[ERROR] No checkpoints found"
  237. return files[-1].read_text(encoding="utf-8")
  238. def human_feedback(self, question: str) -> str:
  239. print(f"\n{'='*60}")
  240. print(f"[HUMAN FEEDBACK REQUESTED]")
  241. print(f"{question}")
  242. print(f"{'='*60}")
  243. try:
  244. answer = input("Your response (or press Enter to skip): ").strip()
  245. return answer if answer else "[NO FEEDBACK]"
  246. except (EOFError, KeyboardInterrupt):
  247. return "[NO FEEDBACK]"
  248. def dispatch_agent(self, args: str) -> str:
  249. """调度子智能体执行"""
  250. d = json.loads(args)
  251. agent_name = d.get("agent", "")
  252. agent_input = d.get("input", "")
  253. phase_dir = d.get("phase_dir", "")
  254. agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
  255. if not agent_yml.exists():
  256. return f"[ERROR] Agent config not found: {agent_yml}"
  257. print(f"\n >> Dispatching sub-agent: {agent_name}")
  258. print(f" Config: {agent_yml}")
  259. print(f" Phase dir: {phase_dir}")
  260. try:
  261. result = Runtime.execute(str(agent_yml), agent_input)
  262. return result.result
  263. except Exception as e:
  264. return f"[AGENT_ERROR] {agent_name} failed: {e}"
  265. # ── 内部辅助 ─────────────────────────────────────────
  266. def _resolve(self, path: str) -> Path:
  267. p = Path(path)
  268. if p.is_absolute():
  269. return p
  270. return self.workspace / p
  271. def get_tool_map(self) -> Dict[str, callable]:
  272. """返回工具名 → 函数的映射,用于注入 runtime"""
  273. return {
  274. # fs-mcp
  275. "read_file": self.read_file,
  276. "write_file": self.write_file,
  277. "list_dir": self.list_dir,
  278. "mkdir": self.mkdir,
  279. "copy_file": self.copy_file,
  280. # scholar-mcp
  281. "semantic_scholar_search": self.semantic_scholar_search,
  282. "get_paper_details": self.get_paper_details,
  283. "get_citations": self.get_citations,
  284. "get_references": self.get_references,
  285. # arxiv-mcp
  286. "arxiv_search": self.arxiv_search,
  287. "arxiv_download": self.arxiv_download,
  288. "pdf_to_text": self.pdf_to_text,
  289. # code-sandbox-mcp
  290. "python_exec": self.python_exec,
  291. "install_package": self.install_package,
  292. "gpu_status": self.gpu_status,
  293. "disk_usage": self.disk_usage,
  294. # latex-mcp
  295. "compile_latex": self.compile_latex,
  296. "latex_lint": self.latex_lint,
  297. "bibtex_format": self.bibtex_format,
  298. # plotting-mcp
  299. "create_figure": self.create_figure,
  300. "create_table": self.create_table,
  301. # local
  302. "terminate": self.terminate,
  303. "read_idea": self.read_idea,
  304. "save_checkpoint": self.save_checkpoint,
  305. "load_checkpoint": self.load_checkpoint,
  306. "human_feedback": self.human_feedback,
  307. "dispatch_agent": self.dispatch_agent,
  308. }
  309. # ═══════════════════════════════════════════════════════════
  310. # 2. Runner — 驱动整个科研流程
  311. # ═══════════════════════════════════════════════════════════
  312. PHASES = [
  313. ("01_idea_analysis", "idea-analyst"),
  314. ("02_literature_review", "lit-searcher"),
  315. ("03_experiment_plan", "exp-planner"),
  316. ("04_experiment_execution","exp-executor"),
  317. ("05_result_analysis", "result-analyzer"),
  318. ("06_paper_writing", "paper-writer"),
  319. ("07_data_verification", "data-verifier"),
  320. ("08_paper_review", "paper-reviewer"),
  321. ]
  322. def create_workspace() -> Path:
  323. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  324. ws = AGENT_DIR / "workspace" / f"run_{ts}"
  325. ws.mkdir(parents=True, exist_ok=True)
  326. return ws
  327. def run_phase(
  328. phase_name: str,
  329. agent_name: str,
  330. workspace: Path,
  331. round_num: int,
  332. toolkit: LocalToolkit,
  333. prev_reports: List[str],
  334. revision_context: str = "",
  335. ) -> Dict[str, Any]:
  336. """运行单个阶段的子智能体"""
  337. phase_dir = workspace / f"round_{round_num}" / phase_name
  338. phase_dir.mkdir(parents=True, exist_ok=True)
  339. (phase_dir / "artifacts").mkdir(exist_ok=True)
  340. agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
  341. # 构建输入上下文
  342. context_parts = [
  343. f"workspace: {workspace}",
  344. f"phase_dir: {phase_dir}",
  345. f"round: {round_num}",
  346. f"dependencies: {json.dumps(prev_reports)}",
  347. ]
  348. if revision_context:
  349. context_parts.append(f"revision_context: {revision_context}")
  350. # 读取 IDEA
  351. idea_content = toolkit.read_idea()
  352. context_parts.append(f"\n## IDEA.md Content:\n{idea_content}")
  353. # 附上前序阶段的关键结果摘要
  354. for rp in prev_reports[-3:]: # 最多带最近 3 个阶段
  355. rp_path = workspace / rp
  356. if rp_path.exists():
  357. try:
  358. data = json.loads(rp_path.read_text(encoding="utf-8"))
  359. # 只取 _meta + 关键字段,避免太长
  360. summary = {k: v for k, v in data.items() if k == "_meta" or not isinstance(v, (list, dict))}
  361. context_parts.append(f"\n## Previous report ({rp}):\n{json.dumps(summary, ensure_ascii=False, indent=2)[:2000]}")
  362. except Exception:
  363. pass
  364. input_text = "\n".join(context_parts)
  365. print(f"\n{'━'*60}")
  366. print(f" Phase: {phase_name}")
  367. print(f" Agent: {agent_name} → {agent_yml.name}")
  368. print(f" Output: {phase_dir}")
  369. print(f"{'━'*60}")
  370. t0 = time.time()
  371. try:
  372. result = Runtime.execute(str(agent_yml), input_text)
  373. duration = time.time() - t0
  374. print(f" ✓ Completed in {duration:.1f}s, {len(result.trace)} trace steps")
  375. # 保存 trace
  376. trace_path = phase_dir / "trace.json"
  377. trace_data = [
  378. {"step": t.step, "term": t.term_name, "type": t.term_type,
  379. "duration_ms": t.duration_ms, "output": t.output[:200]}
  380. for t in result.trace
  381. ]
  382. trace_path.write_text(json.dumps(trace_data, indent=2), encoding="utf-8")
  383. # 尝试解析 report.json
  384. report_path = phase_dir / "report.json"
  385. if report_path.exists():
  386. report = json.loads(report_path.read_text(encoding="utf-8"))
  387. else:
  388. # Agent 可能直接返回了 JSON
  389. try:
  390. report = json.loads(result.result)
  391. report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  392. except (json.JSONDecodeError, TypeError):
  393. report = {"_meta": {"status": "completed"}, "raw_output": result.result[:3000]}
  394. report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  395. return report
  396. except Exception as e:
  397. duration = time.time() - t0
  398. print(f" ✗ Failed after {duration:.1f}s: {e}")
  399. error_report = {
  400. "_meta": {"status": "failed", "error": str(e), "duration_seconds": duration},
  401. }
  402. (phase_dir / "report.json").write_text(json.dumps(error_report, indent=2), encoding="utf-8")
  403. return error_report
  404. def run_full_pipeline(
  405. start_phase: Optional[str] = None,
  406. max_rounds: int = 5,
  407. ):
  408. """运行完整的科研流程"""
  409. # 检查 API key
  410. if not os.environ.get("ANTHROPIC_API_KEY"):
  411. print("ERROR: ANTHROPIC_API_KEY environment variable not set.")
  412. print(" export ANTHROPIC_API_KEY=sk-ant-...")
  413. sys.exit(1)
  414. workspace = create_workspace()
  415. toolkit = LocalToolkit(workspace)
  416. print(f"""
  417. ╔══════════════════════════════════════════════════════════╗
  418. ║ Research-467 Autonomous Research Agent ║
  419. ║ Model: Claude Opus 4.6 (1M context) ║
  420. ║ IDEA: paper/paper01/IDEA.md ║
  421. ╚══════════════════════════════════════════════════════════╝
  422. Workspace: {workspace}
  423. Max rounds: {max_rounds}
  424. """)
  425. # 写入项目计划
  426. idea = toolkit.read_idea()
  427. project_plan = f"""# Research-467 Project Plan
  428. ## IDEA Source
  429. paper/paper01/IDEA.md
  430. ## Model
  431. Claude Opus 4.6 (1M context) — claude-opus-4-6
  432. ## Phases
  433. {chr(10).join(f'{i+1}. {name} → {agent}' for i, (name, agent) in enumerate(PHASES))}
  434. ## Target
  435. CCF-A conference acceptance probability ≥ 50%
  436. ## Created
  437. {datetime.now(timezone.utc).isoformat()}
  438. """
  439. (workspace / "project_plan.md").write_text(project_plan, encoding="utf-8")
  440. progress = {
  441. "project_id": workspace.name,
  442. "current_round": 1,
  443. "current_phase": PHASES[0][0],
  444. "max_rounds": max_rounds,
  445. "best_score": 0.0,
  446. "rounds": [],
  447. }
  448. for round_num in range(1, max_rounds + 1):
  449. print(f"\n{'═'*60}")
  450. print(f" ROUND {round_num} / {max_rounds}")
  451. print(f"{'═'*60}")
  452. progress["current_round"] = round_num
  453. round_reports = []
  454. prev_reports = []
  455. # 确定本轮需要跑哪些阶段
  456. phases_to_run = PHASES
  457. if round_num > 1 and "revision_target" in progress:
  458. # 回退到指定阶段
  459. target = progress["revision_target"]
  460. start_idx = next((i for i, (name, _) in enumerate(PHASES) if name == target), 0)
  461. phases_to_run = PHASES[start_idx:]
  462. print(f" Revision: starting from {target}")
  463. if start_phase and round_num == 1:
  464. start_idx = next((i for i, (name, _) in enumerate(PHASES) if start_phase in name), 0)
  465. phases_to_run = PHASES[start_idx:]
  466. for phase_name, agent_name in phases_to_run:
  467. progress["current_phase"] = phase_name
  468. _save_progress(workspace, progress)
  469. report = run_phase(
  470. phase_name, agent_name, workspace, round_num, toolkit,
  471. prev_reports,
  472. revision_context=progress.get("revision_context", ""),
  473. )
  474. report_rel = f"round_{round_num}/{phase_name}/report.json"
  475. prev_reports.append(report_rel)
  476. round_reports.append({"phase": phase_name, "status": report.get("_meta", {}).get("status", "unknown")})
  477. progress["rounds"].append({"round": round_num, "phases": round_reports})
  478. # 检查评审结果
  479. review_report_path = workspace / f"round_{round_num}" / "08_paper_review" / "report.json"
  480. if review_report_path.exists():
  481. review = json.loads(review_report_path.read_text(encoding="utf-8"))
  482. score = review.get("acceptance_probability", 0)
  483. progress["best_score"] = max(progress.get("best_score", 0), score)
  484. print(f"\n Review Score: {score:.2%}")
  485. print(f" Best Score: {progress['best_score']:.2%}")
  486. if score >= 0.5:
  487. print(f"\n ✓ TARGET REACHED! Acceptance probability: {score:.2%}")
  488. _finalize(workspace, round_num, toolkit, progress)
  489. break
  490. # 决定回退目标
  491. scores = review.get("scores", {})
  492. if scores:
  493. lowest = min(scores.items(), key=lambda x: x[1].get("score", 10) if isinstance(x[1], dict) else 10)
  494. dimension = lowest[0]
  495. target_map = {
  496. "novelty": "01_idea_analysis",
  497. "soundness": "03_experiment_plan",
  498. "clarity": "06_paper_writing",
  499. "significance": "02_literature_review",
  500. "reproducibility": "04_experiment_execution",
  501. }
  502. progress["revision_target"] = target_map.get(dimension, "06_paper_writing")
  503. revision_items = review.get("revision_priority", [])
  504. progress["revision_context"] = json.dumps(revision_items[:3], ensure_ascii=False)
  505. print(f" Lowest dimension: {dimension} → Retrying from {progress['revision_target']}")
  506. else:
  507. print(f"\n [WARN] No review report found, ending pipeline")
  508. break
  509. else:
  510. # 达到 max_rounds
  511. print(f"\n Max rounds ({max_rounds}) reached. Finalizing best version.")
  512. _finalize(workspace, round_num, toolkit, progress)
  513. _save_progress(workspace, progress)
  514. print(f"\n{'═'*60}")
  515. print(f" Pipeline complete. Workspace: {workspace}")
  516. print(f"{'═'*60}\n")
  517. def _save_progress(workspace: Path, progress: dict):
  518. (workspace / "progress.json").write_text(
  519. json.dumps(progress, ensure_ascii=False, indent=2), encoding="utf-8"
  520. )
  521. def _finalize(workspace: Path, last_round: int, toolkit: LocalToolkit, progress: dict):
  522. """归档最终产出到 final/ 目录"""
  523. final_dir = workspace / "final"
  524. final_dir.mkdir(exist_ok=True)
  525. round_dir = workspace / f"round_{last_round}"
  526. # 复制论文
  527. for src_name, dst_name in [
  528. ("06_paper_writing/artifacts/paper.tex", "paper.tex"),
  529. ("06_paper_writing/artifacts/paper.pdf", "paper.pdf"),
  530. ("06_paper_writing/artifacts/references.bib", "references.bib"),
  531. ]:
  532. src = round_dir / src_name
  533. if src.exists():
  534. shutil.copy2(src, final_dir / dst_name)
  535. # 复制图表
  536. figs_src = round_dir / "05_result_analysis" / "artifacts" / "figures"
  537. if figs_src.exists():
  538. shutil.copytree(figs_src, final_dir / "figures", dirs_exist_ok=True)
  539. # 复制实验数据
  540. data_src = round_dir / "04_experiment_execution" / "artifacts" / "raw_results"
  541. if data_src.exists():
  542. shutil.copytree(data_src, final_dir / "experiment_data", dirs_exist_ok=True)
  543. # 生成投稿建议
  544. recommendation = {
  545. "final_paper_path": "final/paper.pdf",
  546. "total_rounds": last_round,
  547. "best_score": progress.get("best_score", 0),
  548. "workspace": str(workspace),
  549. "completed_at": datetime.now(timezone.utc).isoformat(),
  550. }
  551. (final_dir / "submission_recommendation.json").write_text(
  552. json.dumps(recommendation, ensure_ascii=False, indent=2), encoding="utf-8"
  553. )
  554. progress["final_output"] = str(final_dir)
  555. print(f"\n Final output archived to: {final_dir}")
  556. # ═══════════════════════════════════════════════════════════
  557. # 3. XML 辅助
  558. # ═══════════════════════════════════════════════════════════
  559. def _extract_xml(text: str, tag: str) -> str:
  560. start = text.find(f"<{tag}>")
  561. end = text.find(f"</{tag}>")
  562. if start == -1 or end == -1:
  563. return ""
  564. return text[start + len(f"<{tag}>"):end]
  565. # ═══════════════════════════════════════════════════════════
  566. # 4. CLI
  567. # ═══════════════════════════════════════════════════════════
  568. def main():
  569. parser = argparse.ArgumentParser(description="Research-467 Simplified Runner")
  570. parser.add_argument("--phase", type=str, default=None,
  571. help="Start from a specific phase (e.g., idea_analysis, paper_writing)")
  572. parser.add_argument("--max-rounds", type=int, default=5,
  573. help="Maximum iteration rounds (default: 5)")
  574. parser.add_argument("--test-tools", action="store_true",
  575. help="Test local toolkit connectivity and exit")
  576. args = parser.parse_args()
  577. if args.test_tools:
  578. _test_tools()
  579. return
  580. run_full_pipeline(start_phase=args.phase, max_rounds=args.max_rounds)
  581. def _test_tools():
  582. """测试本地工具是否可用"""
  583. ws = Path("/tmp/research67_test")
  584. ws.mkdir(exist_ok=True)
  585. toolkit = LocalToolkit(ws)
  586. tests = [
  587. ("read_idea", lambda: toolkit.read_idea()),
  588. ("semantic_scholar_search", lambda: toolkit.semantic_scholar_search("spectral analysis pretrained features")),
  589. ("arxiv_search", lambda: toolkit.arxiv_search("label-free feature quality")),
  590. ("python_exec", lambda: toolkit.python_exec("import sys; print(f'Python {sys.version}')")),
  591. ("write_file", lambda: toolkit.write_file(json.dumps({"path": "test.txt", "content": "hello"}))),
  592. ("read_file", lambda: toolkit.read_file("test.txt")),
  593. ("list_dir", lambda: toolkit.list_dir(".")),
  594. ("gpu_status", lambda: toolkit.gpu_status()),
  595. ]
  596. print("Testing local toolkit...\n")
  597. for name, fn in tests:
  598. try:
  599. t0 = time.time()
  600. result = fn()
  601. dt = time.time() - t0
  602. ok = "[ERROR]" not in result and "[STUB]" not in result
  603. status = "✓" if ok else "⚠"
  604. print(f" {status} {name:30s} ({dt:.1f}s) {result[:80]}")
  605. except Exception as e:
  606. print(f" ✗ {name:30s} ERROR: {e}")
  607. shutil.rmtree(ws, ignore_errors=True)
  608. print("\nDone.")
  609. if __name__ == "__main__":
  610. main()