Skip to content

Kotlin SDK

The Kotlin SDK gives your code the ten functions as suspend calls on a client; each returns a result whose answer() you can use directly, such as true or false from decide.

Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
ThinkThen().use { client ->
val isRefund = client.decide(
"Does the customer request a refund?",
"Please refund my order.",
).answer()
println(isRefund)
}
}

Output

true

Add it to Gradle or Maven. It includes the native engine:

Terminal window
io.github.botassembly:thinkthen-kotlin:0.2.0

It needs Kotlin/JVM on Java 22 or later. Set the Kotlin JVM target to 22, and run with --enable-native-access=ALL-UNNAMED. The client uses the key you set in the Quickstart.

Give decide a low and a high bar, and a text that falls between them is not sure. answer() then throws UnsureException, so not sure can't pass for a no. This message lands between the bars:

Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
ThinkThen().use { client ->
val isRefund = client.decide(
"Does the customer request a refund?",
"I was charged twice for one invoice.",
threshold = Threshold.band(0.2, 0.8),
)
try {
if (isRefund.answer()) println("Send to refunds.")
else println("Leave in the inbox.")
} catch (notSure: UnsureException) {
println("Not sure. Ask a person.")
}
}
}

Output

Not sure. Ask a person.

Send not sure to a person. To branch without the exception, check isUnsure first, or use when on the result: it is a Row.Answered, a Row.Unsure or a Row.Failed. UnsureException is a NoSuchElementException and carries the probability.

Blank text throws ThinkThenException at once, and nothing is sent:

Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
ThinkThen().use { client ->
try {
client.decide("Does the customer request a refund?", "")
} catch (failure: ThinkThenException) {
println(failure.code.wireName)
}
}
}

Output

record.empty

Any other failure, such as the service being down, comes back as a Row.Failed, with isFailed set. Its answer() throws ThinkThenException, whose code.wireName names the problem. Passing a List or a Flow to a one-message call does not compile; the compiler names the Stream call to use instead. Troubleshooting lists each code and its fix.

Each example asks about the same support-inbox messages as the function pages.

decide: yes, no or not sure
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
ThinkThen().use { client ->
val isRefund = client.decide(
"Does the customer request a refund?",
"Please refund my order.",
).answer()
println(isRefund)
}
}

Output

true

The message asks for a refund, so decide says true. threshold = Threshold.cut(0.8) moves the bar, and Threshold.band(0.2, 0.8) sets a low and a high bar, as above. field = Pointer("/text") picks the field to judge in a JSON message. client.decideStream(question, messages) judges a whole list and gives a Flow of results, one per message. decide shows every answer.

choose: one answer from your list
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
ThinkThen().use { client ->
val topic = client.choose(
"What is the message about?",
"Please refund my order.",
listOf("billing", "shipping", "other"),
).answer()
println(topic)
}
}

Output

billing

A refund request is about billing, so answer() is the String billing. With a threshold, choose is not sure when no choice reaches the bar, and answer() throws UnsureException. choose shows every answer.

filter: keep the messages that answer yes
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val messages = listOf(
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
)
ThinkThen().use { client ->
client.filter(
"Does the customer request a refund?", messages,
).collect { refund -> println(refund) }
}
}

Output

Please refund my order.
I was charged twice for one invoice.

filter gives a Flow of your own messages that answer yes: the refund request and the double charge. Not-sure messages are left out. If a message fails, the kept messages still arrive, then the flow throws one ThinkThenException whose failures lists each failed result. client.filterRows(...) gives every message instead, with item, kept and answer(). filter shows every answer.

tag: every label that fits
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
ThinkThen().use { client ->
val topics = client.tag(
"Which topics appear?",
"I was charged twice and my parcel is late again.",
listOf("billing", "shipping"),
).answer()
println(topics)
}
}

Output

[billing, shipping]

The message names a double charge and a late parcel, so both labels fit. The answer is a List<String>, empty when none fits. tag shows every answer.

score: a number from 0 to 1
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
ThinkThen().use { client ->
val urgency = client.score(
"How urgent is the request?",
"Please refund my order.",
listOf("routine", "urgent"),
).answer()
println(urgency)
}
}

Output

0.05

A refund request is routine, so it scores near 0. The answer is a Double, and levels run lowest first. score shows every answer.

rank: put messages in order
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val messages = listOf(
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
)
ThinkThen().use { client ->
client.rank(
"How urgent is the request?", messages,
listOf("routine", "urgent"),
).collect { ranked ->
val message = messages[ranked.position.toInt() - 1]
println("${ranked.answer()} $message")
}
}
}

Output

1 I was charged twice for one invoice.
2 Where is my parcel?
3 Please refund my order.

The double charge is the most urgent, so it ranks 1. answer() is the rank, a Long from 1, and position is the message's place in your list, counted from 1. top = 2 ranks only the first two. rank shows every answer.

find: the line that answers
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val message =
"Morgan works for Example Group.\nPlease refund my order."
ThinkThen().use { client ->
val action = client.find(
"Which line states the requested action?", message,
).answer()
println(action?.text ?: "no line answers")
}
}

Output

Please refund my order.

The second line holds the request. The answer is a Span? with text, start and end, counted as Kotlin counts a String. It is null when no line answers. find shows every answer.

recognize: names and their kinds
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
ThinkThen().use { client ->
val names = client.recognize(
"Morgan works for Example Group.",
listOf("person", "organization"),
).answer()
for (name in names) {
val mention = name.mention
println("${mention.kind} ${mention.span.text}")
}
}
}

Output

person Morgan
organization Example Group

Morgan is a person and Example Group an organization. Each name is an Entity with a mention, holding its kind and span, and a probability. recognize shows every answer.

relate: how names connect
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val message =
"Morgan works for Example Group.\nPlease refund my order."
val worksFor =
RelationRule("person", "organization", "works_for")
ThinkThen().use { client ->
val relations =
client.relate(message, listOf(worksFor)).answer()
for (relation in relations) {
val source = relation.source.span.text
val target = relation.target.span.text
println("$source ${relation.relation} $target")
}
}
}

Output

Morgan works_for Example Group

Morgan works for Example Group, so relate links the two by the rule. relate finds the names itself, so it needs no recognize call first. RelationRule also takes reads and either = true. relate shows every answer.

annotate: several questions at once
Main.kt
import dev.thinkthen.kotlin.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val messages = listOf(
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
)
val questions = AnnotationQuestions(mapOf(
"refund" to Decide("Does the customer request a refund?"),
"topic" to Choose("What is the message about?",
listOf("billing", "shipping", "other")),
"urgency" to Score("How urgent is the request?",
listOf("routine", "urgent")),
))
ThinkThen().use { client ->
client.annotateStream(questions, messages).collect { form ->
val answers = form.answer()
val isRefund = answers.decide("refund").answer()
val topic = answers.choose("topic").answer()
val urgency = answers.score("urgency").answer()
println("$isRefund $topic $urgency")
}
}
}

Output

true billing 0.05
false shipping 0.14
true billing 0.45

Each message gets all three answers: refund or not, the topic, and its urgency. answers.decide("refund") follows the rules of decide, so its answer() throws UnsureException when not sure. It throws ThinkThenException for a name you never added; decideOrNull("refund") returns null instead. client.annotate(questions, message) answers one message. annotate shows every answer.

Every one-message call has a Stream form, such as decideStream, that takes an Iterable or a Flow. Close the client with use, which also cancels its running flows. The Kotlin source lists every name.