jevframe 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.
- jevframe-0.1.0/.github/workflows/ci.yml +23 -0
- jevframe-0.1.0/.github/workflows/publish.yml +77 -0
- jevframe-0.1.0/.gitignore +9 -0
- jevframe-0.1.0/LICENSE +21 -0
- jevframe-0.1.0/PKG-INFO +289 -0
- jevframe-0.1.0/README.md +261 -0
- jevframe-0.1.0/RELEASING.md +42 -0
- jevframe-0.1.0/examples/reviews.py +222 -0
- jevframe-0.1.0/pyproject.toml +48 -0
- jevframe-0.1.0/src/jevframe/__init__.py +9 -0
- jevframe-0.1.0/src/jevframe/_accessor.py +126 -0
- jevframe-0.1.0/src/jevframe/_cache.py +66 -0
- jevframe-0.1.0/src/jevframe/_context.py +103 -0
- jevframe-0.1.0/src/jevframe/_engine.py +206 -0
- jevframe-0.1.0/src/jevframe/_schema.py +189 -0
- jevframe-0.1.0/src/jevframe/_types.py +70 -0
- jevframe-0.1.0/src/jevframe/pandas.py +73 -0
- jevframe-0.1.0/src/jevframe/polars.py +85 -0
- jevframe-0.1.0/src/jevframe/py.typed +0 -0
- jevframe-0.1.0/tests/conftest.py +67 -0
- jevframe-0.1.0/tests/test_cache.py +178 -0
- jevframe-0.1.0/tests/test_engine.py +367 -0
- jevframe-0.1.0/tests/test_example.py +92 -0
- jevframe-0.1.0/tests/test_imports.py +45 -0
- jevframe-0.1.0/tests/test_output.py +121 -0
- jevframe-0.1.0/tests/test_pandas.py +243 -0
- jevframe-0.1.0/tests/test_polars.py +125 -0
- jevframe-0.1.0/uv.lock +2357 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python: ["3.10", "3.13", "3.14"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v6
|
|
16
|
+
with:
|
|
17
|
+
python-version: ${{ matrix.python }}
|
|
18
|
+
- run: uv sync --all-extras
|
|
19
|
+
- run: uv run pytest
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run ruff format --check .
|
|
22
|
+
- run: uv run marimo check --strict examples/reviews.py
|
|
23
|
+
- run: uv build
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: pypi-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: false
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
- uses: astral-sh/setup-uv@v6
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.13"
|
|
25
|
+
- name: Install test dependencies
|
|
26
|
+
run: uv sync --locked --all-extras
|
|
27
|
+
- name: Check release version
|
|
28
|
+
if: github.ref_type == 'tag'
|
|
29
|
+
env:
|
|
30
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
31
|
+
run: |
|
|
32
|
+
uv run --no-sync python - <<'PY'
|
|
33
|
+
import os
|
|
34
|
+
import tomllib
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
37
|
+
assert os.environ["RELEASE_TAG"] == f"v{version}", "Tag must match package version"
|
|
38
|
+
PY
|
|
39
|
+
- name: Test and check the notebook
|
|
40
|
+
run: |
|
|
41
|
+
uv run --no-sync pytest -q
|
|
42
|
+
uv run --no-sync ruff check .
|
|
43
|
+
uv run --no-sync ruff format --check .
|
|
44
|
+
uv run --no-sync marimo check --strict examples/reviews.py
|
|
45
|
+
- name: Build distributions
|
|
46
|
+
run: uv build --no-sources
|
|
47
|
+
- name: Validate package metadata
|
|
48
|
+
run: uvx --from twine twine check --strict dist/*
|
|
49
|
+
- name: Check isolated imports from both distributions
|
|
50
|
+
run: |
|
|
51
|
+
uv run --isolated --no-project --with dist/*.whl python -c 'import jevframe'
|
|
52
|
+
uv run --isolated --no-project --with dist/*.tar.gz python -c 'import jevframe'
|
|
53
|
+
- uses: actions/upload-artifact@v4
|
|
54
|
+
with:
|
|
55
|
+
name: distributions
|
|
56
|
+
path: dist/
|
|
57
|
+
if-no-files-found: error
|
|
58
|
+
|
|
59
|
+
publish:
|
|
60
|
+
needs: build
|
|
61
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
environment:
|
|
64
|
+
name: pypi
|
|
65
|
+
url: https://pypi.org/project/jevframe/
|
|
66
|
+
permissions:
|
|
67
|
+
id-token: write
|
|
68
|
+
steps:
|
|
69
|
+
- uses: astral-sh/setup-uv@v6
|
|
70
|
+
with:
|
|
71
|
+
enable-cache: false
|
|
72
|
+
- uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: distributions
|
|
75
|
+
path: dist/
|
|
76
|
+
- name: Publish using the GitHub identity
|
|
77
|
+
run: uv publish --trusted-publishing always
|
jevframe-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Konstantin Taletskiy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jevframe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jevframe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed semantic decisions for pandas and Polars with TypeSafe Jev
|
|
5
|
+
Project-URL: Repository, https://github.com/ktaletsk/jevframe
|
|
6
|
+
Project-URL: Issues, https://github.com/ktaletsk/jevframe/issues
|
|
7
|
+
Author: Konstantin Taletskiy
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: tqdm<5,>=4.66
|
|
18
|
+
Requires-Dist: typesafe-sdk<0.8,>=0.7
|
|
19
|
+
Provides-Extra: examples
|
|
20
|
+
Requires-Dist: altair<7,>=5; extra == 'examples'
|
|
21
|
+
Requires-Dist: marimo<1,>=0.19; extra == 'examples'
|
|
22
|
+
Requires-Dist: pandas<4,>=2.2; extra == 'examples'
|
|
23
|
+
Provides-Extra: pandas
|
|
24
|
+
Requires-Dist: pandas<4,>=2.2; extra == 'pandas'
|
|
25
|
+
Provides-Extra: polars
|
|
26
|
+
Requires-Dist: polars<2,>=1; extra == 'polars'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# jevframe
|
|
30
|
+
|
|
31
|
+
[](https://molab.marimo.io/github/ktaletsk/jevframe/blob/main/examples/reviews.py)
|
|
32
|
+
|
|
33
|
+
Ask the same semantic questions about every row of a dataframe using
|
|
34
|
+
[TypeSafe Jev](https://docs.typesafe.ai/). Get ordinary pandas or Polars results,
|
|
35
|
+
with bounded async inference and complete probability distributions.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uv add 'jevframe[pandas]==0.1.0' # in your uv project
|
|
41
|
+
# Or: uv add 'jevframe[polars]==0.1.0' / 'jevframe[pandas,polars]==0.1.0'
|
|
42
|
+
export TYPESAFE_API_KEY='your-key'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Python 3.10+; the official `typesafe-sdk>=0.7,<0.8` handles inference. Dataframe
|
|
46
|
+
dependencies are optional. Importing an integration registers its `.jev` accessor.
|
|
47
|
+
|
|
48
|
+
On **molab**, open the notebook's **Secrets** sidebar panel and add
|
|
49
|
+
`TYPESAFE_API_KEY` with your key as its value. molab loads it automatically and
|
|
50
|
+
persists it between sessions; `.env` secrets are excluded from forks.
|
|
51
|
+
[Molab's secrets documentation](https://marimo.io/pages/molab/storage#secrets-stay-with-your-notebook).
|
|
52
|
+
Then click **Evaluate reviews** in the demo. You do not need to put the key in a code cell.
|
|
53
|
+
|
|
54
|
+
## pandas
|
|
55
|
+
|
|
56
|
+
These examples use notebook top-level `await`. In a script, put them inside an
|
|
57
|
+
`async def main()` and call `asyncio.run(main())`.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import pandas as pd
|
|
61
|
+
import jevframe.pandas
|
|
62
|
+
from typesafe_sdk import Choice, Noul
|
|
63
|
+
|
|
64
|
+
df = pd.DataFrame(
|
|
65
|
+
{
|
|
66
|
+
"title": ["Charged twice", "Great update"],
|
|
67
|
+
"review": ["Please refund the duplicate charge today.", "Everything works well!"],
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
df["dissatisfied"] = await df.jev.noul(
|
|
72
|
+
"Is this customer dissatisfied?",
|
|
73
|
+
state=["title", "review"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
topics = await df.jev.choice(
|
|
77
|
+
"What is the main issue?",
|
|
78
|
+
choices=["billing", "bug", "other"],
|
|
79
|
+
state="review",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
grades = await df.jev.score(
|
|
83
|
+
"How positive is this review?",
|
|
84
|
+
levels=["Negative", "Neutral", "Positive"],
|
|
85
|
+
state="review",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
results = await df.jev.evaluate(
|
|
89
|
+
state=["title", "review"],
|
|
90
|
+
questions={
|
|
91
|
+
"urgent": Noul(instructions="Does the customer need help today?"),
|
|
92
|
+
"topic": Choice(
|
|
93
|
+
instructions="What is the main issue?",
|
|
94
|
+
criteria={"billing": "Charges and refunds", "bug": "Broken behavior", "other": None},
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| Method | Result columns |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `noul` | Series named `probability`, the probability of yes |
|
|
103
|
+
| `choice` | `label`, `confidence`, `p__billing`, `p__bug`, … |
|
|
104
|
+
| `score` | `level`, `label`, `score`, `confidence`, `p__0`, `p__1`, … |
|
|
105
|
+
| `evaluate` | Fields prefixed by question name, e.g. `urgent__probability`, `topic__p__billing` |
|
|
106
|
+
|
|
107
|
+
`score` is the expected **zero-based** level, not a probability: with five levels it
|
|
108
|
+
ranges from 0 to 4. `level`/`label` identify the highest-probability level, with ties
|
|
109
|
+
resolved by level order. Structured SDK level descriptions appear as JSON text in
|
|
110
|
+
`label`. Confidence comes from the SDK; it is separate from the selected option's
|
|
111
|
+
probability. No result is thresholded or silently renormalized.
|
|
112
|
+
|
|
113
|
+
Question and criterion order determine column order. Row order and pandas indexes,
|
|
114
|
+
including duplicates and MultiIndexes, are preserved. The input is never mutated.
|
|
115
|
+
|
|
116
|
+
### Output layout
|
|
117
|
+
|
|
118
|
+
`choice`, `score`, and `evaluate` accept `output="columns"` (the default) or
|
|
119
|
+
`output="struct"`. Both return a DataFrame with one row per input row.
|
|
120
|
+
The struct layout has a single column named `result`: dictionaries in pandas
|
|
121
|
+
(`object` dtype), or a native `Struct` in Polars. Its fields have exactly the same
|
|
122
|
+
names, order, and values as the separate columns, including all probabilities.
|
|
123
|
+
`evaluate` keeps question prefixes inside the struct too.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
packed = await df.jev.evaluate(
|
|
127
|
+
state="review",
|
|
128
|
+
questions={
|
|
129
|
+
"dissatisfied": Noul(instructions="Is this customer dissatisfied?"),
|
|
130
|
+
"urgent": Noul(instructions="Does the customer need help today?"),
|
|
131
|
+
},
|
|
132
|
+
output="struct",
|
|
133
|
+
)
|
|
134
|
+
df["decisions"] = packed["result"]
|
|
135
|
+
# Each successful cell: {"dissatisfied__probability": 0.9, "urgent__probability": 0.8}
|
|
136
|
+
# Illustrative probabilities; actual values come from Jev.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Failed or skipped rows have a missing `result` cell (`None` in pandas, null in
|
|
140
|
+
Polars). Empty results retain their output dtype, including the full Polars struct
|
|
141
|
+
schema. Layout affects only presentation: switching it makes no extra requests
|
|
142
|
+
when you reuse a cache. `noul` always returns its single probability Series.
|
|
143
|
+
|
|
144
|
+
## How requests and results map to rows
|
|
145
|
+
|
|
146
|
+
The engine sends **one input row per request**, with up to 16 row requests in flight
|
|
147
|
+
by default (`max_concurrency`). With `evaluate()`, every question about that row
|
|
148
|
+
shares the same context and request. Different rows are evaluated independently.
|
|
149
|
+
|
|
150
|
+
For example, 100 rows with three questions produce 100 initial requests and 100
|
|
151
|
+
result rows, with answers in separate columns or one structured column. Retries can add
|
|
152
|
+
requests; skipped or cached rows need none. This does not expand one input row
|
|
153
|
+
into multiple generated records. The demo evaluates dissatisfaction, urgency,
|
|
154
|
+
and topic together for each review.
|
|
155
|
+
|
|
156
|
+
## Row selection and context
|
|
157
|
+
|
|
158
|
+
Select rows with the dataframe library before evaluation:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
subset = await df.iloc[:10].jev.noul("Is this urgent?", state="review")
|
|
162
|
+
|
|
163
|
+
custom = await df.jev.noul(
|
|
164
|
+
"Is this urgent?",
|
|
165
|
+
state=lambda row: {"text": f"{row['title']}: {row['review']}"},
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`state="review"` sends `{"review": value}`; a list sends those named columns.
|
|
170
|
+
Indexes are never sent implicitly. Selected column names must be unique strings;
|
|
171
|
+
unrelated duplicate columns are allowed. A synchronous context callable receives
|
|
172
|
+
a detached mapping of all columns and returns text, an object, or an array. It
|
|
173
|
+
runs once per row before cache lookup. Polars temporal values in these mappings
|
|
174
|
+
are ISO strings to retain nanosecond precision.
|
|
175
|
+
|
|
176
|
+
Missing scalars become JSON null; lists and objects are normalized recursively.
|
|
177
|
+
Dates/times become ISO text. Unsupported objects and infinities raise a row-aware
|
|
178
|
+
context error. Supply a callable to convert custom values. Context errors always
|
|
179
|
+
raise, even under `errors="coerce"`.
|
|
180
|
+
|
|
181
|
+
## Eager Polars
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
import polars as pl
|
|
185
|
+
import jevframe.polars
|
|
186
|
+
|
|
187
|
+
df = pl.DataFrame({"review": ["Charged twice", "Works perfectly"]})
|
|
188
|
+
probabilities = await df.jev.noul("Is this customer dissatisfied?", state="review")
|
|
189
|
+
df = df.with_columns(probabilities.alias("dissatisfied"))
|
|
190
|
+
topics = await df.filter(pl.col("dissatisfied") > 0.5).jev.choice(
|
|
191
|
+
"What is the issue?",
|
|
192
|
+
choices=["billing", "bug", "other"],
|
|
193
|
+
state="review",
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The API and columns match pandas; outputs are native `pl.Series`/`pl.DataFrame`.
|
|
198
|
+
Polars uses nulls for missing results; pandas uses NaN for floating outputs and
|
|
199
|
+
native nullable string/integer columns. Empty outputs retain their schemas.
|
|
200
|
+
LazyFrame expressions are outside v0.
|
|
201
|
+
|
|
202
|
+
## Controls, errors, and caching
|
|
203
|
+
|
|
204
|
+
Every method accepts these keyword arguments:
|
|
205
|
+
|
|
206
|
+
| Option | Default | Behavior |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| `client` | `None` | Reuse an official `AsyncTypeSafeClient`; caller owns its lifetime |
|
|
209
|
+
| `model` | `None` | Inherit SDK client/environment default (`jev-latest` otherwise) |
|
|
210
|
+
| `max_concurrency` | `16` | Maximum simultaneous row evaluations, including retries |
|
|
211
|
+
| `cache` | `None` | Opt in with a reusable `MemoryCache` |
|
|
212
|
+
| `progress` | `False` | `True` for tqdm, or a synchronous `Progress` callback |
|
|
213
|
+
| `errors` | `"raise"` | `"coerce"` returns missing results and an `EvaluationWarning` |
|
|
214
|
+
| `nulls` | `"include"` | `"skip"` skips rows with any null context; `"raise"` rejects them |
|
|
215
|
+
|
|
216
|
+
Null policy applies **after** constructing context, including nested nulls. A row
|
|
217
|
+
failure makes every question result for that row missing. `EvaluationError.position`
|
|
218
|
+
and `.cause`, or `EvaluationWarning.failures` (`RowFailure.position`/`.cause`), identify
|
|
219
|
+
failures without confusing duplicate index labels. Positions refer to the evaluated
|
|
220
|
+
frame, starting at zero. Inspect warnings with `warnings.catch_warnings(record=True)`.
|
|
221
|
+
Progress snapshots expose `completed`, `total`, `failed`, `skipped`, and `cache_hits`.
|
|
222
|
+
Callback exceptions and cancellation propagate, and outstanding workers are drained.
|
|
223
|
+
|
|
224
|
+
All questions for a row share one request. SDK retries handle transient failures and
|
|
225
|
+
backoff (two retries by default); configure its policy and timeouts directly:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from jevframe import MemoryCache
|
|
229
|
+
from typesafe_sdk import AsyncTypeSafeClient, RetryPolicy
|
|
230
|
+
|
|
231
|
+
cache = MemoryCache(max_entries=10_000)
|
|
232
|
+
async with AsyncTypeSafeClient(timeout=30, retry=RetryPolicy(max_retries=3)) as client:
|
|
233
|
+
result = await df.jev.noul(
|
|
234
|
+
"Is this urgent?",
|
|
235
|
+
state="review",
|
|
236
|
+
client=client,
|
|
237
|
+
max_concurrency=8,
|
|
238
|
+
cache=cache,
|
|
239
|
+
progress=True,
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The memory cache stores only successful, validated complete responses, including
|
|
244
|
+
all probabilities. Identical in-flight rows within one evaluation share a request.
|
|
245
|
+
Questions, criteria order, context, model, and client/configuration identity affect
|
|
246
|
+
cache keys; indexes do not. Supplied clients have separate cache namespaces. Default
|
|
247
|
+
clients reuse entries across calls when their environment configuration matches.
|
|
248
|
+
There is no disk cache. Model aliases can change: use a pinned model for reproducible
|
|
249
|
+
work and `cache.clear()` when fresh results are needed.
|
|
250
|
+
|
|
251
|
+
No API call runs merely by importing the library. Evaluation sends the chosen
|
|
252
|
+
context to TypeSafe. Late context errors can occur after other rows have completed;
|
|
253
|
+
completed requests cannot be undone. Credentials are read from the environment or
|
|
254
|
+
the SDK client; the library does not discover or load dotenv files.
|
|
255
|
+
|
|
256
|
+
## marimo example and development
|
|
257
|
+
|
|
258
|
+
From a checkout:
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
# For library development only: uv sync --extra pandas (or --extra polars)
|
|
262
|
+
uv sync --extra examples
|
|
263
|
+
uv run marimo edit examples/reviews.py
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Edit the three questions, choose an output layout, and click **Evaluate reviews**
|
|
267
|
+
to see the results and probability histograms. The eight synthetic reviews need eight initial
|
|
268
|
+
requests, each containing all three questions; repeated evaluations reuse the cache.
|
|
269
|
+
The example makes no requests until the button is clicked and a key is configured.
|
|
270
|
+
|
|
271
|
+
The badge opens the GitHub notebook preview. Fork it into your molab workspace to
|
|
272
|
+
add your own secret and run it on a server. The notebook includes inline dependency
|
|
273
|
+
metadata that installs `jevframe[pandas]==0.1.0` from PyPI. The badge requires the
|
|
274
|
+
notebook to be available on this repository's `main` branch.
|
|
275
|
+
|
|
276
|
+
For development checks, install all extras:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
uv sync --all-extras
|
|
280
|
+
uv run pytest
|
|
281
|
+
uv run ruff check .
|
|
282
|
+
uv run ruff format --check .
|
|
283
|
+
uv run marimo check --strict examples/reviews.py
|
|
284
|
+
uv build
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Tests use the official SDK with mocked HTTP transport; no API key or live calls
|
|
288
|
+
are required. v0 focuses on row-wise decisions, with no chat interface, automatic
|
|
289
|
+
analysis, generated code, custom dtypes, Excel integration, or provider framework.
|
jevframe-0.1.0/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# jevframe
|
|
2
|
+
|
|
3
|
+
[](https://molab.marimo.io/github/ktaletsk/jevframe/blob/main/examples/reviews.py)
|
|
4
|
+
|
|
5
|
+
Ask the same semantic questions about every row of a dataframe using
|
|
6
|
+
[TypeSafe Jev](https://docs.typesafe.ai/). Get ordinary pandas or Polars results,
|
|
7
|
+
with bounded async inference and complete probability distributions.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv add 'jevframe[pandas]==0.1.0' # in your uv project
|
|
13
|
+
# Or: uv add 'jevframe[polars]==0.1.0' / 'jevframe[pandas,polars]==0.1.0'
|
|
14
|
+
export TYPESAFE_API_KEY='your-key'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Python 3.10+; the official `typesafe-sdk>=0.7,<0.8` handles inference. Dataframe
|
|
18
|
+
dependencies are optional. Importing an integration registers its `.jev` accessor.
|
|
19
|
+
|
|
20
|
+
On **molab**, open the notebook's **Secrets** sidebar panel and add
|
|
21
|
+
`TYPESAFE_API_KEY` with your key as its value. molab loads it automatically and
|
|
22
|
+
persists it between sessions; `.env` secrets are excluded from forks.
|
|
23
|
+
[Molab's secrets documentation](https://marimo.io/pages/molab/storage#secrets-stay-with-your-notebook).
|
|
24
|
+
Then click **Evaluate reviews** in the demo. You do not need to put the key in a code cell.
|
|
25
|
+
|
|
26
|
+
## pandas
|
|
27
|
+
|
|
28
|
+
These examples use notebook top-level `await`. In a script, put them inside an
|
|
29
|
+
`async def main()` and call `asyncio.run(main())`.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import pandas as pd
|
|
33
|
+
import jevframe.pandas
|
|
34
|
+
from typesafe_sdk import Choice, Noul
|
|
35
|
+
|
|
36
|
+
df = pd.DataFrame(
|
|
37
|
+
{
|
|
38
|
+
"title": ["Charged twice", "Great update"],
|
|
39
|
+
"review": ["Please refund the duplicate charge today.", "Everything works well!"],
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
df["dissatisfied"] = await df.jev.noul(
|
|
44
|
+
"Is this customer dissatisfied?",
|
|
45
|
+
state=["title", "review"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
topics = await df.jev.choice(
|
|
49
|
+
"What is the main issue?",
|
|
50
|
+
choices=["billing", "bug", "other"],
|
|
51
|
+
state="review",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
grades = await df.jev.score(
|
|
55
|
+
"How positive is this review?",
|
|
56
|
+
levels=["Negative", "Neutral", "Positive"],
|
|
57
|
+
state="review",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
results = await df.jev.evaluate(
|
|
61
|
+
state=["title", "review"],
|
|
62
|
+
questions={
|
|
63
|
+
"urgent": Noul(instructions="Does the customer need help today?"),
|
|
64
|
+
"topic": Choice(
|
|
65
|
+
instructions="What is the main issue?",
|
|
66
|
+
criteria={"billing": "Charges and refunds", "bug": "Broken behavior", "other": None},
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Method | Result columns |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `noul` | Series named `probability`, the probability of yes |
|
|
75
|
+
| `choice` | `label`, `confidence`, `p__billing`, `p__bug`, … |
|
|
76
|
+
| `score` | `level`, `label`, `score`, `confidence`, `p__0`, `p__1`, … |
|
|
77
|
+
| `evaluate` | Fields prefixed by question name, e.g. `urgent__probability`, `topic__p__billing` |
|
|
78
|
+
|
|
79
|
+
`score` is the expected **zero-based** level, not a probability: with five levels it
|
|
80
|
+
ranges from 0 to 4. `level`/`label` identify the highest-probability level, with ties
|
|
81
|
+
resolved by level order. Structured SDK level descriptions appear as JSON text in
|
|
82
|
+
`label`. Confidence comes from the SDK; it is separate from the selected option's
|
|
83
|
+
probability. No result is thresholded or silently renormalized.
|
|
84
|
+
|
|
85
|
+
Question and criterion order determine column order. Row order and pandas indexes,
|
|
86
|
+
including duplicates and MultiIndexes, are preserved. The input is never mutated.
|
|
87
|
+
|
|
88
|
+
### Output layout
|
|
89
|
+
|
|
90
|
+
`choice`, `score`, and `evaluate` accept `output="columns"` (the default) or
|
|
91
|
+
`output="struct"`. Both return a DataFrame with one row per input row.
|
|
92
|
+
The struct layout has a single column named `result`: dictionaries in pandas
|
|
93
|
+
(`object` dtype), or a native `Struct` in Polars. Its fields have exactly the same
|
|
94
|
+
names, order, and values as the separate columns, including all probabilities.
|
|
95
|
+
`evaluate` keeps question prefixes inside the struct too.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
packed = await df.jev.evaluate(
|
|
99
|
+
state="review",
|
|
100
|
+
questions={
|
|
101
|
+
"dissatisfied": Noul(instructions="Is this customer dissatisfied?"),
|
|
102
|
+
"urgent": Noul(instructions="Does the customer need help today?"),
|
|
103
|
+
},
|
|
104
|
+
output="struct",
|
|
105
|
+
)
|
|
106
|
+
df["decisions"] = packed["result"]
|
|
107
|
+
# Each successful cell: {"dissatisfied__probability": 0.9, "urgent__probability": 0.8}
|
|
108
|
+
# Illustrative probabilities; actual values come from Jev.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Failed or skipped rows have a missing `result` cell (`None` in pandas, null in
|
|
112
|
+
Polars). Empty results retain their output dtype, including the full Polars struct
|
|
113
|
+
schema. Layout affects only presentation: switching it makes no extra requests
|
|
114
|
+
when you reuse a cache. `noul` always returns its single probability Series.
|
|
115
|
+
|
|
116
|
+
## How requests and results map to rows
|
|
117
|
+
|
|
118
|
+
The engine sends **one input row per request**, with up to 16 row requests in flight
|
|
119
|
+
by default (`max_concurrency`). With `evaluate()`, every question about that row
|
|
120
|
+
shares the same context and request. Different rows are evaluated independently.
|
|
121
|
+
|
|
122
|
+
For example, 100 rows with three questions produce 100 initial requests and 100
|
|
123
|
+
result rows, with answers in separate columns or one structured column. Retries can add
|
|
124
|
+
requests; skipped or cached rows need none. This does not expand one input row
|
|
125
|
+
into multiple generated records. The demo evaluates dissatisfaction, urgency,
|
|
126
|
+
and topic together for each review.
|
|
127
|
+
|
|
128
|
+
## Row selection and context
|
|
129
|
+
|
|
130
|
+
Select rows with the dataframe library before evaluation:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
subset = await df.iloc[:10].jev.noul("Is this urgent?", state="review")
|
|
134
|
+
|
|
135
|
+
custom = await df.jev.noul(
|
|
136
|
+
"Is this urgent?",
|
|
137
|
+
state=lambda row: {"text": f"{row['title']}: {row['review']}"},
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`state="review"` sends `{"review": value}`; a list sends those named columns.
|
|
142
|
+
Indexes are never sent implicitly. Selected column names must be unique strings;
|
|
143
|
+
unrelated duplicate columns are allowed. A synchronous context callable receives
|
|
144
|
+
a detached mapping of all columns and returns text, an object, or an array. It
|
|
145
|
+
runs once per row before cache lookup. Polars temporal values in these mappings
|
|
146
|
+
are ISO strings to retain nanosecond precision.
|
|
147
|
+
|
|
148
|
+
Missing scalars become JSON null; lists and objects are normalized recursively.
|
|
149
|
+
Dates/times become ISO text. Unsupported objects and infinities raise a row-aware
|
|
150
|
+
context error. Supply a callable to convert custom values. Context errors always
|
|
151
|
+
raise, even under `errors="coerce"`.
|
|
152
|
+
|
|
153
|
+
## Eager Polars
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import polars as pl
|
|
157
|
+
import jevframe.polars
|
|
158
|
+
|
|
159
|
+
df = pl.DataFrame({"review": ["Charged twice", "Works perfectly"]})
|
|
160
|
+
probabilities = await df.jev.noul("Is this customer dissatisfied?", state="review")
|
|
161
|
+
df = df.with_columns(probabilities.alias("dissatisfied"))
|
|
162
|
+
topics = await df.filter(pl.col("dissatisfied") > 0.5).jev.choice(
|
|
163
|
+
"What is the issue?",
|
|
164
|
+
choices=["billing", "bug", "other"],
|
|
165
|
+
state="review",
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The API and columns match pandas; outputs are native `pl.Series`/`pl.DataFrame`.
|
|
170
|
+
Polars uses nulls for missing results; pandas uses NaN for floating outputs and
|
|
171
|
+
native nullable string/integer columns. Empty outputs retain their schemas.
|
|
172
|
+
LazyFrame expressions are outside v0.
|
|
173
|
+
|
|
174
|
+
## Controls, errors, and caching
|
|
175
|
+
|
|
176
|
+
Every method accepts these keyword arguments:
|
|
177
|
+
|
|
178
|
+
| Option | Default | Behavior |
|
|
179
|
+
| --- | --- | --- |
|
|
180
|
+
| `client` | `None` | Reuse an official `AsyncTypeSafeClient`; caller owns its lifetime |
|
|
181
|
+
| `model` | `None` | Inherit SDK client/environment default (`jev-latest` otherwise) |
|
|
182
|
+
| `max_concurrency` | `16` | Maximum simultaneous row evaluations, including retries |
|
|
183
|
+
| `cache` | `None` | Opt in with a reusable `MemoryCache` |
|
|
184
|
+
| `progress` | `False` | `True` for tqdm, or a synchronous `Progress` callback |
|
|
185
|
+
| `errors` | `"raise"` | `"coerce"` returns missing results and an `EvaluationWarning` |
|
|
186
|
+
| `nulls` | `"include"` | `"skip"` skips rows with any null context; `"raise"` rejects them |
|
|
187
|
+
|
|
188
|
+
Null policy applies **after** constructing context, including nested nulls. A row
|
|
189
|
+
failure makes every question result for that row missing. `EvaluationError.position`
|
|
190
|
+
and `.cause`, or `EvaluationWarning.failures` (`RowFailure.position`/`.cause`), identify
|
|
191
|
+
failures without confusing duplicate index labels. Positions refer to the evaluated
|
|
192
|
+
frame, starting at zero. Inspect warnings with `warnings.catch_warnings(record=True)`.
|
|
193
|
+
Progress snapshots expose `completed`, `total`, `failed`, `skipped`, and `cache_hits`.
|
|
194
|
+
Callback exceptions and cancellation propagate, and outstanding workers are drained.
|
|
195
|
+
|
|
196
|
+
All questions for a row share one request. SDK retries handle transient failures and
|
|
197
|
+
backoff (two retries by default); configure its policy and timeouts directly:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from jevframe import MemoryCache
|
|
201
|
+
from typesafe_sdk import AsyncTypeSafeClient, RetryPolicy
|
|
202
|
+
|
|
203
|
+
cache = MemoryCache(max_entries=10_000)
|
|
204
|
+
async with AsyncTypeSafeClient(timeout=30, retry=RetryPolicy(max_retries=3)) as client:
|
|
205
|
+
result = await df.jev.noul(
|
|
206
|
+
"Is this urgent?",
|
|
207
|
+
state="review",
|
|
208
|
+
client=client,
|
|
209
|
+
max_concurrency=8,
|
|
210
|
+
cache=cache,
|
|
211
|
+
progress=True,
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The memory cache stores only successful, validated complete responses, including
|
|
216
|
+
all probabilities. Identical in-flight rows within one evaluation share a request.
|
|
217
|
+
Questions, criteria order, context, model, and client/configuration identity affect
|
|
218
|
+
cache keys; indexes do not. Supplied clients have separate cache namespaces. Default
|
|
219
|
+
clients reuse entries across calls when their environment configuration matches.
|
|
220
|
+
There is no disk cache. Model aliases can change: use a pinned model for reproducible
|
|
221
|
+
work and `cache.clear()` when fresh results are needed.
|
|
222
|
+
|
|
223
|
+
No API call runs merely by importing the library. Evaluation sends the chosen
|
|
224
|
+
context to TypeSafe. Late context errors can occur after other rows have completed;
|
|
225
|
+
completed requests cannot be undone. Credentials are read from the environment or
|
|
226
|
+
the SDK client; the library does not discover or load dotenv files.
|
|
227
|
+
|
|
228
|
+
## marimo example and development
|
|
229
|
+
|
|
230
|
+
From a checkout:
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
# For library development only: uv sync --extra pandas (or --extra polars)
|
|
234
|
+
uv sync --extra examples
|
|
235
|
+
uv run marimo edit examples/reviews.py
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Edit the three questions, choose an output layout, and click **Evaluate reviews**
|
|
239
|
+
to see the results and probability histograms. The eight synthetic reviews need eight initial
|
|
240
|
+
requests, each containing all three questions; repeated evaluations reuse the cache.
|
|
241
|
+
The example makes no requests until the button is clicked and a key is configured.
|
|
242
|
+
|
|
243
|
+
The badge opens the GitHub notebook preview. Fork it into your molab workspace to
|
|
244
|
+
add your own secret and run it on a server. The notebook includes inline dependency
|
|
245
|
+
metadata that installs `jevframe[pandas]==0.1.0` from PyPI. The badge requires the
|
|
246
|
+
notebook to be available on this repository's `main` branch.
|
|
247
|
+
|
|
248
|
+
For development checks, install all extras:
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
uv sync --all-extras
|
|
252
|
+
uv run pytest
|
|
253
|
+
uv run ruff check .
|
|
254
|
+
uv run ruff format --check .
|
|
255
|
+
uv run marimo check --strict examples/reviews.py
|
|
256
|
+
uv build
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Tests use the official SDK with mocked HTTP transport; no API key or live calls
|
|
260
|
+
are required. v0 focuses on row-wise decisions, with no chat interface, automatic
|
|
261
|
+
analysis, generated code, custom dtypes, Excel integration, or provider framework.
|