Skip to content

Python SDK

Terminal window
pip install curva-ai

The curva package uses only the Python standard library (Python 3.9+). Platform wheels also ship the curva server binary.

import curva
from curva import Curva, Choice, Score, Noul, Multi, CurvaError
curva.local(model=None, db=None, *, startup_timeout=15.0, **client_options) -> LocalCurva

Starts 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_options go to Curva (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:.

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.

client.feedback(decision_id, question, label) -> dict

Records the true answer: an option key (Choice), a level index (Score) or True/False (Noul).

client.calibration(question, project=None) -> dict

Held-out before/after accuracy, ECE, Brier and reliability bins for a question.

client.audit(project=None, before=None, limit=100) -> dict

Logged 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) -> dict

The question’s weekly answer mix and confidence, with a drift flag on the latest week. See Drift.

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.

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) matches
question.rule(answer, **if) # answer instantly when the conditions hold
question.depends(*keys) # answer after `keys`, seeing their answers
  • options: {key: description} or a list of keys. Order is kept.
  • escape: add none_of_these (default on when options is given).
  • min_confidence: answers below it get abstain=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 be None or JSON, and there is no escape option unless escape=True), Score(..., criteria=[...]), Noul(..., criteria={"true": ..., "false": ...}).
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

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

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)

© 2026 Tarkova Private Limited.