Skip to content

Python SDK

The Python SDK gives your code the ten functions as plain Python calls, such as tt.decide; each returns an answer you can use directly, such as True or False from decide.

import thinkthen as tt
is_refund = tt.decide(
"Does the customer request a refund?",
"Please refund my order.",
)
print(is_refund)

Output

True

Install it (Python 3.10 or later):

Terminal window
python -m pip install "thinkthen==0.2.0"

The calls use 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. Using a not-sure answer as yes or no raises tt.Unsure, so it can't pass for a no. This message lands between the bars:

import thinkthen as tt
is_refund = tt.decide(
"Does the customer request a refund?",
"I was charged twice for one invoice.",
threshold=(0.2, 0.8),
)
try:
if is_refund:
print("Send to refunds.")
else:
print("Leave in the inbox.")
except tt.Unsure:
print("Not sure. Ask a person.")

Output

Not sure. Ask a person.

Send not sure to a person. To branch without the exception, check is_refund.unsure first. Comparing a not-sure answer, as in is_refund == True, raises tt.Unsure too.

Blank text raises tt.RecordError at once, and nothing is sent:

import thinkthen as tt
try:
tt.decide("Does the customer request a refund?", "")
except tt.RecordError as failure:
print(failure.code)

Output

record.empty

Any other failure, such as the service being down, comes back as a result with failed set to True. Using it as an answer raises tt.RecordError, whose code names the problem. Call result.raise_for_error() to raise it sooner. A wrong argument raises tt.UsageError. 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
import thinkthen as tt
is_refund = tt.decide(
"Does the customer request a refund?",
"Please refund my order.",
)
print(is_refund)

Output

True

The message asks for a refund, so decide says True. threshold=0.8 moves the bar, and threshold=(0.2, 0.8) sets a low and a high bar, as above. field="/text" picks the field to judge when a message is a dict. tt.decide_stream(question, messages) judges a whole list and yields one result per message. decide shows every answer.

choose: one answer from your list
import thinkthen as tt
topic = tt.choose(
"What is the message about?",
"Please refund my order.",
choices=["billing", "shipping", "other"],
)
print(topic)

Output

billing

A refund request is about billing. Compare the answer with topic == "billing"; if topic: raises TypeError, because choose is not a yes-or-no answer. With threshold=, choose is not sure when no choice reaches the bar, and the comparison then raises tt.Unsure, so check topic.unsure first. choose shows every answer.

filter: keep the messages that answer yes
import thinkthen as tt
messages = [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
refund_requests = list(
tt.filter("Does the customer request a refund?", messages)
)
for refund_request in refund_requests:
print(refund_request)

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 raises one tt.RecordError whose rows lists each failed result. tt.filter_rows(...) yields a result for every message instead, with kept and item. filter shows every answer.

tag: every label that fits
import thinkthen as tt
topics = tt.tag(
"Which topics appear?",
"I was charged twice and my parcel is late again.",
labels=["billing", "shipping"],
)
print(list(topics))

Output

['billing', 'shipping']

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

score: a number from 0 to 1
import thinkthen as tt
urgency = tt.score(
"How urgent is the request?",
"Please refund my order.",
levels=["routine", "urgent"],
)
print(urgency)

Output

0.05

A refund request is routine, so it scores near 0. Levels run lowest first. float(urgency) gives the number, and urgency > 0.5 compares it. score shows every answer.

rank: put messages in order
import thinkthen as tt
messages = [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
by_urgency = tt.rank(
"How urgent is the request?",
messages,
levels=["routine", "urgent"],
).to_list()
for rank in by_urgency:
print(rank, messages[rank.position - 1])

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.position is the message's place in your list, counted from 1, and rank.item is the message itself. top=2 ranks only the first two. rank shows every answer.

find: the line that answers
import thinkthen as tt
message = "Morgan works for Example Group.\nPlease refund my order."
action = tt.find(
"Which line states the requested action?", message
)
print(action)

Output

Please refund my order.

The second line holds the request. action.start and action.end give where it sits, counted in characters. action.answer is None when no line answers. With threshold=, find is not sure when the best line falls below the bar, and printing it raises tt.Unsure. find shows every answer.

recognize: names and their kinds
import thinkthen as tt
names = tt.recognize(
"Morgan works for Example Group.",
kinds=["person", "organization"],
)
for name in names:
print(name.kind, name.text)

Output

person Morgan
organization Example Group

Morgan is a person and Example Group an organization. Each name also has start, end and probability. Give a list of dicts to recognize_stream, with field="/text", to read many documents and keep each one's id:

import thinkthen as tt
documents: list[tt.Record] = [
{"id": "memo-1", "text": "Ines Costa joined Example Corp."},
{"id": "memo-2", "text": "Tailspin hired Morgan Lee."},
]
with tt.recognize_stream(
documents, kinds=["person", "organization"], field="/text"
) as recognitions:
for recognized in recognitions:
for name in recognized:
print(
recognized.id, name.kind, repr(name.text),
name.start, name.end,
)

Output

memo-1 person 'Ines Costa' 0 10
memo-1 organization 'Example Corp' 18 30
memo-2 organization 'Tailspin' 0 8
memo-2 person 'Morgan Lee' 15 25

recognize shows every answer.

relate: how names connect
import thinkthen as tt
message = "Morgan works for Example Group.\nPlease refund my order."
rules = [
tt.RelationRule(
source="person", target="organization", relation="works_for"
)
]
relations = tt.relate(message, rules=rules)
for relation in relations:
print(relation.source.text, relation.relation, relation.target.text)

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: several questions at once
import thinkthen as tt
messages = [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
questions = {
"refund": tt.DecideQuestion("Does the customer request a refund?"),
"topic": tt.ChooseQuestion(
"What is the message about?",
choices=["billing", "shipping", "other"],
),
"urgency": tt.ScoreQuestion(
"How urgent is the request?",
levels=["routine", "urgent"],
),
}
forms = tt.annotate_stream(questions, messages).to_list()
for form in forms:
print(form["refund"], form["topic"], form["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. form["refund"] follows the rules of decide, so a not-sure answer inside it raises tt.Unsure too. tt.annotate(questions, message) answers one message. annotate shows every answer.

import thinkthen.pandas adds a .thinkthen accessor to every Series and DataFrame. A column goes in, and an answer column comes out on the same index. Install it with python -m pip install "thinkthen[pandas]==0.2.0".

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame({"message": ["Please refund my order."]})
messages["is_refund"] = messages["message"].thinkthen.decide(
"Does the customer request a refund?"
)
print(messages)

Output

                   message  is_refund
0  Please refund my order.       True

Not sure is <NA>. A mask that holds <NA> leaves those messages out, as if they were a no, so take the not-sure messages out first:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
messages["is_refund"] = messages["message"].thinkthen.decide(
"Does the customer request a refund?",
threshold=(0.2, 0.8),
)
print(messages)
not_sure = messages["is_refund"].isna()
print("Ask a person:", messages.loc[not_sure, "message"].tolist())

Output

                                message  is_refund
0               Please refund my order.       True
1                   Where is my parcel?      False
2  I was charged twice for one invoice.       <NA>
Ask a person: ['I was charged twice for one invoice.']
show.py:1: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd

A missing cell fails and sends nothing. The accessor answers every other message, then raises tt.RecordError. Its outcomes holds every message's result, on the input's index:

import pandas as pd
import thinkthen as tt
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame({"message": ["Please refund my order.", None]})
try:
messages["is_refund"] = messages["message"].thinkthen.decide(
"Does the customer request a refund?"
)
except tt.RecordError as failure:
outcomes = failure.outcomes
print(outcomes[["status", "answer", "error_code"]])

Output

     status  answer      error_code
0  answered    True             NaN
1    failed    <NA>  record.missing
show.py:1: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd
Every function on a pandas column

choose gives a string column:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame({"message": ["Please refund my order."]})
messages["topic"] = messages["message"].thinkthen.choose(
"What is the message about?",
choices=["billing", "shipping", "other"],
)
print(messages)

Output

                   message    topic
0  Please refund my order.  billing

filter gives your own messages that answer yes, with their index. Not-sure messages are left out:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
refund_requests = messages["message"].thinkthen.filter(
"Does the customer request a refund?"
)
print(refund_requests)

Output

0                 Please refund my order.
2    I was charged twice for one invoice.
Name: message, dtype: object

On a DataFrame, field picks the column to judge, and filter keeps every column:

import pandas as pd
import thinkthen.pandas # registers the .thinkthen accessor
messages = pd.read_csv("messages.csv")
refund_requests = messages.thinkthen.filter(
"Does the customer request a refund?",
field="/text",
)
print(refund_requests)

Output

   id                                  text
0  a1               Please refund my order.
2  a3  I was charged twice for one invoice.
show.py:1: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd

tag gives a tuple of labels per message:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{"message": ["I was charged twice and my parcel is late again."]}
)
messages["topics"] = messages["message"].thinkthen.tag(
"Which topics appear?",
labels=["billing", "shipping"],
)
print(messages[["topics"]])

Output

                topics
0  (billing, shipping)

score gives a Float64 column:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame({"message": ["Please refund my order."]})
messages["urgency"] = messages["message"].thinkthen.score(
"How urgent is the request?",
levels=["routine", "urgent"],
)
print(messages)

Output

                   message  urgency
0  Please refund my order.     0.05

rank gives an Int64 column, 1 for the highest:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
messages["urgency_rank"] = messages["message"].thinkthen.rank(
"How urgent is the request?",
levels=["routine", "urgent"],
)
print(messages.sort_values("urgency_rank"))

Output

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

find gives a span per message, or <NA> when no line answers or find is not sure:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{
"message": [
"Morgan works for Example Group.\nPlease refund my order."
]
}
)
actions = messages["message"].thinkthen.find(
"Which line states the requested action?"
)
# NA when no line answers or find is not sure.
for action in actions:
if action is pd.NA:
print("no answer")
else:
print(action.text)

Output

Please refund my order.
show.py:1: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd

recognize gives a tuple of names per message:

import pandas as pd
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{"message": ["Morgan works for Example Group."]}
)
names = messages["message"].thinkthen.recognize(
kinds=["person", "organization"]
)
for name in names.explode():
print(name.kind, name.text)

Output

person Morgan
organization Example Group
show.py:1: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd

relate gives a tuple of relations per message:

import pandas as pd
import thinkthen as tt
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{
"message": [
"Morgan works for Example Group.\nPlease refund my order."
]
}
)
rules = [
tt.RelationRule(
source="person", target="organization", relation="works_for"
)
]
relations = messages["message"].thinkthen.relate(
rules=rules
)
for relation in relations.explode():
print(relation.source.text, relation.relation, relation.target.text)

Output

Morgan works_for Example Group
show.py:1: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd

annotate gives one mapping per message, which apply(pd.Series) spreads into columns:

import pandas as pd
import thinkthen as tt
import thinkthen.pandas # adds the .thinkthen accessor
messages = pd.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
questions = {
"refund": tt.DecideQuestion("Does the customer request a refund?"),
"topic": tt.ChooseQuestion(
"What is the message about?",
choices=["billing", "shipping", "other"],
),
"urgency": tt.ScoreQuestion(
"How urgent is the request?",
levels=["routine", "urgent"],
),
}
forms = messages["message"].thinkthen.annotate(questions)
print(messages.join(forms.apply(pd.Series)))

Output

                                message  refund     topic  urgency
0               Please refund my order.    True   billing     0.05
1                   Where is my parcel?   False  shipping     0.14
2  I was charged twice for one invoice.    True   billing     0.45
show.py:1: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd

details=True returns each message's full result instead of the answer: its status, answer, probability and error columns.

import thinkthen.polars adds a .thinkthen namespace to every Series and DataFrame. A column goes in, and a native Polars column comes out in the same order. Install it with python -m pip install "thinkthen[polars]==0.2.0".

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame({"message": ["Please refund my order."]})
is_refund = messages["message"].thinkthen.decide(
"Does the customer request a refund?"
)
print(messages.with_columns(is_refund=is_refund))

Output

shape: (1, 2)
┌─────────────────────────┬───────────┐
│ message                 ┆ is_refund │
│ ---                     ┆ ---       │
│ str                     ┆ bool      │
╞═════════════════════════╪═══════════╡
│ Please refund my order. ┆ true      │
└─────────────────────────┴───────────┘

Not sure is null. Polars filter drops null entries along with the false ones, so take the not-sure messages out first:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
is_refund = messages["message"].thinkthen.decide(
"Does the customer request a refund?",
threshold=(0.2, 0.8),
)
messages = messages.with_columns(is_refund=is_refund)
print(messages)
not_sure = messages.filter(pl.col("is_refund").is_null())
print("Ask a person:", not_sure["message"].to_list())

Output

shape: (3, 2)
┌─────────────────────────────────┬───────────┐
│ message                         ┆ is_refund │
│ ---                             ┆ ---       │
│ str                             ┆ bool      │
╞═════════════════════════════════╪═══════════╡
│ Please refund my order.         ┆ true      │
│ Where is my parcel?             ┆ false     │
│ I was charged twice for one in… ┆ null      │
└─────────────────────────────────┴───────────┘
Ask a person: ['I was charged twice for one invoice.']

A missing cell fails and sends nothing. The namespace answers every other message, then raises tt.RecordError. Its outcomes holds every message's result as a struct column:

import polars as pl
import thinkthen as tt
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame({"message": ["Please refund my order.", None]})
try:
is_refund = messages["message"].thinkthen.decide(
"Does the customer request a refund?"
)
except tt.RecordError as failure:
outcomes = failure.outcomes.struct.unnest()
print(
outcomes.select(
"status",
"answer",
pl.col("error").struct.field("code"),
)
)

Output

shape: (2, 3)
┌──────────┬────────┬────────────────┐
│ status   ┆ answer ┆ code           │
│ ---      ┆ ---    ┆ ---            │
│ enum     ┆ bool   ┆ enum           │
╞══════════╪════════╪════════════════╡
│ answered ┆ true   ┆ null           │
│ failed   ┆ null   ┆ record.missing │
└──────────┴────────┴────────────────┘
Every function on a Polars column

choose gives a String column:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame({"message": ["Please refund my order."]})
topic = messages["message"].thinkthen.choose(
"What is the message about?",
choices=["billing", "shipping", "other"],
)
print(messages.with_columns(topic=topic))

Output

shape: (1, 2)
┌─────────────────────────┬─────────┐
│ message                 ┆ topic   │
│ ---                     ┆ ---     │
│ str                     ┆ str     │
╞═════════════════════════╪═════════╡
│ Please refund my order. ┆ billing │
└─────────────────────────┴─────────┘

filter gives your own messages that answer yes. Not-sure messages are left out:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
refund_requests = messages["message"].thinkthen.filter(
"Does the customer request a refund?"
)
print(refund_requests)

Output

shape: (2,)
Series: 'message' [str]
[
	"Please refund my order."
	"I was charged twice for one in…
]

tag gives a List(String) column:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{"message": ["I was charged twice and my parcel is late again."]}
)
topics = messages["message"].thinkthen.tag(
"Which topics appear?",
labels=["billing", "shipping"],
)
print(messages.with_columns(topics=topics).select("topics"))

Output

shape: (1, 1)
┌─────────────────────────┐
│ topics                  │
│ ---                     │
│ list[str]               │
╞═════════════════════════╡
│ ["billing", "shipping"] │
└─────────────────────────┘

score gives a Float64 column:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame({"message": ["Please refund my order."]})
urgency = messages["message"].thinkthen.score(
"How urgent is the request?",
levels=["routine", "urgent"],
)
print(messages.with_columns(urgency=urgency))

Output

shape: (1, 2)
┌─────────────────────────┬─────────┐
│ message                 ┆ urgency │
│ ---                     ┆ ---     │
│ str                     ┆ f64     │
╞═════════════════════════╪═════════╡
│ Please refund my order. ┆ 0.05    │
└─────────────────────────┴─────────┘

rank gives an Int64 column, 1 for the highest:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
urgency_rank = messages["message"].thinkthen.rank(
"How urgent is the request?",
levels=["routine", "urgent"],
)
ranked = messages.with_columns(urgency_rank=urgency_rank)
print(ranked.sort("urgency_rank"))

Output

shape: (3, 2)
┌─────────────────────────────────┬──────────────┐
│ message                         ┆ urgency_rank │
│ ---                             ┆ ---          │
│ str                             ┆ i64          │
╞═════════════════════════════════╪══════════════╡
│ I was charged twice for one in… ┆ 1            │
│ Where is my parcel?             ┆ 2            │
│ Please refund my order.         ┆ 3            │
└─────────────────────────────────┴──────────────┘

find gives a struct of text, start and end:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{
"message": [
"Morgan works for Example Group.\nPlease refund my order."
]
}
)
actions = messages["message"].thinkthen.find(
"Which line states the requested action?"
)
print(actions.struct.unnest())

Output

shape: (1, 3)
┌─────────────────────────┬───────┬─────┐
│ text                    ┆ start ┆ end │
│ ---                     ┆ ---   ┆ --- │
│ str                     ┆ i64   ┆ i64 │
╞═════════════════════════╪═══════╪═════╡
│ Please refund my order. ┆ 32    ┆ 55  │
└─────────────────────────┴───────┴─────┘

recognize gives a list of names per message:

import polars as pl
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{"message": ["Morgan works for Example Group."]}
)
names = messages["message"].thinkthen.recognize(
kinds=["person", "organization"]
)
print(names.explode().struct.unnest())

Output

shape: (2, 5)
┌───────────────┬───────┬─────┬──────────────┬─────────────┐
│ text          ┆ start ┆ end ┆ kind         ┆ probability │
│ ---           ┆ ---   ┆ --- ┆ ---          ┆ ---         │
│ str           ┆ i64   ┆ i64 ┆ str          ┆ f64         │
╞═══════════════╪═══════╪═════╪══════════════╪═════════════╡
│ Morgan        ┆ 0     ┆ 6   ┆ person       ┆ 1.0         │
│ Example Group ┆ 17    ┆ 30  ┆ organization ┆ 0.9702      │
└───────────────┴───────┴─────┴──────────────┴─────────────┘

relate gives a list of relations per message:

import polars as pl
import thinkthen as tt
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{
"message": [
"Morgan works for Example Group.\nPlease refund my order."
]
}
)
rules = [
tt.RelationRule(
source="person", target="organization", relation="works_for"
)
]
relations = messages["message"].thinkthen.relate(
rules=rules
)
print(
relations.explode().struct.unnest().select(
pl.col("source").struct.field("text").alias("source"),
"relation",
pl.col("target").struct.field("text").alias("target"),
)
)

Output

shape: (1, 3)
┌────────┬───────────┬───────────────┐
│ source ┆ relation  ┆ target        │
│ ---    ┆ ---       ┆ ---           │
│ str    ┆ str       ┆ str           │
╞════════╪═══════════╪═══════════════╡
│ Morgan ┆ works_for ┆ Example Group │
└────────┴───────────┴───────────────┘

annotate gives one struct per message, with a field for each question, which unnest spreads into columns:

import polars as pl
import thinkthen as tt
import thinkthen.polars # adds the .thinkthen namespace
messages = pl.DataFrame(
{
"message": [
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
]
}
)
questions = {
"refund": tt.DecideQuestion("Does the customer request a refund?"),
"topic": tt.ChooseQuestion(
"What is the message about?",
choices=["billing", "shipping", "other"],
),
"urgency": tt.ScoreQuestion(
"How urgent is the request?",
levels=["routine", "urgent"],
),
}
forms = messages["message"].thinkthen.annotate(questions)
print(messages.hstack(forms.struct.unnest()))

Output

shape: (3, 4)
┌─────────────────────────────────┬────────┬──────────┬─────────┐
│ message                         ┆ refund ┆ topic    ┆ urgency │
│ ---                             ┆ ---    ┆ ---      ┆ ---     │
│ str                             ┆ bool   ┆ str      ┆ f64     │
╞═════════════════════════════════╪════════╪══════════╪═════════╡
│ Please refund my order.         ┆ true   ┆ billing  ┆ 0.05    │
│ Where is my parcel?             ┆ false  ┆ shipping ┆ 0.14    │
│ I was charged twice for one in… ┆ true   ┆ billing  ┆ 0.45    │
└─────────────────────────────────┴────────┴──────────┴─────────┘

details=True returns each message's full result instead of the answer.

Each call above uses one shared client, made on the first call and closed when Python exits. It reads the THINKTHEN_ variables when it is made, so set them before the first call. Make your own client when it needs settings of its own, such as model=, backend=, url= or retry= (Configuration lists the settings), or when you want to close it yourself. This one retries a failed request up to five times:

import thinkthen as tt
retry = tt.RetryPolicy(max_retries=5)
with tt.Client(retry=retry) as client:
is_refund = client.decide(
"Does the customer request a refund?",
"Please refund my order.",
)
print(is_refund)

Output

True

A client has the same calls as the module, and the with block closes it. To make the plain calls such as tt.decide use it, pass it to tt.set_default_client(client).

thinkthen.aio has the same calls, to use with await:

import asyncio
import thinkthen.aio as aio
async def main() -> None:
is_refund = await aio.decide(
"Does the customer request a refund?",
"Please refund my order.",
)
print(is_refund)
asyncio.run(main())

Output

True

Each running event loop gets its own shared client. asyncio.gather runs many calls at once, and tt.AsyncClient takes the same settings as tt.Client.

The Python source lists every name.