Quick Start¶
Your first query¶
With TYPESAFE_API_KEY set, this complete program asks three questions about a support message in one
request:
import com.pambrose.jev4k.JevClient
import com.pambrose.jev4k.query
suspend fun main() {
// Reads the API key from the TYPESAFE_API_KEY environment variable.
JevClient().use { jev ->
val ticket = "Hi, my Stripe connection has failed for 3 days and I'm losing sales. Help ASAP!"
val result = jev.query(state = ticket) {
noul("urgent", "Does this message convey urgency or time-sensitivity?")
choice("department", "Which team should handle this?") {
"billing" means "Payment or subscription issues"
"technical" means "Bugs or integration problems"
"sales" means "Pricing or account questions"
}
score("frustration", "How frustrated does the customer appear?") {
levels("Calm, just stating facts", "Frustrated but civil", "Very angry, strong language")
}
}
println(result.noul("urgent").noul) // e.g. 0.999: probability of yes
println(result.choice("department").choice) // e.g. "technical"
println(result.score("frustration").score) // e.g. 1.04: position along the three levels
}
}
A few things to notice:
- One request, three answers. The questions are answered in parallel against the same state, so adding a question barely changes latency.
- Ids are for your code.
"urgent","department"and"frustration"label the answers. The model never sees them, so each instruction states the whole question. - Answers are typed.
noul(...)returns aNoulAnswer,choice(...)aChoiceAnswer<String>, andscore(...)aScoreAnswer. - Close the client when you're done.
use { }does it for you.
A reusable, typed query¶
For questions you ask repeatedly, declare them once as properties of a JevQuery. Each property name becomes
the question id, and the property is a typed handle to its answer. Here the team is an enum:
enum class Team(
override val description: String,
) : JevOption {
BILLING("Payments, invoicing, refunds"),
TECHNICAL("Bugs, outages, integrations"),
SALES("Pricing, upgrades, new accounts"),
}
object FirstTriage : JevQuery() {
val urgent by noul("Does this message convey urgency or time-sensitivity?")
val team by choice<Team>("Which team should handle this message?")
val frustration by score("How frustrated does the customer appear?") {
levels("Calm, just stating facts", "Frustrated but civil", "Very angry, strong language")
}
}
suspend fun routeTicket(
jev: JevApi,
ticket: String,
): String {
val result = jev.ask(FirstTriage, state = ticket)
val queue =
when (result[FirstTriage.team].choice) {
Team.BILLING -> "billing"
Team.TECHNICAL -> "engineering"
Team.SALES -> "sales"
}
val priority = if (result[FirstTriage.urgent].isTrue(threshold = 0.7)) "high" else "normal"
return "$queue ($priority)"
}
result[FirstTriage.team].choice is a Team, so the when is exhaustive: add a constant to Team and the
compiler points at every place that needs to handle it.
Without coroutines¶
jev4k's calls are suspend functions. From ordinary code (a script, main, a Java caller), use the blocking
mirror on jev.blocking:
fun routeTicketBlocking(ticket: String): String =
JevClient().use { jev ->
val result = jev.blocking.ask(FirstTriage, state = ticket)
result[FirstTriage.team].choice.name
}
Where to next¶
- Choosing a question type: Noul, Choice, or Score?
- Inline DSL and Typed Queries: the two ways to build requests.
- Reading Results: everything a
JevResultgives you. - Patterns: end-to-end examples of common designs.