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.
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:
io.github.botassembly:thinkthen-kotlin:0.2.0It 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.
When decide is not sure
Section titled “When decide is not sure”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:
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.
When a call fails
Section titled “When a call fails”Blank text throws ThinkThenException at once, and nothing is sent:
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.
The ten functions
Section titled “The ten functions”Each example asks about the same support-inbox messages as the function pages.
decide
Section titled “decide”decide: yes, no or not sure
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
Section titled “choose”choose: one answer from your list
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
Section titled “filter”filter: keep the messages that answer yes
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
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
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
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
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
Section titled “recognize”recognize: names and their kinds
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
Section titled “relate”relate: how names connect
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
Section titled “annotate”annotate: several questions at once
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.
The full API
Section titled “The full API”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.