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 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,8 @@
1
+ README.md
2
+ jev.py
3
+ pyproject.toml
4
+ jev.egg-info/PKG-INFO
5
+ jev.egg-info/SOURCES.txt
6
+ jev.egg-info/dependency_links.txt
7
+ jev.egg-info/requires.txt
8
+ jev.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ jinja2>=3.1.6
2
+ pydantic>=2.13.5
3
+ python-dotenv>=1.2.3
4
+ typesafe-sdk>=0.6.0
@@ -0,0 +1 @@
1
+ jev