Python SDK

Not on PyPI yet. For now: uv add "decidedb @ git+https://github.com/SultanAlzoghaibi/JevDB#subdirectory=sdk/python"

Install

TL;DR

Add the package. That's the only dependency.

uv add decidedb
terminal
uv add decidedb        # or: pip install decidedb

It wraps TypeSafe's official Jev client, so you get that too.

Environment variables

TL;DR

Three variables: your Jev key, your DecideDB key, and where DecideDB runs.

DECIDEDB_API_KEY=jdb_live_…
.env
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:3000

Your 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

TL;DR

Make one Jev call through AsyncDecideDB. The answer comes back as usual, and DecideDB keeps a copy.

main.py
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

TL;DR

Create the client in one file. Import it everywhere you call Jev.

app/decidedb_client.py
app/decidedb_client.py
from 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

TL;DR

Set the group on the client. Pass the user on each call.

user_id
app/tickets.py
from 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
    )
OptionWhat it's forUsually set
group_idThe feature, team or app the call belongs to.On the client
user_idWho triggered the call.Per call
tagsFree 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

TL;DR

Every call has an id. Keep it, so you can grade the decision later.

id_of(response)
app/tickets.py
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

TL;DR

DecideDB never breaks your app. Call flush() before your process exits.

flush()
app/tickets.py
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

TL;DR

One record per call: the exact request, the exact response, timing and your labels.

record.json
{
  "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.