jevfilter
Judge content against plain-English definitions with TypeSafe's Jev. Describe what you care about in a few words; get back typed answers with calibrated probabilities: which topics match, which category, which company, how urgent.
jevfilter is stateless: no storage, no cache and no network except Jev. You get plain results back and store them however you like.
Pre-alpha. The API may change before 1.0.
Install
pip install "jevfilter[yaml]"
export TYPESAFE_API_KEY=...
On PyPI: https://pypi.org/project/jevfilter/
Python 3.10+. You can also set the key in code with
jf.configure(api_key=..., model="jev-1.13.0"). jevfilter never reads
.env files, so load one yourself if you use it.
Quick start
import jevfilter as jf
jf.choose("I was charged twice", ["billing", "bug", "feature request"])
# Choice(value='billing', confidence=0.96, ...)
jf.check("Can you send the form by Friday?", {"needs_reply": "The sender wants a reply"})
# {'needs_reply': 0.98}
jf.rate("The site is down for everyone", "severity", ["cosmetic", "degraded", "blocking"])
# Score(value=2.0, level='blocking', ...)
Topics
A topic only needs a name and a description. Categories, fields, scores and flags are optional.
# topics/jobs.yaml
name: Jobs
description: Applications I submitted, and recruiters contacting me about a role.
exclude: Job alerts, digests, newsletters.
categories:
applied: Confirms I submitted an application.
interview: Invites me to an interview.
rejection: Tells me I'm not moving forward.
fields:
company: {about: The hiring company, required: true}
flags:
needs_reply: The sender is asking me to reply.
f = jf.Filter(jf.Topic.load("topics/"))
email = {"from": "...", "subject": "...", "body": "..."}
r = f.judge(jf.Content(email, candidates={"Jobs": {"company": ["Acme", "Initech"]}}))
for t in r.matches:
print(t.topic, t.p, t.category.value, t.fields["company"].value, t.flags)
for t in r.review: # uncertain: let a person decide
print(t.topic, t.reasons)
r.cost_usd # every result reports its cost
db.save(r.to_dict()) # plain JSON; restore with jf.Result.from_dict
Each topic comes back as match, review or no. All questions for one
piece of content go to Jev in a single request. Field values are picked
from the candidates you pass in, never generated. f.explain(email) shows
the exact request and its estimated cost without sending it.
If most of your content matches no topic, jf.Filter(topics, speculative=False)
asks membership first and the rest only for topics that might match: a
second round trip, but about 30% fewer tokens in our tests.
Tracked items and batches
# Which of your tracked items is this email about? (you store the items)
m = f.match_item(email, "Jobs", my_jobs, result=r["Jobs"])
m.item_id # an id from my_jobs, or None for a new one
jf.track.next_status(topics["Jobs"], "applied", "interview") # "interviewing"
# Many emails at once, with a spend cap that refuses instead of overspending
af = jf.AsyncFilter(topics, budget=jf.Budget(usd=0.50, per_minute=120))
results = await af.judge_many(emails, concurrency=8)
See docs/topic-format.md for every topic option
(scores, composites, when, thresholds, meta) and docs/api.md
for the rest of the API.
Command line
jevfilter try topics/ "Your application to Acme was received"
jevfilter explain topics/ --file email.json # requests + cost, sends nothing
jevfilter lint topics/
jevfilter eval topics/ labelled.jsonl --sweep --record runs/cassette.jsonl
eval scores your topics against labelled examples (precision, recall,
review rate, calibration) and shows which thresholds trade off best.
Testing without an API key
from jevfilter.judges import FakeJudge
fake = FakeJudge({"Jobs/membership": 0.95, "Jobs/categories": "applied"})
r = jf.Filter(topics, judge=fake).judge("...")
Or record real answers once and replay them: RecordingJudge /
ReplayJudge in jevfilter.judges.
Development
uv sync
uv run pytest # unit tests, no network
JEVFILTER_LIVE=1 uv run pytest tests/live -s # real Jev, prints spend
uv run ruff check . && uv run ruff format .
License
MIT
Release files for jevfilter 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jevfilter-0.4.0.tar.gz | 112.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jevfilter-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 163.8 kB
Release files / jevfilter-0.4.0.tar.gz
| Download URL | jevfilter-0.4.0.tar.gz |
|---|---|
| Size | 112.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fcad4d4fa7475ae9879d9dc5547e85949c5fb81679b3e5b0b27d5c56bbcc8701
|
|
BLAKE2b-256 checksum How to use checksums |
73314b0ec8fdd690efc6cf33eeeac17033c5f86400dbc780c0db1de4e44c948f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jevfilter-0.4.0-py3-none-any.whl
| Download URL | jevfilter-0.4.0-py3-none-any.whl |
|---|---|
| Size | 51.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
78545c98e55d89d5107b7f058b6309272380eccc45e5ec42f13f0a2dfbc0eae6
|
|
BLAKE2b-256 checksum How to use checksums |
c00ba4bf8bbba448312b0638ede973464a72c020f3fc9cde37a22466d3cb94d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|