Python SDK
jepela-sdk 0.2 needs Python 3.10 or newer and nothing else; Pydantic is used only if you give it a Pydantic model. Install it from GitHub:
pip install "git+https://github.com/jepelasweden/jepela#subdirectory=sdk/python"
export JEPELA_API_KEY=jj_live_...
The client
JepelaClient(api_key=None, base_url=None, timeout=120.0, max_retries=2, backoff_seconds=0.5): the key falls back to JEPELA_API_KEY, the gateway to JEPELA_BASE_URL and then the API of the key's region (jj_live_eu_... goes to https://eu.api.jepela.com; see Regions); timeout is seconds per call. It is safe to share between threads. AsyncJepelaClient takes the same keyword arguments. Use it as a context manager (with JepelaClient() as client:) or call client.close() when done.
Questions and answers
from jepela import JepelaClient, Choice, Noul, Score
with JepelaClient() 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="jepela-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["intent"].answer_confidence # the probability of the answer given; compare GET /v1/quality's thresholds with it
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
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.forget("acme", "Ann Lindqvist", preview=True) # what would go, and related; nothing removed
client.delete("acme")
client.memory("acme").lines_stored
client.memory("acme", lines=True).lines # every stored line, word for word
memory takes use, top_k (1 to 20, default 6), min_share (0.05 to 1, default 0.2), compare, placebo, names (default true: the subject's name and aliases are searched with the state), question_words (default true: the questions' instructions are searched too), focus (default false: keep only lines that share a word with the state beyond those names) and explain (default false).
client.pin("acme", "ACME pays for premium support.") # sent first at every decision about acme
client.exclude("player-9", ["gold_coins"], "Coin bug.", question="reward") # removed in code, probability 0
client.aliases("acct-1", ["Globex Corporation"]) # other names recall searches
client.aliases_delete("acct-1", ["Globex Corporation"]) # those names out again, none added
r = client.system_one(state, questions, subject="acme", memory={"explain": True})
r.memory.pinned # ids of the pinned rules sent
r.memory.excluded # {"reward": {"gold_coins": "Coin bug."}}
r.memory.exclusions_skipped # exclusions that would have removed every option
r.memory.explain # [{"line", "kind": "pinned" | "rule" | "recalled", "effect": {question: move}}]
# (and "measured_on_first" where the second tier gave the answer: see Memory)
explain asks once more per memory line (up to 8) with that line left out; those passes are not billed. See Memory.
Request options and what comes back
r = client.system_one(state, questions, model="jepela-english", robust=True, windows=True,
derive={"over": "reading.t > limit"}, values={"limit": 75}, cache=True, method="auto")
r.decision_id # for feedback
r.warnings # [JepelaWarning(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
r.answers["intent"].method # "engine" or "match"
r.answers["intent"].similarity # matched answers: {"best", "second"} cosine
r = client.system_one(state, questions, quality="balanced") # "fast" (default), "balanced" or "best"
r.quality # {"level", "below", "asked_again", "why"}, and "fallback" when the second tier failed
r.answers["intent"].right_probability # the estimate that the engine's answer is right
r.answers["intent"].tier, r.answers["intent"].first # 2 and the engine's own answer, when the second tier answered
method says how choice questions are answered: "auto" (the default) matches choices of more than 20 options by vectors and asks the engine the rest, "engine" reads every option, "match" matches every choice question (not score or noul). Measured on BANKING77 (600 bank messages, 77 intents) on a GPU test server, 2026-09-27: matching 65.2% right in a median of 12 ms, the engine reading all 77 options 62.2% in 57 ms. See Choice.
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 and windows read more, and the tokens they read are billed; the compare, explain and placebo passes are not billed. 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.
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from jepela import JepelaClient, 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 = JepelaClient().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
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("jepela-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, and its batches
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 jepela 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.
Fine-tuning
Your golden cases (at least 20) are the training data; see Fine-tuning.
client.finetunes() # {"jobs": [...], "min_cases": 20, "bases": ["english", "multilingual", "typed-decisions"]}
job = client.finetune(base="english") # {"job", "model", "cases", "held_out", "state": "running"}
done = client.finetune_wait(job["job"]) # polls client.finetune_job(job["job"]) until done or failed
done["usable"], done["result"] # usable: it beat the standard engine and still listens to memory
client.system_one(state, questions, model=job["model"])
client.finetune_delete(job["model"]) # its files are deleted and its name stops working
finetune raises ConflictError while another of your jobs runs or the engine is off, and BadRequestError when there are too few cases. finetune_wait(job, timeout=3600.0, poll_seconds=10.0) raises TimeoutError after timeout seconds.
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 JepelaError 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
import asyncio
from jepela import AsyncJepelaClient
async def main():
async with AsyncJepelaClient() 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.