TypeScript SDK
The TypeScript SDK gives your Node.js code the ten functions as methods on a client; awaiting a call gives the answer itself, such as true or false from decide.
import { ThinkThen } from "thinkthen";
const tt = new ThinkThen();try { // await throws UnsureError when not sure, RecordError on failure. const isRefund = await tt.decide( "Does the customer request a refund?", "Please refund my order.", ); console.log(isRefund);} finally { await tt.close();}Output
true
Install it (Node.js 22.12 or later, in a project with "type": "module"):
npm install thinkthen@0.2.0The client uses the key you set in the Quickstart. Close it with await tt.close(), or open it with await using.
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. Awaiting a not-sure answer throws UnsureError, so it can't pass for false. This message lands between the bars:
import { ThinkThen, UnsureError } from "thinkthen";
const tt = new ThinkThen();try { const isRefund = await tt.decide( "Does the customer request a refund?", "I was charged twice for one invoice.", { threshold: { low: 0.2, high: 0.8 } }, ); console.log(isRefund ? "Send to refunds." : "Leave in the inbox.");} catch (error) { if (!(error instanceof UnsureError)) throw error; console.log("Not sure. Ask a person.");} finally { await tt.close();}Output
Not sure. Ask a person.
Send not sure to a person. To read the answer without the throw, call .row() on the call, as in await tt.decide(...).row(). The result's status is "answered", "unsure" or "failed", and TypeScript lets you read answer only once status is "answered". The error's probability and row hold the details.
When a call fails
Section titled “When a call fails”Awaiting a call that failed throws RecordError. Blank text fails before anything is sent:
import { RecordError, ThinkThen } from "thinkthen";
const tt = new ThinkThen();try { await tt.decide("Does the customer request a refund?", "");} catch (error) { if (!(error instanceof RecordError)) throw error; console.log(error.code);} finally { await tt.close();}Output
record.empty
The error's code names the problem, and row holds the failed result. When the service fails for a message, such as when it is down, that message's result fails the same way. A problem that stops the whole call throws instead: UsageError for a wrong argument, BackendError for the service, and CancelledError when an AbortSignal passed as signal fires. 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 { ThinkThen } from "thinkthen";
const tt = new ThinkThen();try { // await throws UnsureError when not sure, RecordError on failure. const isRefund = await tt.decide( "Does the customer request a refund?", "Please refund my order.", ); console.log(isRefund);} finally { await tt.close();}Output
true
The message asks for a refund, so decide says true. threshold: 0.8 moves the bar, and threshold: { low: 0.2, high: 0.8 } sets a low and a high bar, as above. decideStream judges many messages in one call. Read it with for await, and check each result's status. field: "/text" picks the field to judge in each message:
import { ThinkThen } from "thinkthen";
const messages = [ { id: "a1", text: "Please refund my order." }, { id: "a2", text: "Where is my parcel?" }, { id: "a3", text: "I was charged twice for one invoice." },];
const question = "Does the customer request a refund?";const client = new ThinkThen();try { // One stream for every message, so the engine can batch them. const refunds = client.decideStream(question, messages, { field: "/text", threshold: { low: 0.3, high: 0.7 }, }); for await (const refund of refunds) { if (refund.status === "answered") { const queue = refund.answer ? "refund queue" : "inbox"; console.log(refund.id, queue, refund.probability); } else if (refund.status === "unsure") { console.log(refund.id, "a person reads it", refund.probability); } else { console.error(refund.id, refund.error.code, refund.error.message); } }} finally { await client.close();}Output
a1 refund queue 0.99
a2 inbox 0.03
a3 refund queue 0.7
decide shows every answer.
choose
Section titled “choose”choose: one answer from your list
import { ThinkThen } from "thinkthen";
const tt = new ThinkThen();try { // await throws UnsureError when not sure, RecordError on failure. const topic = await tt.choose( "What is the message about?", "Please refund my order.", { choices: ["billing", "shipping", "other"] }, ); console.log(topic);} finally { await tt.close();}Output
billing
A refund request is about billing. The answer's type is the union of your choices. With threshold, choose is not sure when no choice reaches the bar, and awaiting it throws UnsureError. choose shows every answer.
filter
Section titled “filter”filter: keep the messages that answer yes
import { ThinkThen } from "thinkthen";
const messages = [ "Please refund my order.", "Where is my parcel?", "I was charged twice for one invoice.",];const tt = new ThinkThen();try { // Only the messages answered yes. Not sure is left out. const refundRequests = await tt.filter( "Does the customer request a refund?", messages, ).toArray(); for (const refundRequest of refundRequests) { console.log(refundRequest); }} finally { await tt.close();}Output
Please refund my order.
I was charged twice for one invoice.
filter yields your own messages that answer yes: the refund request and the double charge. Not-sure messages are left out. If a message fails, filter first yields every kept message, then throws one RecordError whose rows lists each failed result. filterStream yields every message instead, as { item, row, kept }. filter shows every answer.
tag: every label that fits
import { ThinkThen } from "thinkthen";
const tt = new ThinkThen();try { // await throws RecordError on failure. const topics = await tt.tag( "Which topics appear?", "I was charged twice and my parcel is late again.", { labels: ["billing", "shipping"] }, ); console.log(topics);} finally { await tt.close();}Output
[ 'billing', 'shipping' ]
The message names a double charge and a late parcel, so both labels fit. The array is empty when none fits. tag shows every answer.
score: a number from 0 to 1
import { ThinkThen } from "thinkthen";
const tt = new ThinkThen();try { // await throws RecordError on failure. const urgency = await tt.score( "How urgent is the request?", "Please refund my order.", { levels: ["routine", "urgent"] }, ); console.log(urgency);} finally { await tt.close();}Output
0.05
A refund request is routine, so it scores near 0. Levels run lowest first. score shows every answer.
rank: put messages in order
import { ThinkThen, answer } from "thinkthen";
const messages = [ "Please refund my order.", "Where is my parcel?", "I was charged twice for one invoice.",];const tt = new ThinkThen();try { // Most urgent first. answer() gives the rank, from 1. const byUrgency = tt.rankStream( "How urgent is the request?", messages, { levels: ["routine", "urgent"] }, ); for await (const ranked of byUrgency) { const message = messages[ranked.position - 1]; console.log(answer(ranked), message); }} finally { await tt.close();}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. rank works only as a stream, rankStream. answer(ranked) gives the rank and throws RecordError for a failed message. ranked.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 { ThinkThen } from "thinkthen";
const message = "Morgan works for Example Group.\nPlease refund my order.";const tt = new ThinkThen();try { // The line that answers, or null when none does. const action = await tt.find( "Which line states the requested action?", message, ); console.log(action ? action.text : "no line answers");} finally { await tt.close();}Output
Please refund my order.
The second line holds the request. The answer is null when no line answers. start and end count UTF-16 code units, so message.slice(action.start, action.end) gives the same text. find shows every answer.
recognize
Section titled “recognize”recognize: names and their kinds
import { ThinkThen } from "thinkthen";
const tt = new ThinkThen();try { // await throws RecordError on failure. const names = await tt.recognize( ["person", "organization"], "Morgan works for Example Group.", ); for (const name of names) console.log(name.kind, name.text);} finally { await tt.close();}Output
person Morgan
organization Example Group
Morgan is a person and Example Group an organization. The kinds come first, then the text. Each name also has start, end and probability. recognize shows every answer.
relate
Section titled “relate”relate: how names connect
import { ThinkThen } from "thinkthen";
const message = "Morgan works for Example Group.\nPlease refund my order.";const rules = [ { name: "works_for", source: "person", target: "organization", },] as const;const tt = new ThinkThen();try { const relations = await tt.relate(rules, message); for (const { source, relation, target } of relations) { console.log(source.text, relation, target.text); }} finally { await tt.close();}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. Each relation also has probability. relate shows every answer.
annotate
Section titled “annotate”annotate: several questions at once
import { ThinkThen, answer, defineToolbox } from "thinkthen";
const messages = [ "Please refund my order.", "Where is my parcel?", "I was charged twice for one invoice.",];const questions = defineToolbox({ refund: { function: "decide", question: "Does the customer request a refund?", }, topic: { function: "choose", question: "What is the message about?", choices: ["billing", "shipping", "other"], }, urgency: { function: "score", question: "How urgent is the request?", levels: ["routine", "urgent"], },});const tt = new ThinkThen();try { const forms = tt.annotateStream(questions, messages); for await (const form of forms) { // answer() throws UnsureError or RecordError. const triage = answer(form); const isRefund = answer(triage.refund); const topic = answer(triage.topic); const urgency = answer(triage.urgency); console.log(isRefund, topic, urgency); }} finally { await tt.close();}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. answer() on each entry follows the rules of its function, so a not-sure refund throws UnsureError. tt.annotate(questions, message) answers one message. annotate shows every answer.
The full API
Section titled “The full API”Each call is also exported on its own, such as decide(...). It opens and closes a client for that call, or uses one you set with setDefaultClient. The TypeScript declarations list every name.