Score¶
A Score places the state on ordered levels that you describe, from low to high. Its answer is a
position, score, that can fall between levels, together with a probability per level and a confidence.
val result =
jev.query(state = "The export button crashes the settings page in Safari. It works in Chrome.") {
score("severity", "How severe is the reported issue?") {
level("Cosmetic; no impact to functionality") // level 0
level("Broken or degraded feature, but a workaround exists") // level 1
level("Blocking issue; no workaround exists") // level 2
}
}
println(result.score("severity").score) // e.g. 1.3: mostly level 1, some weight on level 2
Levels¶
Each level is one point on the spectrum, described in words. A level's number is its position: the first
level(...) is level 0. A Score needs 2 to 10 levels.
The model sees each level's description and nothing else, and judges each level on its own. It doesn't see the level numbers or the neighboring levels, so "worse than the previous level" means nothing to it.
Reading a Score¶
val severity = result.score("severity")
severity.score // 1.3 = 0 x 0.0 + 1 x 0.7 + 2 x 0.3
severity.probabilities // {0=0.0, 1=0.7, 2=0.3}
severity.confidence // 0.54: probability is split between two levels
severity.levelCount // 3
severity.normalized // 0.65 = score / (levelCount - 1), always 0..1
severity.nearestLevel // 1: the score rounded to a level
severity.mostLikelyLevel // 1: the level with the highest probability
severity.legendText(2) // "Blocking issue; no workaround exists"
score is the probability-weighted mean of the level numbers, so different distributions can produce the same
score. 1.0 might mean all probability is on level 1, or half on each of levels 0 and 2. Read probabilities or
confidence alongside the score when the difference matters.
Low confidence on a Score usually means one of three things:
- the levels overlap for this state
- the question measures more than one thing
- the state doesn't say enough to place it
Writing good levels¶
Describe situations, not degrees. "Broken or degraded feature, but a workaround exists" gives the model something to match against. "Moderately severe" doesn't, and levels made only of numbers perform worst:
object LevelWording : JevQuery() {
// Bad: numbers and degrees give the model nothing to match against.
val vague by score("Rate severity from 0 to 2, where 2 is worst") {
levels("0", "1", "2")
}
// Good: each level describes a situation that can be recognized on its own.
val concrete by score("How severe is the reported issue?") {
level("Cosmetic; no impact to functionality")
level("Broken or degraded feature, but a workaround exists")
level("Blocking issue; no workaround exists")
}
}
- Keep each Score to one dimension. If a level says "punctual and smart and experienced", split it into one Score per quality and combine them in code.
- Give a rare extreme its own level when you'd act on it differently, such as "abusive or threatening" above "very angry".
- Use as many levels as you can describe distinctly, up to 10. Three is fine.
When the model keeps landing between two levels on inputs you think are clear, add a few example situations to each level:
// Give each level a description plus a few example situations. Use the same field names on every level.
val result =
jev.query(state = "Export to PDF fails with a spinner that never finishes. CSV export still works.") {
score("severity", "How severe is the reported issue?") {
level(
entry(
"what" to "Cosmetic; no impact to functionality",
"examples" to listOf("typo in a label", "misaligned icon"),
),
)
level(
entry(
"what" to "Broken or degraded feature, but a workaround exists",
"examples" to listOf("export fails in one browser but works in another"),
),
)
level(
entry(
"what" to "Blocking issue; no workaround exists",
"examples" to listOf("cannot log in", "data loss"),
),
)
}
}
Examples only help when they resemble your real inputs.
Combining Scores¶
A judgment that depends on several things works best as one Score per thing, combined in code with weights you
control. Normalize each Score first: normalized divides by the top level number, putting every Score on 0–1
regardless of its level count.
object TicketPriority : JevQuery() {
val severity by score("How severe is the reported issue?") {
level("Cosmetic; no impact to functionality")
level("Broken or degraded feature, but a workaround exists")
level("Blocking issue; no workaround exists")
}
val frustration by score("How frustrated is the customer?") {
level("Calm, just stating facts")
level("Frustrated but civil")
level("Very angry, strong language or threatening to leave")
}
val reportQuality by score("How much does the report give an engineer to work with?") {
level("No detail; just says something is broken")
level("Names the feature but no steps or environment")
level("Steps to reproduce or environment, but not both")
level("Steps to reproduce and environment")
}
}
// Weights live in code: change them, not the questions, when the ranking doesn't match your team's judgment.
suspend fun priority(
jev: JevApi,
ticket: String,
): Double {
val result = jev.ask(TicketPriority, state = ticket)
return 0.6 * result[TicketPriority.severity].normalized +
0.3 * result[TicketPriority.frustration].normalized +
0.1 * result[TicketPriority.reportQuality].normalized
}
See Composite Scoring for more.
Levels as actions¶
When the ordered outcomes are the actions (reject, review, accept), write one level per action and round to the nearest level. There's no threshold to tune:
enum class LinkAction { LEAVE_UNLINKED, CURATOR_QUEUE, MERGE }
// When the ordered outcomes are actions, write one level per action and round to the nearest level.
suspend fun linkDecision(
jev: JevApi,
productA: String,
productB: String,
): LinkAction {
val state =
buildJsonObject {
put("entity_a", productA)
put("entity_b", productB)
}
val result =
jev.query(state = state) {
score("link", "How do the two entity descriptions relate as products?") {
level("They describe two different products.")
level("They describe closely related products that may or may not be the same one.")
level("They describe one and the same product.")
}
}
return LinkAction.entries[result.score("link").nearestLevel]
}
Don't interpolate numbers
A score of 1.5 between "$1,000" and "$10,000" doesn't mean $5,500. Score levels aren't numerically calibrated. Use scores for thresholds and ranking, and extract exact values another way; see Extraction.