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 summarizes how
concentrated those probabilities are.
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. It describes the shape of the whole distribution: 0.45 with a
runner-up at 0.44 is far less decisive than 0.45 with the rest scattered thinly. See
Confidence & Thresholds.
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 (up to 255) rather than a shortlist:
// Options are cheap: offer the full list (up to 255) 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) }
"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.
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.