Noul¶
A Noul asks whether something is true. Its answer is a single number, noul: the probability, from 0 to
1, that the answer is yes.
val result =
jev.query(state = "I have asked three times now. Can I please just talk to a real person?") {
noul("wants_human", "Is the customer asking for a human agent?")
}
val wantsHuman = result.noul("wants_human").noul // e.g. 0.99
Phrase the question so that a high value means "yes". Near 1 is a strong yes, near 0 a strong no, and near
0.5 means both are about equally likely. Nouls have no separate confidence; the probability itself is the
signal.
Defining yes and no¶
When the boundary is subtle, spell out what each outcome means with whenTrue and whenFalse:
val result =
jev.query(state = "I have asked three times now. Can I please just talk to a real person?") {
noul("repeat_contact", "Has the customer contacted support about this before?") {
whenTrue("Mentions a prior attempt, ticket, or that they have asked before")
whenFalse("No sign of any previous contact")
}
}
Try your questions with and without criteria on real inputs; sometimes the instruction alone is clearer. Keep
the criteria consistent with the instruction. A Noul whose whenTrue describes a "no" confuses the model.
Criteria can be structured, too; see Structured Criteria.
Questions or statements¶
A Noul can be a question or a statement for the model to judge. Try both on your data:
// A Noul can also be phrased as a statement to judge; a value near 1 means "true".
val result =
jev.query(state = "My card was charged twice for order A-104.") {
noul("refund_statement", "The customer is requesting a refund.")
noul("refund_question", "Is the customer requesting a refund?")
}
Turning probabilities into decisions¶
isTrue(threshold) gives a boolean. band(no, yes) gives a three-way decision, so you can send the uncertain
middle to a person instead of guessing:
val urgent = result.noul("urgent")
// A hard decision: the probability of yes exceeds a threshold you choose.
if (urgent.isTrue(threshold = 0.7)) println("page the on-call engineer")
// Three-way decision: send the uncertain middle to a person instead of guessing.
when (urgent.band(no = 0.3, yes = 0.7)) {
NoulBand.YES -> println("escalate")
NoulBand.NO -> println("normal queue")
NoulBand.UNCERTAIN -> println("human review")
}
The default band (below 0.30 is no, above 0.70 is yes) comes from TypeSafe's examples. Tune both thresholds on labeled examples from your own domain.
One Noul per item¶
Jev doesn't count reliably, and a Choice can only pick one option. When several things might each be true, ask one Noul per thing and combine the answers in code:
// Jev doesn't count reliably. Ask one Noul per item and count the answers in code.
suspend fun countFruits(
jev: JevApi,
items: List<String>,
): Int {
val state = buildJsonObject { putJsonArray("items") { addAll(items) } }
val result =
jev.query(state = state) {
items.indices.forEach { i -> noul("item_$i", "Is `items[$i]` the name of a fruit?") }
}
return items.indices.count { i -> result.noul("item_$i").isTrue() }
}
In a typed query¶
object RefundSignals : JevQuery() {
val requested by noul("Does the customer explicitly ask for a refund or credit?") {
whenTrue("Directly asks for money back or an account credit")
whenFalse("A complaint or billing question with no requested remedy")
}
val duplicateCharge by noul("Does the message describe being charged more than once for the same thing?")
}
suspend fun needsRefundReview(
jev: JevApi,
message: String,
): Boolean {
val result = jev.ask(RefundSignals, state = message)
return result[RefundSignals.requested].isTrue() || result[RefundSignals.duplicateCharge].isTrue(0.8)
}
Negations don't add up
P(refund) and P(not a refund), asked as two Nouls, needn't sum to 1, and a Noul and a yes/no Choice on
the same question can differ. Ask each question the way you mean it, and don't reuse a threshold tuned for
one form with another.