File size: 5,866 Bytes
9009a09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python3
"""Measure the relevance gap and suggest a MIN_COSINE for the current corpus.

`MIN_COSINE` in `controlai_rag/retriever.py` is the gate that decides whether a
retrieved passage is worth putting in front of the model. It is not a universal
constant: it depends on the embedding model *and* on the corpus. A larger corpus
raises every query's best match, off-domain ones included, so the threshold has
to be re-measured whenever the index changes size materially.

The method is to score two sets of probes -- questions the corpus should be able
to answer, and questions it definitely cannot -- and put the threshold in the gap
between them. If there is no gap, the report says so rather than inventing one.

    python scripts/calibrate_retrieval.py
    python scripts/calibrate_retrieval.py --json report.json
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import numpy as np

PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

# Topics a control-engineering library is expected to cover. Deliberately spans
# classical, modern, optimal, robust, nonlinear, estimation and MPC so the
# threshold is not tuned to one corner of the field.
IN_DOMAIN = [
    "Routh-Hurwitz stability criterion table construction",
    "root locus asymptotes and breakaway points",
    "Nyquist stability criterion encirclements and the Z = N + P rule",
    "Kalman filter measurement update equations",
    "why does LQG have no guaranteed stability margins",
    "Bode sensitivity integral waterbed effect right half plane zero",
    "terminal cost and terminal region for model predictive control stability",
    "small gain theorem and when it is conservative",
    "sliding mode control chattering and the boundary layer",
    "PBH rank test for controllability and stabilizability",
    "describing function analysis of a limit cycle",
    "persistent excitation in system identification",
]

# Questions with no plausible answer in a control library. If any of these
# clears the threshold, the gate is too loose and the model will be handed a
# confident irrelevance.
OUT_OF_DOMAIN = [
    "how do I bake sourdough bread with a levain starter",
    "best hiking trails in Patagonia in November",
    "React useState hook rerender behaviour in strict mode",
    "who won the 1998 FIFA World Cup final",
    "symptoms and treatment of seasonal pollen allergy",
]


def best_similarities(retriever, embedder, query: str, depth: int = 60, top: int = 5) -> list[float]:
    """Top similarities for `query`, after the low-value filter is applied.

    Filtering first matters: index pages match nearly any control query and
    would otherwise set the threshold from chunks that can never be returned.
    """
    from controlai_rag.retriever import _is_low_value

    by_id = {c["chunk_id"]: c for c in retriever.index.chunks}
    sims = retriever.vectors @ embedder.encode_query(query)
    order = np.argsort(-sims)[:depth]
    kept = [
        float(sims[i])
        for i in order
        if not _is_low_value(str(by_id.get(retriever.vector_ids[i], {}).get("text", "")))
    ]
    return kept[:top]


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--json", type=Path, help="also write the raw measurements here")
    args = parser.parse_args()

    from controlai_rag.embeddings import get_embedder
    from controlai_rag.retriever import MIN_COSINE, get_retriever

    retriever = get_retriever()
    if not retriever.has_dense:
        print("No dense index. Run: python -m controlai_rag.retriever --build")
        return 1
    embedder = get_embedder()
    print(f"corpus: {len(retriever.index.chunks)} chunks, {len(retriever.vector_ids)} vectors\n")

    report: dict[str, dict[str, list[float]]] = {"in_domain": {}, "out_of_domain": {}}
    for label, queries, key in (
        ("IN ", IN_DOMAIN, "in_domain"),
        ("OUT", OUT_OF_DOMAIN, "out_of_domain"),
    ):
        for query in queries:
            tops = best_similarities(retriever, embedder, query)
            report[key][query] = tops
            shown = np.round(tops, 3) if tops else "none"
            print(f"{label} {str(shown):32} {query[:58]}")
        print()

    # A passage is only useful if it clears the gate, so what matters for the
    # in-domain set is its *weakest* best-match, and for the out-of-domain set
    # its strongest.
    in_best = [max(v) for v in report["in_domain"].values() if v]
    out_best = [max(v) for v in report["out_of_domain"].values() if v]
    floor, ceiling = min(in_best), max(out_best)

    print(f"in-domain  worst best-match : {floor:.3f}  (mean {np.mean(in_best):.3f})")
    print(f"off-domain best  best-match : {ceiling:.3f}  (mean {np.mean(out_best):.3f})")
    print(f"current MIN_COSINE          : {MIN_COSINE}")

    if floor > ceiling:
        suggestion = round((floor + ceiling) / 2, 2)
        print(f"\ngap of {floor - ceiling:.3f} -> suggested MIN_COSINE = {suggestion}")
        if not (ceiling < MIN_COSINE < floor):
            print(f"the current value sits outside that gap; update it in controlai_rag/retriever.py")
    else:
        print(
            f"\nNO GAP: an off-domain query scores {ceiling:.3f} while an in-domain one "
            f"scores only {floor:.3f}. No single threshold separates them -- tighten "
            f"_is_low_value, or accept losing the weakest in-domain topics by setting the "
            f"threshold above {ceiling:.2f}."
        )

    if args.json:
        args.json.write_text(json.dumps(report, indent=2), encoding="utf-8")
        print(f"\nwrote {args.json}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())