# Python SDK

`jers-sdk` 0.2 needs Python 3.10 or newer and nothing else; Pydantic is used only if you give it a Pydantic model. It is not on PyPI yet; until it is, call the HTTP API directly (every page shows the request).

```bash
export JERS_API_KEY=jj_live_...
export JERS_BASE_URL=https://api.getjers.com    # the default
```

## The client

`JersClient(api_key=None, base_url=None, timeout=120.0, max_retries=2, backoff_seconds=0.5)`: the key falls back to `JERS_API_KEY`, the gateway to `JERS_BASE_URL` and then `https://api.getjers.com`; `timeout` is seconds per call. It is safe to share between threads. `AsyncJersClient` takes the same keyword arguments. Use it as a context manager (`with JersClient() as client:`) or call `client.close()` when done.

## Questions and answers

```python
from jers import JersClient, Choice, Noul, Score

with JersClient() as client:
    r = client.system_one(
        state={"message": "I have asked three times now. Can I please talk to a real person?"},
        questions={
            "wants_human": Noul("Is the customer asking for a human agent?"),
            "intent": Choice("What does the customer want?", {"talk_to_human": "asks for a person", "status": "asks where something is", "other": "anything else"}),
            "frustration": Score("How frustrated is the customer?", ["calm", "annoyed", "angry"]),
        },
        model="jers-english")

r.answers["wants_human"].noul            # probability of yes, 0 to 1
r.answers["intent"].choice               # the most probable option name
r.answers["intent"].probabilities        # every option with its probability
r.answers["intent"].confidence           # 0 to 1: how concentrated the probabilities are
r.answers["frustration"].score           # probability-weighted level, 0 = the lowest
r.usage.answers, r.usage.cost, r.usage.balance
```

Question objects or plain dictionaries both work. The Quick start shows a measured answer.

## Memory

```python
client.remember("acme", "ACME holds an enterprise contract; refunds go to billing within one business day.")
r = client.system_one(state, questions, subject="acme", memory={"compare": True})
r.memory.lines_used, r.memory.lines_seen, r.memory.lines_dropped
r.memory.hits[0]["text"]
r.memory.without_memory["intent"].choice  # the answer without the memory
r.memory.changes["intent"]                # {"answer_changed": ..., ...}
client.forget("acme", "enterprise contract")["verified_forgotten"]
client.delete("acme")
client.memory("acme").lines_stored
client.memory("acme", lines=True).lines   # every stored line, word for word
```

## Request options and what comes back

```python
r = client.system_one(state, questions, model="jers-english", robust=True, windows=True,
                      derive={"over": "reading.t > limit"}, values={"limit": 75}, cache=True)
r.decision_id                              # for feedback
r.warnings                                 # [JersWarning(code, message, question)]
r.reading["intent"]                        # what the engine read of the state
r.usage.answers_billed, r.usage.state_truncated
r.answers["intent"].orders, r.answers["intent"].agreement   # with robust
```

`robust=True` asks each choice of up to 20 options in 3 option orders (2 for a two-option choice; `{"orders": n}` for 1 to 5) and averages them; `agreement` is the share of orders that picked the answer. `windows=True`, or `{"combine": {"question_id": "max" | "mean" | "min"}}`, reads a state that is cut in up to 16 overlapping windows. Robust orders, windows and the placebo pass are billed as answers; the `compare` pass is not. See Request options.

## Pydantic models

A `Literal` or `Enum` field is a choice, a `bool` field a yes/no question, and an `int` or `float` field marked with `Levels(...)` a score. The field's description is the question; the answers come back as the model: an `int` score field gets the most likely level, a `float` the probability-weighted level, and a `bool` is true when the probability of yes is at least 0.5.

```python
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from jers import JersClient, Levels, Options

class Ticket(BaseModel):
    team: Annotated[Literal["billing", "support", "other"],
                    Options({"billing": "Charges and refunds", "support": "Product problems", "other": "None of these"})] = Field(
        description="Which team should handle this?")
    refund: bool = Field(description="Is the customer asking for money back?")
    urgency: Annotated[int, Levels("Can wait a week", "Should be handled today", "Blocking the customer now")] = Field(
        description="How urgent is this?")

r = JersClient().system_one("We were charged twice and need the money back today.", Ticket)
r.parsed.team, r.parsed.refund, r.parsed.urgency
```

## Rules, labels, golden sets, batches, account

```python
client.rules_add("mia", "session_result <= -30", "Mia's stop-loss is reached: she stops playing now.", ttl_seconds=3600)
client.rules("mia"); client.rules_delete("mia", rule_id)
client.feedback(r.decision_id, "intent", "status")
client.quality("intent"); client.calibration_fit(); client.calibration()   # the temperatures in use
client.golden_add([...]); client.golden(); client.golden_run("jers-english")
client.golden_delete(["refund-1"])          # these cases; [] deletes nothing; no argument deletes every case
client.decisions_delete()                   # every stored decision record and label of this tenant
job = client.batch_upload("requests.jsonl")  # or client.batch_create([{...}, ...])
client.batch_wait(job["id"]); client.batch_results(job["id"]); client.batch(job["id"]); client.batches()
client.models().available(); client.usage()
client.request("GET", "/v1/usage")          # any route, as parsed JSON

from jers import signup
signup("you@example.com", invite_code="...")   # no key needed; the key is shown once
```

`golden_run` answers every case in one request. If that takes longer than the client's `timeout` (120 s by default), the client raises `ConnectionFailed` and does not send it again while the gateway finishes and bills the run; give a large golden set a larger `timeout`.

## Errors and retries

One class per status: `AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409), `BadRequestError` (413, 415, 422), `RateLimitError` (429, with `retry_after`), `EngineError` (502, 503), `ServerError` (500), `ConnectionFailed`. All inherit `JersError` and carry `status` and `kind`. The client keeps one connection open per thread, retries 429, 502 and 503 up to `max_retries` (default 2) with backoff, honouring `Retry-After` up to 30 seconds, and retries a connection that failed before the request was sent. A request that was sent and then failed is sent again only if it is a GET, so a decision is never billed twice.

## Async

```python
import asyncio
from jers import AsyncJersClient

async def main():
    async with AsyncJersClient() as client:
        a, b = await asyncio.gather(client.system_one(state_a, questions), client.system_one(state_b, questions))

asyncio.run(main())
```

Each call runs the synchronous client in a worker thread, so several requests can be in flight at once; the gateway accepts them concurrently and the engine answers them one at a time.
