Skip to content

week_5: Module C (The Librarian) — C.3 confidence calibration (temperature scaling)#974

Open
PRAteek-singHWY wants to merge 3 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_5
Open

week_5: Module C (The Librarian) — C.3 confidence calibration (temperature scaling)#974
PRAteek-singHWY wants to merge 3 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_5

Conversation

@PRAteek-singHWY

Copy link
Copy Markdown
Contributor

Hi @northdpole — Week 5 of Module C. This one turns the cross-encoder's raw score into an honest probability (temperature scaling), so Week 6 can threshold "auto-link vs. send-to-human" on a number that actually means what it says.

Based on latest main — this PR is rebased onto main after Week 1–4 (#922#925#937#957) all merged, so the diff is a clean Week-5-only surface (no stacked commits).

Overview

Week 3 built the search step (C.1) — a bi-encoder that cosine-ranks the whole CRE hub down to a top-20 shortlist. Week 4 built the rerank step (C.2) — a cross-encoder that reads each (section, candidate-CRE) pair together and re-sorts the shortlist. C.2 hands up a raw relevance logit per candidate: +1.5 for a good match, −11.3 for a bad one. Perfect for ordering — but it is not a probability. +1.5 does not mean "82% sure this is the right CRE," and cross-encoders are systematically over-confident, so even sigmoid(z) lies.

The problem: Week 6's decision — auto-link vs. send to a human — is a threshold on confidence ("link if ≥ 90%"). That threshold is only safe if 90% really means 90%. A raw score cannot be thresholded honestly.

This PR's role: build the calibration step (C.3). Learn a single scalar temperature T by minimising negative-log-likelihood on the golden set, map every rerank logit to a real probability p = sigmoid(z / T), and prove the result honest with Expected Calibration Error < 0.10. Dividing every logit by the same T never changes the ranking, so calibration cannot hurt C.1/C.2 recall — it only makes the confidence trustworthy.

One line: the bi-encoder ranks the whole map (W3), the cross-encoder re-ranks the shortlist (W4), and calibration turns the top score into an honest probability (W5) that the W6 decision engine can threshold on.

Scope: 1 new package (1 module + 1 test) + harness wiring for the ECE gate. UpdateDetection (the other W5 schema deliverable) already exists in schemas.py — verified, nothing to add. No frontend, no migration, no behaviour change to OpenCRE proper.

What changed

Area Files Description
C.3 calibration calibration/temperature.py (new), calibration/__init__.py (new) TemperatureScaler(T) maps a logit to p = sigmoid(z/T) (.probability single / .calibrate batch); fit_temperature learns the single scalar T by minimising NLL (scipy.optimize.minimize_scalar, bounded), guarding single-class data (DegenerateLabelsError); expected_calibration_error is the 10-bin ECE gate; negative_log_likelihood is the fit objective (exposed for testing/reporting). Model-free (numpy + scipy.special.expit) so it stays import-light and hermetically testable — mirrors the C.1 embed_fn / C.2 score_fn seams. CALIBRATOR_NAME audit tag (mirrors RETRIEVER_NAME/RERANKER_NAME).
Eval harness evaluate_librarian.py report_calibration builds a (logit, label) set from the live C.1→C.2 pipeline over the positive + hard_negative slices (top-1 rerank logit; label 1 iff top-1 is an expected CRE — hard_negatives supply the 0 class), fits T, and reports ECE at T=1 vs. the fitted T with the < 0.10 gate. _build_live_pipeline extracted so recall and calibration share one hub + model load. Offline path unchanged (still honest: no live number is faked).
Docs __init__.py Scope note now covers C.3.
Tests temperature_test.py (new) 19 hermetic tests: sigmoid map + T cool/heat, ranking preserved by T, NLL hand-check, fit recovers a known T and reduces NLL vs. T=1, ECE on perfectly-/mis-calibrated data + a hand-checked two-bin value + the p == 1.0 edge, and every guard (single-class, non-binary, length mismatch, empty, n_bins < 1, non-positive/non-finite T).

How the pieces connect

flowchart TB
    audit["RetrievalAudit from C.2 (W4)<br/>reranked[] with score_rerank (raw logit z)"]
    subgraph FIT["fit once on the golden set"]
        g["(logit, label) pairs<br/>positive + hard_negative slices"]
        nll["fit_temperature: argmin NLL(T)"]
        Tstar["T*  (one scalar)"]
        ece["expected_calibration_error<br/>gate: ECE &lt; 0.10"]
        g --> nll --> Tstar --> ece
    end
    subgraph APPLY["apply per candidate"]
        z["top-1 rerank logit z"]
        p["p = sigmoid(z / T*)<br/>honest probability"]
        z --> p
    end
    audit --> g
    audit --> z
    Tstar -.-> p
    p --> dec["W6 decision engine<br/>p &gt;= τ -> LinkProposal ; else ReviewItem"]
Loading

Results

# offline (CI default)
110 librarian tests passing (91 from W1–W4 + 19 new)
explicit slice (C.0.5 resolver): 5/5 — gate 100%: PASS
calibration (C.3): wired; ECE gate needs --use_live_embeddings

# live (local, against a populated cache DB + embedding model)
calibration (C.3, __ rows): fitted T=__; ECE __ (raw, T=1) -> __ (calibrated); gate ECE<0.10: __

Live T and ECE are measured against a populated cache DB before this PR leaves draft — I'll paste the real numbers into the block above. It needs the paid embedding key (same live-gate as W4). The calibration math itself is covered offline by the 19 hermetic tests, which reproduce the worked numbers exactly: σ(1.5) = 0.8176 (raw, over-confident) → σ(1.5/2.1) = 0.6713 (cooled, honest), plus the NLL and ECE arithmetic.

What is intentionally not here

  • Decision / threshold routing (C.4, W6) — turning p into a LinkProposal (≥ τ) vs. a ReviewItem, and writing the calibrated confidence into ProposedLink. This PR produces the honest probability; W6 thresholds it.
  • Persisting the fitted T for the live decision path (loaded by the W6 engine).
  • Graph writes / worker wiring (W8) — the pipeline stays dry-run; nothing here costs the embedding API outside a manual --use_live_embeddings run.

How to verify locally

# all librarian tests (calibration is hermetic — no key, DB, or model)
python3 -m unittest discover -s application/tests/librarian -p '*_test.py' -t .
# or just the new ones:
python3 -m unittest application.tests.librarian.temperature_test

# offline harness: C.0 pass rate + explicit gate + calibration wired
python3 scripts/evaluate_librarian.py --dataset application/tests/librarian/fixtures/golden_dataset.json

# live ECE gate (needs a populated cache DB + an embedding-capable LLM)
python3 scripts/evaluate_librarian.py \
    --dataset application/tests/librarian/fixtures/golden_dataset.json \
    --use_live_embeddings --cache_file standards_cache.sqlite

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c4428e49-0437-4e34-87f1-1a2bf68d4171

📥 Commits

Reviewing files that changed from the base of the PR and between fc68304 and 68a07ee.

📒 Files selected for processing (1)
  • scripts/evaluate_librarian.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/evaluate_librarian.py

Summary by CodeRabbit

  • New Features
    • Added temperature-based confidence calibration (calibrated probabilities, calibration error/ECE measurement, and calibrator version tracking).
    • Expanded live evaluation with a confidence-calibration gate that can fail the run when ECE is too high.
  • Tests
    • Added deterministic unit tests covering calibration, likelihood/NLL behavior, temperature fitting, ECE correctness, and error handling.
  • Documentation
    • Updated Librarian module documentation to clarify the scope of confidence calibration and its role in the evaluation workflow.

Walkthrough

Adds a temperature-scaling calibration module with error types and fitting/ECE utilities, unit tests, docstring updates, and live calibration gating in evaluate_librarian.py.

Changes

Temperature Calibration Feature

Layer / File(s) Summary
Module docstrings
application/utils/librarian/__init__.py, application/utils/librarian/calibration/__init__.py
Documents W5 (C.3) confidence calibration scope: temperature scaling fitted by NLL, gated by ECE < 0.10, with decision routing noted as unimplemented.
Core calibration implementation
application/utils/librarian/calibration/temperature.py
Adds CALIBRATOR_NAME, CalibrationError/DegenerateLabelsError, TemperatureScaler dataclass, negative_log_likelihood, fit_temperature, and expected_calibration_error.
Unit tests for calibration utilities
application/tests/librarian/temperature_test.py
Adds tests covering scaler confidence/probability behavior, NLL correctness, temperature fitting recovery and error cases, ECE correctness and validation, an end-to-end check, and metadata assertion.
Live evaluation calibration gate
scripts/evaluate_librarian.py
Adds _build_live_pipeline helper, refactors report_retrieval_recall, adds report_calibration computing raw/calibrated ECE with a <0.10 gate, and updates main to run calibration and return its status.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: C.3 confidence calibration via temperature scaling.
Description check ✅ Passed The description is directly related to the calibration changes and harness updates in this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PRAteek-singHWY PRAteek-singHWY marked this pull request as ready for review July 9, 2026 17:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
scripts/evaluate_librarian.py (1)

182-184: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Live pipeline is built (and positives reranked) twice per run.

With --use_live_embeddings, report_retrieval_recall and report_calibration each call _build_live_pipeline(...) independently, so the DB connection, embedding model, and cross-encoder are loaded twice, and every positive row is retrieved+reranked in both reports. Since the cross-encoder load and per-pair inference are the expensive steps here, building the pipeline once in main and passing (retriever, reranker) into both reports would roughly halve the live cost. This also matches the intent stated in _build_live_pipeline's docstring that the heavy hub + model load happens once.

♻️ Sketch: build once in main, pass into reports
     if args.use_live_embeddings:
+        retriever, reranker = _build_live_pipeline(
+            args.cache_file,
+            args.top_k_retrieval,
+            args.threshold,
+            args.top_k_rerank,
+            cfg.crossencoder_model,
+        )
-        report_retrieval_recall(
-            rows,
-            args.cache_file,
-            args.top_k_retrieval,
-            args.threshold,
-            args.top_k_rerank,
-            cfg.crossencoder_model,
-        )
-        calib_status = report_calibration(
-            rows,
-            args.cache_file,
-            args.top_k_retrieval,
-            args.threshold,
-            args.top_k_rerank,
-            cfg.crossencoder_model,
-        )
+        report_retrieval_recall(rows, retriever, reranker, args.top_k_retrieval, args.top_k_rerank)
+        calib_status = report_calibration(rows, retriever, reranker)

(Adjust the two report signatures to accept the prebuilt retriever, reranker.)

Also applies to: 238-251

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/evaluate_librarian.py` around lines 182 - 184, The live pipeline is
being constructed twice because both report_retrieval_recall and
report_calibration call _build_live_pipeline independently. Build the pipeline
once in main by calling _build_live_pipeline(cache_file, top_k, threshold,
top_n_rerank, crossencoder_model) a single time, then pass the resulting
retriever and reranker into both report functions. Update the signatures of
report_retrieval_recall and report_calibration to accept the prebuilt pipeline
objects and remove their internal _build_live_pipeline calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@scripts/evaluate_librarian.py`:
- Around line 182-184: The live pipeline is being constructed twice because both
report_retrieval_recall and report_calibration call _build_live_pipeline
independently. Build the pipeline once in main by calling
_build_live_pipeline(cache_file, top_k, threshold, top_n_rerank,
crossencoder_model) a single time, then pass the resulting retriever and
reranker into both report functions. Update the signatures of
report_retrieval_recall and report_calibration to accept the prebuilt pipeline
objects and remove their internal _build_live_pipeline calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e64443ca-ff6c-46c9-bff4-b4820fd64a97

📥 Commits

Reviewing files that changed from the base of the PR and between 7d0634e and fc68304.

📒 Files selected for processing (5)
  • application/tests/librarian/temperature_test.py
  • application/utils/librarian/__init__.py
  • application/utils/librarian/calibration/__init__.py
  • application/utils/librarian/calibration/temperature.py
  • scripts/evaluate_librarian.py

PRAteek-singHWY and others added 2 commits July 9, 2026 23:06
… once

report_retrieval_recall and report_calibration each built the live
pipeline (DB + embedding model + cross-encoder) independently, loading it
twice and reranking every positive row twice per --use_live_embeddings run.
Build it once in main and pass (retriever, reranker) into both reports,
matching _build_live_pipeline's stated intent. Behavior-preserving:
recall@20 285/292, rerank top-1 220/292, ECE 0.046 PASS unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant