302 lines
9.7 KiB
Python
302 lines
9.7 KiB
Python
"""Markdown report generator for evaluation benchmark results.
|
|
|
|
Takes a list of benchmark result dicts and produces a formatted markdown table
|
|
suitable for inclusion in documentation or CI output.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def generate_markdown_report(results: list[dict[str, Any]]) -> str:
|
|
"""Generate a markdown report from benchmark results.
|
|
|
|
Each result dict should contain at minimum a ``benchmark`` key identifying
|
|
the benchmark name, plus any metric keys (e.g. ``ratio``,
|
|
``reduction_percent``, ``mrr``, ``precision``, ``recall``, ``f1``).
|
|
|
|
Args:
|
|
results: List of result dicts from benchmark runs.
|
|
|
|
Returns:
|
|
A markdown string containing a summary table and per-benchmark details.
|
|
"""
|
|
if not results:
|
|
return "# Evaluation Report\n\nNo benchmark results to report.\n"
|
|
|
|
lines: list[str] = []
|
|
lines.append("# Evaluation Report")
|
|
lines.append("")
|
|
|
|
# Collect all metric keys across results (excluding 'benchmark')
|
|
all_keys: list[str] = []
|
|
seen: set[str] = set()
|
|
for r in results:
|
|
for k in r:
|
|
if k != "benchmark" and k not in seen:
|
|
all_keys.append(k)
|
|
seen.add(k)
|
|
|
|
# Summary table
|
|
lines.append("## Summary")
|
|
lines.append("")
|
|
|
|
header = "| Benchmark | " + " | ".join(all_keys) + " |"
|
|
separator = "| --- | " + " | ".join("---" for _ in all_keys) + " |"
|
|
lines.append(header)
|
|
lines.append(separator)
|
|
|
|
for r in results:
|
|
name = r.get("benchmark", "unknown")
|
|
values = [str(r.get(k, "-")) for k in all_keys]
|
|
lines.append(f"| {name} | " + " | ".join(values) + " |")
|
|
|
|
lines.append("")
|
|
|
|
# Per-benchmark detail sections
|
|
lines.append("## Details")
|
|
lines.append("")
|
|
for r in results:
|
|
name = r.get("benchmark", "unknown")
|
|
lines.append(f"### {name}")
|
|
lines.append("")
|
|
for k in all_keys:
|
|
v = r.get(k, "-")
|
|
lines.append(f"- **{k}**: {v}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _read_csvs(results_dir: Path, prefix: str) -> list[dict[str, str]]:
|
|
"""Read all CSV files matching a prefix from the results directory."""
|
|
rows: list[dict[str, str]] = []
|
|
for p in sorted(results_dir.glob(f"*_{prefix}_*.csv")):
|
|
with open(p, newline="") as f:
|
|
reader = csv.DictReader(f)
|
|
rows.extend(reader)
|
|
return rows
|
|
|
|
|
|
def _md_table(headers: list[str], rows: list[list[str]]) -> str:
|
|
"""Build a markdown table from headers and rows."""
|
|
lines = []
|
|
lines.append("| " + " | ".join(headers) + " |")
|
|
lines.append("| " + " | ".join("---" for _ in headers) + " |")
|
|
for row in rows:
|
|
lines.append("| " + " | ".join(row) + " |")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate_full_report(results_dir: str | Path) -> str:
|
|
"""Generate a full markdown evaluation report from CSV result files.
|
|
|
|
Reads all CSV files in *results_dir*, groups them by benchmark type,
|
|
and produces a markdown report with methodology notes and per-benchmark
|
|
result tables.
|
|
|
|
Args:
|
|
results_dir: Directory containing CSV result files.
|
|
|
|
Returns:
|
|
Markdown string with the full report.
|
|
"""
|
|
results_dir = Path(results_dir)
|
|
lines: list[str] = []
|
|
lines.append("# Evaluation Report")
|
|
lines.append("")
|
|
lines.append("## Methodology")
|
|
lines.append("")
|
|
lines.append("Benchmarks are run against real open-source repositories.")
|
|
lines.append("Token counts use a consistent `len(text) // 4` approximation.")
|
|
lines.append(
|
|
"Impact accuracy reports two ground-truth modes: "
|
|
"graph-derived (circular — upper bound) and co-change "
|
|
"(files co-changed in the same commit, seed excluded)."
|
|
)
|
|
lines.append(
|
|
"Rows with `status=error` are kept for forensics but excluded "
|
|
"from all aggregates."
|
|
)
|
|
lines.append("")
|
|
|
|
benchmark_types = [
|
|
"token_efficiency",
|
|
"impact_accuracy",
|
|
"agent_baseline",
|
|
"flow_completeness",
|
|
"search_quality",
|
|
"build_performance",
|
|
"multi_hop_retrieval",
|
|
]
|
|
|
|
for btype in benchmark_types:
|
|
rows = _read_csvs(results_dir, btype)
|
|
if not rows:
|
|
continue
|
|
|
|
title = btype.replace("_", " ").title()
|
|
lines.append(f"## {title}")
|
|
lines.append("")
|
|
|
|
headers = list(rows[0].keys())
|
|
table_rows = [[r.get(h, "-") for h in headers] for r in rows]
|
|
lines.append(_md_table(headers, table_rows))
|
|
lines.append("")
|
|
|
|
if len(lines) <= 6:
|
|
lines.append("No benchmark results found.")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate_readme_tables(results_dir: str | Path) -> str:
|
|
"""Generate concise README-ready tables from CSV result files.
|
|
|
|
Produces three tables:
|
|
- Table A: Token Efficiency
|
|
- Table B: Accuracy & Quality
|
|
- Table C: Performance
|
|
|
|
Args:
|
|
results_dir: Directory containing CSV result files.
|
|
|
|
Returns:
|
|
Markdown string with the three tables.
|
|
"""
|
|
results_dir = Path(results_dir)
|
|
lines: list[str] = []
|
|
|
|
# Table A: Token Efficiency
|
|
te_rows = _read_csvs(results_dir, "token_efficiency")
|
|
if te_rows:
|
|
lines.append("### Token Efficiency")
|
|
lines.append("")
|
|
headers = [
|
|
"Repo", "Files", "Naive Tokens", "Standard Tokens",
|
|
"Graph Tokens", "Naive/Graph", "Std/Graph",
|
|
]
|
|
table_rows = []
|
|
for r in te_rows:
|
|
table_rows.append([
|
|
r.get("repo", "-"),
|
|
r.get("changed_files", "-"),
|
|
r.get("naive_tokens", "-"),
|
|
r.get("standard_tokens", "-"),
|
|
r.get("graph_tokens", "-"),
|
|
r.get("naive_to_graph_ratio", "-"),
|
|
r.get("standard_to_graph_ratio", "-"),
|
|
])
|
|
lines.append(_md_table(headers, table_rows))
|
|
lines.append("")
|
|
|
|
# Table B: Accuracy & Quality
|
|
ia_rows = _read_csvs(results_dir, "impact_accuracy")
|
|
fc_rows = _read_csvs(results_dir, "flow_completeness")
|
|
sq_rows = _read_csvs(results_dir, "search_quality")
|
|
|
|
if ia_rows or fc_rows or sq_rows:
|
|
lines.append("### Accuracy & Quality")
|
|
lines.append("")
|
|
headers = ["Repo", "Impact F1 (graph-derived)", "Flow Recall", "Search MRR"]
|
|
# Build a per-repo summary
|
|
repo_data: dict[str, dict[str, object]] = {}
|
|
mrr_accum: dict[str, list[float]] = {}
|
|
f1_accum: dict[str, list[float]] = {}
|
|
for r in ia_rows:
|
|
# Failed rows are kept in the CSV for forensics but must never
|
|
# contribute to a headline number; co-change rows are a
|
|
# different metric and get their own reporting.
|
|
if r.get("status", "ok") not in ("", "ok"):
|
|
continue
|
|
mode = r.get("ground_truth_mode", "")
|
|
if mode and not mode.startswith("graph-derived"):
|
|
continue
|
|
repo = r.get("repo", "?")
|
|
repo_data.setdefault(repo, {})
|
|
try:
|
|
f1_accum.setdefault(repo, []).append(float(r.get("f1", "")))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
for r in fc_rows:
|
|
repo_data.setdefault(r.get("repo", "?"), {})["recall"] = r.get("recall", "-")
|
|
for r in sq_rows:
|
|
repo = r.get("repo", "?")
|
|
repo_data.setdefault(repo, {})
|
|
try:
|
|
mrr_accum.setdefault(repo, []).append(float(r.get("reciprocal_rank", 0)))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
table_rows = []
|
|
for repo, d in sorted(repo_data.items()):
|
|
mrr_vals = mrr_accum.get(repo, [])
|
|
mrr = (
|
|
str(round(sum(mrr_vals) / len(mrr_vals), 3))
|
|
if mrr_vals
|
|
else "-"
|
|
)
|
|
f1_vals = f1_accum.get(repo, [])
|
|
f1 = (
|
|
str(round(sum(f1_vals) / len(f1_vals), 3))
|
|
if f1_vals
|
|
else "-"
|
|
)
|
|
table_rows.append([
|
|
repo,
|
|
f1,
|
|
str(d.get("recall", "-")),
|
|
mrr,
|
|
])
|
|
lines.append(_md_table(headers, table_rows))
|
|
lines.append("")
|
|
|
|
# Table B2: Agent Baseline (grep top-k vs graph query)
|
|
ab_rows = _read_csvs(results_dir, "agent_baseline")
|
|
if ab_rows:
|
|
lines.append("### Agent Baseline (grep top-k vs graph query)")
|
|
lines.append("")
|
|
headers = [
|
|
"Repo", "Question", "Baseline Tokens", "Graph Tokens",
|
|
"Baseline/Graph", "Status",
|
|
]
|
|
table_rows = []
|
|
for r in ab_rows:
|
|
table_rows.append([
|
|
r.get("repo", "-"),
|
|
r.get("question", "-"),
|
|
r.get("baseline_tokens", "-"),
|
|
r.get("graph_tokens", "-"),
|
|
r.get("baseline_to_graph_ratio", "-"),
|
|
r.get("status", "ok") or "ok",
|
|
])
|
|
lines.append(_md_table(headers, table_rows))
|
|
lines.append("")
|
|
|
|
# Table C: Performance
|
|
bp_rows = _read_csvs(results_dir, "build_performance")
|
|
if bp_rows:
|
|
lines.append("### Performance")
|
|
lines.append("")
|
|
headers = ["Repo", "Files", "Nodes", "Flow Det. (s)", "Search (ms)"]
|
|
table_rows = []
|
|
for r in bp_rows:
|
|
table_rows.append([
|
|
r.get("repo", "-"),
|
|
r.get("file_count", "-"),
|
|
r.get("node_count", "-"),
|
|
r.get("flow_detection_seconds", "-"),
|
|
r.get("search_avg_ms", "-"),
|
|
])
|
|
lines.append(_md_table(headers, table_rows))
|
|
lines.append("")
|
|
|
|
if not lines:
|
|
return "No benchmark results found.\n"
|
|
|
|
return "\n".join(lines)
|