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.
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.3.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.3.0.tar.gz | 107.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jevfilter-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 156.0 kB
Release files / jevfilter-0.3.0.tar.gz
| Download URL | jevfilter-0.3.0.tar.gz |
|---|---|
| Size | 107.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
22d360ab92a2a5182e979d285a6d640dbdc1f93f5b9a76b09b421518f31361ce
|
|
BLAKE2b-256 checksum How to use checksums |
46eb1204ea0248c157f45ea1f051a3c704f47e60b1ed626750c9a9d0a5d1804d
|
| 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.3.0-py3-none-any.whl
| Download URL | jevfilter-0.3.0-py3-none-any.whl |
|---|---|
| Size | 49.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dfb63b75bfce50aea9241605510449a57ea4b5afe81fe3f8c5b919fec5edaecf
|
|
BLAKE2b-256 checksum How to use checksums |
ccc6b75f8a7b85fcc656c504bafe741bad80f6e0d7b3075dd649b3378050d11b
|
| 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}
|