# Wity: guide for AI agents and developers

This file is the complete, self-contained reference for integrating **Wity** (model `wity-1`) into software.
It is written so an AI coding agent can read it once and build a correct, well-tuned integration.
Human-readable docs: https://wity-frontend.vercel.app/docs

---

## 1. What Wity is (and when to use it)

Wity answers **typed decisions** about a piece of **state** (text, JSON, optionally an image). You define the
shape of the answer; Wity can only answer inside it, and returns a **probability for every option**.

Use Wity when your code needs to **branch** on a judgement:

- route a ticket, email or request to a queue, team or handler
- check a rule or policy (is this allowed? does this break the guideline?)
- grade severity, urgency, quality or risk on a scale
- filter or rank retrieved passages, candidates or tool calls
- read a field, value or short reply out of a document (`generate`)
- look at an image: classify a defect, check a screen, read a label

Do **not** use Wity for long-form writing or open chat. It is built for bounded questions that sit inside a
request path: clear questions come back in about 0.1 s of model time.

Four primitives:

| primitive | endpoint | returns | use it for |
|---|---|---|---|
| `choice` | `POST /v1/systemone` | one option id + probability of every option | pick one of many |
| `noul` | `POST /v1/systemone` | `noul` = P(yes), 0 to 1 | a yes-or-no call |
| `score` | `POST /v1/systemone` | probability of each level + expected level | where on a scale it sits |
| `generate` | `POST /v1/generate` | short text, or JSON matching a schema you give | the words an action needs |

---

## 2. Connection

- Base URL: `https://wity-proxy-production-2c33.up.railway.app`
- Auth: header `Authorization: Bearer <WITY_API_KEY>` (keys look like `wity_...`)
- Content type: `application/json`
- Health: `GET /health` returns `{"status": "ok", "model": "wity-1"}` (no key needed)

Rules for agents writing integrations:

- Read the key from an environment variable (`WITY_API_KEY`). Never hard-code it, commit it, log it, or send it
  from browser/mobile code. Call Wity from a server.
- Use a client timeout of at least 60 s (answers that think take seconds, not minutes).
- Requests are read-only: retrying is always safe.

---

## 3. Decisions: `POST /v1/systemone`

### Request

```json
{
  "state": "text, or any JSON object/array",
  "questions": {
    "<your_name>": { "type": "choice" | "noul" | "score", "instructions": "...", "criteria": ... }
  },
  "reasoning": "auto",
  "image": "data:image/png;base64,..."
}
```

| field | required | notes |
|---|---|---|
| `state` | yes | string, object or array; objects are read as JSON. Max 32,000 characters after JSON encoding (in practice keep it to a few thousand words: the model reads state + question within an 8k-token window). |
| `questions` | yes | map of **your** names → question objects. Answers come back under the same names. Names ≤ 128 chars. |
| `reasoning` | no | `"auto"` (default), `"off"`, `"always"`. See §6. |
| `max_latency_ms` | no | latency budget, 200–120000. Thinking and forecasts stop in time to answer within it (§6). |
| `image` | no | one image as a data URL (PNG, JPEG, WebP, GIF, BMP). Every question sees it with the state. See §8. |
| `model` | no | accepted and ignored (for compatibility). |

Unknown fields inside a question object are rejected with HTTP 400.

### Question types

**choice**: pick exactly one option.

```json
"intent": {
  "type": "choice",
  "instructions": "What does the customer want?",
  "criteria": {
    "billing": "A charge, refund, invoice or payment problem",
    "technical_support": "Something in the product is broken or not working",
    "cancellation": "Wants to cancel or downgrade",
    "other": "Anything that fits none of the above"
  }
}
```
`criteria`: option id → description, 2 to 256 options. Ids (≤128 chars) are returned; descriptions (≤2,000 chars)
are what Wity reads.

**noul**: yes or no.

```json
"phishing": {
  "type": "noul",
  "instructions": "Is this message a phishing attempt?",
  "criteria": { "true": "It tries to trick the reader into giving credentials, money or access",
                "false": "It is a genuine message from who it claims to be" }
}
```
`criteria` is optional; if present it has exactly the keys `true` and `false`.

**score**: a level on an ordered scale.

```json
"urgency": {
  "type": "score",
  "instructions": "How urgent is maintenance on this pump?",
  "criteria": ["Can wait for the next planned stop", "This week", "Today", "Stop the pump now"]
}
```
`criteria`: 2 to 10 levels, **lowest first**. Level keys in the answer are positions: `"0"`, `"1"`, …

### Response

```json
{
  "model": "wity-1",
  "answers": {
    "intent":   { "type": "choice", "choice": "billing",
                  "probabilities": { "billing": 0.991, "technical_support": 0.003, "cancellation": 0.004, "other": 0.002 },
                  "confidence": 0.93,
                  "reasoning": { "mode": "auto", "thought": false, "reason": null, "forecast": false, "thought_tokens": null } },
    "phishing": { "type": "noul", "noul": 0.98 },
    "urgency":  { "type": "score", "score": 2.02,
                  "legend": { "0": "Can wait for the next planned stop", "1": "This week", "2": "Today", "3": "Stop the pump now" },
                  "probabilities": { "0": 0.02, "1": 0.13, "2": 0.66, "3": 0.19 }, "confidence": 0.32 }
  },
  "usage": { "input_tokens": 212, "output_tokens": 0 },
  "metadata": { "reasoning": "auto", "elapsed_ms": 94.1 }
}
```

Reading answers:

- `choice`: branch on `choice`; its probability is `probabilities[choice]`. All probabilities sum to 1.
- `noul`: the probability of yes. P(no) = 1 − noul. There is no label; you choose where "yes" starts.
- `score`: `score` is the expected level (probability-weighted mean of level numbers), good for sorting and
  thresholds; `probabilities` gives each level; `legend` maps keys back to your level text.
- `confidence` (choice, score) is **1 − normalised entropy**: how peaked the distribution is. It is **not** the
  chance of being right. Use `probabilities` for decisions.
- When Wity thought first, the answer also has `direct_probabilities` / `direct_noul` (what it would have said
  without thinking). Use the main fields; the direct ones are for inspection.

---

## 4. Text: `POST /v1/generate`

Writes short text from the state, or JSON that always matches a schema you provide.

```json
{
  "state": "Invoice INV-2291 from Nordic Steel AB, total EUR 18,440.00, due 2026-10-30.",
  "instructions": "Extract the invoice fields.",
  "shape": {
    "type": "object",
    "properties": {
      "invoice_id": { "type": "string" },
      "vendor":     { "type": "string" },
      "total_eur":  { "type": "number" },
      "due_date":   { "type": "string" }
    },
    "required": ["invoice_id", "vendor", "total_eur", "due_date"]
  },
  "max_tokens": 120
}
```

| field | required | notes |
|---|---|---|
| `state` | no | what to write from (string/object/array, ≤ 32,000 chars) |
| `instructions` | yes | what to write, ≤ 8,000 chars |
| `shape` | no | a JSON Schema with `"type": "object"`. Output is **constrained** to it while writing. Supports `properties`, `required`, `pattern`, `enum`, `maxLength`, numbers, booleans, nested objects, arrays. Omit for free text. |
| `max_tokens` | no | 1 to 512, default 128 |
| `image` | no | one image as a data URL |

Response:

```json
{ "model": "wity-1",
  "text": "{\"invoice_id\": \"INV-2291\", ...}",
  "value": { "invoice_id": "INV-2291", "vendor": "Nordic Steel AB", "total_eur": 18440.0, "due_date": "2026-10-30" },
  "finish_reason": "stop",
  "usage": { "input_tokens": 121, "output_tokens": 53 },
  "metadata": { "elapsed_ms": 1350.2 } }
```

- With a `shape`, use `value` (already parsed). It is `null` only if `finish_reason` is `"length"`: raise `max_tokens`.
- Without a shape, use `text`. Say the length and audience in `instructions` ("one sentence to the customer").
- Pin formats with `pattern`, e.g. `{"type": "string", "pattern": "^[0-9]{4}$"}` for a 4-digit code.
- Generate has **no probabilities**. If the answer is one of a known set, use `choice`/`noul`/`score` instead.

---

## 5. Probabilities: acting on them

Wity's probabilities are meant to be taken literally: across many answers at 0.9, about 9 in 10 should be right.
That is what makes them usable as thresholds. Treat this as well-behaved on average, not a guarantee per answer.

Standard pattern: **a threshold per action, set by the cost of being wrong.**

```python
ans = r.json()["answers"]["route"]
p = ans["probabilities"][ans["choice"]]

if ans["choice"] == "refund" and p >= 0.95:
    issue_refund()             # cheap to undo: act automatically
elif p >= 0.80:
    queue_for_one_click(ans)   # a person confirms with one click
else:
    send_to_human(ans)         # genuinely unsure: full review
```

How to choose thresholds: run 100 to 500 past cases with known answers, then for each cut-off measure accuracy
above it and the share of cases above it. Pick the cut-off that gives the accuracy you need.

Also look at the runner-up: a 0.55 / 0.40 split between two options is a case for a person.

---

## 6. Reasoning modes

| `reasoning` | behaviour | typical latency | use for |
|---|---|---|---|
| `"auto"` (default) | answers directly when the question is clear; thinks when it is a close call, then stops as soon as the answer settles | ~0.1 s clear, 1–4 s when it thinks | almost everything |
| `"off"` | never thinks | ~0.1 s, predictable | high-volume, clear-cut routing |
| `"always"` | thinks before every answer | 1–4 s every time | batch jobs where each decision is costly to get wrong |

In `auto`/`always`, each answer includes `reasoning`: `thought` (bool), `reason` (`close_call`,
`order_sensitive`, `forecast`, `requested` or `null`), `forecast` (bool), `thought_tokens`.
Thinking is never billed. Latency figures are model time; add your network round trip.

**Latency budget.** Add `max_latency_ms` (200–120000) when a step has a deadline: Wity thinks only as long as the
budget allows, then answers from the reasoning so far; with no time to think it answers directly and softens close
calls. Cut-short answers carry `reasoning.budget_limited: true`. It covers server time (add your network) and is a
target, not a hard cutoff. Measured on a deliberately hard set: no budget 79.5% accurate (p99 36 s); 5000 ms 70.8%
(p99 5.0 s); 2000 ms 60.3% (p99 2.3 s). Everyday decisions finish long before any budget.

**Forecast questions.** When a question asks how likely outcomes are ("Will this lot fail inspection? Give
probabilities that reflect the evidence."), Wity works the odds out from the evidence instead of guessing a side,
and sets `reasoning.forecast: true`. For good forecasts: put the numbers in the state (counts, rates, base rates,
the rule deciding the outcome), ask for probabilities in the instructions, and make outcomes exhaustive and
mutually exclusive. Forecasting applies to text states (not to requests with an image) and only in `auto`/`always`.

---

## 7. Writing questions that work

The option descriptions are the definition Wity uses. Clear definitions are the biggest lever on accuracy.

1. **Describe when each option applies**, not just its name.
   Weak: `"urgent": "Urgent"`. Strong: `"urgent": "Production is stopped, safety is at risk, or a customer deadline is within 24 hours"`.
2. **Make options mutually exclusive.** If two can both be true, split into two questions.
3. **Add a catch-all** (`other`, `none`, `needs_review`) whenever a real input might fit nothing. Wity picks it
   when the evidence supports none of the others, which gives you a clean hand-off signal.
4. **One judgement per question.** "Refund over €100 from a new customer?" is three questions; ask them in one
   request and combine in code.
5. **Put the evidence in the state.** Wity uses only what you send. Include the policy clause, threshold or
   history the decision depends on. JSON field names help.
6. **Use `score` for ordered judgements** (severity, quality, risk), levels lowest first, 4–5 levels usually enough.
7. **Keep your rules out of the state.** The state is the evidence; put your policy and definitions in
   `instructions`/`criteria`. If the state holds untrusted user text, label it (e.g. `{"customer_message": ...}`)
   and gate high-stakes actions on probabilities and review, as with any model.
8. **Keep a test set.** 20–50 real cases with expected answers; rerun after every wording change and compare
   probabilities as well as choices.

In `auto`, Wity also checks that its answer does not depend on the order you listed the options in, and thinks
when it does, so you do not need to shuffle options yourself.

---

## 8. Images

- Add a top-level `image`: a data URL (`data:image/png;base64,...`). PNG, JPEG, WebP, GIF, BMP. **One image per request.**
- The format is read from the file itself: a generic media type (`application/octet-stream`), bare base64, line breaks
  or URL-safe base64 all work. Remote URLs are not fetched. An undecodable image gets a 400 saying why.
- Works on `/v1/systemone` (every question sees it) and `/v1/generate`.
- Explain the image in the state: what it shows and what colours/symbols mean ("green die passed, red die failed").
  Put numbers you already know in the state rather than asking Wity to read them off a chart.
- Request body limit is 12 MB; downscale photos to ~1,500 px on the long side. Images add a few hundred ms to
  about a second.

```python
import base64
with open("wafer.png", "rb") as f:
    image = "data:image/png;base64," + base64.b64encode(f.read()).decode()
```

---

## 9. Common patterns

**Intent routing.** One `choice` over your handlers + a catch-all; route on `choice` when its probability clears
your threshold, otherwise to a person.

**Confidence-gated automation.** Act automatically above a high threshold, ask for one-click confirmation in the
middle band, send the rest to review (§5).

**Guardrails.** Several `noul` questions in one request (jailbreak? leaks secrets? harmful?) run on an LLM's
output before it is shown; block if any is high.

**Composite scoring.** Several `score`/`noul` questions on the same state (a lead, a candidate, a change request),
combined in code with your own weights.

**Retrieval filtering.** For each retrieved passage, a `noul` "Does this passage help answer the question?";
keep passages above a threshold, sort by `noul`.

**Decide, then generate.** Decide first; call `generate` only when the decision needs text (a field to fill,
a reply to send), with a `shape` so the output is always valid.

```python
step = decide(state, {"next": {"type": "choice", "instructions": "What next?",
                               "criteria": {"type_origin": "The origin field is empty and visible",
                                            "search": "All fields are filled",
                                            "done": "Results are shown"}}})
if step["answers"]["next"]["choice"] == "type_origin":
    out = generate(state, "Origin city, as printed on the ticket",
                   shape={"type": "object", "properties": {"city": {"type": "string", "pattern": "^[A-Za-z ]{1,40}$"}},
                          "required": ["city"]})
    fill("#from", out["value"]["city"])
```

**Parallel questions.** Ask everything you need about one state in one request: one round trip, answers keyed by
your names.

---

## 10. Reference client

### Python

```python
import os, requests

BASE = "https://wity-proxy-production-2c33.up.railway.app"
HEADERS = {"Authorization": f"Bearer {os.environ['WITY_API_KEY']}"}

def decide(state, questions, reasoning="auto", image=None, retries=2):
    body = {"state": state, "questions": questions, "reasoning": reasoning}
    if image:
        body["image"] = image
    for attempt in range(retries + 1):
        r = requests.post(f"{BASE}/v1/systemone", json=body, headers=HEADERS, timeout=60)
        if r.status_code in (502, 503) and attempt < retries:
            import time; time.sleep(2 ** attempt); continue
        r.raise_for_status()
        return r.json()

def generate(state, instructions, shape=None, max_tokens=128, image=None):
    body = {"state": state, "instructions": instructions, "max_tokens": max_tokens}
    if shape:
        body["shape"] = shape
    if image:
        body["image"] = image
    r = requests.post(f"{BASE}/v1/generate", json=body, headers=HEADERS, timeout=60)
    r.raise_for_status()
    return r.json()
```

### TypeScript

```ts
const BASE = "https://wity-proxy-production-2c33.up.railway.app";

export async function decide(state: unknown, questions: Record<string, unknown>, reasoning = "auto", image?: string) {
  const res = await fetch(`${BASE}/v1/systemone`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.WITY_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ state, questions, reasoning, ...(image ? { image } : {}) }),
    signal: AbortSignal.timeout(60_000),
  });
  if (!res.ok) throw new Error(`Wity ${res.status}: ${await res.text()}`);
  return res.json();
}
```

---

## 11. Errors, limits, billing

| status | meaning | action |
|---|---|---|
| 400 | invalid body or field (message says what) | fix the request; do not retry |
| 401 | missing or invalid key | check `WITY_API_KEY` |
| 502 | the model could not produce a valid answer (rare) | retry once |
| 429 | over the key's rate limits (20 req/s, bursts of 40, 32 in flight) | wait `Retry-After` seconds, then retry |
| 503 | starting, waking from idle (`Retry-After: 90`, ~1.5–2 min), or temporarily unavailable | honour `Retry-After`; otherwise back off (1 s, 2 s, 4 s) |

Limits: 20 requests/s per key (bursts of 40), 32 in flight · body ≤ 12 MB · one image · state ≤ 32,000 chars · instructions ≤ 4,000 (decide) / 8,000 (generate) ·
choice 2–256 options · score 2–10 levels · generate `max_tokens` ≤ 512.

Billing: **$0.042 per million input tokens; output free.** `usage.input_tokens` is billable: your request as
Wity reads it, counted once per question (each question is billed with the state it reads). Thinking, internal
checks, forecasts, images and generated text are not billed. Asking several questions in one request saves round
trips, not tokens: keep the state to what the decisions need.

Privacy: request and response bodies are not logged; only key name, time, status, latency and question count.

---

## 12. Checklist for an integration

- [ ] Key in `WITY_API_KEY`, calls made from the server, timeout ≥ 60 s, retries on 502/503.
- [ ] Every `choice` has descriptions that say when each option applies, plus a catch-all where needed.
- [ ] Thresholds per action chosen on 100+ labelled past cases; low-probability answers go to a person.
- [ ] `reasoning: "auto"` unless latency must be flat (`"off"`) or every call is high-stakes (`"always"`).
- [ ] How-likely questions ask for probabilities and carry the numbers in the state.
- [ ] `generate` calls use a `shape` whenever the output feeds code.
- [ ] A small regression set rerun after every prompt/criteria change.
