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
mkdir jev-tutorial && cd jev-tutorial
python3 -m venv .venvsource .venv/bin/activatepip install typesafe-sdkexport TYPESAFE_API_KEY=your-key-hereWindows (PowerShell)
mkdir jev-tutorial; cd jev-tutorial
python -m venv .venv.venv\Scripts\Activate.ps1pip install typesafe-sdk$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.
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",
},
)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.
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.",
],
)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.
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",
),
)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.
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,
},
)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 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.")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
.confidencehere: 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:
# 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)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.
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}")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
- Try the category question in the message routing recipe with sample messages, or run your own text with your own TypeSafe API key.
- Jev pricing explained: what a request like this one actually costs, and how that scales.
- How to try Jev covers getting an API key, what works without one, and a first request with cURL.
- Jev vs LLMs: when to use TypeSafe AI if you're still deciding whether this is the right kind of call for the job.
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
- Quickstart: install command, client setup, API-key variable.
- Primitives: Choice, Score, and Noul, and when to use each.
- Confidence: its derivation, why Noul carries none, why thresholds scale with risk.
- Models: the jev-latest alias and versioning.
- Python SDK reference: install command, sync and async client shape.
- typesafe-sdk-python on GitHub: version 0.7.1, Python ≥3.10, and the client/question/response/error/retry source.
- typesafe-sdk on PyPI: confirms 0.7.1 is current, matching the repository.
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.