jev 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jev-0.1.0/PKG-INFO +158 -0
- jev-0.1.0/README.md +147 -0
- jev-0.1.0/jev.egg-info/PKG-INFO +158 -0
- jev-0.1.0/jev.egg-info/SOURCES.txt +8 -0
- jev-0.1.0/jev.egg-info/dependency_links.txt +1 -0
- jev-0.1.0/jev.egg-info/requires.txt +4 -0
- jev-0.1.0/jev.egg-info/top_level.txt +1 -0
- jev-0.1.0/jev.py +871 -0
- jev-0.1.0/pyproject.toml +20 -0
- jev-0.1.0/setup.cfg +4 -0
jev-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jev
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Decorator that compiles Python function definitions into Jev (TypeSafe System One) queries
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: jinja2>=3.1.6
|
|
8
|
+
Requires-Dist: pydantic>=2.13.5
|
|
9
|
+
Requires-Dist: python-dotenv>=1.2.3
|
|
10
|
+
Requires-Dist: typesafe-sdk>=0.6.0
|
|
11
|
+
|
|
12
|
+
# jev
|
|
13
|
+
|
|
14
|
+
`@jev` turns a Python function definition into a query against [Jev](https://typesafe.ai), 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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
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` 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.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
uv sync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Python 3.14. Get an API key from [console.typesafe.ai](https://console.typesafe.ai) and put it in `.env`:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
TYPESAFE_API_KEY=...
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The pattern
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from typing import Literal
|
|
36
|
+
from pydantic import BaseModel, Field
|
|
37
|
+
from jev import jev
|
|
38
|
+
|
|
39
|
+
class Triage(BaseModel):
|
|
40
|
+
department: Literal["billing", "technical", "sales"]
|
|
41
|
+
is_urgent: bool
|
|
42
|
+
frustration: int = Field(ge=0, le=2)
|
|
43
|
+
|
|
44
|
+
@jev
|
|
45
|
+
def triage(ticket: str) -> Triage:
|
|
46
|
+
"""A customer support ticket:
|
|
47
|
+
|
|
48
|
+
{{ ticket }}
|
|
49
|
+
"""
|
|
50
|
+
return triage.state()
|
|
51
|
+
|
|
52
|
+
triage("I was charged twice. Fix this NOW.")
|
|
53
|
+
# Triage(department='billing', is_urgent=True, frustration=2)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
What happens:
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
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.
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
For the body-less form the docstring does all the work, which may make it the only place in Python where documentation outranks implementation.
|
|
63
|
+
|
|
64
|
+
## Field mapping
|
|
65
|
+
|
|
66
|
+
| Field type | Jev question | Coerced back as |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `bool` | Noul | `p(yes) >= threshold` (default 0.5) |
|
|
69
|
+
| `Literal[...]` | Choice | the selected label |
|
|
70
|
+
| `Enum` | Choice | the selected member |
|
|
71
|
+
| `int` with `Field(ge=, le=)` | Score | `lo + round(expected_score)` |
|
|
72
|
+
| `float` with `Field(ge=, le=)` | Score | linear interpolation over the levels |
|
|
73
|
+
|
|
74
|
+
- `Field(description=...)` becomes the question's instructions; without one the field name is humanized (`is_urgent` → "is urgent"). Write descriptions; they are the questions.
|
|
75
|
+
- Score levels default to the numbers in range. Override them with `Field(..., json_schema_extra={"levels": ["cold", "warm", "hot"]})`.
|
|
76
|
+
- Limits: 255 options per choice, 256 levels per score. Exceeding either is a `TypeError` at decoration time.
|
|
77
|
+
- Anything else (`str`, nested models, lists, `Optional`) raises `TypeError` at decoration time, because Jev cannot produce those values.
|
|
78
|
+
|
|
79
|
+
## Evaluated bodies
|
|
80
|
+
|
|
81
|
+
The body always runs, and there are three useful things it can do:
|
|
82
|
+
|
|
83
|
+
- **Return `fn.state()` with no value**: the body-less form; the rendered docstring is the whole state. (`...` or `raise NotImplementedError` work too.)
|
|
84
|
+
- **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.
|
|
85
|
+
- **Answer directly** with `return Model(...)`: skips the API call entirely, the mock seam for tests.
|
|
86
|
+
|
|
87
|
+
Building the state looks like this:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
@jev
|
|
91
|
+
def triage_batch(tickets: list[str]) -> BatchTriage:
|
|
92
|
+
"""Triage a batch of support tickets."""
|
|
93
|
+
numbered = [f"[{i}] {t}" for i, t in enumerate(tickets)]
|
|
94
|
+
return triage_batch.state(
|
|
95
|
+
"Triage this batch of support tickets.\n\n" + "\n".join(numbered)
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Framing like "Triage this batch" lives in the body now, in the open, rather than being lifted out of the docstring.
|
|
100
|
+
|
|
101
|
+
`fn.state(...)` is typed `value -> return-annotation`, so the body's `return` type-checks against the annotation.
|
|
102
|
+
|
|
103
|
+
Async functions work identically (`await` the call; the body may also `await`). Configure per function with `@jev(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(bool_threshold=0.7)` or globally with the `JEV_BOOL_THRESHOLD` environment variable.
|
|
104
|
+
|
|
105
|
+
## Batch with `.map`
|
|
106
|
+
|
|
107
|
+
`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.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
triage.map(tickets) # sync: list[Triage]
|
|
111
|
+
await atriage.map(tickets) # async: list[Triage]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Class form: `JevModel`
|
|
115
|
+
|
|
116
|
+
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):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from jev import JevModel
|
|
120
|
+
|
|
121
|
+
class Triage(JevModel):
|
|
122
|
+
department: Literal["billing", "technical", "sales"]
|
|
123
|
+
is_urgent: bool
|
|
124
|
+
frustration: int = Field(ge=0, le=2)
|
|
125
|
+
|
|
126
|
+
Triage.decide("I was charged twice. Fix this NOW.")
|
|
127
|
+
# Triage(department='billing', is_urgent=True, frustration=2)
|
|
128
|
+
|
|
129
|
+
await Triage.adecide("...") # async form
|
|
130
|
+
Triage(department="billing", is_urgent=False, frustration=0) # plain constructor: no API call
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
## Testing
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from jev import builder, state_payload
|
|
139
|
+
|
|
140
|
+
marker = builder(triage_batch)(["a", "b"]) # runs the body, no API call
|
|
141
|
+
assert state_payload(marker) == "Triage this batch of support tickets.\n\n[0] a\n[1] b"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Or return a model from the body to short-circuit the call in tests.
|
|
145
|
+
|
|
146
|
+
## Type checking
|
|
147
|
+
|
|
148
|
+
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` 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.
|
|
149
|
+
|
|
150
|
+
See `example.py` for a runnable tour (`uv run python example.py`).
|
|
151
|
+
|
|
152
|
+
## Limitations
|
|
153
|
+
|
|
154
|
+
- **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.
|
|
155
|
+
- **No streaming.** Jev samples in parallel in a single shot, so there is nothing to stream.
|
|
156
|
+
- **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.
|
|
157
|
+
- **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.
|
|
158
|
+
- **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.
|
jev-0.1.0/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# jev
|
|
2
|
+
|
|
3
|
+
`@jev` turns a Python function definition into a query against [Jev](https://typesafe.ai), 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.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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` 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.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Python 3.14. Get an API key from [console.typesafe.ai](https://console.typesafe.ai) and put it in `.env`:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
TYPESAFE_API_KEY=...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The pattern
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from typing import Literal
|
|
25
|
+
from pydantic import BaseModel, Field
|
|
26
|
+
from jev import jev
|
|
27
|
+
|
|
28
|
+
class Triage(BaseModel):
|
|
29
|
+
department: Literal["billing", "technical", "sales"]
|
|
30
|
+
is_urgent: bool
|
|
31
|
+
frustration: int = Field(ge=0, le=2)
|
|
32
|
+
|
|
33
|
+
@jev
|
|
34
|
+
def triage(ticket: str) -> Triage:
|
|
35
|
+
"""A customer support ticket:
|
|
36
|
+
|
|
37
|
+
{{ ticket }}
|
|
38
|
+
"""
|
|
39
|
+
return triage.state()
|
|
40
|
+
|
|
41
|
+
triage("I was charged twice. Fix this NOW.")
|
|
42
|
+
# Triage(department='billing', is_urgent=True, frustration=2)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
What happens:
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
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.
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
For the body-less form the docstring does all the work, which may make it the only place in Python where documentation outranks implementation.
|
|
52
|
+
|
|
53
|
+
## Field mapping
|
|
54
|
+
|
|
55
|
+
| Field type | Jev question | Coerced back as |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `bool` | Noul | `p(yes) >= threshold` (default 0.5) |
|
|
58
|
+
| `Literal[...]` | Choice | the selected label |
|
|
59
|
+
| `Enum` | Choice | the selected member |
|
|
60
|
+
| `int` with `Field(ge=, le=)` | Score | `lo + round(expected_score)` |
|
|
61
|
+
| `float` with `Field(ge=, le=)` | Score | linear interpolation over the levels |
|
|
62
|
+
|
|
63
|
+
- `Field(description=...)` becomes the question's instructions; without one the field name is humanized (`is_urgent` → "is urgent"). Write descriptions; they are the questions.
|
|
64
|
+
- Score levels default to the numbers in range. Override them with `Field(..., json_schema_extra={"levels": ["cold", "warm", "hot"]})`.
|
|
65
|
+
- Limits: 255 options per choice, 256 levels per score. Exceeding either is a `TypeError` at decoration time.
|
|
66
|
+
- Anything else (`str`, nested models, lists, `Optional`) raises `TypeError` at decoration time, because Jev cannot produce those values.
|
|
67
|
+
|
|
68
|
+
## Evaluated bodies
|
|
69
|
+
|
|
70
|
+
The body always runs, and there are three useful things it can do:
|
|
71
|
+
|
|
72
|
+
- **Return `fn.state()` with no value**: the body-less form; the rendered docstring is the whole state. (`...` or `raise NotImplementedError` work too.)
|
|
73
|
+
- **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.
|
|
74
|
+
- **Answer directly** with `return Model(...)`: skips the API call entirely, the mock seam for tests.
|
|
75
|
+
|
|
76
|
+
Building the state looks like this:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
@jev
|
|
80
|
+
def triage_batch(tickets: list[str]) -> BatchTriage:
|
|
81
|
+
"""Triage a batch of support tickets."""
|
|
82
|
+
numbered = [f"[{i}] {t}" for i, t in enumerate(tickets)]
|
|
83
|
+
return triage_batch.state(
|
|
84
|
+
"Triage this batch of support tickets.\n\n" + "\n".join(numbered)
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Framing like "Triage this batch" lives in the body now, in the open, rather than being lifted out of the docstring.
|
|
89
|
+
|
|
90
|
+
`fn.state(...)` is typed `value -> return-annotation`, so the body's `return` type-checks against the annotation.
|
|
91
|
+
|
|
92
|
+
Async functions work identically (`await` the call; the body may also `await`). Configure per function with `@jev(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(bool_threshold=0.7)` or globally with the `JEV_BOOL_THRESHOLD` environment variable.
|
|
93
|
+
|
|
94
|
+
## Batch with `.map`
|
|
95
|
+
|
|
96
|
+
`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.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
triage.map(tickets) # sync: list[Triage]
|
|
100
|
+
await atriage.map(tickets) # async: list[Triage]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Class form: `JevModel`
|
|
104
|
+
|
|
105
|
+
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):
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from jev import JevModel
|
|
109
|
+
|
|
110
|
+
class Triage(JevModel):
|
|
111
|
+
department: Literal["billing", "technical", "sales"]
|
|
112
|
+
is_urgent: bool
|
|
113
|
+
frustration: int = Field(ge=0, le=2)
|
|
114
|
+
|
|
115
|
+
Triage.decide("I was charged twice. Fix this NOW.")
|
|
116
|
+
# Triage(department='billing', is_urgent=True, frustration=2)
|
|
117
|
+
|
|
118
|
+
await Triage.adecide("...") # async form
|
|
119
|
+
Triage(department="billing", is_urgent=False, frustration=0) # plain constructor: no API call
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
## Testing
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from jev import builder, state_payload
|
|
128
|
+
|
|
129
|
+
marker = builder(triage_batch)(["a", "b"]) # runs the body, no API call
|
|
130
|
+
assert state_payload(marker) == "Triage this batch of support tickets.\n\n[0] a\n[1] b"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or return a model from the body to short-circuit the call in tests.
|
|
134
|
+
|
|
135
|
+
## Type checking
|
|
136
|
+
|
|
137
|
+
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` 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.
|
|
138
|
+
|
|
139
|
+
See `example.py` for a runnable tour (`uv run python example.py`).
|
|
140
|
+
|
|
141
|
+
## Limitations
|
|
142
|
+
|
|
143
|
+
- **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.
|
|
144
|
+
- **No streaming.** Jev samples in parallel in a single shot, so there is nothing to stream.
|
|
145
|
+
- **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.
|
|
146
|
+
- **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.
|
|
147
|
+
- **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.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jev
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Decorator that compiles Python function definitions into Jev (TypeSafe System One) queries
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: jinja2>=3.1.6
|
|
8
|
+
Requires-Dist: pydantic>=2.13.5
|
|
9
|
+
Requires-Dist: python-dotenv>=1.2.3
|
|
10
|
+
Requires-Dist: typesafe-sdk>=0.6.0
|
|
11
|
+
|
|
12
|
+
# jev
|
|
13
|
+
|
|
14
|
+
`@jev` turns a Python function definition into a query against [Jev](https://typesafe.ai), 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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
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` 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.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
uv sync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Python 3.14. Get an API key from [console.typesafe.ai](https://console.typesafe.ai) and put it in `.env`:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
TYPESAFE_API_KEY=...
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The pattern
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from typing import Literal
|
|
36
|
+
from pydantic import BaseModel, Field
|
|
37
|
+
from jev import jev
|
|
38
|
+
|
|
39
|
+
class Triage(BaseModel):
|
|
40
|
+
department: Literal["billing", "technical", "sales"]
|
|
41
|
+
is_urgent: bool
|
|
42
|
+
frustration: int = Field(ge=0, le=2)
|
|
43
|
+
|
|
44
|
+
@jev
|
|
45
|
+
def triage(ticket: str) -> Triage:
|
|
46
|
+
"""A customer support ticket:
|
|
47
|
+
|
|
48
|
+
{{ ticket }}
|
|
49
|
+
"""
|
|
50
|
+
return triage.state()
|
|
51
|
+
|
|
52
|
+
triage("I was charged twice. Fix this NOW.")
|
|
53
|
+
# Triage(department='billing', is_urgent=True, frustration=2)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
What happens:
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
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.
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
For the body-less form the docstring does all the work, which may make it the only place in Python where documentation outranks implementation.
|
|
63
|
+
|
|
64
|
+
## Field mapping
|
|
65
|
+
|
|
66
|
+
| Field type | Jev question | Coerced back as |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `bool` | Noul | `p(yes) >= threshold` (default 0.5) |
|
|
69
|
+
| `Literal[...]` | Choice | the selected label |
|
|
70
|
+
| `Enum` | Choice | the selected member |
|
|
71
|
+
| `int` with `Field(ge=, le=)` | Score | `lo + round(expected_score)` |
|
|
72
|
+
| `float` with `Field(ge=, le=)` | Score | linear interpolation over the levels |
|
|
73
|
+
|
|
74
|
+
- `Field(description=...)` becomes the question's instructions; without one the field name is humanized (`is_urgent` → "is urgent"). Write descriptions; they are the questions.
|
|
75
|
+
- Score levels default to the numbers in range. Override them with `Field(..., json_schema_extra={"levels": ["cold", "warm", "hot"]})`.
|
|
76
|
+
- Limits: 255 options per choice, 256 levels per score. Exceeding either is a `TypeError` at decoration time.
|
|
77
|
+
- Anything else (`str`, nested models, lists, `Optional`) raises `TypeError` at decoration time, because Jev cannot produce those values.
|
|
78
|
+
|
|
79
|
+
## Evaluated bodies
|
|
80
|
+
|
|
81
|
+
The body always runs, and there are three useful things it can do:
|
|
82
|
+
|
|
83
|
+
- **Return `fn.state()` with no value**: the body-less form; the rendered docstring is the whole state. (`...` or `raise NotImplementedError` work too.)
|
|
84
|
+
- **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.
|
|
85
|
+
- **Answer directly** with `return Model(...)`: skips the API call entirely, the mock seam for tests.
|
|
86
|
+
|
|
87
|
+
Building the state looks like this:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
@jev
|
|
91
|
+
def triage_batch(tickets: list[str]) -> BatchTriage:
|
|
92
|
+
"""Triage a batch of support tickets."""
|
|
93
|
+
numbered = [f"[{i}] {t}" for i, t in enumerate(tickets)]
|
|
94
|
+
return triage_batch.state(
|
|
95
|
+
"Triage this batch of support tickets.\n\n" + "\n".join(numbered)
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Framing like "Triage this batch" lives in the body now, in the open, rather than being lifted out of the docstring.
|
|
100
|
+
|
|
101
|
+
`fn.state(...)` is typed `value -> return-annotation`, so the body's `return` type-checks against the annotation.
|
|
102
|
+
|
|
103
|
+
Async functions work identically (`await` the call; the body may also `await`). Configure per function with `@jev(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(bool_threshold=0.7)` or globally with the `JEV_BOOL_THRESHOLD` environment variable.
|
|
104
|
+
|
|
105
|
+
## Batch with `.map`
|
|
106
|
+
|
|
107
|
+
`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.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
triage.map(tickets) # sync: list[Triage]
|
|
111
|
+
await atriage.map(tickets) # async: list[Triage]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Class form: `JevModel`
|
|
115
|
+
|
|
116
|
+
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):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from jev import JevModel
|
|
120
|
+
|
|
121
|
+
class Triage(JevModel):
|
|
122
|
+
department: Literal["billing", "technical", "sales"]
|
|
123
|
+
is_urgent: bool
|
|
124
|
+
frustration: int = Field(ge=0, le=2)
|
|
125
|
+
|
|
126
|
+
Triage.decide("I was charged twice. Fix this NOW.")
|
|
127
|
+
# Triage(department='billing', is_urgent=True, frustration=2)
|
|
128
|
+
|
|
129
|
+
await Triage.adecide("...") # async form
|
|
130
|
+
Triage(department="billing", is_urgent=False, frustration=0) # plain constructor: no API call
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
## Testing
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from jev import builder, state_payload
|
|
139
|
+
|
|
140
|
+
marker = builder(triage_batch)(["a", "b"]) # runs the body, no API call
|
|
141
|
+
assert state_payload(marker) == "Triage this batch of support tickets.\n\n[0] a\n[1] b"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Or return a model from the body to short-circuit the call in tests.
|
|
145
|
+
|
|
146
|
+
## Type checking
|
|
147
|
+
|
|
148
|
+
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` 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.
|
|
149
|
+
|
|
150
|
+
See `example.py` for a runnable tour (`uv run python example.py`).
|
|
151
|
+
|
|
152
|
+
## Limitations
|
|
153
|
+
|
|
154
|
+
- **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.
|
|
155
|
+
- **No streaming.** Jev samples in parallel in a single shot, so there is nothing to stream.
|
|
156
|
+
- **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.
|
|
157
|
+
- **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.
|
|
158
|
+
- **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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jev
|