Skip to content
Jev Guide

Start building / Guide 02 · Python · Beginner

Jev Python Tutorial: Classify and Score Text

This typesafe-sdk tutorial builds one Python script around a single support message: it chooses a category, scores how urgently the message needs a reply, and estimates whether it explicitly asks for a refund. All three are independent questions over the same text, in one request.

Jev doesn't draft a reply here. It returns bounded decisions (a category, a score, a probability) that your own application code acts on. Nothing in this script writes text back to the customer.

Illustrative result

category
billing (confidence 0.86)
urgency
level 1: Time-sensitive (score 1.00)
wants_refund
0.97

Not from a live call. See “Reading the results” below for where these numbers come from.

Before you start

You'll need Python 3.10 or newer and a TypeSafe API key. No key yet? See how to get a TypeSafe API key, which also covers what works without one and what API use costs. This page focuses on the TypeSafe AI Python SDK itself.

Set up the project

A virtual environment keeps this SDK separate from any other Python project on your machine. Pick the column for your platform.

macOS / Linux

Create project + venv
mkdir jev-tutorial && cd jev-tutorial
python3 -m venv .venv
Activate
source .venv/bin/activate
Install the SDK
pip install typesafe-sdk
Set your key
export TYPESAFE_API_KEY=your-key-here

Windows (PowerShell)

Create project + venv
mkdir jev-tutorial; cd jev-tutorial
python -m venv .venv
Activate
.venv\Scripts\Activate.ps1
Install the SDK
pip install typesafe-sdk
Set your key
$env:TYPESAFE_API_KEY = "your-key-here"

TypeSafeClient() reads TYPESAFE_API_KEY automatically, so the key never appears as a literal in your source. Save the script below as classify_message.py and run python classify_message.py. No package beyond typesafe-sdk is needed here.

Build the request, one question at a time

Each question below asks something different about the same message, so each reaches for a different Jev primitive. Building them separately first makes it clear what each one is actually for before they're combined into a single Jev API Python example. This is the message every question in this guide is asked about:

“Hi, I was charged twice for my subscription this month. The two charges show up on my card statement on the 3rd and the 4th. Can you refund one of them?”

Choice: pick a category

A small, closed set of options with no order between them fits Choice. The "other" option matters: without it, a message that fits none of the four real categories still has to be forced into one.

category.pyPython
from typesafe_sdk import Choice

category = Choice(
    instructions="Which category best describes this customer message?",
    criteria={
        "billing": "Charges, invoices, refunds, payment methods, subscription costs",
        "account": "Login, password, profile, permissions, closing or changing an account",
        "technical": "Bugs, errors, outages, integrations, something not working",
        "sales": "Pricing questions, plan comparisons, upgrades, purchasing for a team",
        "other": "Anything that does not fit the departments above",
    },
)
Reviewed against typesafe-sdk 0.7.1 on PyPI and its published source (September 2026) · syntax-checked by this repo's test suite · not installed or executed against the live API by this guide

Score: rate urgency

Urgency sits on a spectrum, so an ordered rubric fits Score better than a label. The rubric asks how urgently the message needs a reply, not how upset the customer sounds: a calm message about an active outage is urgent, an angry message about a months-old typo is not.

urgency.pyPython
from typesafe_sdk import Score

urgency = Score(
    instructions="How urgently does this message need a response from support?",
    criteria=[
        "Routine. A general question, comment, or minor request with no deadline or breakage implied.",
        "Time-sensitive. Something is blocked or inconvenient, but there is a reasonable window to respond.",
        "Urgent. Active harm, an outage, a security exposure, or a deadline that has already arrived.",
    ],
)
Reviewed against typesafe-sdk 0.7.1 on PyPI and its published source (September 2026) · syntax-checked by this repo's test suite · not installed or executed against the live API by this guide

Noul: estimate a yes/no

“Does the customer explicitly request a refund?” is a clean yes/no question where the probability itself is the useful signal, not a forced label, which makes it a fit for Noul. NoulCriteria describes both outcomes.

refund.pyPython
from typesafe_sdk import Noul, NoulCriteria

wants_refund = Noul(
    instructions="Does the customer explicitly request a refund?",
    criteria=NoulCriteria(
        true="The message explicitly asks for money back or a refund",
        false="The message does not ask for money back",
    ),
)
Reviewed against typesafe-sdk 0.7.1 on PyPI and its published source (September 2026) · syntax-checked by this repo's test suite · not installed or executed against the live API by this guide

Combine into one request

All three share the same state but are independent of each other: one round trip, and each answer would come back the same if the other two questions weren't asked.

combine.pyPython
from typesafe_sdk import TypeSafeClient

# category, urgency, and wants_refund are the three Question objects built above.
# All three are asked over the same message in a single round trip; none of them
# depends on another's answer.
with TypeSafeClient() as client:
    response = client.system_one(
        state=message,
        model="jev-latest",
        questions={
            "category": category,
            "urgency": urgency,
            "wants_refund": wants_refund,
        },
    )
Reviewed against typesafe-sdk 0.7.1 on PyPI and its published source (September 2026) · syntax-checked by this repo's test suite · not installed or executed against the live API by this guide

The complete script

One copyable file: imports, a missing-key check, the message and three questions above, the request, typed response access, error handling, and readable output: the same code, assembled.

classify_message.pyPython
"""Classify a support message with Jev: category, urgency, and refund intent.

Requires Python 3.10+ and TYPESAFE_API_KEY in the environment.
    pip install typesafe-sdk
Walkthrough: https://jevguide.ai/guides/jev-python
Not installed, executed, or type-checked here; its syntax is checked by this
repo's test suite (tests/classify-message-example.test.ts) and its category
and urgency criteria are checked against src/lib/classification.ts and
src/lib/recipes.ts so this file can't silently drift from the rest of the site.
"""

import os
import sys

from typesafe_sdk import (
    Choice,
    Noul,
    NoulCriteria,
    Score,
    TypeSafeAPIConnectionError,
    TypeSafeAPIError,
    TypeSafeClient,
    TypeSafeError,
)

if not os.environ.get("TYPESAFE_API_KEY"):
    print("Set TYPESAFE_API_KEY first, e.g.: export TYPESAFE_API_KEY=your-key-here", file=sys.stderr)
    raise SystemExit(1)

message = (
    "Hi, I was charged twice for my subscription this month. The two charges show up "
    "on my card statement on the 3rd and the 4th. Can you refund one of them?"
)

try:
    with TypeSafeClient() as client:
        response = client.system_one(
            state=message,
            model="jev-latest",
            questions={
                # Choice: a small, closed set of categories with no order between them.
                # "other" is required so a message that fits none of them still gets a clean answer.
                "category": Choice(
                    instructions="Which category best describes this customer message?",
                    criteria={
                        "billing": "Charges, invoices, refunds, payment methods, subscription costs",
                        "account": "Login, password, profile, permissions, closing or changing an account",
                        "technical": "Bugs, errors, outages, integrations, something not working",
                        "sales": "Pricing questions, plan comparisons, upgrades, purchasing for a team",
                        "other": "Anything that does not fit the departments above",
                    },
                ),
                # Score: urgency sits on a spectrum, so an ordered rubric fits better than a
                # yes/no split. This asks how urgently the message needs a reply, not how
                # frustrated the customer sounds; a calm message can still be urgent, and an
                # angry one can still be routine.
                "urgency": Score(
                    instructions="How urgently does this message need a response from support?",
                    criteria=[
                        "Routine. A general question, comment, or minor request with no deadline or breakage implied.",
                        "Time-sensitive. Something is blocked or inconvenient, but there is a reasonable window to respond.",
                        "Urgent. Active harm, an outage, a security exposure, or a deadline that has already arrived.",
                    ],
                ),
                # Noul: a single yes/no probability is the useful signal here, not a label.
                "wants_refund": Noul(
                    instructions="Does the customer explicitly request a refund?",
                    criteria=NoulCriteria(
                        true="The message explicitly asks for money back or a refund",
                        false="The message does not ask for money back",
                    ),
                ),
            },
        )
except TypeSafeAPIConnectionError as err:
    print(f"Could not reach TypeSafe: {err}", file=sys.stderr)
    raise SystemExit(1)
except TypeSafeAPIError as err:
    print(f"TypeSafe rejected the request: {err}", file=sys.stderr)
    raise SystemExit(1)
except TypeSafeError as err:
    # Covers local validation errors, e.g. an empty questions dict or an
    # empty Score criteria list. Raised before any request is sent.
    print(f"Bad request to TypeSafe: {err}", file=sys.stderr)
    raise SystemExit(1)

# The three questions share the same `message` as context but do not depend on
# each other's answers: this is one round trip, not three, and the request
# would return the same category, urgency, and refund answers if only one of
# the three questions were asked.
category = response.answers["category"]
urgency = response.answers["urgency"]
refund = response.answers["wants_refund"]

print(f"Category:  {category.choice}  (confidence {category.confidence:.2f})")
print(f"           {category.probabilities}")

level = round(urgency.score)
print(f"Urgency:   level {level}: {urgency.legend[level]}")
print(f"           (score {urgency.score:.2f}, confidence {urgency.confidence:.2f})")

if refund.noul >= 0.7:
    refund_read = "likely yes"
elif refund.noul <= 0.3:
    refund_read = "likely no"
else:
    refund_read = "unclear"
print(f"Refund ask: {refund.noul:.2f}  ({refund_read})")

# Your code decides what to do with these answers; Jev only classifies.
# A confidence floor like this is a starting point, not a number TypeSafe
# recommends universally; evaluate it against your own data before trusting it.
if category.confidence < 0.5:
    print("Category is uncertain enough to route to a person instead of automating.")
Reviewed against typesafe-sdk 0.7.1 on PyPI and its published source (September 2026) · syntax-checked by this repo's test suite · not installed or executed against the live API by this guide

Reading the results

One illustrative response to the message above, annotated. These numbers weren't returned by a live call: the category figures are the fixture this site already uses for this exact message elsewhere; urgency and refund were written to show the shape of a real answer, not measured.

category.choice
"billing": the option with the highest probability among the five criteria keys.
category.probabilities
One value per category, summing to 1: billing 0.91, account 0.03, technical 0.02, sales 0.01, other 0.03.
category.confidence
0.86: how peaked that distribution is, not a probability itself and not a measured accuracy rate.
urgency.score
1.00: the probability-weighted average across the three rubric levels, which is why it can fall between two whole numbers.
urgency.legend[level]
Rounding 1.00 to level 1 reads “Time-sensitive”: money charged incorrectly, not active harm.
urgency.confidence
0.70: same derivation as Choice's, over three levels instead of five categories.
wants_refund.noul
0.97: the probability of a yes, on its own scale. There is no .confidence here: TypeSafe's docs are explicit that Noul answers don't carry one. Don't read 0.97 as a confidence.

A valid, well-typed answer isn't a correct one. category.choice is always one of the keys you defined, but that's a guarantee about shape, not about whether “billing” is the right call for this message. Act on every answer as a judgment.

A common next step is routing anything uncertain to a person instead of acting on it automatically:

review-routing.pyPython
# Illustrative: 0.5 is a starting point, not a value TypeSafe recommends
# universally. Tune it against your own data before trusting it.
if category.confidence < 0.5:
    send_to_review_queue(message)  # your own queue, not part of the SDK
else:
    route_to(category.choice)
Reviewed against typesafe-sdk 0.7.1 on PyPI and its published source (September 2026) · syntax-checked by this repo's test suite · not installed or executed against the live API by this guide

0.5 is a starting point, not a value TypeSafe recommends universally; its docs say thresholds should scale with the stakes of the action they gate. Evaluate any cutoff on your own data first.

Error handling

typesafe-sdk raises a typed subclass of TypeSafeError for every failure: one TypeSafeAPIError subclass per HTTP status (including TypeSafeAuthenticationError for 401 and TypeSafeRateLimitError for 429), and TypeSafeAPIConnectionError when the request never got a response at all. Catch the specific type you care about, then fall back to the shared base classes.

error-handling.pyPython
from typesafe_sdk import (
    TypeSafeAPIConnectionError,
    TypeSafeAuthenticationError,
    TypeSafeRateLimitError,
    TypeSafeAPIError,
    TypeSafeError,
)

try:
    response = client.system_one(state=message, model="jev-latest", questions=questions)
except TypeSafeAuthenticationError:
    print("Invalid or missing TYPESAFE_API_KEY.")
except TypeSafeRateLimitError as err:
    # The SDK already retried this internally before raising; it only
    # reaches here once the default retry budget is exhausted.
    print(f"Still rate limited after retries; server asked for {err.retry_after_ms}ms.")
except TypeSafeAPIConnectionError:
    print("Could not reach TypeSafe, or the request timed out.")
except TypeSafeAPIError as err:
    print(f"TypeSafe rejected the request: {err.status} {err}")
except TypeSafeError as err:
    print(f"Bad question definition: {err}")
Reviewed against typesafe-sdk 0.7.1 on PyPI and its published source (September 2026) · syntax-checked by this repo's test suite · not installed or executed against the live API by this guide

Troubleshooting

typesafe_sdk not found / ModuleNotFoundError
The SDK is installed in a different Python environment than the one running the script, usually an inactive venv. Run python -c "import sys; print(sys.executable)" to confirm it's inside .venv, then reinstall with python -m pip install typesafe-sdk so pip matches the running interpreter.
“No API key was provided”
TYPESAFE_API_KEY isn't set in the shell running the script. This raises TypeSafeError immediately, before any network call. Set it in that same terminal (echo $TYPESAFE_API_KEY, or echo $env:TYPESAFE_API_KEY on PowerShell) and confirm only that it's non-empty; never print the key itself.
401 / TypeSafeAuthenticationError
The key reached TypeSafe but was rejected. Regenerate it on the console's Keys page. A key with a stray space or non-ASCII character instead fails earlier, as a local TypeSafeError, before any request is sent.
Question definition fails validation
An empty questions dict or an empty Score criteria list raises TypeSafeError locally, before any request is sent. A request that reaches the server with a malformed shape instead comes back 422 as TypeSafeUnprocessableEntityError.
Rate limits (429) or timeouts
The SDK already retries 408, 429, and 5xx by default (up to two extra attempts with backoff from 0.5s, honoring Retry-After, within a 30-second budget), plus connection errors and timeouts. Don't add a retry loop on top of this. If TypeSafeRateLimitError still reaches your code, back off longer than err.retry_after_ms suggests, or pass a larger timeout= to that one call.

Next steps

Out of scope here on purpose: processing a CSV of many messages, running requests concurrently, and deploying this script as a service. Those deserve their own guides.

Sources and verification

What was verified: imports, client construction, the system_one signature, question constructors, response fields, the exception hierarchy, the 10-second default timeout, and the default retry policy were all read directly from typesafe-sdk 0.7.1's published source, matching the current PyPI release. This guide's example was not installed, run, or executed against the live API; its syntax was checked with Python's own parser as part of this repository's test suite, which confirms it parses, not that it behaves correctly at runtime. The illustrative response values above are labeled as such because no live or recorded Jev call produced them.