Typed Queries¶
A JevQuery declares questions as properties. Each property name becomes the question id, and each property
is a typed handle (QuestionRef<A>) that reads its answer back from a result.
object Triage : JevQuery() {
val urgent by noul("Does this message convey urgency or time-sensitivity?") {
whenTrue("Explicitly time-sensitive, or the customer is blocked right now")
whenFalse("No urgency expressed")
}
val team by choice<Team>("Which team should handle this message?")
val frustration by score("How frustrated does the customer appear?") {
level("Calm, just stating facts")
level("Frustrated but civil")
level("Very angry, strong language or threatening to leave")
}
}
Team is an ordinary enum; see Enum Choices.
Asking and reading¶
jev.ask(query, state) sends every question in the query as one request. result[handle] returns the answer,
already typed:
val result = jev.ask(Triage, state = ticket)
val urgent: NoulAnswer = result[Triage.urgent]
val team: ChoiceAnswer<Team> = result[Triage.team] // team.choice is a Team
val frustration: ScoreAnswer = result[Triage.frustration]
println("${team.choice} urgent=${urgent.noul} frustration=${frustration.normalized}")
A handle only works on results of requests that included it. Reading one from an unrelated result throws
IllegalArgumentException.
The builder functions¶
A JevQuery has the same builders as the inline DSL, minus the id argument, which comes from the
property name:
| Function | Returns a handle to |
|---|---|
noul(instructions) { ... } |
NoulAnswer |
choice(instructions) { ... } |
ChoiceAnswer<String> |
choice<E>(instructions) |
ChoiceAnswer<E> |
score(instructions) { ... } |
ScoreAnswer |
Custom ids¶
id = sends a different id than the property name:
object Sentiment : JevQuery() {
// The property name is the default id; `id =` sends something else.
val tone by choice("What is the customer's tone?", id = "customer_tone") {
options("calm", "frustrated", "angry")
}
}
Inheritance¶
Queries can extend other queries. The base class's questions come first, then the subclass's, each in declaration order:
open class BaseChecks : JevQuery() {
val spam by noul("Is this message unsolicited advertising or spam?")
val abusive by noul("Does this message contain abusive or threatening language?")
}
// Subclass questions come after the base class's, in declaration order.
object ForumPostChecks : BaseChecks() {
val offTopic by noul("Is the post unrelated to software development?")
}
Parameterized queries¶
A query can be a class, so its questions can depend on runtime values. Here there's one query per policy:
// A query can be a class, so its questions can depend on runtime values.
class PolicyCheck(
policy: String,
) : JevQuery() {
val violates by noul("Does the message violate this policy: $policy")
val severity by score("How serious is the violation of this policy: $policy") {
levels("No violation", "Minor, a reminder is enough", "Serious, needs moderator action")
}
}
val policyChecks = listOf("No personal data", "No medical advice").map(::PolicyCheck)
suspend fun policyViolations(
jev: JevApi,
message: String,
): List<Double> = policyChecks.map { check -> jev.ask(check, state = message)[check.violates].noul }
Combining with ad-hoc questions¶
include() inside an inline query asks a typed query's questions alongside one-off questions, in one request:
// 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
Many states¶
The same query works for any number of states. Here they run concurrently:
// The same query works for any number of states; run them concurrently if you like.
suspend fun triageAll(
jev: JevApi,
tickets: List<String>,
): Map<String, Team> =
coroutineScope {
tickets
.map { ticket -> async { ticket to jev.ask(Triage, state = ticket)[Triage.team].choice } }
.awaitAll()
.toMap()
}
Keep concurrency modest on a shared key; see Making Calls.
When definitions are checked¶
A query's questions are validated the first time they're used, typically on the first ask, not when the
object is created. Invalid definitions, such as blank instructions or duplicate ids, surface as a
JevValidationException listing every problem.