Skip to content

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.