Skip to main content

jev

@jev.fn turns a Python function definition into a query against Jev, TypeSafe's System One model. You declare the function (parameters, docstring, return annotation) and the decorator compiles it into a state + typed questions request. Calling the function sends the request and returns a validated instance of the return annotation.

Jev generates no text. It answers typed questions about a state (yes/no probabilities, choices, scores) in one parallel call, with calibrated probabilities. That constraint drives the design: the return annotation must be a Pydantic model, and each of its fields maps onto one of Jev's three question types.

A function signature is already a complete specification of a decision. The name says what to decide, the parameters say what to decide it from, and the return annotation says what shape the answer takes; the docstring supplies the judgment. @jev.fn treats that specification as sufficient and lets Jev fill in the body. The spec is made of things you already write: a signature, a docstring, a Pydantic model. There is no prompt string to maintain, no JSON schema to keep in sync, no parsing layer between the call and the answer.

Install

uv sync

Python 3.14. Get an API key from console.typesafe.ai and put it in .env:

TYPESAFE_API_KEY=...

The pattern

from typing import Literal
from pydantic import BaseModel, Field
import jev

class Triage(BaseModel):
    department: Literal["billing", "technical", "sales"]
    is_urgent: bool
    frustration: int = Field(ge=0, le=2)

@jev.fn
def triage(ticket: str) -> Triage:
    """A customer support ticket:

    {{ ticket }}
    """
    return triage.state()

triage("I was charged twice. Fix this NOW.")
# Triage(department='billing', is_urgent=True, frustration=2)

What happens:

  1. At decoration time, the return annotation is checked (must be a BaseModel subclass; anything else raises TypeError at import rather than at first call) and each field is compiled into a question.
  2. At call time, the docstring is rendered as a Jinja2 template with the bound arguments and sent as the state. With no docstring, the arguments themselves are sent as a JSON state.
  3. The answers are coerced back and validated by pydantic. For the body-less form, write the body as return triage.state(); it type-checks like any other return and leaves the docstring as the whole state. A bare ... or raise NotImplementedError also works.

For the body-less form the docstring does all the work, which may make it the only place in Python where documentation outranks implementation.

Field mapping

Field type Jev question Coerced back as
bool Noul p(yes) >= threshold (default 0.5)
Literal[...] Choice the selected label
Enum Choice the selected member
int with Field(ge=, le=) Score lo + round(expected_score)
float with Field(ge=, le=) Score linear interpolation over the levels
  • Field(description=...) becomes the question's instructions; without one the field name is humanized (is_urgent → "is urgent"). Write descriptions; they are the questions.
  • Score levels default to the numbers in range. Override them with Field(..., json_schema_extra={"levels": ["cold", "warm", "hot"]}).
  • Limits: 255 options per choice, 256 levels per score. Exceeding either is a TypeError at decoration time.
  • Anything else (str, nested models, lists, Optional) raises TypeError at decoration time, because Jev cannot produce those values.

Evaluated bodies

The body always runs, and there are three useful things it can do:

  • Return fn.state() with no value: the body-less form; the rendered docstring is the whole state. (... or raise NotImplementedError work too.)
  • Build the state with return fn.state(value): loops, conditionals, f-strings, whatever Python you need; the returned value is the state, verbatim, and the docstring is documentation in this form.
  • Answer directly with return Model(...): skips the API call entirely, the mock seam for tests.

Building the state looks like this:

@jev.fn
def triage_batch(tickets: list[str]) -> BatchTriage:
    """Triage a batch of support tickets."""
    numbered = [f"[{i}] {t}" for i, t in enumerate(tickets)]
    return triage_batch.state(
        "Triage this batch of support tickets.\n\n" + "\n".join(numbered)
    )

Framing like "Triage this batch" lives in the body now, in the open, rather than being lifted out of the docstring.

fn.state(...) is typed value -> return-annotation, so the body's return type-checks against the annotation.

Async functions work identically (await the call; the body may also await). Configure per function with @jev.fn(model="jev-latest", client=...); the default client reads TYPESAFE_API_KEY and calls jev-latest. The bool threshold defaults to 0.5; tune it per function with @jev.fn(bool_threshold=0.7) or globally with the JEV_BOOL_THRESHOLD environment variable.

Batch with .map

triage.map(tickets) applies the function to each item in ONE call: the items become a JSON state array, the fields become one set of questions per item, and Jev answers all of them in parallel. The result is a list[Triage] in input order. Each item runs through the same body machinery as a direct call, so short-circuited items (return Model(...)) skip the API and slot into the results, and evaluated bodies run per item. Items must bind as the function's only positional argument. Batches are a single request: hundreds of questions per call have worked in TypeSafe's own cookbooks, but there is no documented limit, so chunk very large batches yourself.

triage.map(tickets)          # sync:  list[Triage]
await atriage.map(tickets)   # async: list[Triage]

Class form: jev.BaseModel

For the common case of one blob of state in, one struct out, there is a class interface (the same field machinery, no docstring involved):

import jev

class Triage(jev.BaseModel):
    department: Literal["billing", "technical", "sales"]
    is_urgent: bool
    frustration: int = Field(ge=0, le=2)

Triage.decide("I was charged twice. Fix this NOW.")
# Triage(department='billing', is_urgent=True, frustration=2)

await Triage.adecide("...")                        # async form
Triage(department="billing", is_urgent=False, frustration=0)  # plain constructor: no API call

Fields compile at class definition (unsupported types raise at import). Class attributes __jev_model__ and __jev_bool_threshold__ pin the model and threshold per class. Deciding is a classmethod rather than a constructor overload because pydantic's dataclass_transform synthesizes a field-only __init__ for subclasses in both mypy and pyright; the classmethod keeps the call typed as -> Self in both.

Function form: jev.decide

The same decision on a plain BaseModel — no subclass, no decorator:

from pydantic import BaseModel
import jev

class Triage(BaseModel):
    department: Literal["billing", "technical", "sales"]
    is_urgent: bool
    frustration: int = Field(ge=0, le=2)

jev.decide("I was charged twice. Fix this NOW.", Triage)
# Triage(department='billing', is_urgent=True, frustration=2)

await jev.adecide("...", Triage)  # async form

Keyword arguments model= and bool_threshold= mirror the class attributes. Handed a jev.BaseModel subclass, jev.decide falls back to __jev_model__ / __jev_bool_threshold__ and reuses the questions compiled at class definition; plain models compile per call, so prefer the class form in hot loops.

Testing

from jev import builder, state_payload

marker = builder(triage_batch)(["a", "b"])   # runs the body, no API call
assert state_payload(marker) == "Triage this batch of support tickets.\n\n[0] a\n[1] b"

Or return a model from the body to short-circuit the call in tests.

Type checking

The decorated function has type JevFn[P, R] (or AsyncJevFn[P, R] for async), so call sites see the original parameter signature and the declared return model, and fn.state returns the same model. Bare @jev.fn rejects a non-BaseModel return annotation statically, before any code runs. The package passes pyright --strict and mypy --strict with no casts and no ignore comments.

See example.py for a runnable tour (uv run python example.py).

Limitations

  • Probabilities are discarded. bool is thresholded (0.5 by default, tunable), Choice takes the argmax, Score returns the expected value. Jev's calibrated probabilities and confidence scores never reach you; if you need them (confidence-gated routing is the main reason to use Jev), use typesafe_sdk directly.
  • No streaming. Jev samples in parallel in a single shot, so there is nothing to stream.
  • Legacy sentinel. raise NotImplementedError (or ...) still marks a body-less function, so a body that raises it incidentally is silently treated as body-less. Prefer return fn.state(), which carries no such ambiguity.
  • Clients live for the process lifetime. The default sync client is created lazily and never closed; async clients are created per event loop (connection pools are loop-bound) and never closed.
  • The docstring is a template rather than documentation. help(fn) shows Jinja. If that bothers you, keep the docstring minimal and do the work in an evaluated body.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jev-0.3.0.tar.gz (14.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jev-0.3.0-py3-none-any.whl (14.2 kB view details)

Uploaded Python 3

File details

Details for the file jev-0.3.0.tar.gz.

File metadata

  • Download URL: jev-0.3.0.tar.gz
  • Upload date:
  • Size: 14.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for jev-0.3.0.tar.gz
Algorithm Hash digest
SHA256 0a80b0de2759c39f737c4c35f51ac0d9d0e3cca60afd616c168c094c62a2d989
MD5 6ebcbf36a2df1ba67d46f05ad3afd19f
BLAKE2b-256 53bf24d3a9ed91d6c9d9d4e3d20cb2d39377a281b999fcb489b36d208f945b0f

See more details on using hashes here.

Provenance

The following attestation bundles were made for jev-0.3.0.tar.gz:

Publisher: workflow.yml on aaazzam/jev

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file jev-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: jev-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 14.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for jev-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e7163901c9b3f7d184e5b7732d5243e877d8d47664a99637a9283fb60dbba84e
MD5 f451b2e386a591aa9143185e9d7ffa99
BLAKE2b-256 99f1fbe8f6c316936f7694cba5af8b65c3c7b925988e5280a91dfbce0b98281b

See more details on using hashes here.

Provenance

The following attestation bundles were made for jev-0.3.0-py3-none-any.whl:

Publisher: workflow.yml on aaazzam/jev

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page