| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746 |
- #!/usr/bin/env python3
- """
- research67/launch_paas.py — 通过 AgentPaaS API 启动科研智能体
- ================================================================
- 两种运行模式:
- 1. AgentPaaS API 模式: 启动 PaaS 服务,注册所有 agent,通过 REST API 执行
- 2. Claude Code CLI 桥接模式: 利用 Claude Max Plan 的 claude CLI 作为 LLM 后端
- 用法:
- # 模式 1: 启动 PaaS 服务 + 注册 + 执行
- python launch_paas.py serve # 启动服务 (端口 8000)
- python launch_paas.py register # 注册所有 agent 到 PaaS
- python launch_paas.py run # 通过 API 执行科研流程
- python launch_paas.py all # serve + register + run (一键启动)
- # 模式 2: 使用 Claude Code CLI (Max Plan)
- python launch_paas.py claude-code # 通过 claude CLI 执行
- """
- from __future__ import annotations
- import argparse
- import hashlib
- import json
- import os
- import secrets
- import subprocess
- import sys
- import time
- import urllib.request
- import urllib.error
- from pathlib import Path
- from typing import Dict, List, Optional
- PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
- AGENT_DIR = Path(__file__).resolve().parent
- sys.path.insert(0, str(PROJECT_ROOT))
- PAAS_URL = os.environ.get("AGENTPAAS_URL", "http://127.0.0.1:8000")
- API_KEY = os.environ.get("AGENTPAAS_API_KEY", "")
- AGENT_CONFIGS = [
- ("research-67-orchestrator", "科研主编排器", AGENT_DIR / "orchestrator.yml"),
- ("research-67-idea-analyst", "创意分析师", AGENT_DIR / "agents" / "idea-analyst.yml"),
- ("research-67-lit-searcher", "文献检索员", AGENT_DIR / "agents" / "lit-searcher.yml"),
- ("research-67-exp-planner", "实验规划师", AGENT_DIR / "agents" / "exp-planner.yml"),
- ("research-67-exp-executor", "实验执行员", AGENT_DIR / "agents" / "exp-executor.yml"),
- ("research-67-result-analyzer", "结果分析师", AGENT_DIR / "agents" / "result-analyzer.yml"),
- ("research-67-paper-writer", "论文撰写员", AGENT_DIR / "agents" / "paper-writer.yml"),
- ("research-67-data-verifier", "数据核验员", AGENT_DIR / "agents" / "data-verifier.yml"),
- ("research-67-paper-reviewer", "论文评审员", AGENT_DIR / "agents" / "paper-reviewer.yml"),
- ("research-67-review-feedback", "评审反馈合成器", AGENT_DIR / "agents" / "review-feedback.yml"),
- ]
- # 迭代控制参数
- MAX_ROUNDS = 5 # 最大迭代轮数
- ACCEPTANCE_THRESHOLD = 0.50 # 目标接收概率 (50%)
- MAX_TURNS_PER_PHASE = 30 # 每个 agent 最大工具调用轮数
- PHASE_TIMEOUT = 3600 # 每个 phase 超时 (60分钟,Round1实测单阶段可达20min+)
- API_RETRY_MAX = 3 # API 失败最大重试次数
- API_RETRY_BACKOFF = 30 # 重试间隔基数 (秒)
- PHASE_COOLDOWN = 10 # 阶段间冷却 (秒,防 429/529)
- # ═══════════════════════════════════════════════════════════
- # 1. PaaS API Helpers
- # ═══════════════════════════════════════════════════════════
- def api_call(method: str, path: str, data: dict = None) -> dict:
- url = f"{PAAS_URL}/api/v1{path}"
- body = json.dumps(data).encode("utf-8") if data else None
- req = urllib.request.Request(url, data=body, method=method)
- req.add_header("Content-Type", "application/json")
- req.add_header("Authorization", f"Bearer {API_KEY}")
- try:
- with urllib.request.urlopen(req, timeout=600) as resp:
- return json.loads(resp.read())
- except urllib.error.HTTPError as e:
- body = e.read().decode()
- print(f" API Error {e.code}: {body[:500]}")
- return {"error": body}
- except urllib.error.URLError as e:
- print(f" Connection error: {e}")
- return {"error": str(e)}
- def wait_for_server(timeout: int = 30):
- """等待 PaaS 服务就绪"""
- for i in range(timeout):
- try:
- req = urllib.request.Request(f"{PAAS_URL}/health")
- with urllib.request.urlopen(req, timeout=2):
- return True
- except Exception:
- time.sleep(1)
- return False
- # ═══════════════════════════════════════════════════════════
- # 2. 命令: serve — 启动 AgentPaaS 服务
- # ═══════════════════════════════════════════════════════════
- def cmd_serve(args):
- """启动 AgentPaaS 并自动创建租户"""
- print("Starting AgentPaaS server...")
- # 启动服务(后台进程)
- proc = subprocess.Popen(
- [sys.executable, "-m", "agentpaas", "serve", "--port", str(args.port), "--dev"],
- cwd=str(PROJECT_ROOT),
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- )
- print(f" PID: {proc.pid}")
- print(f" URL: http://127.0.0.1:{args.port}")
- print(" Waiting for server to be ready...")
- if not wait_for_server(30):
- print(" ERROR: Server failed to start within 30s")
- proc.kill()
- return None
- print(" ✓ Server ready")
- return proc
- def cmd_create_tenant(args):
- """创建租户并返回 API key"""
- print("Creating tenant...")
- result = subprocess.run(
- [sys.executable, "-m", "agentpaas", "create-tenant", "--name", "research-lab"],
- capture_output=True, text=True, cwd=str(PROJECT_ROOT),
- )
- print(result.stdout)
- # Extract API key from output
- for line in result.stdout.splitlines():
- if line.startswith("API Key:"):
- key = line.split(":", 1)[1].strip()
- return key
- return None
- # ═══════════════════════════════════════════════════════════
- # 3. 命令: register — 注册所有 agent
- # ═══════════════════════════════════════════════════════════
- def cmd_register(args):
- """注册所有 research67 agent 到 AgentPaaS"""
- import yaml
- print(f"\nRegistering {len(AGENT_CONFIGS)} agents to AgentPaaS...")
- agent_ids = {}
- for agent_id, name, yml_path in AGENT_CONFIGS:
- with open(yml_path, "r", encoding="utf-8") as f:
- config = yaml.safe_load(f)
- result = api_call("POST", "/agents", {
- "name": name,
- "description": config.get("description", ""),
- "config": config,
- "tags": ["research67", config.get("type", "react")],
- "environment": "production",
- })
- if "agent_id" in result:
- agent_ids[agent_id] = result["agent_id"]
- print(f" ✓ {name:20s} → {result['agent_id']} (v{result['version']})")
- else:
- print(f" ✗ {name:20s} → {result.get('error', 'Unknown error')}")
- # 保存 agent ID 映射
- mapping_path = AGENT_DIR / ".paas_agent_ids.json"
- mapping_path.write_text(json.dumps(agent_ids, indent=2), encoding="utf-8")
- print(f"\n Agent IDs saved to {mapping_path}")
- return agent_ids
- # ═══════════════════════════════════════════════════════════
- # 4. 命令: run — 通过 API 执行科研流程
- # ═══════════════════════════════════════════════════════════
- def cmd_run(args):
- """通过 AgentPaaS API 执行 orchestrator"""
- mapping_path = AGENT_DIR / ".paas_agent_ids.json"
- if not mapping_path.exists():
- print("ERROR: No agent ID mapping found. Run 'register' first.")
- return
- agent_ids = json.loads(mapping_path.read_text())
- orchestrator_id = agent_ids.get("research-67-orchestrator")
- if not orchestrator_id:
- print("ERROR: Orchestrator not registered.")
- return
- # 读取 IDEA
- idea_path = AGENT_DIR / "paper" / "paper01" / "IDEA.md"
- idea = idea_path.read_text(encoding="utf-8") if idea_path.exists() else "[IDEA.md not found]"
- print(f"\n{'═'*60}")
- print(f" Executing research67 orchestrator via AgentPaaS API")
- print(f" Agent ID: {orchestrator_id}")
- print(f" IDEA: {idea[:80]}...")
- print(f"{'═'*60}\n")
- result = api_call("POST", f"/agents/{orchestrator_id}/run", {
- "input": idea,
- "parameters": {},
- "context": {"sub_agent_ids": agent_ids},
- })
- if "error" not in result:
- print(f"\n ✓ Run completed")
- print(f" Run ID: {result.get('run_id')}")
- print(f" Status: {result.get('status')}")
- print(f" Tokens: {result.get('usage', {}).get('total_tokens', 0)}")
- print(f" Duration: {result.get('usage', {}).get('duration_ms', 0)}ms")
- print(f"\n Output (first 500 chars):")
- print(f" {result.get('output', '')[:500]}")
- else:
- print(f"\n ✗ Execution failed: {result['error'][:500]}")
- # ═══════════════════════════════════════════════════════════
- # 5. 命令: all — 一键启动全流程
- # ═══════════════════════════════════════════════════════════
- def cmd_all(args):
- """一键: 启动服务 + 创建租户 + 注册 agent + 执行"""
- global API_KEY
- proc = cmd_serve(args)
- if not proc:
- return
- try:
- # 创建租户
- key = cmd_create_tenant(args)
- if key:
- API_KEY = key
- os.environ["AGENTPAAS_API_KEY"] = key
- print(f" API Key: {key}")
- # 注册 agents
- cmd_register(args)
- # 执行
- cmd_run(args)
- finally:
- print("\nShutting down server...")
- proc.terminate()
- proc.wait(timeout=5)
- # ═══════════════════════════════════════════════════════════
- # 6. 命令: claude-code — 使用 Claude Code CLI 作为 LLM 后端
- # ═══════════════════════════════════════════════════════════
- def run_phase(phase_dir: Path, agent_name: str, desc: str,
- prompt_content: str, round_num: int) -> str:
- """
- 执行单个阶段,带自动重试和详细日志。
- 重试策略:
- - subprocess.TimeoutExpired → 不重试(已用完时间预算)
- - API 529/overloaded → 指数退避重试
- - "Request timed out" → 指数退避重试(通常是 API 网关超时)
- - 其他错误 → 重试 1 次
- """
- phase_dir.mkdir(parents=True, exist_ok=True)
- (phase_dir / "artifacts").mkdir(exist_ok=True)
- # 写入 prompt
- (phase_dir / "prompt.md").write_text(prompt_content, encoding="utf-8")
- print(f"\n{'━'*60}")
- print(f" Phase: {phase_dir.name} — {desc}")
- print(f" Agent: {agent_name} | Round: {round_num}")
- print(f" Max turns: {MAX_TURNS_PER_PHASE} | Timeout: {PHASE_TIMEOUT}s | Retries: {API_RETRY_MAX}")
- print(f"{'━'*60}")
- cmd = [
- "claude",
- "-p",
- "--model", "claude-opus-4-6",
- "--max-turns", str(MAX_TURNS_PER_PHASE),
- "--allowedTools", "Edit,Write,Read,Bash,Glob,Grep,WebSearch,WebFetch",
- ]
- last_error = ""
- for attempt in range(1, API_RETRY_MAX + 1):
- t0 = time.time()
- attempt_label = f"[attempt {attempt}/{API_RETRY_MAX}]"
- try:
- print(f" 🚀 {attempt_label} Running claude CLI ...")
- result = subprocess.run(
- cmd,
- input=prompt_content,
- capture_output=True, text=True,
- timeout=PHASE_TIMEOUT,
- cwd=str(phase_dir),
- )
- elapsed = time.time() - t0
- output = result.stdout or ""
- stderr = result.stderr or ""
- # --- 检测 API 层面错误(进程成功退出但内容是错误信息)---
- is_api_error = False
- for err_pattern in ["Request timed out", "overloaded_error", "529",
- "rate_limit", "500 Internal", "502 Bad Gateway",
- "API Error"]:
- if err_pattern in output or err_pattern in stderr:
- is_api_error = True
- last_error = (output + stderr)[:300]
- break
- if is_api_error and attempt < API_RETRY_MAX:
- wait = API_RETRY_BACKOFF * (2 ** (attempt - 1)) # 30s, 60s, 120s
- print(f" ⚠ {attempt_label} API error after {elapsed:.0f}s: {last_error[:100]}")
- print(f" ⏳ Retrying in {wait}s ...")
- time.sleep(wait)
- continue
- # --- 成功(或最后一次尝试,即使有 API 错误也保存)---
- if result.returncode != 0 and stderr:
- output = f"[STDERR] {stderr[:500]}\n{output}"
- (phase_dir / "claude_output.md").write_text(output, encoding="utf-8")
- # 产出检查
- report_path = phase_dir / "report.json"
- wp_path = phase_dir / "work_plan.md"
- artifacts = list((phase_dir / "artifacts").rglob("*"))
- artifacts_count = sum(1 for a in artifacts if a.is_file())
- status_parts = []
- if wp_path.exists():
- status_parts.append("✅ work_plan")
- if report_path.exists():
- status_parts.append("✅ report.json")
- else:
- status_parts.append("⚠ no report.json")
- status_parts.append(f"📎 {artifacts_count} artifacts")
- print(f" ✓ {attempt_label} Done in {elapsed:.0f}s | {len(output)} chars | {' | '.join(status_parts)}")
- return output
- except subprocess.TimeoutExpired:
- elapsed = time.time() - t0
- msg = f"[TIMEOUT] Phase {phase_dir.name} timed out after {elapsed:.0f}s (limit {PHASE_TIMEOUT}s)"
- print(f" ✗ {msg}")
- # 超时不重试(已用完整个时间窗口),但保存已有产出
- partial = ""
- if (phase_dir / "work_plan.md").exists():
- partial += " (work_plan exists, partial progress saved)"
- (phase_dir / "claude_output.md").write_text(msg + partial, encoding="utf-8")
- return msg
- except FileNotFoundError:
- print(" ✗ 'claude' CLI not found. Is Claude Code installed?")
- return "[ERROR] claude CLI not found"
- except Exception as e:
- last_error = str(e)
- if attempt < API_RETRY_MAX:
- wait = API_RETRY_BACKOFF * attempt
- print(f" ⚠ {attempt_label} Error: {last_error[:100]}. Retrying in {wait}s ...")
- time.sleep(wait)
- else:
- msg = f"[ERROR] {last_error}"
- print(f" ✗ {msg}")
- (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
- return msg
- # 所有重试都失败
- msg = f"[FAILED] All {API_RETRY_MAX} attempts failed. Last: {last_error[:200]}"
- (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
- return msg
- def build_prompt(workspace: Path, phase_dir: Path, desc: str,
- system_prompt: str, idea: str, round_num: int,
- prev_outputs: list, revision_context: str = "") -> str:
- """构建阶段 prompt"""
- input_parts = [
- f"workspace: {workspace}",
- f"phase_dir: {phase_dir}",
- f"round: {round_num}",
- f"\n## IDEA:\n{idea[:8000]}",
- ]
- if revision_context:
- input_parts.append(f"\n## 评审反馈与修改指令 (来自 Round {round_num - 1}):\n{revision_context[:6000]}")
- for prev in prev_outputs[-2:]:
- input_parts.append(f"\n## Previous phase output:\n{prev[:3000]}")
- full_input = "\n".join(input_parts)
- return f"""# {desc}
- ## System Instructions
- {system_prompt[:8000]}
- ## Input Context
- {full_input}
- ## Important
- - 将所有产出文件写入 {phase_dir}/
- - 先写 work_plan.md,再执行,最后写 report.json 和 report.md
- - report.json 必须包含 _meta 字段(含 phase, round, status)
- - 这是 Round {round_num}{',请根据评审反馈重点改进' if round_num > 1 else ''}
- """
- def read_review_score(workspace: Path, round_num: int) -> tuple:
- """读取评审报告的 acceptance_probability 和 overall_score
- 优先从 artifacts/review_report.json 读取(详细评审),
- 其次从 report.json 读取(元数据摘要)。
- """
- review_dir = workspace / f"round_{round_num}" / "08_paper_review"
- # 优先: artifacts/review_report.json (完整评审)
- for candidate in [
- review_dir / "artifacts" / "review_report.json",
- review_dir / "report.json",
- ]:
- if candidate.exists():
- try:
- data = json.loads(candidate.read_text(encoding="utf-8"))
- prob = data.get("estimated_acceptance_probability",
- data.get("acceptance_probability", 0))
- score = data.get("overall_score", 0)
- if score > 0 or prob > 0:
- return prob, score
- except Exception:
- continue
- return 0.0, 0
- def read_revision_context(workspace: Path, round_num: int) -> str:
- """读取上一轮的评审 + 反馈合成结果,作为下一轮的 revision_context"""
- parts = []
- # 评审报告摘要 (优先从 artifacts/review_report.json)
- review_dir = workspace / f"round_{round_num}" / "08_paper_review"
- review_report = review_dir / "artifacts" / "review_report.json"
- if not review_report.exists():
- review_report = review_dir / "report.json"
- if review_report.exists():
- try:
- data = json.loads(review_report.read_text(encoding="utf-8"))
- parts.append(f"### 上轮评审 (Round {round_num})")
- parts.append(f"- 总评分: {data.get('overall_score', '?')}/10")
- parts.append(f"- 接收概率: {data.get('estimated_acceptance_probability', data.get('acceptance_probability', '?'))}")
- parts.append(f"- 建议: {data.get('recommendation', '?')}")
- if "weaknesses" in data:
- parts.append("\n#### 弱点:")
- for w in data["weaknesses"][:6]:
- w_text = w if isinstance(w, str) else w.get("title", str(w)[:100])
- parts.append(f" - {w_text[:150]}")
- if "required_revisions" in data:
- parts.append("\n#### 必须修订:")
- for r in data["required_revisions"][:6]:
- r_text = r if isinstance(r, str) else r.get("title", str(r)[:100])
- parts.append(f" - {r_text[:150]}")
- except Exception:
- pass
- # 反馈合成的修改后 IDEA
- revised_idea = workspace / f"round_{round_num}" / "09_review_feedback" / "artifacts" / "revised_idea.md"
- if revised_idea.exists():
- parts.append(f"\n### 修改后的 IDEA (Round {round_num} 反馈合成):")
- parts.append(revised_idea.read_text(encoding="utf-8")[:4000])
- # 反馈合成的行动计划
- action_plan = workspace / f"round_{round_num}" / "09_review_feedback" / "artifacts" / "action_plan.json"
- if action_plan.exists():
- parts.append(f"\n### 各阶段行动计划:")
- parts.append(action_plan.read_text(encoding="utf-8")[:3000])
- return "\n".join(parts)
- def cmd_claude_code(args):
- """
- 通过 Claude Code CLI (Max Plan) 执行科研流程。
- 支持迭代: 评审 → 反馈合成 → 修改 IDEA → 重跑流程,直到 acceptance ≥ 50%。
- 可通过 --workspace 继续已有工作区的下一轮。
- """
- import yaml
- from datetime import datetime
- global MAX_ROUNDS, MAX_TURNS_PER_PHASE, PHASE_TIMEOUT, PHASE_COOLDOWN
- if hasattr(args, "max_rounds") and args.max_rounds:
- MAX_ROUNDS = args.max_rounds
- if hasattr(args, "max_turns") and args.max_turns:
- MAX_TURNS_PER_PHASE = args.max_turns
- if hasattr(args, "phase_timeout") and args.phase_timeout:
- PHASE_TIMEOUT = args.phase_timeout
- if hasattr(args, "cooldown") and args.cooldown is not None:
- PHASE_COOLDOWN = args.cooldown
- print(f"""
- ╔══════════════════════════════════════════════════════════════╗
- ║ Research-67 via Claude Code CLI (Max Plan) ║
- ║ Model: Claude Opus 4.6 (1M context) ║
- ║ Max turns/phase: {MAX_TURNS_PER_PHASE:<3d} | Threshold: {ACCEPTANCE_THRESHOLD:.0%} | Max rounds: {MAX_ROUNDS} ║
- ╚══════════════════════════════════════════════════════════════╝
- """)
- # 检查 claude CLI
- try:
- r = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=10)
- print(f" Claude CLI: {r.stdout.strip()}")
- except FileNotFoundError:
- print(" ERROR: 'claude' not found. Install Claude Code CLI.")
- return
- # 工作区: 复用已有 or 新建
- if hasattr(args, "workspace") and args.workspace:
- workspace = Path(args.workspace)
- if not workspace.exists():
- print(f" ERROR: Workspace not found: {workspace}")
- return
- print(f" Resuming workspace: {workspace}")
- else:
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
- workspace = AGENT_DIR / "workspace" / f"claude_run_{ts}"
- workspace.mkdir(parents=True, exist_ok=True)
- print(f" New workspace: {workspace}")
- # 读取 IDEA
- idea_path = AGENT_DIR / "paper" / "paper01" / "IDEA.md"
- idea_original = idea_path.read_text(encoding="utf-8") if idea_path.exists() else ""
- # 确定起始轮次
- start_round = 1
- for i in range(1, MAX_ROUNDS + 1):
- if (workspace / f"round_{i}" / "08_paper_review" / "report.json").exists():
- prob, score = read_review_score(workspace, i)
- print(f" Round {i} review found: score={score}/10, acceptance={prob:.0%}")
- if prob >= ACCEPTANCE_THRESHOLD:
- print(f" ✓ Already meets threshold! No further rounds needed.")
- return
- start_round = i + 1 # 从评审后的下一步开始
- else:
- break
- # 检查是否需要先跑 feedback (上一轮有评审但还没跑 feedback)
- need_feedback_first = False
- if start_round > 1:
- prev_round = start_round - 1
- feedback_dir = workspace / f"round_{prev_round}" / "09_review_feedback"
- if not (feedback_dir / "report.json").exists():
- need_feedback_first = True
- print(f" Round {prev_round} 评审已完成但 feedback 未执行,先执行反馈合成...")
- # ═══ 主循环 ═══
- MAIN_PHASES = [
- ("01_idea_analysis", "idea-analyst", "分析 IDEA 可行性"),
- ("02_literature_review", "lit-searcher", "检索相关文献"),
- ("03_experiment_plan", "exp-planner", "制定实验方案"),
- ("04_experiment_execution","exp-executor", "执行实验"),
- ("05_result_analysis", "result-analyzer", "分析实验结果"),
- ("06_paper_writing", "paper-writer", "撰写论文"),
- ("07_data_verification", "data-verifier", "核对数据"),
- ("08_paper_review", "paper-reviewer", "评审论文"),
- ]
- for round_num in range(start_round, MAX_ROUNDS + 1):
- print(f"\n{'═'*60}")
- print(f" 🔄 ROUND {round_num}/{MAX_ROUNDS}")
- print(f"{'═'*60}")
- # --- Phase 09: 评审反馈合成 (Round 2+ 之前,或 need_feedback_first) ---
- if round_num > 1 or need_feedback_first:
- feedback_round = round_num - 1 if round_num > 1 else start_round - 1
- feedback_dir = workspace / f"round_{feedback_round}" / "09_review_feedback"
- if not (feedback_dir / "report.json").exists():
- agent_yml = AGENT_DIR / "agents" / "review-feedback.yml"
- with open(agent_yml, "r", encoding="utf-8") as f:
- config = yaml.safe_load(f)
- review_context = read_revision_context(workspace, feedback_round)
- prompt = build_prompt(
- workspace, feedback_dir, "评审反馈合成 → 修改 IDEA",
- config.get("systemPrompt", ""), idea_original,
- feedback_round, [], revision_context=review_context
- )
- run_phase(feedback_dir, "review-feedback", "评审反馈合成", prompt, feedback_round)
- # 读取修改后的 IDEA(如果有)
- idea = idea_original
- revision_context = ""
- if round_num > 1:
- revision_context = read_revision_context(workspace, round_num - 1)
- revised_idea_path = workspace / f"round_{round_num - 1}" / "09_review_feedback" / "artifacts" / "revised_idea.md"
- if revised_idea_path.exists():
- idea = revised_idea_path.read_text(encoding="utf-8")
- print(f" 📝 Using revised IDEA from Round {round_num - 1}")
- # --- Main phases 01-08 ---
- prev_outputs = []
- failed_phases = []
- for phase_idx, (phase_name, agent_name, desc) in enumerate(MAIN_PHASES):
- phase_dir = workspace / f"round_{round_num}" / phase_name
- # 跳过已完成的阶段(支持断点续跑)
- if (phase_dir / "report.json").exists():
- print(f"\n ⏭ Skipping {phase_name} (report.json exists)")
- existing_output = (phase_dir / "claude_output.md").read_text(encoding="utf-8") \
- if (phase_dir / "claude_output.md").exists() else ""
- prev_outputs.append(existing_output[:5000])
- continue
- # 上游连续失败 ≥2 个阶段 → 后续阶段跳过(防级联空转)
- if len(failed_phases) >= 2:
- consecutive_fail = all(
- f_idx >= phase_idx - 2 for f_idx in failed_phases[-2:]
- )
- if consecutive_fail:
- print(f"\n ⏭ Skipping {phase_name} (upstream cascade failure)")
- prev_outputs.append("[SKIPPED] upstream failure")
- continue
- agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
- with open(agent_yml, "r", encoding="utf-8") as f:
- config = yaml.safe_load(f)
- prompt = build_prompt(
- workspace, phase_dir, desc,
- config.get("systemPrompt", ""), idea,
- round_num, prev_outputs,
- revision_context=revision_context if round_num > 1 else ""
- )
- output = run_phase(phase_dir, agent_name, desc, prompt, round_num)
- prev_outputs.append(output[:5000])
- # 检测失败
- if any(tag in output for tag in ["[TIMEOUT]", "[ERROR]", "[FAILED]",
- "Request timed out", "overloaded_error"]):
- failed_phases.append(phase_idx)
- # 阶段间冷却(防 API 限速)
- if phase_idx < len(MAIN_PHASES) - 1:
- print(f" ⏳ Cooldown {PHASE_COOLDOWN}s before next phase...")
- time.sleep(PHASE_COOLDOWN)
- # --- 检查评审结果 ---
- prob, score = read_review_score(workspace, round_num)
- print(f"\n{'─'*60}")
- print(f" 📊 Round {round_num} 评审结果: score={score}/10, acceptance={prob:.0%}")
- if prob >= ACCEPTANCE_THRESHOLD:
- print(f" ✅ 达到目标阈值 ({ACCEPTANCE_THRESHOLD:.0%})! 停止迭代。")
- break
- elif round_num < MAX_ROUNDS:
- print(f" ⚠ 未达阈值 ({prob:.0%} < {ACCEPTANCE_THRESHOLD:.0%}),进入 Round {round_num + 1}...")
- else:
- print(f" ❌ 已达最大轮数 ({MAX_ROUNDS}),停止。最终: score={score}, acceptance={prob:.0%}")
- # 最终统计
- print(f"\n{'═'*60}")
- print(f" Pipeline complete. Workspace: {workspace}")
- print(f" Total rounds: {round_num}")
- print(f" Final score: {score}/10, acceptance: {prob:.0%}")
- # 各轮得分追踪
- print(f"\n 📈 Score progression:")
- for r in range(1, round_num + 1):
- rp, rs = read_review_score(workspace, r)
- marker = " ✅" if rp >= ACCEPTANCE_THRESHOLD else ""
- print(f" Round {r}: score={rs}/10, acceptance={rp:.0%}{marker}")
- # 文件统计
- total_files = sum(1 for _ in workspace.rglob("*") if _.is_file())
- total_size = sum(f.stat().st_size for f in workspace.rglob("*") if f.is_file())
- print(f"\n 📦 Total files: {total_files}")
- print(f" 💾 Total size: {total_size / 1024 / 1024:.1f} MB")
- print(f"{'═'*60}\n")
- # ═══════════════════════════════════════════════════════════
- # CLI Entry Point
- # ═══════════════════════════════════════════════════════════
- def main():
- parser = argparse.ArgumentParser(description="Research-67 AgentPaaS Launcher")
- sub = parser.add_subparsers(dest="command")
- # serve
- serve_p = sub.add_parser("serve", help="Start AgentPaaS server")
- serve_p.add_argument("--port", type=int, default=8000)
- # register
- sub.add_parser("register", help="Register all agents to PaaS")
- # run
- sub.add_parser("run", help="Execute research pipeline via API")
- # all
- all_p = sub.add_parser("all", help="Serve + register + run (one-shot)")
- all_p.add_argument("--port", type=int, default=8000)
- # claude-code
- cc_p = sub.add_parser("claude-code", help="Execute via Claude Code CLI (Max Plan)")
- cc_p.add_argument("--workspace", type=str, default=None,
- help="Resume from existing workspace (skip completed rounds)")
- cc_p.add_argument("--max-rounds", type=int, default=None,
- help=f"Override max rounds (default: {MAX_ROUNDS})")
- cc_p.add_argument("--max-turns", type=int, default=None,
- help=f"Override max turns per phase (default: {MAX_TURNS_PER_PHASE})")
- cc_p.add_argument("--phase-timeout", type=int, default=None,
- help=f"Override phase timeout in seconds (default: {PHASE_TIMEOUT})")
- cc_p.add_argument("--cooldown", type=int, default=None,
- help=f"Override inter-phase cooldown in seconds (default: {PHASE_COOLDOWN})")
- args = parser.parse_args()
- commands = {
- "serve": cmd_serve,
- "register": cmd_register,
- "run": cmd_run,
- "all": cmd_all,
- "claude-code": cmd_claude_code,
- }
- if args.command in commands:
- commands[args.command](args)
- else:
- parser.print_help()
- print("\nExamples:")
- print(" python launch_paas.py all --port 8000 # 一键启动全流程 (需要 ANTHROPIC_API_KEY)")
- print(" python launch_paas.py claude-code # 使用 Claude Code CLI (Max Plan, 无需 API key)")
- if __name__ == "__main__":
- main()
|