hands-on labIDE in your browser

Calibrate Jev's Probabilities and Thresholds

Turn Jev's probabilities into numbers you can set a policy on.

time
60 min
checked steps
5
level
Intermediate
setup
None

$29.99a month with Pro

Pro opens all 5 steps of this lab and every other lab. Cancel any time.

no account needed · Pro opens all 5 steps

See Pro plans

New to Jev? Gate an Agent’s Tool Calls with Jev is free and takes 45 minutes.

Lab cockpit60 min · 5 stepsSession running
3 / 5 steps passingA threshold for a precision target · step 4 of 5
calib.py▶ Run✓ Check
# ---------------------------------------------------------------- Step 4CANDIDATES = [round(i / 100, 2) for i in range(1, 100)]  def precision_recall(p, y, t):    """(precision, recall) when every pair with p >= t is reused.    Precision is 1.0 when nothing is reused."""      def threshold_for_precision(p, y, target, min_hits=10):    """The lowest candidate threshold that reuses at least min_hits pairs with    precision >= target, or None."""     
TerminalOutput
step 4 of 5, replayed: A threshold for a precision target. The code, the output and the check are the lab's own.

The job

Tallyboard's support bot keeps a cache of questions it has already answered. When a new question comes in, Jev decides whether a cached answer can be sent unchanged, which saves a model call and a couple of seconds. A wrong reuse sends a customer a wrong answer, so the team wants the cache to reuse only when that is at least 98% likely to be right. You find out what Jev's probabilities actually mean on 160 labelled pairs, fix them, and set the threshold.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Collect Jev's probabilities

    Tallyboard's support bot caches every answer it writes.

    you writeask()collect()read this step below ↓

  2. 2

    Is 0.8 really 80%?

    A probability is calibrated when it matches how often things happen: of all the pairs Jev scores near 0.8, about 80% should be reusable.

    you writereliability()ece()

  3. 3

    Recalibrate with Platt scaling

    Jev ranks the pairs well, and its numbers sit too close to the middle.

    you writefit_platt()calibrate()

  4. 4

    A threshold for a precision target

    The team's rule is that a reused answer must be right at least 98% of the time.

    you writeprecision_recall()threshold_for_precision()

  5. 5

    Does the wording matter?

    Calibration fixes timid numbers and leaves the order of the pairs as it is, so a question that ranks pairs badly stays that way.

    you writeauc()win_rate()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Collect Jev's probabilities

Tallyboard's support bot caches every answer it writes. When a new question arrives, it finds the closest cached question and asks Jev one thing: can the cached answer be sent, unchanged, as the answer to the new question? A yes skips a model call and replies at once.

pairs.jsonl holds 160 such pairs, each labelled reusable by hand. Many are easy: a paraphrase ("team plan price per user?") or a different topic. The hard ones differ by one detail. "Team, 5 seats, annual" against "Team, 6 seats, annual" is not reusable. "Can we pay in euros?" against "Can we pay in British pounds?" is: the cached "No. We bill only in US dollars." answers both.

The pairs are split by cache entry. Eight cached questions and their pairs form dev; the other eight form test. Everything you tune in this lab is tuned on dev and then checked on questions it never saw.

do this

Open calib.py.

1. Write ask(pair, question=REUSE_QUESTION, post=jev.post). One post with the state {"cached_question", "cached_answer", "new_question"} from the pair and a single noul question named "reuse" whose instructions are question. Return the probability.

2. Write collect(pairs, question=REUSE_QUESTION, post=jev.post). Ask about every pair through ThreadPoolExecutor(16), keep the order, and return a numpy array.

3. Run. A few dev pairs with their probability and label, then a histogram of the dev split by probability. Reusable pairs average about 0.66 and the rest about 0.10, and almost no pair crosses sides. Look where the reusable pairs sit, though: most of them are between 0.5 and 0.9, and few are above 0.9.

calib.py, the file you edit118 lines
"""Calibrate Jev's probabilities for a semantic cache.

Before Tallyboard's support bot writes a new answer, it looks for a cached
question close to the new one and asks Jev whether the cached answer can be
sent as it is. A wrong reuse sends a customer a wrong answer, so the cache
may only reuse when it is very likely right. "Very likely" has to be a
number you can trust, which is what calibration is about.
"""
import json
import os
from concurrent.futures import ThreadPoolExecutor

import numpy as np

import jev

HERE = os.path.dirname(os.path.abspath(__file__))

REUSE_QUESTION = "Can the cached answer be sent, unchanged, as the answer to the new question?"
WORDINGS = {
    "reuse": REUSE_QUESTION,
    "same": "Do the two questions ask the same thing?",
    "verify": ("Is the cached answer a correct and complete answer to the new question, "
               "with nothing in it that would mislead the customer?"),
}


def load_pairs(path="pairs.jsonl"):
    with open(os.path.join(HERE, path), encoding="utf-8") as f:
        return [json.loads(line) for line in f if line.strip()]


def labels(pairs):
    """1 for a reusable pair, 0 otherwise, as a numpy array."""
    return np.array([1 if p["reusable"] else 0 for p in pairs])


# ---------------------------------------------------------------- Step 1
def ask(pair, question=REUSE_QUESTION, post=jev.post):
    """Jev's probability that the pair's cached answer can be reused."""
    # TODO (Step 1): ONE post({"model": jev.MODEL, "state": {...}, "questions": {...}})
    #   state: cached_question, cached_answer and new_question from the pair
    #   questions: {"reuse": {"type": "noul", "instructions": question}}
    # return the noul probability
    raise NotImplementedError("Step 1: write ask()")


def collect(pairs, question=REUSE_QUESTION, post=jev.post):
    """Probabilities for every pair, in the order of `pairs`, as a numpy array."""
    # TODO (Step 1): ask() every pair through ThreadPoolExecutor(16), keep the order,
    # return np.array(...) of the probabilities
    raise NotImplementedError("Step 1: write collect()")


# ---------------------------------------------------------------- Step 2
def reliability(p, y, bins=5):
    """One row per equal-width bin: {"lo", "hi", "n", "mean_p", "rate"}.
    The last bin includes 1.0. Empty bins have n 0 and None for mean_p and rate."""
    raise NotImplementedError("reliability() arrives in Step 2")


def ece(p, y, bins=5):
    """Expected calibration error: the bins' gaps |mean_p - rate|, weighted by their share of pairs."""
    raise NotImplementedError("ece() arrives in Step 2")


def brier(p, y):
    """Mean squared gap between probability and outcome."""
    raise NotImplementedError("brier() arrives in Step 2")


# ---------------------------------------------------------------- Step 3
def logit(p):
    p = np.clip(np.asarray(p, dtype=float), 1e-4, 1 - 1e-4)
    return np.log(p / (1 - p))


def fit_platt(p, y):
    """Platt scaling: fit a logistic regression on logit(p). Returns (a, b)."""
    raise NotImplementedError("fit_platt() arrives in Step 3")


def calibrate(p, a, b):
    """The recalibrated probabilities: sigmoid(a * logit(p) + b)."""
    raise NotImplementedError("calibrate() arrives in Step 3")


# ---------------------------------------------------------------- Step 4
CANDIDATES = [round(i / 100, 2) for i in range(1, 100)]


def precision_recall(p, y, t):
    """(precision, recall) when every pair with p >= t is reused.
    Precision is 1.0 when nothing is reused."""
    raise NotImplementedError("precision_recall() arrives in Step 4")


def threshold_for_precision(p, y, target, min_hits=10):
    """The lowest candidate threshold that reuses at least min_hits pairs with
    precision >= target, or None."""
    raise NotImplementedError("threshold_for_precision() arrives in Step 4")


# ---------------------------------------------------------------- Step 5
def auc(p, y):
    """The chance that a random reusable pair scores above a random non-reusable one (ties count half)."""
    raise NotImplementedError("auc() arrives in Step 5")


def win_rate(p_challenger, p_current, y, n_boot=1000, seed=0):
    """Share of bootstrap resamples where the challenger's AUC beats the current one's.
    Both are scored on the SAME resample each time; resamples with one class are skipped."""
    raise NotImplementedError("win_rate() arrives in Step 5")


def keep_or_switch(win, bar=0.95):
    """Switch wordings only when the challenger wins at least `bar` of the resamples."""
    raise NotImplementedError("keep_or_switch() arrives in Step 5")

provided for you:jev.pypairs.jsonltry_it.py

what you get

  • An editor and terminal in your browser
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Python with numpy arrays
  • Precision and recall, at the level of knowing what they count
  • No API keys: the lab's proxy provides access to Jev

Part of the AI agents and MCP track

Lab 25 of 26. Tool loops, ReAct, MCP servers and clients, multi-agent supervisors and agent evaluation.

Questions about this lab

Do I need a Jev or OpenRouter API key for this lab?

No. The lab sandbox reaches Jev through Preporato's model proxy, which holds the key.

What is calibration?

A model is calibrated when its probabilities match observed frequencies: of all the cases it scores 0.8, about 80% turn out true. The lab measures it with a reliability table, expected calibration error (ECE) and the Brier score.

Why not just pick the threshold on raw probabilities?

You can, and recalibrating with a monotone map such as Platt scaling does not change which cases can be separated. What it changes is what the number means: after calibration a threshold of 0.9 really means about 90% likely, so a precision target can be written as a probability and it still holds on new data.

What is Platt scaling?

A logistic regression fitted on the logit of the model's probability, sigmoid(a * logit(p) + b). An a above 1 stretches underconfident probabilities apart; b shifts them. It needs only a small labelled set, which is why the lab fits it on the dev split.

How to calibrate a decision model's probabilities

A decision model that returns probabilities invites a simple policy: act when the probability is above some number. That policy only works if the numbers mean what they say. A model can rank cases almost perfectly and still be underconfident or overconfident, and then a threshold such as 0.9 either lets mistakes through or throws most of the good cases away. In this lab you measure that for Jev, TypeSafe's decision model, on the reuse check of a semantic cache. You build reliability tables and compute expected calibration error and the Brier score. You recalibrate with Platt scaling fitted on a dev split, and choose the threshold that meets a precision target, then confirm it on a test split made of cache entries the fit never saw. Last, you compare question wordings with AUC and a paired bootstrap, and see why a small AUC lead on one sample is no reason to switch.