EviScene / scripts /summarize_agent_test_evaluation.py
ZiAnwang's picture
Initial EviScene release
c10636a
Raw History Blame Contribute Delete
10.4 kB
#!/usr/bin/env python3
"""Summarize one or more evaluator-only Agent private-gold audits."""
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter, defaultdict
from collections.abc import Mapping
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from eviscene.eval.private_gold_metrics import (
agent_private_gold_category,
agent_private_gold_category_counts,
)
SCHEMA_VERSION = "ifv-agent-test-evaluation-summary-v1"
VALID_VERDICTS = frozenset({"real", "fake"})
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
with path.open(encoding="utf-8") as handle:
for line_number, line in enumerate(handle, start=1):
if not line.strip():
continue
row = json.loads(line)
if not isinstance(row, dict):
raise ValueError(f"{path}:{line_number} must be an object")
rows.append(row)
return rows
def _write_json(path: Path, payload: Mapping[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(dict(payload), ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def _write_jsonl(path: Path, rows: list[Mapping[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
"".join(json.dumps(dict(row), ensure_ascii=False) + "\n" for row in rows),
encoding="utf-8",
)
def _case_id(row: Mapping[str, Any]) -> str:
value = str(
row.get("unified_case_id")
or row.get("archive_source_version_id")
or row.get("case_id")
or ""
).strip()
if not value:
raise ValueError("row lacks a stable case ID")
return value
def _parse_input(value: str) -> tuple[str, Path]:
name, separator, rendered_path = value.partition("=")
if not separator or not name.strip() or not rendered_path.strip():
raise argparse.ArgumentTypeError(
"--input must be NAME=PATH, for example old100=/tmp/audit-results.jsonl"
)
return name.strip(), Path(rendered_path.strip()).expanduser().resolve()
def _metric_payload(rows: list[Mapping[str, Any]]) -> dict[str, Any]:
completed = [
row
for row in rows
if row.get("status") == "completed"
and str(row.get("gold_verdict") or "").lower() in VALID_VERDICTS
and str(row.get("candidate_verdict") or "").lower() in VALID_VERDICTS
]
matrix = {
expected: {predicted: 0 for predicted in sorted(VALID_VERDICTS)}
for expected in sorted(VALID_VERDICTS)
}
for row in completed:
expected = str(row["gold_verdict"]).lower()
predicted = str(row["candidate_verdict"]).lower()
matrix[expected][predicted] += 1
total = sum(sum(predictions.values()) for predictions in matrix.values())
correct = sum(matrix[label][label] for label in matrix)
recall = {
label: (
matrix[label][label] / sum(matrix[label].values())
if sum(matrix[label].values())
else None
)
for label in matrix
}
available_recalls = [value for value in recall.values() if value is not None]
return {
"evaluated": total,
"correct": correct,
"accuracy": correct / total if total else None,
"balanced_accuracy": (
sum(available_recalls) / len(available_recalls)
if available_recalls
else None
),
"recall_by_gold_verdict": recall,
"confusion_gold_rows_candidate_columns": matrix,
}
def _route_breakdown(rows: list[Mapping[str, Any]]) -> dict[str, dict[str, Any]]:
groups: dict[str, list[Mapping[str, Any]]] = defaultdict(list)
for row in rows:
groups[str(row.get("construction_subroute") or "unknown")].append(row)
output: dict[str, dict[str, Any]] = {}
for route, route_rows in sorted(groups.items()):
metrics = _metric_payload(route_rows)
categories = agent_private_gold_category_counts(route_rows)
output[route] = {
"selected": len(route_rows),
"completed": sum(row.get("status") == "completed" for row in route_rows),
"engineering_or_judge_errors": sum(
row.get("status") != "completed" for row in route_rows
),
**metrics,
"three_way_private_gold_categories": categories,
}
return output
def summarize_agent_test_evaluation(
*,
inputs: list[tuple[str, Path]],
test_manifest: Path,
output_dir: Path,
expected_case_count: int | None = None,
) -> dict[str, Any]:
"""Join audit records with test metadata and calculate reported metrics."""
test_manifest = test_manifest.expanduser().resolve()
output_dir = output_dir.expanduser().resolve()
manifest_rows = _read_jsonl(test_manifest)
manifest_by_case: dict[str, dict[str, Any]] = {}
for row in manifest_rows:
case_id = _case_id(row)
if case_id in manifest_by_case:
raise ValueError(f"duplicate case ID in test manifest: {case_id}")
manifest_by_case[case_id] = row
merged: list[dict[str, Any]] = []
seen: set[str] = set()
source_counts: dict[str, int] = {}
for source_name, path in inputs:
if source_name in source_counts:
raise ValueError(f"duplicate input name: {source_name}")
source_rows = _read_jsonl(path)
source_counts[source_name] = len(source_rows)
for row in source_rows:
case_id = _case_id(row)
if case_id in seen:
raise ValueError(f"case appears in multiple audit inputs: {case_id}")
seen.add(case_id)
manifest_row = manifest_by_case.get(case_id)
if manifest_row is None:
raise ValueError(f"audit case is absent from test manifest: {case_id}")
combined = dict(row)
combined["case_id"] = case_id
combined["audit_source"] = source_name
for field in (
"construction_subroute",
"production_class",
"target_route",
"capability_cell",
"factual_status",
):
combined[field] = manifest_row.get(field)
combined["private_gold_category"] = agent_private_gold_category(combined)
merged.append(combined)
if expected_case_count is not None and len(merged) != expected_case_count:
raise ValueError(
f"expected {expected_case_count} audit rows, found {len(merged)}"
)
merged.sort(key=lambda row: row["case_id"])
status_counts = Counter(str(row.get("status") or "unknown") for row in merged)
reason_quality = Counter(
str(row.get("reason_quality") or "unknown")
for row in merged
if row.get("status") == "completed"
)
fact_alignment = Counter(
str(row.get("fact_alignment") or "unknown")
for row in merged
if row.get("status") == "completed"
)
failure_modes = Counter(
str(mode)
for row in merged
if row.get("status") == "completed"
for mode in (row.get("failure_modes") or [])
if str(mode).strip()
)
wrong_rows = [
row
for row in merged
if row.get("private_gold_category") == "wrong_verdict"
]
insufficient_rows = [
row
for row in merged
if row.get("private_gold_category") == "correct_verdict_insufficient_evidence"
]
result = {
"schema_version": SCHEMA_VERSION,
"test_manifest": str(test_manifest),
"inputs": {name: str(path) for name, path in inputs},
"input_row_counts": source_counts,
"selected": len(merged),
"status_counts": dict(status_counts),
"engineering_or_judge_errors": sum(
row.get("status") != "completed" for row in merged
),
"binary_metrics": _metric_payload(merged),
"three_way_private_gold_categories": agent_private_gold_category_counts(
merged
),
"by_construction_subroute": _route_breakdown(merged),
"failure_analysis": {
"wrong_verdict_count": len(wrong_rows),
"correct_but_insufficient_evidence_count": len(insufficient_rows),
"wrong_verdict_by_subroute": dict(
Counter(
str(row.get("construction_subroute") or "unknown")
for row in wrong_rows
)
),
"wrong_verdict_fact_alignment": dict(
Counter(str(row.get("fact_alignment") or "unknown") for row in wrong_rows)
),
"insufficient_evidence_reason_quality": dict(
Counter(
str(row.get("reason_quality") or "unknown")
for row in insufficient_rows
)
),
"all_completed_reason_quality": dict(reason_quality),
"all_completed_fact_alignment": dict(fact_alignment),
"judge_failure_modes": dict(failure_modes),
},
}
_write_jsonl(output_dir / "combined-audit-results.jsonl", merged)
result["combined_audit_results"] = str(
output_dir / "combined-audit-results.jsonl"
)
_write_json(output_dir / "summary.json", result)
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", action="append", required=True, type=_parse_input)
parser.add_argument("--test-manifest", required=True)
parser.add_argument("--output-dir", required=True)
parser.add_argument("--expected-case-count", type=int)
args = parser.parse_args()
if args.expected_case_count is not None and args.expected_case_count < 1:
parser.error("--expected-case-count must be positive")
result = summarize_agent_test_evaluation(
inputs=args.input,
test_manifest=Path(args.test_manifest),
output_dir=Path(args.output_dir),
expected_case_count=args.expected_case_count,
)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())