#!/usr/bin/env python3 """kb/eval - recall@5 gate for the brain. bin/kb/eval [--json] Control questions are answered from the graph; recall@5 >= 0.95 gates CI. Each question maps to leaf ids that MUST appear in the top 5 hits. """ from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools")) from kblib import open_readonly, query_fts # noqa: E402 from yamlout import to_yaml # noqa: E402 RECALL_THRESHOLD = 0.95 # (query, expected leaf id) CONTROL_QUESTIONS: list[tuple[str, str]] = [ ("which database does the brain use", "facts:ladybug"), ("hybrid search weights fts and vector equally", "info:hybrid"), ] def hit_ids_of(query: str, conn, limit: int = 5) -> list[str]: try: hits = query_fts(conn, query, limit) return [h["id"] for h in hits] except Exception: return [] def main(argv: list[str]) -> int: import argparse p = argparse.ArgumentParser(description="recall@5 gate") p.add_argument("--json", action="store_true") a = p.parse_args(argv) db, conn = open_readonly() recalled = 0 detail = [] for query, expected in CONTROL_QUESTIONS: hits = hit_ids_of(query, conn) ok = any(expected in h or h in expected for h in hits) recalled += int(ok) detail.append({"q": query, "expected": expected, "in_top5": ok, "hits": hits[:5]}) recall = recalled / len(CONTROL_QUESTIONS) if CONTROL_QUESTIONS else 1.0 passed = recall >= RECALL_THRESHOLD out = {"recall@5": round(recall, 3), "passed": passed, "gate": len(CONTROL_QUESTIONS), "details": detail} print(json.dumps(out, indent=2) if a.json else to_yaml(out)) conn.close() db.close() return 0 if passed else 2 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))