Python SDK
Not on PyPI yet. For now: uv add "decidedb @ git+https://github.com/SultanAlzoghaibi/JevDB#subdirectory=sdk/python"
Install
Add the package. That's the only dependency.
uv add decidedbuv add decidedb # or: pip install decidedbIt wraps TypeSafe's official Jev client, so you get that too.
Environment variables
Three variables: your Jev key, your DecideDB key, and where DecideDB runs.
DECIDEDB_API_KEY=jdb_live_…TYPESAFE_API_KEY=sk-… # Jev, from console.typesafe.ai
DECIDEDB_API_KEY=jdb_live_… # DecideDB, from your DecideDB console
DECIDEDB_URL=https://<decidedb-cloud-url> # self-hosting: http://localhost:3000Your Jev key only goes to Jev. Your DecideDB key only goes to DecideDB.
DECIDEDB_URL is the DecideDB Cloud URL. Self-hosting? Use http://localhost:3000.
An argument passed to the client always wins over its environment variable.
Quickstart
Make one Jev call through AsyncDecideDB. The answer comes back as usual, and DecideDB keeps a copy.
import asyncio
from typesafe_sdk import Choice, Noul, Score
from decidedb import AsyncDecideDB
async def main() -> None:
async with AsyncDecideDB(group_id="support-bot") as decidedb:
response = await decidedb.system_one(
state={"document": "I was charged twice. Please fix this ASAP."},
questions={
"billing": Noul(instructions="Is this ticket about billing?"),
"tone": Choice(
instructions="What is the customer's tone?",
criteria={"calm": None, "frustrated": None, "angry": None},
),
"urgency": Score(
instructions="How urgent is this ticket?",
criteria=["can wait", "this week", "today"],
),
},
)
print(response.nouls["billing"].noul)
print(response.choices["tone"].choice)
print(response.scores["urgency"].score)
asyncio.run(main())system_one takes exactly TypeSafe's arguments and returns TypeSafe's response unchanged.
The copy is sent in the background. Your call doesn't wait for it.
Open the console to see the decision.
Set it up once
Create the client in one file. Import it everywhere you call Jev.
app/decidedb_client.pyfrom decidedb import AsyncDecideDB
# One client for the whole app. Keys and URL come from .env.
decidedb = AsyncDecideDB(group_id="support-bot", tags=["prod"])Keys, group and tags then live in one place.
Creating a client per request works, but wastes connections. Make one and reuse it.
See the full project tree in where it goes in your project.
Per-call options
Set the group on the client. Pass the user on each call.
user_idfrom app.decidedb_client import decidedb
from app.questions import QUESTIONS
async def triage(ticket, current_user):
return await decidedb.system_one(
state={"document": ticket.text},
questions=QUESTIONS,
user_id=current_user.id, # changes every call
tags=["prod", "beta-form"], # replaces the client's tags for this call
)| Option | What it's for | Usually set |
|---|---|---|
| group_id | The feature, team or app the call belongs to. | On the client |
| user_id | Who triggered the call. | Per call |
| tags | Free labels, e.g. prod or an experiment arm. | On the client |
A value passed on a call replaces the client's value for that call only.
Request IDs + grading
Every call has an id. Keep it, so you can grade the decision later.
id_of(response)response = await decidedb.system_one(state=..., questions=QUESTIONS)
decision_id = decidedb.id_of(response) # a UUID made by the SDK
# Or use your own id, so grading later is just a lookup:
await decidedb.system_one(state=..., questions=QUESTIONS, decision_id=ticket.id)Get the id with id_of, or pass your own with decision_id.
Your own id (a ticket or order id) is easiest: you already have it when the outcome is known.
Grading tags a decision good or bad once you know the real outcome. It's coming soon; ids you keep now will work with it.
Errors, flush() and shutdown
DecideDB never breaks your app. Call flush() before your process exits.
flush()from typesafe_sdk import TypeSafeAPIError
try:
response = await decidedb.system_one(state=..., questions=QUESTIONS)
except TypeSafeAPIError as err: # logged to DecideDB, then re-raised
print(err.status, err.body)
# On shutdown (the with-block does this for you):
await decidedb.aclose()- Jev fails: the failure is logged, then the original error is raised again. Your code behaves exactly as without DecideDB.
- DecideDB is down: nothing breaks. The failed send calls
on_error(default: a warning). - Shutdown: logs still in flight are lost if the process exits first. Flush before exiting.
What gets logged
One record per call: the exact request, the exact response, timing and your labels.
{
"id": "0192c0b8-…", // the decision id
"started_at": "2026-09-27T14:02:11Z",
"latency_ms": 184.2,
"status": 200, // Jev's HTTP status
"error": null, // set when the Jev call failed
"user_id": "user_482",
"group_id": "support-bot",
"tags": ["prod"],
"request": { … }, // the exact body sent to Jev
"response": { … }, // the exact body Jev returned
"sdk": "…"
}The DecideDB key travels in the Authorization header. No key is ever in the body.