Skip to content

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.