---
title: Python SDK
description: "Python SDK: the ten functions as plain Python calls, with not sure raised as tt.Unsure, async calls, and pandas and Polars columns."
goal: A Python developer installs the SDK, gets a first answer from decide, learns how not sure and failure arrive, and finds the Python call for each function, in plain Python, pandas and Polars.
reader: A Python developer or data analyst who wants answers about text inside a script, notebook or DataFrame
kind: reference
verified: 2026-11-04
---

# 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.

```python
import thinkthen as tt

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

Output

```text
True
```


Install it (Python 3.10 or later):

```sh
python -m pip install "thinkthen==0.2.0"
```

The calls use the key you set in the [Quickstart](/quickstart/).

## When decide is not sure

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:

```python
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

```text
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.

## When a call fails

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

```python
import thinkthen as tt

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

Output

```text
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](/reference/troubleshooting/) lists each code and its fix.

## The ten functions

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

### decide

<details class="example-disclosure">
<summary>decide: yes, no or not sure</summary>

```python
import thinkthen as tt

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

Output

```text
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](/functions/decide/) shows every answer.

</details>

### choose

<details class="example-disclosure">
<summary>choose: one answer from your list</summary>

```python
import thinkthen as tt

topic = tt.choose(
    "What is the message about?",
    "Please refund my order.",
    choices=["billing", "shipping", "other"],
)
print(topic)
```

Output

```text
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](/functions/choose/) shows every answer.

</details>

### filter

<details class="example-disclosure">
<summary>filter: keep the messages that answer yes</summary>

```python
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

```text
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](/functions/filter/) shows every answer.

</details>

### tag

<details class="example-disclosure">
<summary>tag: every label that fits</summary>

```python
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

```text
['billing', 'shipping']
```


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

</details>

### score

<details class="example-disclosure">
<summary>score: a number from 0 to 1</summary>

```python
import thinkthen as tt

urgency = tt.score(
    "How urgent is the request?",
    "Please refund my order.",
    levels=["routine", "urgent"],
)
print(urgency)
```

Output

```text
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](/functions/score/) shows every answer.

</details>

### rank

<details class="example-disclosure">
<summary>rank: put messages in order</summary>

```python
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

```text
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](/functions/rank/) shows every answer.

</details>

### find

<details class="example-disclosure">
<summary>find: the line that answers</summary>

```python
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

```text
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](/functions/find/) shows every answer.

</details>

### recognize

<details class="example-disclosure">
<summary>recognize: names and their kinds</summary>

```python
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

```text
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`:

```python
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

```text
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](/functions/recognize/) shows every answer.

</details>

### relate

<details class="example-disclosure">
<summary>relate: how names connect</summary>

```python
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

```text
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](/functions/relate/) shows every answer.

</details>

### annotate

<details class="example-disclosure">
<summary>annotate: several questions at once</summary>

```python
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

```text
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](/functions/annotate/) shows every answer.

</details>

## pandas

`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"`.

```python
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

```text
                   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:

```python
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

```text
                                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.']
```

```text
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:

```python
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

```text
     status  answer      error_code
0  answered    True             NaN
1    failed    <NA>  record.missing
```

```text
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 class="example-disclosure">
<summary>Every function on a pandas column</summary>

choose gives a `string` column:

```python
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

```text
                   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:

```python
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

```text
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:

```python
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

```text
   id                                  text
0  a1               Please refund my order.
2  a3  I was charged twice for one invoice.
```

```text
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:

```python
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

```text
                topics
0  (billing, shipping)
```


score gives a `Float64` column:

```python
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

```text
                   message  urgency
0  Please refund my order.     0.05
```


rank gives an `Int64` column, 1 for the highest:

```python
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

```text
                                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:

```python
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

```text
Please refund my order.
```

```text
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:

```python
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

```text
person Morgan
organization Example Group
```

```text
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:

```python
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

```text
Morgan works_for Example Group
```

```text
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:

```python
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

```text
                                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
```

```text
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.

</details>

## Polars

`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"`.

```python
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

```text
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:

```python
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

```text
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:

```python
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

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


<details class="example-disclosure">
<summary>Every function on a Polars column</summary>

choose gives a `String` column:

```python
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

```text
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:

```python
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

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


tag gives a `List(String)` column:

```python
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

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


score gives a `Float64` column:

```python
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

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


rank gives an `Int64` column, 1 for the highest:

```python
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

```text
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`:

```python
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

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


recognize gives a list of names per message:

```python
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

```text
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:

```python
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

```text
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:

```python
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

```text
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.

</details>

## When to make a client

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](/configuration/) lists the settings), or when you want to close it yourself. This one retries a failed request up to five times:

```python
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

```text
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)`.

## Async

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

```python
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

```text
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 full API

The [Python source](https://github.com/botassembly/thinkthen/blob/v0.2.0/sdk/python/thinkthen/__init__.py) lists every name.
