feat: citations, eval sidecar/pvalue/trace, nomic prefixes
mcp 2.x already on main. Does not replace the variety chunker (one chunk per variety is the anti-hallucination contract). - Numbered [1] citations on search_docs / search_trials - Eval JSONL sidecar, k-curve, eval.pvalue, eval.trace - Nomic prefixes at embed time only; stored text unprefixed Closes #25
This commit is contained in:
+41
-1
@@ -265,10 +265,15 @@ def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--queries", type=Path, default=Path("eval/queries.jsonl"))
|
||||
p.add_argument("--k", type=int, default=5)
|
||||
p.add_argument("--ks", default="1,5,10,20", help="comma-separated k-curve")
|
||||
p.add_argument("--output", type=Path, default=Path("eval/results/baseline.md"))
|
||||
p.add_argument("--compare", type=Path, default=None)
|
||||
p.add_argument("--trace", action="store_true")
|
||||
p.add_argument("--rerank-url", default=os.environ.get("RERANK_URL", ""))
|
||||
p.add_argument("--product-name", default=os.environ.get("PRODUCT_NAME", "crop_seed"))
|
||||
args = p.parse_args()
|
||||
ks = sorted({int(x) for x in args.ks.split(",") if x.strip()}) or [args.k]
|
||||
max_k = max(ks + [args.k])
|
||||
|
||||
if not args.queries.exists():
|
||||
print(f"queries file not found: {args.queries}")
|
||||
@@ -300,14 +305,40 @@ def main() -> int:
|
||||
for r in retrievers:
|
||||
print(f"running {r.name}...")
|
||||
for q in queries:
|
||||
res = _evaluate_one(r, q, args.k, col)
|
||||
res = _evaluate_one(r, q, max_k, col)
|
||||
all_results.append(res)
|
||||
|
||||
summary = _aggregate(all_results)
|
||||
md = _emit_markdown(queries, all_results, summary, args.k)
|
||||
# k-curve from rank_first_match at max_k
|
||||
md += "\n## k-curve (P@1 / recall from rank_first_match)\n\n"
|
||||
md += "| Retriever | " + " | ".join(f"P@1@k={k}" for k in ks) + " |\n"
|
||||
md += "|" + "---|" * (len(ks) + 1) + "\n"
|
||||
by_r: dict[str, list[dict]] = {}
|
||||
for row in all_results:
|
||||
by_r.setdefault(row["retriever"], []).append(row)
|
||||
for name, rows in by_r.items():
|
||||
cells = []
|
||||
for kk in ks:
|
||||
hits = sum(1 for r in rows if r.get("rank_first_match") and r["rank_first_match"] <= kk)
|
||||
cells.append(f"{hits / len(rows):.3f}" if rows else "0")
|
||||
md += f"| `{name}` | " + " | ".join(cells) + " |\n"
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(md, encoding="utf-8")
|
||||
sidecar = args.output.with_suffix(".jsonl")
|
||||
with open(sidecar, "w") as fh:
|
||||
for r in all_results:
|
||||
rank = r.get("rank_first_match")
|
||||
fh.write(json.dumps({
|
||||
"query": r["query"],
|
||||
"retriever": r["retriever"],
|
||||
"passed": bool(r.get("passed")),
|
||||
"p_at_1": 1 if rank == 1 else 0,
|
||||
"rr": (1.0 / rank) if rank else 0.0,
|
||||
"rank_first_match": rank,
|
||||
}) + "\n")
|
||||
print(f"\nreport: {args.output}")
|
||||
print(f"sidecar: {sidecar}")
|
||||
print()
|
||||
# Print summary to stdout too
|
||||
for line in md.split("\n"):
|
||||
@@ -315,6 +346,15 @@ def main() -> int:
|
||||
print(line)
|
||||
if line.startswith("## Per-query"):
|
||||
break
|
||||
if args.compare:
|
||||
from eval.pvalue import compare, load_sidecar, render
|
||||
print()
|
||||
print(render(compare(load_sidecar(sidecar), load_sidecar(args.compare))), end="")
|
||||
if args.trace:
|
||||
from eval.trace import render_misses
|
||||
misses_path = args.output.with_name("misses.md")
|
||||
misses_path.write_text(render_misses(all_results))
|
||||
print(f"misses: {misses_path}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user