Choice¶
A Choice picks one option from a set you define. Its answer has the most likely option (choice), the
probability of every option (probabilities, summing to 1), and a confidence from 0 to 1 that says how far the
winner sits above an even split.
val result =
jev.query(state = "My running shoes arrived in the wrong size. Can I swap them for a size 10?") {
choice("department", "Which team should handle this?") {
"returns" means "Exchanges, refunds, wrong or damaged items"
"shipping" means "Delivery status, delays, lost packages"
"billing" means "Charges, invoices, payment problems"
}
}
val department = result.choice("department")
println(department.choice) // "returns"
println(department.confidence) // e.g. 1.0: all probability on one option
Option names and their descriptions are both sent to the model, so write descriptions that separate the options from each other.
Undescribed options¶
When an option's name says it all, leave it undescribed; it's sent as null:
// When the option names say it all, leave them undescribed; they are sent as null.
val result =
jev.query(state = "This is the third time I'm writing. Fix it or I'm cancelling.") {
choice("tone", "What is the customer's tone?") {
options("calm", "frustrated", "angry")
}
}
In a builder, "key" means "description", option(key, description), option(key) and options(vararg keys)
all add options. Option keys must be unique.
Always offer a way out¶
Some option always wins a Choice, because the probabilities sum to 1. If the list might not cover every input,
add an explicit other or none option:
// Add an explicit way out when the list might not cover every input.
val result =
jev.query(state = "What's your company's policy on remote work?") {
choice("meeting_type", "What type of meeting is this, based on the title and description?") {
options("standup", "planning", "retrospective", "one_on_one")
"none" means "Not a meeting, or a kind of meeting not listed here"
}
}
if (result.choice("meeting_type").choice == "none") println("not a meeting")
To know whether any option applies, pair the Choice with an independent Noul; see Search & Ranking.
Reading the distribution¶
The runner-up often matters as much as the winner:
fun describe(result: JevResult) {
val department = result.choice("department")
println(department.choice) // most likely option
println(department.topProbability) // its probability, e.g. 0.60
println(department.probability("billing")) // any option's probability (0.0 if absent)
// Options from most to least likely.
department.ranked().forEach { (option, p) -> println("$option: $p") }
// A second team with a real share of the probability gets a copy.
department.probabilities
.filter { (team, p) -> team != department.choice && p > 0.25 }
.forEach { (team, _) -> routeTo(team, "cc") }
}
confidence is not the winner's probability, but it depends on nothing else: it rescales that probability so
that an even split across the options is 0 and certainty is 1. A winner at 0.45 with a runner-up at 0.44 gets the
same confidence as a winner at 0.45 with the rest scattered thinly, so when the runner-up matters, read it from
ranked() or probabilities, as above. See Confidence & Thresholds.
A string-keyed Choice returns the option the server chose as it was sent, even one the question didn't declare, as
both official SDKs do. An enum-backed Choice can't hold an undeclared option, so reading one
throws a JevResponseValidationException instead.
Speculative questions¶
Ask the follow-up Choices you might need in the same request, and let code read only the relevant one:
val result =
jev.query(state = "Shoes arrived two weeks late and in the wrong size. Also I see two charges on my card.") {
choice("department", "Which team should handle this?") {
"returns" means "Exchanges, refunds, wrong or damaged items"
"shipping" means "Delivery status, delays, lost packages"
"billing" means "Charges, invoices, payment problems"
}
// Speculative: only used if the department is "returns".
choice("return_reason", "If the customer wants to return something, why?") {
"wrong_size" means "The item doesn't fit"
"damaged" means "The item arrived broken or faulty"
"changed_mind" means "The item is fine, the customer no longer wants it"
"other" means "A return reason that fits none of the above"
}
// Speculative: only used if the department is "shipping".
choice("shipping_issue", "If this is a shipping problem, which kind is it?") {
"not_delivered" means "The package never arrived"
"delayed" means "The package is late but still on its way"
"other" means "A shipping problem that fits none of the above"
}
}
val department = result.choice("department")
when (department.choice) {
"returns" -> routeTo("returns", result.choice("return_reason").choice)
"shipping" -> routeTo("shipping", result.choice("shipping_issue").choice)
else -> routeTo("billing", "charges")
}
This costs a few extra input tokens and saves a second round trip. See Speculative Fan-Out.
Many options¶
Options are cheap. Give the model the full list of categories, teams or products rather than a shortlist. A
Choice takes up to 255 options, so a list that gets an other added can hold up to 254:
// Options are cheap: offer the full list (up to 254, leaving room for "other") rather than a shortlist.
suspend fun categorize(
jev: JevApi,
listing: String,
categories: List<String>,
): String {
val result =
jev.query(state = listing) {
choice("category", "Which product category does this listing belong to?") {
categories.forEach { option(it) }
if ("other" !in categories) "other" means "None of the listed categories fits"
}
}
return result.choice("category").choice
}
TypeSafe reports that a Choice works reliably up to about 240 options. For larger sets, go level by level through a hierarchy or rank in stages; see Classification.
Order matters too: TypeSafe has seen jev-1.13 lean toward the option that comes first. For a decision that counts,
check that the answer holds with the options reordered. An enum-backed Choice offers its constants in declaration
order, so build that check from a reordered list of string options.
Enum options¶
choice<E>() offers every constant of an enum and answers with the constant itself:
enum class Team(
override val description: String,
) : JevOption {
BILLING("Payments, invoicing, refunds"),
TECHNICAL("Bugs, outages, integrations"),
SALES("Pricing, upgrades, new accounts"),
}
object Triage : JevQuery() {
val urgent by noul("Does this message convey urgency or time-sensitivity?") {
whenTrue("Explicitly time-sensitive, or the customer is blocked right now")
whenFalse("No urgency expressed")
}
val team by choice<Team>("Which team should handle this message?")
val frustration by score("How frustrated does the customer appear?") {
level("Calm, just stating facts")
level("Frustrated but civil")
level("Very angry, strong language or threatening to leave")
}
}
See Enum Choices for descriptions, custom option keys and structured entries.