| """Merge a base eval report with a tier-specific override report. |
| |
| Produces a hybrid report where chosen-tier records come from the override |
| file and the rest come from the base. Used for the codestral-default + |
| Sonnet-on-challenging recipe: codestral handles simple/moderate (where |
| both models tie) and Sonnet handles challenging (where it gains +3pp). |
| |
| Usage:: |
| |
| uv run python scripts/merge_hybrid_eval.py \\ |
| --base eval/reports/2026-05-11/G_dense_fewshot_verify_retry-verify-retry.json \\ |
| --override eval/reports/2026-05-11/G_dense_fewshot_verify_retry-sonnet-challenging.json \\ |
| --override-difficulty challenging \\ |
| --suffix hybrid-codestral-sonnet |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import sys |
| from pathlib import Path |
|
|
| from nl_sql.eval.report import load_run_from_json, write_html_report, write_json_report |
| from nl_sql.eval.runner import EvalRecord, EvalRun, _summarise |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--base", type=Path, required=True) |
| parser.add_argument("--override", type=Path, required=True) |
| parser.add_argument( |
| "--override-difficulty", |
| choices=["simple", "moderate", "challenging"], |
| required=True, |
| ) |
| parser.add_argument("--suffix", default="hybrid") |
| parser.add_argument("--reports", type=Path, default=Path("eval/reports")) |
| args = parser.parse_args(argv) |
|
|
| base = load_run_from_json(args.base) |
| over = load_run_from_json(args.override) |
|
|
| over_index = {r.question_id: r for r in over.records} |
| swapped = 0 |
| merged: list[EvalRecord] = [] |
| for rec in base.records: |
| if rec.difficulty == args.override_difficulty and rec.question_id in over_index: |
| merged.append(over_index[rec.question_id]) |
| swapped += 1 |
| else: |
| merged.append(rec) |
|
|
| sql_models = sorted({base.sql_model, over.sql_model}) |
| hybrid = _summarise( |
| configuration=base.configuration, |
| sql_model=" + ".join(sql_models), |
| records=merged, |
| ) |
|
|
| print(f"[info] base records: {len(base.records)} ({base.sql_model})") |
| print(f"[info] override records: {len(over.records)} ({over.sql_model})") |
| print(f"[info] swapped: {swapped} ({args.override_difficulty} tier)") |
| print(f"[info] merged: {len(merged)}") |
| print() |
| print(f"EA (final): {hybrid.overall.ea * 100:.1f}%") |
| for d in ("simple", "moderate", "challenging"): |
| slice_ = hybrid.per_difficulty[d] |
| print(f" {d:12} {slice_.ea * 100:.1f}% (n={slice_.n})") |
|
|
| json_path = write_json_report(hybrid, root=args.reports, name_suffix=args.suffix) |
|
|
| today_dir = json_path.parent |
| siblings: list[EvalRun] = [] |
| for other in sorted(today_dir.glob("*.json")): |
| if other == json_path: |
| continue |
| try: |
| siblings.append(load_run_from_json(other)) |
| except (KeyError, ValueError): |
| continue |
| html_path = write_html_report([*siblings, hybrid], root=args.reports) |
|
|
| print() |
| print(f"[json] {json_path}") |
| print(f"[html] {html_path}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|