File size: 5,215 Bytes
e201ae4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | """Tool-output eval runner (PR-2 Layer 2).
Runs every case in `cases.py` — each builds a fixture with a property planted and
asserts the tool reports it — then prints a per-tool table + aggregate summary and
writes a timestamped JSON report under `results/` (never overwritten).
**No LLM, no DB, no network.** The only compute is the tools themselves (which may
fit sklearn/statsmodels — fast, local, deterministic). This is the shared, committed
version of the fixtures that otherwise live gitignored under `tests/`.
Invoke as a module so `src` imports resolve:
PYTHONPATH=. python -m eval.tools.run_eval
PYTHONPATH=. python -m eval.tools.run_eval --tool analyze_cluster
"""
from __future__ import annotations
import argparse
import asyncio
import json
import time
import traceback
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from eval.tools.cases import CASES, EvalCase
from src.tools.registry import ACTIVE_ANALYTICS_TOOLS
_HERE = Path(__file__).resolve().parent
RESULTS_DIR = _HERE / "results"
@dataclass
class CaseResult:
id: str
tool: str
prop: str
passed: bool
latency_ms: float
error: str | None
async def run_case(case: EvalCase) -> CaseResult:
start = time.perf_counter()
error: str | None = None
try:
await case.fn()
passed = True
except Exception as exc: # noqa: BLE001 — a failed assertion is a failed case, recorded
passed = False
error = f"{type(exc).__name__}: {exc}".strip()
latency_ms = round((time.perf_counter() - start) * 1000, 1)
return CaseResult(case.id, case.tool, case.prop, passed, latency_ms, error)
def _coverage() -> dict[str, list[str]]:
"""Which ACTIVE analytics tools have >= 1 case, and which have none (§4 acceptance)."""
covered = {c.tool for c in CASES}
active = {t.name for t in ACTIVE_ANALYTICS_TOOLS}
return {
"covered": sorted(active & covered),
"uncovered": sorted(active - covered),
}
def format_table(results: list[CaseResult]) -> str:
header = f"{'ID':<38} {'TOOL':<22} {'ms':>6} OK"
rule = "-" * len(header)
lines = [rule, header, rule]
for r in results:
ok = "ok" if r.passed else "X"
lines.append(f"{r.id:<38} {r.tool:<22} {r.latency_ms:>6.1f} {ok}")
if not r.passed:
lines.append(f" -> {r.error}")
lines.append(rule)
return "\n".join(lines)
def format_summary(results: list[CaseResult], coverage: dict[str, list[str]]) -> str:
passed = sum(r.passed for r in results)
total = len(results)
lines = ["SUMMARY", f" {passed}/{total} cases passed"]
by_tool: dict[str, list[CaseResult]] = {}
for r in results:
by_tool.setdefault(r.tool, []).append(r)
lines.append("")
lines.append(" By tool")
for tool in sorted(by_tool):
sub = by_tool[tool]
lines.append(f" {tool:<24} {sum(s.passed for s in sub)}/{len(sub)}")
lines.append("")
lines.append(
f" Coverage: {len(coverage['covered'])}/"
f"{len(coverage['covered']) + len(coverage['uncovered'])} ACTIVE analytics tools"
)
if coverage["uncovered"]:
lines.append(f" UNCOVERED: {', '.join(coverage['uncovered'])}")
failures = [r for r in results if not r.passed]
if failures:
lines.append("")
lines.append(f" FAILURES ({len(failures)})")
for r in failures:
lines.append(f" {r.id:<38} {r.error}")
return "\n".join(lines)
async def main() -> int:
parser = argparse.ArgumentParser(description="Tool-output eval (PR-2 Layer 2)")
parser.add_argument("--tool", default="", help="run only cases for this tool")
parser.add_argument("--no-table", action="store_true", help="skip the detail table")
args = parser.parse_args()
cases = [c for c in CASES if not args.tool or c.tool == args.tool]
started = datetime.now()
print(f"Tool-Output Eval -- {started:%Y-%m-%d %H:%M:%S}")
print(f"cases: {len(cases)} (no LLM, no DB)")
results = [await run_case(c) for c in cases]
coverage = _coverage()
if not args.no_table:
print(format_table(results))
print(format_summary(results, coverage))
report = {
"run": {
"timestamp": started.isoformat(timespec="seconds"),
"total": len(results),
"passed": sum(r.passed for r in results),
"runtime_ms": round(sum(r.latency_ms for r in results), 1),
"llm_calls": 0,
},
"coverage": coverage,
"cases": [asdict(r) for r in results],
}
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
out_path = RESULTS_DIR / f"tools_result_{started:%Y-%m-%d_%H%M%S}.json"
out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n-> saved: {out_path.relative_to(_HERE.parent.parent)}")
# Non-zero exit on any failure so CI can gate on it.
return 0 if all(r.passed for r in results) else 1
if __name__ == "__main__":
try:
raise SystemExit(asyncio.run(main()))
except KeyboardInterrupt:
traceback.print_exc()
raise SystemExit(130) from None
|