#!/usr/bin/env python3 """Aggregate Agent evaluation JSONL and compare paired variants.""" from __future__ import annotations import argparse import json import math import statistics from collections import defaultdict from pathlib import Path from typing import Any def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("input", help="JSONL result records.") parser.add_argument("--baseline", help="Baseline variant_id for paired comparison.") parser.add_argument("--variant", help="Candidate variant_id for paired comparison.") parser.add_argument("--format", choices=("json", "markdown"), default="json") parser.add_argument("--output", help="Optional output file.") return parser.parse_args() def load_records(path: Path) -> list[dict[str, Any]]: records = [] for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): if not raw_line.strip(): continue value = json.loads(raw_line) if not isinstance(value, dict): raise ValueError(f"line {line_number}: record must be an object") for field in ("task_id", "variant_id", "model_id", "run_id", "status"): if not str(value.get(field, "")).strip(): raise ValueError(f"line {line_number}: missing {field}") records.append(value) if not records: raise ValueError("input contains no result records") return records def metric(record: dict[str, Any], name: str) -> float: value = (record.get("metrics") or {}).get(name, 0) return float(value or 0) def is_infrastructure_error(record: dict[str, Any]) -> bool: return record.get("status") in {"provider_error", "infrastructure_error"} def is_success(record: dict[str, Any]) -> bool: return ( not is_infrastructure_error(record) and record.get("status") == "completed" and bool((record.get("validator") or {}).get("passed", False)) ) def percentile(values: list[float], fraction: float) -> float | None: if not values: return None ordered = sorted(values) index = (len(ordered) - 1) * fraction lower = int(index) upper = min(lower + 1, len(ordered) - 1) weight = index - lower return round(ordered[lower] * (1 - weight) + ordered[upper] * weight, 3) def rate(numerator: int, denominator: int) -> float | None: return round(numerator / denominator, 4) if denominator else None def wilson_interval(successes: int, total: int, z: float = 1.959963984540054) -> list[float] | None: if total == 0: return None proportion = successes / total z_squared = z * z denominator = 1 + z_squared / total center = (proportion + z_squared / (2 * total)) / denominator margin = ( z * math.sqrt( proportion * (1 - proportion) / total + z_squared / (4 * total * total) ) / denominator ) return [round(max(0.0, center - margin), 4), round(min(1.0, center + margin), 4)] def mcnemar_exact_p(gains: int, regressions: int) -> float: discordant = gains + regressions if discordant == 0: return 1.0 tail = sum(math.comb(discordant, value) for value in range(min(gains, regressions) + 1)) return round(min(1.0, 2 * tail / (2**discordant)), 6) def mean(values: list[float]) -> float | None: return round(statistics.fmean(values), 3) if values else None def aggregate(records: list[dict[str, Any]]) -> dict[str, Any]: eligible = [record for record in records if not is_infrastructure_error(record)] successes = [record for record in eligible if is_success(record)] false_completions = [ record for record in eligible if bool(record.get("agent_claimed_complete", False)) and not is_success(record) ] safety_violations = sum(len(record.get("safety_violations") or []) for record in eligible) failed_tools = sum(int(metric(record, "failed_tool_calls")) for record in eligible) recovered_failures = sum(int(metric(record, "recovered_failures")) for record in eligible) unresolved_failures = sum(int(metric(record, "unresolved_tool_failures")) for record in eligible) elapsed = [metric(record, "elapsed_seconds") for record in eligible] return { "records": len(records), "eligible_records": len(eligible), "infrastructure_errors": len(records) - len(eligible), "successes": len(successes), "success_rate": rate(len(successes), len(eligible)), "success_rate_ci95": wilson_interval(len(successes), len(eligible)), "false_completions": len(false_completions), "false_completion_rate": rate(len(false_completions), len(eligible)), "false_completion_rate_ci95": wilson_interval(len(false_completions), len(eligible)), "safety_violations": safety_violations, "failed_tool_calls": failed_tools, "recovered_failures": recovered_failures, "failure_recovery_rate": rate(recovered_failures, failed_tools), "unresolved_tool_failures": unresolved_failures, "elapsed_seconds_p50": percentile(elapsed, 0.50), "elapsed_seconds_p95": percentile(elapsed, 0.95), "mean_total_tokens": mean([metric(record, "total_tokens") for record in eligible]), "mean_tool_calls": mean([metric(record, "tool_calls") for record in eligible]), } def pair_key(record: dict[str, Any]) -> tuple[str, str, str, int]: return ( str(record["task_id"]), str(record.get("task_version") or "1"), str(record["model_id"]), int(record.get("repetition") or 0), ) def records_by_pair( records: list[dict[str, Any]], variant_id: str, ) -> dict[tuple[str, str, str, int], dict[str, Any]]: indexed: dict[tuple[str, str, str, int], dict[str, Any]] = {} for record in records: if record["variant_id"] != variant_id or is_infrastructure_error(record): continue key = pair_key(record) if key in indexed: raise ValueError(f"duplicate paired record for {variant_id}: {key}") indexed[key] = record return indexed def compare( records: list[dict[str, Any]], baseline_id: str, variant_id: str, ) -> dict[str, Any]: baseline = records_by_pair(records, baseline_id) variant = records_by_pair(records, variant_id) keys = sorted(set(baseline) & set(variant)) counts = {"both_pass": 0, "gain": 0, "regression": 0, "both_fail": 0} for key in keys: baseline_pass = is_success(baseline[key]) variant_pass = is_success(variant[key]) if baseline_pass and variant_pass: counts["both_pass"] += 1 elif not baseline_pass and variant_pass: counts["gain"] += 1 elif baseline_pass and not variant_pass: counts["regression"] += 1 else: counts["both_fail"] += 1 baseline_summary = aggregate([baseline[key] for key in keys]) variant_summary = aggregate([variant[key] for key in keys]) blockers = [] if variant_summary["safety_violations"] > baseline_summary["safety_violations"]: blockers.append("安全违规增加") if variant_summary["false_completions"] > baseline_summary["false_completions"]: blockers.append("假完成增加") recommendation = "continue" if blockers: recommendation = "reject" elif counts["gain"] > counts["regression"]: recommendation = "promotion-candidate" elif ( counts["gain"] == counts["regression"] and variant_summary["success_rate"] == baseline_summary["success_rate"] and variant_summary["elapsed_seconds_p50"] is not None and baseline_summary["elapsed_seconds_p50"] is not None and variant_summary["elapsed_seconds_p50"] <= baseline_summary["elapsed_seconds_p50"] * 0.85 ): recommendation = "efficiency-candidate" return { "baseline": baseline_id, "variant": variant_id, "paired_records": len(keys), "unpaired_baseline_records": len(set(baseline) - set(variant)), "unpaired_variant_records": len(set(variant) - set(baseline)), **counts, "discordant_pairs": counts["gain"] + counts["regression"], "net_gain": counts["gain"] - counts["regression"], "mcnemar_exact_p_two_sided": mcnemar_exact_p(counts["gain"], counts["regression"]), "baseline_summary": baseline_summary, "variant_summary": variant_summary, "blockers": blockers, "advisory_recommendation": recommendation, } def render_markdown(report: dict[str, Any]) -> str: lines = ["# Agent 评估汇总", ""] lines.append("| 变体 | 可判分运行 | 成功率 [95% CI] | 假完成率 [95% CI] | 安全违规 | p50 秒 | 平均 Token |") lines.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: |") for variant, summary in sorted(report["variants"].items()): lines.append( f"| {variant} | {summary['eligible_records']} | " f"{summary['success_rate']} {summary['success_rate_ci95']} | " f"{summary['false_completion_rate']} {summary['false_completion_rate_ci95']} | " f"{summary['safety_violations']} | {summary['elapsed_seconds_p50']} | " f"{summary['mean_total_tokens']} |" ) comparison = report.get("comparison") if comparison: lines.extend( [ "", "## 配对对比", "", f"- 基线:`{comparison['baseline']}`", f"- 候选变体:`{comparison['variant']}`", f"- 配对运行数:{comparison['paired_records']}", f"- 未配对基线运行:{comparison['unpaired_baseline_records']}", f"- 未配对候选运行:{comparison['unpaired_variant_records']}", f"- 新增成功:{comparison['gain']}", f"- 回归失败:{comparison['regression']}", f"- 双方成功:{comparison['both_pass']}", f"- 双方失败:{comparison['both_fail']}", f"- 净增益:{comparison['net_gain']}", f"- 配对差异精确检验 p 值:{comparison['mcnemar_exact_p_two_sided']}", f"- 机械建议:`{comparison['advisory_recommendation']}`", ] ) if comparison["blockers"]: lines.append(f"- 阻断项:{', '.join(comparison['blockers'])}") return "\n".join(lines) + "\n" def main() -> int: args = parse_args() if bool(args.baseline) != bool(args.variant): raise SystemExit("--baseline and --variant must be provided together") records = load_records(Path(args.input)) grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) for record in records: grouped[str(record["variant_id"])].append(record) report: dict[str, Any] = { "input_records": len(records), "variants": {variant: aggregate(items) for variant, items in sorted(grouped.items())}, } if args.baseline and args.variant: report["comparison"] = compare(records, args.baseline, args.variant) output = ( json.dumps(report, ensure_ascii=False, indent=2) + "\n" if args.format == "json" else render_markdown(report) ) if args.output: Path(args.output).write_text(output, encoding="utf-8") else: print(output, end="") return 0 if __name__ == "__main__": raise SystemExit(main())