#!/usr/bin/env python3 from __future__ import annotations import argparse import csv import gzip import json from pathlib import Path from typing import Iterable DEFAULT_BENCH_ROOT = Path('/225040511/project/bioagent-bench') TRUTH_FILES = { 'alzheimer-mouse': 'pathway_comparison.csv', 'comparative-genomics': 'cluster_annotation_mapping.csv', 'cystic-fibrosis': 'cf_variants.csv', 'deseq': 'up_regulated_genes.csv', 'evolution': 'variants_shared.csv', 'giab': 'HG001_GRCh38_1_22_v4.2.1_benchmark.vcf.gz', 'metagenomics': 'phylum_relative_abundances.csv', 'single-cell': 'all_clusters_de_genes.csv', 'transcript-quant': 'truth.tsv', 'viral-metagenomics': 'taxonomy.csv', } ANSWER_FILES = { 'giab': 'answer.vcf.gz', 'transcript-quant': 'answer.tsv', } FIELDNAMES = [ 'Tasks', 'results_match', 'Selected Tools', 'Overhead/planning占整个流', 'Gold Items', 'Context Tokens', 'Planning Latency', 'Selection Rate', ] def main() -> None: parser = argparse.ArgumentParser(description='Summarize Biomni-ReAct BioAgentBench metrics.') parser.add_argument('--run-root', required=True, type=Path, help='Run root produced by run_bioagent_bench_deepseek.sh') parser.add_argument('--bench-root', type=Path, default=DEFAULT_BENCH_ROOT) parser.add_argument('--output', type=Path, help='CSV output path. Default: RUN_ROOT/bioagent_bench_metrics.csv') args = parser.parse_args() rows = list(summarize(args.run_root, args.bench_root)) output = args.output or args.run_root / 'bioagent_bench_metrics.csv' output.parent.mkdir(parents=True, exist_ok=True) with output.open('w', newline='', encoding='utf-8') as handle: writer = csv.DictWriter(handle, fieldnames=FIELDNAMES) writer.writeheader() writer.writerows(rows) print(f'Wrote {len(rows)} rows to {output}') def summarize(run_root: Path, bench_root: Path) -> Iterable[dict[str, object]]: for summary_path in sorted(run_root.glob('*/run_summary.json')): task_id = summary_path.parent.name summary = json.loads(summary_path.read_text(encoding='utf-8')) metrics = summary.get('metrics') or {} gold_path = truth_path(bench_root, task_id) answer_path = answer_path_for(summary_path.parent, task_id) gold_items = count_items(gold_path) if gold_path else 0 selected = int(metrics.get('selected_tools') or len(summary.get('selected_resources') or [])) available = int(metrics.get('available_tools') or 0) selection_rate = selected / available if available else '' yield { 'Tasks': task_id, 'results_match': compare_outputs(answer_path, gold_path) if gold_path else False, 'Selected Tools': selected, 'Overhead/planning占整个流': round_float(metrics.get('overhead_planning_ratio')), 'Gold Items': gold_items, 'Context Tokens': int(metrics.get('context_tokens') or 0), 'Planning Latency': round_float(metrics.get('planning_latency_s')), 'Selection Rate': round_float(selection_rate), } def truth_path(bench_root: Path, task_id: str) -> Path | None: filename = TRUTH_FILES.get(task_id) if filename: path = bench_root / 'dataset' / task_id / 'results' / filename if path.exists(): return path results_dir = bench_root / 'dataset' / task_id / 'results' candidates = sorted(p for p in results_dir.glob('*') if p.is_file()) return candidates[0] if candidates else None def answer_path_for(workspace: Path, task_id: str) -> Path | None: preferred = workspace / ANSWER_FILES.get(task_id, 'answer.csv') if preferred.exists(): return preferred candidates = [] for pattern in ('answer.*', '*.csv', '*.tsv', '*.vcf', '*.vcf.gz'): candidates.extend(workspace.glob(pattern)) candidates = [path for path in candidates if path.name not in {'retrieval_plan.json', 'run_summary.json'}] return sorted(set(candidates))[0] if candidates else None def count_items(path: Path) -> int: if path.suffix == '.gz': return sum(1 for line in open_text(path) if line.strip() and not line.startswith('#')) if path.suffix.lower() in {'.csv', '.tsv'}: rows = read_table(path) return len(rows) return sum(1 for line in open_text(path) if line.strip() and not line.startswith('#')) def compare_outputs(answer: Path | None, truth: Path | None) -> bool: if not answer or not truth or not answer.exists() or answer.stat().st_size == 0: return False if answer.suffix.lower() in {'.csv', '.tsv'} and truth.suffix.lower() in {'.csv', '.tsv'}: return normalize_table_values(answer) == normalize_table_values(truth) if answer.name.endswith('.vcf.gz') and truth.name.endswith('.vcf.gz'): return normalize_lines(answer) == normalize_lines(truth) return normalize_text(answer) == normalize_text(truth) def read_table(path: Path) -> list[dict[str, str]]: sample = ''.join(list(open_text(path))[:5]) delimiter = '\t' if path.suffix.lower() == '.tsv' or sample.count('\t') > sample.count(',') else ',' with open_plain_text(path) as handle: reader = csv.DictReader(handle, delimiter=delimiter) return [{clean(key): clean(value) for key, value in row.items()} for row in reader] def normalize_table_values(path: Path) -> list[tuple[str, ...]]: sample = ''.join(list(open_text(path))[:5]) delimiter = '\t' if path.suffix.lower() == '.tsv' or sample.count('\t') > sample.count(',') else ',' rows: list[tuple[str, ...]] = [] with open_plain_text(path) as handle: for raw in handle: if not raw.strip(): continue parts = tuple(clean(part) for part in raw.rstrip('\n').split(delimiter)) if not rows and looks_like_header(parts): continue rows.append(parts) return sorted(rows) def looks_like_header(parts: tuple[str, ...]) -> bool: lowered = {part.lower() for part in parts} known_headers = { 'transcript_id', 'count', 'pathway', 'cluster_number', 'consensus_annotation', 'chromosome', 'position', 'gene_id', 'log2foldchange', 'pvalue', 'padj', 'otu', 'kingdom', 'phylum', 'cluster_id', 'predicted_cell_type', 'gene_name', 'contig_count', 'domain', 'species', } return bool(lowered & known_headers) def normalize_rows(rows: list[dict[str, str]]) -> list[tuple[tuple[str, str], ...]]: return sorted(tuple(sorted((clean(k), clean(v)) for k, v in row.items())) for row in rows) def normalize_lines(path: Path) -> list[str]: return sorted(clean(line) for line in open_text(path) if line.strip() and not line.startswith('#')) def normalize_text(path: Path) -> str: return '\n'.join(normalize_lines(path)) def open_text(path: Path) -> Iterable[str]: with open_plain_text(path) as handle: yield from handle def open_plain_text(path: Path): if path.name.endswith('.gz'): return gzip.open(path, 'rt', encoding='utf-8', errors='replace') return path.open('r', encoding='utf-8', errors='replace', newline='') def clean(value: object) -> str: if value is None: return '' return ' '.join(str(value).strip().split()) def round_float(value: object) -> object: if value == '': return '' try: return round(float(value), 6) except (TypeError, ValueError): return '' if __name__ == '__main__': main()