Skip to content

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 a NoulAnswer, choice(...) a ChoiceAnswer<String>, and score(...) a ScoreAnswer.
  • 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