Reference / Evaluating a Socratic Tutor
JevEval Cheat Sheet
Everything needed to design, score and debug a JevEval metric. Source: the deepeval JevEval docs, unless marked otherwise. Print this one.
The evaluation loop
| Step | What it means |
|---|---|
| 1. Spec | The behaviour you want: the tutor's system prompt, verbatim in NOTES.md. |
| 2. Test cases | Conversations that trigger each rule, including adversarial ones. Look at lots of outputs before trusting any metric (Husain). |
| 3. Questions | One bounded question per rule, weighted by how much you care. |
| 4. Run and read | The total says whether something failed; the per-question breakdown says which rule. |
| 5. Iterate | Change the prompt or the question, then rerun the same test cases. |
Question types
| Type | Use when | Value | Gotcha |
|---|---|---|---|
| Noul | A true-or-false statement. Use several for a checklist. | P(true) | Phrase it so true is the behaviour you want. |
| Score | Ordered outcomes, where "in between" means something. 2–10 levels. | Σ k·P(k) / (n−1) | Levels worst first, or the score silently inverts. |
| Choice | Unordered outcomes; options may share a credit; the behaviour may not come up at all. | Σ P(o)·c(o) / Σ P(o), over options with a credit | Drops out if the None options hold ≥ 0.5. |
The score
JevEval = Σ wᵢ·vᵢ / Σ wᵢ over applicable questions only| Rule | Detail |
|---|---|
| Nothing applies | Score is 1: nothing applicable was left to fail. |
| Pass | score ≥ threshold. Default threshold 0.5; None runs the metric score-only. |
strict_mode=True | Score is 0 or 1, and the threshold becomes 1. |
| What Jev can see | "A field that is not in evaluation_params is invisible to Jev, so every field your questions mention must be listed." |
| Weights and credits | Stay in your code. Jev only ever sees the questions and the option descriptions. |
TypeScript: the arithmetic
From lesson 1, where every line was run against the docs' worked example and printed their 0.461.
src/jev-eval.tsthe scoring functions
type Question = { name: string; weight: number; value: number | null };
// A weighted mean of every question's value.
function jevEval(questions: Question[]): number {
let weighted = 0;
let totalWeight = 0;
for (const q of questions) {
if (q.value === null) continue; // this question doesn't apply
weighted += q.weight * q.value;
totalWeight += q.weight;
}
if (totalWeight === 0) return 1; // nothing applicable was left to fail
return weighted / totalWeight;
}
// A Score's value is the expected level, counted from 0, over the top level.
function scoreValue(levels: string[], answer: Record<string, number>): number {
let expected = 0;
for (const [level, label] of levels.entries()) {
expected += level * (answer[label] ?? 0);
}
return expected / (levels.length - 1);
}
type Credits = Record<string, number | null>;
// A Choice's value is the credit Jev's answer earns, averaged over the options
// that count. When the null options hold half the probability or more, the
// question doesn't apply, and the value is null.
function choiceValue(
credits: Credits,
answer: Record<string, number>
): number | null {
let noneMass = 0;
let mass = 0;
let earned = 0;
for (const [option, credit] of Object.entries(credits)) {
const p = answer[option] ?? 0;
if (credit === null) {
noneMass += p;
continue;
}
mass += p;
earned += p * credit;
}
if (noneMass >= 0.5) return null;
return earned / mass;
}
const missingCredits: Credits = {
left_it_out: 1,
flagged_it_as_unknown: 1,
hedged_it: 0.5,
stated_it_as_fact: 0,
nothing_missing: null,
};Not here yet: the Jev requestThe request that fills in these probabilities is added after lesson 2 runs it. Until then, two things are unchecked: what
answer.score means, and whether a Score's probabilities are keyed by position or by label. There are also two routes to choose between: the OpenRouter decisions API from exercise 21, and the@typesafe-ai/sdk package the docs list (with TYPESAFE_API_KEY).Python: deepeval
pip install typesafe-sdk # the docs' command; deepeval itself as well
export TYPESAFE_API_KEY=...| Single turn | Conversation |
|---|---|
JevEval + LLMTestCaseparams: SingleTurnParams | ConversationalJevEval + ConversationalTestCase(scenario, expected_outcome, turns=[Turn(role, content)])params: MultiTurnParams; ROLE and CONTENT are added automatically |
from deepeval.metrics.jev_eval import Noul, Score, Choice
from deepeval.metrics import JevEval, ConversationalJevEval
from deepeval.test_case import SingleTurnParams, MultiTurnParams
metric.measure(test_case)
print(metric.score, metric.reason)
print(metric.score_breakdown)
# each entry: question, type, weight, value, applicable, probabilities, confidence
from deepeval import evaluate
evaluate(test_cases=[test_case], metrics=[metric])Questions about a conversation talk about turns as a whole ("by the end of turns", "at any point in turns") or about a role ("the assistant", "the user").
The worked example
| Question | Weight | Jev's answer | Value |
|---|---|---|---|
| Noul: every fact appears in a tool output | 2 | P(true) 0.30 | 0.30 |
| Noul: every tool value reported accurately | 1 | P(true) 0.90 | 0.90 |
| Score: how grounded, 4 levels | 1 | 0.05 / 0.30 / 0.55 / 0.10 | 1.7 ÷ 3 = 0.567 |
| Choice: what it did with missing information | 1 | None option 0.05 | 0.225 ÷ 0.95 = 0.237 |
| (2 · 0.30 + 0.90 + 0.567 + 0.237) ÷ 5 | 0.461 | ||