Inline DSL¶
jev.query(state) { ... } builds and sends a request in one step. Inside the block, noul, choice and
score each add a question under an id you choose:
val result =
jev.query(state = ticket) {
noul("refund", "Does the customer explicitly ask for a refund or credit?") {
whenTrue("Directly asks for money back or an account credit")
whenFalse("A complaint or question with no requested remedy")
}
choice("topic", "Which returns topic is the customer asking about?") {
"return_policy" means "Whether and how an item can be returned"
option("return_status", "Progress of a return already sent")
option("other") // no description: sent as null
}
score("severity", "How severe is the reported issue?") {
levels("Cosmetic", "Broken, but a workaround exists", "Blocking, no workaround")
}
}
result.noul("refund").isTrue()
result.choice("topic").choice
result.score("severity").score
Read answers back by id with result.noul(id), result.choice(id) and result.score(id). Asking for an
id that wasn't in the request, or reading a Noul as a Choice, throws IllegalArgumentException.
The builder functions¶
| Function | Adds | Criteria block |
|---|---|---|
noul(id, instructions) { ... } |
a Noul | optional: whenTrue(...), whenFalse(...) |
choice(id, instructions) { ... } |
a Choice with string options | "key" means "...", option(key, desc?), options(vararg keys) |
choice<E>(id, instructions) |
a Choice over the constants of an enum | none: the enum is the option list |
score(id, instructions) { ... } |
a Score | level(...), levels(vararg ...) |
question(id, question) |
a prebuilt Question value |
none |
include(query) |
every question of a JevQuery |
none |
Every instructions argument can be a String or a JsonElement; see
Structured Criteria.
Typed handles¶
Each builder function also returns a typed QuestionRef, which reads its answer without repeating the string
id:
// Builder functions return typed handles too, which avoids repeating string ids.
lateinit var refund: QuestionRef<NoulAnswer>
lateinit var tone: QuestionRef<ChoiceAnswer<String>>
val result =
jev.query(state = ticket) {
refund = noul("refund", "Does the customer explicitly ask for a refund?")
tone = choice("tone", "What is the customer's tone?") { options("calm", "frustrated", "angry") }
}
val wantsRefund: NoulAnswer = result[refund]
val customerTone: ChoiceAnswer<String> = result[tone]
For questions you reuse, a typed query is usually tidier.
Generating questions¶
The block is ordinary Kotlin, so loops and conditionals work:
// Questions are just code, so generate them. Here: one Noul per policy clause.
suspend fun violatedClauses(
jev: JevApi,
post: String,
clauses: Map<String, String>,
): List<String> {
val result =
jev.query(state = post) {
clauses.forEach { (id, clause) -> noul(id, "Does the post violate this rule: $clause?") }
}
return clauses.keys.filter { result.noul(it).isTrue(threshold = 0.7) }
}
Reusing a question set¶
questions { ... } builds a validated QuestionSet without sending it. Pass it to evaluate to ask the same
questions about many states:
// Build a QuestionSet once and evaluate it against many states.
val toneCheck =
questions {
choice("tone", "What is the customer's tone?") { options("calm", "frustrated", "angry") }
noul("threat", "Does the message threaten to cancel or leave?")
}
suspend fun tones(
jev: JevApi,
messages: List<String>,
): List<String> = messages.map { jev.evaluate(JsonPrimitive(it), toneCheck).choice("tone").choice }
Building questions directly¶
The DSL builds NoulQuestion, ChoiceQuestion and ScoreQuestion values, named after TypeSafe's JS SDK. You can
build them yourself, for example from configuration, and add them with question(id, value):
// Question values can also be built directly, e.g. from configuration or a database.
val region =
ChoiceQuestion(
instructions = JsonPrimitive("Which region is the customer writing from?"),
options =
mapOf(
"na" to JsonPrimitive("North America"),
"eu" to JsonPrimitive("Europe"),
"other" to JsonNull,
),
)
val result =
jev.query(state = "Bonjour, ma commande n'est jamais arrivée à Lyon.") {
question("region", region)
question("is_complaint", NoulQuestion(JsonPrimitive("Is the customer complaining?")))
}
Mixing in a typed query¶
include(query) adds a typed query's questions to the same request, and their handles still work
on the result:
// Mix a typed query with ad-hoc questions in one request; the typed handles still work.
val result =
jev.query(state = ticket) {
include(Triage)
noul("mentions_competitor", "Does the message mention a competing product?")
}
val team = result[Triage.team].choice
val competitor = result.noul("mentions_competitor").noul
Validation¶
jev4k checks the request before sending it and reports every problem at once in a JevValidationException:
at least one question, unique non-blank ids, non-empty instructions, 1–255 Choice options, and 2–10 Score
levels. A Choice option key offered twice, including two enum constants with the same optionKey, is
reported there too, alongside anything else that is wrong.