Python SDK
pip install curva-aiThe curva package uses only the Python standard library (Python 3.9+). Platform wheels also
ship the curva server binary.
import curvafrom curva import Curva, Choice, Score, Noul, Multi, CurvaErrorClients
Section titled “Clients”curva.local()
Section titled “curva.local()”curva.local(model=None, db=None, *, startup_timeout=15.0, **client_options) -> LocalCurvaStarts curva serve on a free 127.0.0.1 port and returns a client bound to it. The server stops
when Python exits, when you call close(), or at the end of a with block.
model: the server’s default model.db: SQLite file for decisions, feedback and calibrators (default~/.curva/curva.db).- The server inherits this process’s environment (
OPENROUTER_API_KEY,CURVA_RPM, …). client_optionsgo toCurva(timeout,max_retries,api_key).
Curva(base_url=None, api_key=None, *, model=None, timeout=30.0, max_retries=3, retry_statuses=(429, 5xx), headers=None)A client for a running server. Reads CURVA_BASE_URL (default http://localhost:7777) and
CURVA_API_KEY when not given. 429, 5xx and dropped connections are retried up to
max_retries times, honouring Retry-After. headers are sent with every request. Also usable
as with Curva() as client:.
Methods
Section titled “Methods”decide
Section titled “decide”client.decide(state, questions, *, model=None, mode=None, debias=None, project=None, council=None, cascade=None, escalate_below=None, race=None, explain=False, config=None, privacy=None) -> Decision| Argument | Meaning |
|---|---|
state |
Any JSON-serialisable value |
questions |
{key: question}: Choice, Score, Noul or Multi objects, or plain dicts in the HTTP shape (needed for when) |
model |
A model id, or a list tried in order (fallback chain) |
mode |
"auto" (default), "logprobs" or "verbal" |
debias |
Ask in both option orders and average (server default True) |
project |
Calibration namespace |
council / cascade / race |
Multi-model plans; use at most one. escalate_below goes with cascade |
explain |
Add per-field attribution to every answer |
config |
A pinned config such as "curva-1.0.0" |
privacy |
"strict" to use only providers that neither store nor train on data |
system_one is an alias of decide.
feedback
Section titled “feedback”client.feedback(decision_id, question, label) -> dictRecords the true answer: an option key (Choice), a level index (Score) or True/False (Noul).
calibration
Section titled “calibration”client.calibration(question, project=None) -> dictHeld-out before/after accuracy, ECE, Brier and reliability bins for a question.
client.audit(project=None, before=None, limit=100) -> dictLogged decisions, newest first: {"entries": [...], "next_before": id or None}. Pass
next_before back as before for the next page.
client.drift(question, project=None, weeks=8) -> dictThe question’s weekly answer mix and confidence, with a drift flag on the latest week. See Drift.
health, models
Section titled “health, models”client.health() returns {"status", "version"}; client.models() lists the pinned configs.
AsyncCurva takes the same arguments as Curva and has the same methods, awaitable:
from curva import AsyncCurva
async with AsyncCurva() as client: d = await client.decide(state, questions) decisions = await client.decide_many([(state_a, questions), (state_b, questions)])decide_many runs several decisions, at most concurrency (default 8) at once, and keeps the
input order; return_exceptions=True puts each failure in its slot instead of raising. It runs the blocking client in worker threads,
which suits up to a few hundred concurrent calls.
Questions
Section titled “Questions”Choice(instructions, options=None, *, criteria=None, escape=None, min_confidence=None, examples=None)Score(instructions, levels=None, *, criteria=None, min_confidence=None, examples=None)Noul(instructions, examples=None, *, criteria=None)Multi(instructions, options, *, threshold=0.5)question.when(**fields) # ask only when the state (or an "@key" answer) matchesquestion.rule(answer, **if) # answer instantly when the conditions holdquestion.depends(*keys) # answer after `keys`, seeing their answersoptions:{key: description}or a list of keys. Order is kept.escape: addnone_of_these(default on whenoptionsis given).min_confidence: answers below it getabstain=True.examples: up to 10(state, label)pairs.think=True(any question): reason before answering, in a call of its own. See Workflows in one call for.depends().criteria=: the compatible style.Choice(..., criteria={...})(descriptions may beNoneor JSON, and there is no escape option unlessescape=True),Score(..., criteria=[...]),Noul(..., criteria={"true": ..., "false": ...}).
Results
Section titled “Results”Decision
Section titled “Decision”| Attribute | Meaning |
|---|---|
id |
dec_…, used for feedback |
model, mode, project, config |
How the decision was made |
latency_ms, cost_usd, cached |
Time, cost, and whether it came from the cache |
request_id |
The response’s x-request-id, for matching server logs |
usage |
Token usage, when the server reports it |
answers |
{key: Answer}; also d["key"] |
choices, scores, nouls |
Answers grouped by type |
Answer
Section titled “Answer”Only the fields for the question’s type are set; the rest are None.
| Attribute | Set for |
|---|---|
choice |
Choice: the chosen option key |
probabilities |
Choice (per key), Score (per level), Multi (per option) |
confidence |
Choice and Score: probability of the most likely answer |
score |
Score: expected level, 0 = lowest |
noul |
Noul: P(yes) |
selected |
Multi: options at or above the threshold |
abstain |
When min_confidence was set |
calibrated |
True when a calibrator adjusted the probabilities |
agreement |
Council: share of members agreeing with the council |
answered_by |
Cascade: the model that gave the final answer |
explain |
explain=True: probability drop without each state field |
stage |
With .depends(): the stage (0-based) that answered it |
Errors
Section titled “Errors”Anything that isn’t retried raises CurvaError, with status, type and message matching the
HTTP error. Subclasses let you catch specific cases:
| Class | Status |
|---|---|
AuthError |
401 |
InvalidRequestError |
400, 422 |
NotFoundError |
404 |
RateLimitError |
429 (has retry_after) |
ModelError |
502, 504 |
from curva import CurvaError
try: client.decide(state, questions)except CurvaError as e: print(e.status, e.type, e.message)
