biodsbench-adapter / examples /run_imaging101_true_serial.py
starpacker52's picture
Add files using upload-large-folder tool
4c0ecfa verified
Raw
History Blame Contribute Delete
22 kB
#!/usr/bin/env python3
"""
BioDSBench-imaging101-format 真正串行测评脚本
与旧版 run_imaging101_*_serial.py 的关键区别:
1. 真正传递 context:每个子任务通过 --prior-context prior_context.json
把前面已完成子任务的 description、generated_code、judge_feedback 传给模型
2. 解决"发现 3"(外层 retry 丢 feedback):用 --max-rounds 2,让 CLI 内部承接
judge feedback(同一 LLM session),外层不再 retry
3. 每个子任务用独立 outputs 目录(避免共享 outputs 触发 judge.py 的 monkey-patch
副作用差异)
适用场景:
- 同一 PMID 的多个子任务(如 25303977_0 ~ 25303977_7)
- 任务间有共同的数据格式、列名、分析模式,希望模型复用经验而非每次从头推
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
class TrueSerialEvaluator:
def __init__(self,
study_id: str = "25303977",
start_idx: int = 0,
end_idx: int = 7,
tasks_dir: str = "/home/yjh/BioDSBench-imaging101-format/tasks",
results_dir: str = "/data/yjh/imaging101_true_serial_results",
max_rounds: int = 2,
timeout_seconds: int = 1800):
"""
初始化真正串行测评器
Args:
study_id: 母任务ID
start_idx: 起始子任务索引
end_idx: 结束子任务索引(包含)
tasks_dir: 任务目录
results_dir: 结果目录
max_rounds: 每个子任务的 judge 轮次(CLI 内部承接 feedback)
timeout_seconds: 单次 CLI 执行超时(秒)
"""
self.study_id = study_id
self.start_idx = start_idx
self.end_idx = end_idx
self.tasks_dir = Path(tasks_dir)
self.results_dir = Path(results_dir)
self.max_rounds = max_rounds
self.timeout_seconds = timeout_seconds
# 创建运行目录
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.run_dir = self.results_dir / f"{study_id}_true_serial_{timestamp}"
self.run_dir.mkdir(parents=True, exist_ok=True)
# my_claude CLI 工作目录
self.my_claude_dir = Path("/home/yjh/my_claude")
# 测评状态(会被每个子任务的结果填充)
self.state = {
"study_id": study_id,
"start_idx": start_idx,
"end_idx": end_idx,
"model": "claude-4.7-opus",
"mode": "true_serial_with_prior_context",
"max_rounds": max_rounds,
"status": "not_started",
"completed_tasks": 0,
"passed_tasks": 0,
"failed_tasks": 0,
"tasks": [],
"start_time": datetime.now().isoformat(),
"end_time": None
}
self._save_state()
def _save_state(self):
"""保存测评状态"""
state_file = self.run_dir / "evaluation_state.json"
with open(state_file, "w") as f:
json.dump(self.state, f, indent=2, ensure_ascii=False)
def run(self) -> Dict:
"""
运行真正串行测评
Returns:
测评结果字典
"""
print(f"\n{'='*80}")
print(f"BioDSBench-Imaging101-Format 真正串行测评 (True Serial with Prior Context)")
print(f"{'='*80}")
print(f"母任务: {self.study_id}")
print(f"子任务范围: {self.start_idx} ~ {self.end_idx}")
print(f"模型: claude-4.7-opus")
print(f"运行目录: {self.run_dir}")
print(f"每子任务 judge 轮次: {self.max_rounds} (CLI 内部承接 feedback)")
print(f"上下文传递: 通过 --prior-context prior_context.json")
print(f"{'='*80}\n")
self.state["status"] = "running"
self._save_state()
# 串行执行每个子任务
for task_idx in range(self.start_idx, self.end_idx + 1):
task_id = f"{self.study_id}_{task_idx}"
print(f"\n{'='*80}")
print(f"子任务 [{task_idx - self.start_idx + 1}/{self.end_idx - self.start_idx + 1}]: {task_id}")
print(f"{'='*80}")
# 执行子任务(只执行一次,失败也进入下一个)
result = self._execute_task(task_id, task_idx)
# 记录结果
self.state["tasks"].append(result)
self.state["completed_tasks"] += 1
if result["status"] == "passed":
self.state["passed_tasks"] += 1
print(f"✅ {task_id} 通过")
else:
self.state["failed_tasks"] += 1
print(f"❌ {task_id} 失败 (仍继续执行后续任务,失败信息会传给下个任务)")
self._save_state()
# 完成
self.state["end_time"] = datetime.now().isoformat()
if self.state["passed_tasks"] == (self.end_idx - self.start_idx + 1):
self.state["status"] = "all_passed"
elif self.state["passed_tasks"] > 0:
self.state["status"] = "partial_passed"
else:
self.state["status"] = "all_failed"
self._save_state()
# 打印总结
total_tasks = self.end_idx - self.start_idx + 1
print(f"\n{'='*80}")
print(f"真正串行测评完成!")
print(f"{'='*80}")
print(f"通过: {self.state['passed_tasks']}/{total_tasks}")
print(f"失败: {self.state['failed_tasks']}/{total_tasks}")
print(f"成功率: {self.state['passed_tasks']/total_tasks*100:.1f}%")
print(f"结果目录: {self.run_dir}")
print(f"{'='*80}\n")
return self.state
def _execute_task(self, task_id: str, task_idx: int) -> Dict:
"""
执行单个子任务
关键差异:
- 只执行一次(不 retry)
- 失败也返回结果(会被记入 prior_context 供下个任务参考)
- 用 --max-rounds 2,让 CLI 内部承接 judge feedback
Args:
task_id: 子任务ID
task_idx: 子任务索引
Returns:
子任务执行结果
"""
task_dir = self.run_dir / f"task_{task_idx}"
task_dir.mkdir(exist_ok=True)
# 每个子任务有独立 outputs 目录(避免共享触发 judge.py 副作用)
outputs_dir = task_dir / "outputs"
outputs_dir.mkdir(exist_ok=True)
result = {
"task_id": task_id,
"task_idx": task_idx,
"status": "failed",
"start_time": datetime.now().isoformat(),
"end_time": None,
"cli_status": None,
"judge_status": None,
"reward": 0,
"cli_run_dir": None,
"error": None,
"judge_feedback": None,
}
try:
# 1. 构建 prior_context.json
print(f" 1. 构建 prior_context.json...")
prior_context = self._build_prior_context(task_idx)
prior_context_file = task_dir / "prior_context.json"
with open(prior_context_file, "w") as f:
json.dump(prior_context, f, indent=2, ensure_ascii=False)
print(f" - 前置子任务数: {len(prior_context.get('priorSubtasks', []))}")
# 2. 调用 CLI(只执行一次,用 --max-rounds 2 让 CLI 内部重试)
print(f" 2. 调用 CLI (--max-rounds {self.max_rounds})...")
cli_result = self._run_cli(task_id, task_dir, prior_context_file, outputs_dir)
result["cli_status"] = cli_result.get("status")
result["judge_status"] = cli_result.get("judge_status")
result["reward"] = cli_result.get("reward", 0)
result["cli_run_dir"] = cli_result.get("run_dir")
result["judge_feedback"] = cli_result.get("judge_feedback")
if cli_result.get("success"):
result["status"] = "passed"
print(f" ✅ 通过! (judge={cli_result.get('judge_status')}, reward={cli_result.get('reward')})")
# 保存生成的代码
self._save_generated_code(task_id, task_idx, task_dir, cli_result.get("run_dir"))
else:
result["error"] = cli_result.get("error", "CLI execution failed")
result["status"] = "failed"
print(f" ❌ 失败: {result['error'][:200]}")
# 即使失败,也尝试保存代码片段(下个任务能看到错误模式)
self._save_generated_code(task_id, task_idx, task_dir, cli_result.get("run_dir"))
except Exception as e:
result["error"] = str(e)
result["status"] = "failed"
print(f" ❌ 执行出错: {e}")
result["end_time"] = datetime.now().isoformat()
return result
def _build_prior_context(self, current_idx: int) -> Dict:
"""
构建前置子任务上下文(传给 CLI 的 --prior-context 文件)
Args:
current_idx: 当前子任务索引
Returns:
包含 priorSubtasks 数组的字典
"""
prior_subtasks = []
# 收集前面已完成的子任务
if current_idx > self.start_idx:
for prev_idx in range(self.start_idx, current_idx):
prev_task_id = f"{self.study_id}_{prev_idx}"
# 从 state["tasks"] 找对应结果
prev_result = None
for task in self.state["tasks"]:
if task["task_idx"] == prev_idx:
prev_result = task
break
if not prev_result:
continue
# 组装成 PriorSubtaskContext 格式
prior_info = {
"taskId": prev_task_id,
"taskIdx": prev_idx,
"status": prev_result["status"],
"passed": prev_result["status"] == "passed",
"description": self._read_task_description(prev_task_id),
"generatedCode": self._read_generated_code(prev_idx),
"judgeFeedback": prev_result.get("judge_feedback"),
"notes": self._infer_notes(prev_result),
}
prior_subtasks.append(prior_info)
return {"priorSubtasks": prior_subtasks}
def _run_cli(self, task_id: str, task_dir: Path, prior_context_file: Path, outputs_dir: Path) -> Dict:
"""
调用 CLI 执行任务
关键参数:
- --prior-context: 传递前置子任务上下文
- --max-rounds 2: CLI 内部承接 judge feedback(解决"发现 3")
- 独立 outputs_dir: 避免共享 outputs 触发 judge.py 副作用
Args:
task_id: 任务ID
task_dir: 当前任务目录
prior_context_file: prior_context.json 路径
outputs_dir: 此任务的独立 outputs 目录
Returns:
CLI 执行结果
"""
# 环境变量(不再设置 BIODSBENCH_OUTPUTS_DIR,避免共享副作用)
env = {
**subprocess.os.environ.copy(),
"ANTHROPIC_API_KEY": os.environ.get("LLM_API_KEY", ""),
"ANTHROPIC_BASE_URL": "https://api.gpugeek.com",
"ANTHROPIC_MODEL": "Vendor2/Claude-4.7-opus",
"ANTHROPIC_SMALL_FAST_MODEL": "Vendor2/Claude-4.7-opus",
"MODEL_NAME": "Vendor2/Claude-4.7-opus",
"BASE_URL": "https://api.gpugeek.com",
"AGENT_LOG_DIR": str(self.run_dir / "agent_logs")
}
# CLI 命令
cmd = [
"/home/yjh/.bun/bin/bun",
"src/harness/evaluation/cli.ts",
"--task", task_id,
"--tasks-dir", str(self.tasks_dir.absolute()),
"--runs-dir", str(task_dir.absolute()),
"--max-rounds", str(self.max_rounds), # CLI 内部承接 feedback
"--timeout-seconds", str(self.timeout_seconds),
"--temperature", "1",
"--thinking", "disabled",
"--agent-runtime", "source",
"--prior-context", str(prior_context_file.absolute()), # 真正串行的关键!
]
try:
print(f" 执行命令: bun cli.ts --task {task_id} --prior-context {prior_context_file.name} ...")
result = subprocess.run(
cmd,
cwd=str(self.my_claude_dir),
env=env,
capture_output=True,
text=True,
timeout=self.timeout_seconds + 120
)
# 保存 CLI 输出
log_file = task_dir / "cli_output.log"
with open(log_file, "w") as f:
f.write(f"Command: {' '.join(cmd)}\n")
f.write(f"Exit code: {result.returncode}\n\n")
f.write("=== STDOUT ===\n")
f.write(result.stdout)
f.write("\n\n=== STDERR ===\n")
f.write(result.stderr)
# 解析 stdout JSON
cli_result = {
"exit_code": result.returncode,
"status": "unknown",
"reward": 0,
"judge_status": "unknown",
"run_dir": None,
"stdout": result.stdout,
"stderr": result.stderr,
"judge_feedback": None,
}
try:
stdout_data = json.loads(result.stdout.strip())
cli_result["status"] = stdout_data.get("status", "unknown")
cli_result["reward"] = stdout_data.get("reward", 0)
cli_result["judge_status"] = stdout_data.get("last_judge_status", "unknown")
cli_result["run_dir"] = stdout_data.get("run_dir")
# 尝试提取 judge feedback(从 trajectory 或 stderr)
cli_result["judge_feedback"] = self._extract_judge_feedback(stdout_data, result.stderr)
except (json.JSONDecodeError, ValueError) as e:
print(f" ⚠️ 无法解析 CLI stdout 为 JSON: {e}")
# 成功条件
cli_result["success"] = (
result.returncode == 0
and cli_result["status"] == "success"
and cli_result["reward"] >= 1
and cli_result["judge_status"] == "pass"
)
if not cli_result["success"]:
error_parts = []
if result.returncode != 0:
error_parts.append(f"exit_code={result.returncode}")
if cli_result["status"] != "success":
error_parts.append(f"status={cli_result['status']}")
if cli_result["judge_status"] != "pass":
error_parts.append(f"judge={cli_result['judge_status']}")
if cli_result["reward"] < 1:
error_parts.append(f"reward={cli_result['reward']}")
cli_result["error"] = "; ".join(error_parts) if error_parts else "Unknown failure"
return cli_result
except subprocess.TimeoutExpired:
return {
"success": False,
"error": f"Timeout after {self.timeout_seconds + 120} seconds",
"exit_code": -1,
"status": "timeout",
"reward": 0,
"judge_status": "timeout",
"judge_feedback": None,
}
except Exception as e:
return {
"success": False,
"error": str(e),
"exit_code": -1,
"status": "error",
"reward": 0,
"judge_status": "error",
"judge_feedback": None,
}
def _extract_judge_feedback(self, stdout_data: Dict, stderr: str) -> Optional[str]:
"""
从 CLI 输出中提取 judge feedback(供下个任务参考)
Args:
stdout_data: 解析后的 stdout JSON
stderr: stderr 文本
Returns:
judge feedback 字符串,或 None
"""
# 尝试从 stdout_data 的 trajectory_path 读取(如果可用)
# 或者简单返回 last_judge_status
judge_status = stdout_data.get("last_judge_status", "unknown")
reward = stdout_data.get("reward", 0)
return f"judge_status={judge_status}, reward={reward}"
def _save_generated_code(self, task_id: str, task_idx: int, task_dir: Path, cli_run_dir: Optional[str]):
"""
保存生成的代码(供下个任务参考)
Args:
task_id: 任务ID
task_idx: 任务索引
task_dir: 任务目录
cli_run_dir: CLI 输出的 run_dir 路径
"""
if not cli_run_dir:
print(f" ⚠️ 未找到 CLI run_dir,跳过代码保存")
return
cli_run_path = Path(cli_run_dir)
if not cli_run_path.exists():
print(f" ⚠️ CLI run_dir 不存在: {cli_run_path}")
return
# 查找 outputs/case_*.py
outputs_dir = cli_run_path / "outputs"
if outputs_dir.exists():
case_files = sorted(outputs_dir.glob("case_*.py"))
if case_files:
combined_code = ""
for case_file in case_files:
with open(case_file) as f:
combined_code += f"# === {case_file.name} ===\n"
combined_code += f.read()
combined_code += "\n\n"
target_file = task_dir / "generated_code.py"
with open(target_file, "w") as f:
f.write(combined_code)
print(f" 💾 保存生成的代码: {target_file} ({len(case_files)} 个 case)")
return
print(f" ⚠️ 未在 CLI run 目录找到 case_*.py")
def _read_task_description(self, task_id: str) -> Optional[str]:
"""读取任务描述(README.md)"""
readme_path = self.tasks_dir / task_id / "README.md"
if readme_path.exists():
try:
with open(readme_path) as f:
content = f.read()
# 截断过长的 README(保留前 2000 字符)
if len(content) > 2000:
return content[:2000] + "\n... [truncated]"
return content
except Exception:
pass
return None
def _read_generated_code(self, task_idx: int) -> Optional[str]:
"""读取已生成的代码"""
code_file = self.run_dir / f"task_{task_idx}" / "generated_code.py"
if code_file.exists():
try:
with open(code_file) as f:
content = f.read()
# 截断过长的代码(保留前 8000 字符)
if len(content) > 8000:
return content[:8000] + "\n... [code truncated]"
return content
except Exception:
pass
return None
def _infer_notes(self, task_result: Dict) -> Optional[str]:
"""
从任务结果中推断 notes(供下个任务参考)
Args:
task_result: 任务结果字典
Returns:
notes 字符串,或 None
"""
notes_parts = []
if task_result.get("status") == "passed":
notes_parts.append("✅ This sub-task passed judge.")
else:
notes_parts.append("❌ This sub-task failed judge.")
if task_result.get("judge_status"):
notes_parts.append(f"Judge status: {task_result['judge_status']}")
if task_result.get("reward") is not None:
notes_parts.append(f"Reward: {task_result['reward']}")
return " | ".join(notes_parts) if notes_parts else None
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(
description="BioDSBench-imaging101 真正串行测评(传递 prior context,CLI 内部承接 feedback)"
)
parser.add_argument("--study-id", default="25303977", help="母任务ID")
parser.add_argument("--start", type=int, default=0, help="起始子任务索引")
parser.add_argument("--end", type=int, default=7, help="结束子任务索引")
parser.add_argument("--max-rounds", type=int, default=2, help="每个任务的 judge 轮次(CLI 内部)")
parser.add_argument("--timeout", type=int, default=1800, help="单次 CLI 执行超时(秒)")
args = parser.parse_args()
# 创建并运行测评器
evaluator = TrueSerialEvaluator(
study_id=args.study_id,
start_idx=args.start,
end_idx=args.end,
max_rounds=args.max_rounds,
timeout_seconds=args.timeout
)
result = evaluator.run()
# 打印最终结果
print(f"\n完整结果已保存到: {evaluator.run_dir / 'evaluation_state.json'}")
# 返回退出码
if result["status"] == "all_passed":
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()