Structured Criteria¶
Instructions, Choice option descriptions, Score levels, and Noul whenTrue/whenFalse criteria all accept
JSON, not just strings. Start with plain strings. Reach for structure when guidance would otherwise blur
together in a dense sentence:
- what an option covers, what it doesn't, and a few examples
- a field being checked, described by name, type and unit
- supporting data that's already structured, such as a schema, a taxonomy or a database row
The field names are yours; none are reserved. The model reads both names and values, so use short names that label what follows.
Helpers¶
| Helper | Builds |
|---|---|
entry("a" to x, "b" to y) |
a JSON object from pairs, keeping their order; values can be strings, numbers, booleans, lists, maps, or JSON |
rubric(what, notFor, examples) |
{"what", "not_for", "examples"}, the contrastive shape TypeSafe recommends |
jsonOf(value) |
JSON from plain Kotlin values |
jsonEntry(value) |
JSON from any @Serializable value |
kotlinx.serialization's buildJsonObject { } works as well.
Structured instructions¶
val result =
jev.query(state = "I ordered the standing desk two weeks ago and tracking still says label created.") {
choice(
"department",
entry(
"question" to "Which team should handle this message?",
"focus" to "Classify the customer's primary request, not every topic mentioned.",
),
) {
options("billing", "orders", "account")
}
}
Contrastive options¶
When the model confuses two options, tell it what each one isn't:
// Contrastive descriptions sharpen the boundary between options that are easy to confuse.
val result =
jev.query(state = "I sent the shoes back a week ago. When do I get my money?") {
choice("return_topic", "Which returns topic is the customer asking about?") {
"return_policy" means
rubric(
what = "Whether and how an item can be returned",
notFor = "Progress of a return already sent",
examples = listOf("Can I return shoes I've worn once?", "How long do I have to return it?"),
)
"return_status" means
rubric(
what = "Progress of a return already sent",
notFor = "Whether and how an item can be returned",
examples = listOf("Has my return arrived yet?", "When will my refund be paid?"),
)
}
}
Use the same field names on every option, so the model compares like with like.
Describing a field¶
A single field object can drive several kinds of question. Here it drives a Noul that verifies a value and a
Score that buckets a magnitude:
// One `field` object describes the value being checked; each question refers to it by key.
val invoiceNumber =
entry("name" to "invoice_number", "type" to "string", "description" to "The identifier printed on the invoice.")
val amountDue =
entry("name" to "amount_due", "type" to "number", "unit" to "USD", "description" to "The total to be paid.")
val state = buildJsonObject { put("source_text", "Invoice #4471 issued March 3, 2026 for $12,840.00, net 30.") }
val result =
jev.query(state = state) {
noul(
"invoice_number_ok",
entry(
"field" to invoiceNumber,
"extracted_value" to "4471",
"question" to "Does `extracted_value` match the `field` as it appears in `source_text`?",
),
)
score("amount_band", entry("field" to amountDue, "question" to "How large is `field` in `source_text`?")) {
levels("Under $1,000", "$1,000 to $10,000", "$10,000 to $100,000", "Over $100,000")
}
}
A taxonomy as option values¶
An option's description can be its whole subtree, so the model can see what lives under a branch before committing to it:
// An option's description can be its whole subtree, so the model sees what lives under each branch.
val result =
jev.query(state = "32oz plastic bottle with a flip straw lid. Fits most bike cages.") {
choice("department", "Which top-level department does this product belong to?") {
"Sporting Goods" means
jsonOf(
mapOf(
"Cycling" to listOf("Bike Bottles & Cages", "Bike Lights", "Helmets"),
"Outdoor" to listOf("Tents", "Sleeping Bags", "Hydration Packs"),
),
)
"Home & Kitchen" means
jsonOf(
mapOf(
"Drinkware" to listOf("Water Bottles", "Travel Mugs"),
"Cookware" to listOf("Pots & Pans", "Bakeware"),
),
)
}
}
For a deep tree, walk it one level per request; see Classification.
Noul criteria¶
val result =
jev.query(state = "Your Q3 bonus is ready. Reply with your login password so we can release the funds.") {
noul(
"requests_credentials",
entry(
"question" to "Does the message ask the recipient to disclose a sensitive credential?",
"focus" to "Look for a request to send the credential itself, not to change or reset it.",
),
) {
whenTrue(
rubric(
what = "Asks the recipient to reply with or send a password, PIN, or one-time code",
examples = listOf("Reply with your password", "Send us the 6-digit code"),
),
)
whenFalse(
rubric(
what = "No sensitive credential is requested",
examples = listOf("Reset your password from the settings page"),
),
)
}
}
@Serializable values¶
jsonEntry turns any @Serializable value into structured JSON, keeping fields that equal their defaults:
@Serializable
data class FieldSpec(
val path: String,
val type: String,
val description: String,
val required: Boolean = true,
)
suspend fun checkField(
jev: JevApi,
sourceText: String,
spec: FieldSpec,
value: String,
): Boolean {
val result =
jev.query(state = sourceText) {
noul(
"unsupported",
entry(
"field_spec" to jsonEntry(spec), // any @Serializable value becomes structured JSON
"extracted_field" to value,
"main_question" to "Is the `extracted_field` unsupported by, or absent from, the source text?",
),
)
}
return !result.noul("unsupported").isTrue(threshold = 0.7)
}
Pointing at part of the state¶
When the state is a JSON object with several parts, name the part a question is about with a backticked
dot-and-index path, such as `ticket.messages[0].text`.
See State.