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:
2026-09-30 08:22:50 -04:00
parent 4de5aaa02f
commit d131e93252
12 changed files with 509 additions and 53 deletions
+41 -1
View File
@@ -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