Wity Python SDK
Python client for Wity, the typed-decision API. Send some text and a few questions with fixed options. Get back a probability for every option.
Install
pip install wity-sdk
Or with uv: uv add wity-sdk. Needs Python 3.10 or newer. The SDK depends on httpx and pydantic.
Quick start
Set your key in the environment:
export WITY_API_KEY="wity_..."
Then ask questions:
from wity import WityClient, choice, noul, score
with WityClient() as client:
res = client.system_one(
state="I was charged twice for my subscription this month. Please refund one of them today.",
questions={
"team": choice("Which team should handle this ticket?", {
"billing": "Payments, charges and refunds",
"technical": "Bugs, errors and outages",
"other": "Anything else",
}),
"urgent": noul(
"Does the customer ask for something to happen soon?",
yes="They ask for fast action",
no="No time pressure",
),
"severity": score("How much is this hurting the customer?", ["Not at all", "A little", "A lot"]),
},
)
res.choices["team"].choice # "billing"
res.choices["team"].probabilities # {"billing": 0.99993..., "technical": 0.00003..., "other": 0.00003...}
res.nouls["urgent"].noul # 0.99989... = probability of yes
res.scores["severity"].score # 1.97322... = expected level, from 0 (Not at all) to 2 (A lot)
Answers come back under the names you gave the questions.
res.choices,res.noulsandres.scoreseach hold one type of answer, so your editor knows their fields.res.answersholds every answer, whatever its type.- Answers are Pydantic models.
res.model_dump()turns the whole response into a dict.
Async
AsyncWityClient takes the same options and has the same methods. Use await:
from wity import AsyncWityClient
async with AsyncWityClient() as client:
res = await client.system_one(state=state, questions=questions)
Questions
| Builder | Asks | Answer fields |
|---|---|---|
choice(instructions, {"id": "description", ...}) |
Pick one of 2–256 options | choice, probabilities, confidence |
noul(instructions, yes="...", no="...") |
Yes or no. yes and no are optional, but give both or neither |
noul (probability of yes) |
score(instructions, ["lowest", ..., "highest"]) |
Pick one of 2–10 levels | score, legend, probabilities, confidence |
statecan be text, or a dict or list, which Wity reads as JSON.- Option ids written as numbers (
{1: "Ground"}) come back as strings ("1"). scorelevels can come from a variable, like a list loaded from a database.- The builders return
Choice,NoulandScoremodels. You can create those directly too. A plain dict in the API's own format also works as a question. - Wity checks the limits (option counts, lengths). If one is broken, it says which in a
BadRequestError. Failed calls aren't billed. - Probabilities sum to 1, but they are not calibrated. Treat them as a ranking of how sure the model is, not as exact odds.
Reasoning
Wity answers most questions in one fast pass, and thinks first only when a question needs it.
client.system_one(state=state, questions=questions, reasoning="off", max_latency_ms=3000)
reasoning:"auto"(the default) thinks only when needed."off"never thinks. It's the fastest."always"thinks before every answer.
max_latency_mscaps the time per request, from 200 to 120000 ms. If thinking was cut short, the answer'sreasoning.budget_limitedisTrue. The SDK waits at leastmax_latency_msplus 10 s before timing out, even iftimeoutis shorter.- When Wity thought, the answer has
reasoning.thought == True. It also carries the answer from before thinking:direct_probabilitiesfor choice and score,direct_noulfor noul.
Images
Both system_one and generate take one image as a data URL:
client.system_one(state="Photo of the delivered parcel", questions=questions, image="data:image/jpeg;base64,...")
Generate
Choice, noul and score pick from options you wrote in advance. generate writes text you couldn't list ahead of time: a value read off a document, a form field, a one-line reply. It returns one piece of text per call, with no probabilities. If the answer is one of a known set, use a question instead.
Free text:
reply = client.generate(
state="I was charged twice for my subscription this month. Please refund one of them today.",
instructions="Write the one-sentence reply the agent sends to the customer.",
max_tokens=80,
)
reply.text # "Thank you for bringing this to our attention; I've reviewed your account..."
JSON in a shape you define. shape is a JSON Schema whose top level is an object:
origin = client.generate(
state="Ticket scan: LISBOA (LIS) -> BERLIN BRANDENBURG (BER), 14 Oct, seat 23C.",
instructions="Origin city name, in English",
shape={
"type": "object",
"properties": {"city": {"type": "string", "pattern": "^[A-Za-z ]{1,40}$"}},
"required": ["city"],
},
max_tokens=40,
)
origin.value # {"city": "Lisbon"}
- When Wity finishes (
finish_reason == "stop"), the output matches the shape and comes back parsed invalue. - If
finish_reasonis"length", Wity hitmax_tokens(1 to 512, default 128) before finishing.textis cut off andvalueisNone. Raise the limit, or send the item to a person. - Only input tokens are billed.
- Check generated text before acting on it. It can state things that aren't in
state.
Errors
Every error the SDK raises extends WityError:
from wity import RateLimitError, WityError
try:
client.system_one(state=state, questions=questions)
except RateLimitError:
... # still rate limited after retries
except WityError as err:
print(err)
| Error | When |
|---|---|
BadRequestError |
400: the request is wrong. The message says which part. |
AuthenticationError |
401: the key is missing or wrong. |
InsufficientBalanceError |
402: the account has no balance left. |
RateLimitError |
429: too many requests, after retries. |
InternalServerError |
5xx: a problem on Wity's side, after retries. |
APIError |
Any other status. All the classes above extend it. It has status, body, headers and request_id. |
APIConnectionError |
The API couldn't be reached, or the connection broke while reading its answer. |
APITimeoutError |
No answer within the timeout. |
WityError |
Anything else: a setup problem like a missing key, a request that can't be sent as JSON, or an answer that isn't from Wity. |
request_id is the server's id for the request. Quote it when you report a problem.
Building a question with the wrong types, like Choice(criteria=["a", "b"]), raises Pydantic's ValidationError on that line, before anything is sent.
Typos and new fields. A misspelled argument, like max_latency=3000 instead of max_latency_ms, raises TypeError straight away. To send a field the API has but this SDK doesn't know yet, use extra_body:
client.system_one(state=state, questions=questions, extra_body={"new_field": True})
The SDK logs a warning that names each field it doesn't know. If Wity rejects the request, the BadRequestError names them too.
Retries, timeouts and cancelling
- 408, 429, 5xx and network failures are retried up to 2 times. The SDK waits longer each time, or as long as the
Retry-Afterheader asks. - If
Retry-Afterasks for more than 60 s, the SDK raises straight away and leaves the decision to you. - Timeouts aren't retried. The request may already have reached Wity, and you've already waited the full timeout.
- If the connection breaks while the answer is on its way back, Wity may already have billed the call. The retry is billed again.
WityClient(timeout=30, max_retries=0) # defaults: 60 seconds and 2
Cancel an async call the usual asyncio way. This also stops any retries and the waits between them. asyncio.timeout() (Python 3.11+) or asyncio.wait_for() caps the total time across all attempts:
async with asyncio.timeout(20):
await client.system_one(state=state, questions=questions)
WityClient calls can't be cancelled once started, like any blocking HTTP call. Use AsyncWityClient if you need that.
Logging
The SDK logs through Python's standard logging module, under the logger name wity. Only warnings show unless you turn it up:
export WITY_LOG_LEVEL=debug # or info, warn, error, off
Or from code, like any logger:
import logging
logging.getLogger("wity").setLevel(logging.DEBUG)
| Level | What you see |
|---|---|
warn (the default) |
Unknown fields in a request, and a Retry-After too long to wait for. Nothing when calls succeed. |
info |
Also each retry (why, and how long it waits), timeouts and connection errors. |
debug |
Also each request: route, attempt, size in bytes, question names, status, time and request id. |
off |
Nothing. |
If your app hasn't set up logging, WITY_LOG_LEVEL=debug or info prints to stderr with a [wity] prefix. If it has, the messages go to your handlers.
Logs never contain your API key (not even part of it) or the state text, which is usually customer data. Errors are always raised to you. Logs only add context, whatever the level.
Configuration
| Option | Environment variable | Default |
|---|---|---|
api_key |
WITY_API_KEY |
required |
base_url |
WITY_BASE_URL |
DEFAULT_BASE_URL (the production API) |
timeout |
60 seconds |
|
max_retries |
2 |
|
WITY_LOG_LEVEL |
warn |
|
http_client |
a new httpx.Client (or httpx.AsyncClient) |
An option passed in code wins over the environment variable. An empty api_key is an error. It never falls back to WITY_API_KEY, which could belong to a different account. client.base_url shows which address the client uses.
Pass your own http_client for proxies, custom certificates or HTTP/2 (httpx.Client(http2=True), which needs pip install httpx[http2]). The SDK doesn't close a client you pass in.
Security
- Keep the key on your server. Anyone who can see your browser code can read a key inside it.
- The client refuses to run in a browser (Pyodide or PyScript), to catch that mistake early.
- Load the key from the environment or a secret manager. Don't write it in code.
- The client never shows the key when it's printed or logged, and errors never include it.
base_urlmust usehttps://. Plainhttp://is allowed only forlocalhost,127.0.0.1and[::1].- The client never follows redirects, so the key only goes to the address you set. This holds even if your own
http_clientis set to follow them. - An answer larger than 10 MB can't be from Wity. The client stops reading it and raises a
WityError.
Support
Questions, bugs and security problems: info@alphanimble.com. Please report security problems by email, not in public.
License
Apache-2.0. See LICENSE.
Release files for wity-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| wity_sdk-0.1.0.tar.gz | 83.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wity_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 107.2 kB
Release files / wity_sdk-0.1.0.tar.gz
| Download URL | wity_sdk-0.1.0.tar.gz |
|---|---|
| Size | 83.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8a55fd0cbce43cdb7927b676c2a9e71316fc7ec542d64487a1ec7f045759ffe2
|
|
BLAKE2b-256 checksum How to use checksums |
5da6abcaf9d19740b1c119857633c8ea68082977b334e9c45ac2827e36e3d2cc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.22
|
Release files / wity_sdk-0.1.0-py3-none-any.whl
| Download URL | wity_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 24.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e199996d138b4b755d2bf540a3accdf12fe24c9524140a8e6d37ded6bfc5d0f3
|
|
BLAKE2b-256 checksum How to use checksums |
be7973a9a5ca9b14b591065e61e9db35844a750d1b128caeda1d9457f1e8f47f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.22
|