jevfilter 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.
Files changed (42) hide show
  1. jevfilter-0.1.0/.gitignore +9 -0
  2. jevfilter-0.1.0/CLAUDE.md +47 -0
  3. jevfilter-0.1.0/LICENSE +21 -0
  4. jevfilter-0.1.0/PKG-INFO +124 -0
  5. jevfilter-0.1.0/README.md +105 -0
  6. jevfilter-0.1.0/docs/api.md +209 -0
  7. jevfilter-0.1.0/docs/background.md +85 -0
  8. jevfilter-0.1.0/docs/design.md +366 -0
  9. jevfilter-0.1.0/docs/requirements.md +122 -0
  10. jevfilter-0.1.0/docs/topic-format.md +154 -0
  11. jevfilter-0.1.0/pyproject.toml +44 -0
  12. jevfilter-0.1.0/src/jevfilter/__init__.py +50 -0
  13. jevfilter-0.1.0/src/jevfilter/content.py +46 -0
  14. jevfilter-0.1.0/src/jevfilter/defaults.py +52 -0
  15. jevfilter-0.1.0/src/jevfilter/engine.py +285 -0
  16. jevfilter-0.1.0/src/jevfilter/errors.py +19 -0
  17. jevfilter-0.1.0/src/jevfilter/facets/__init__.py +214 -0
  18. jevfilter-0.1.0/src/jevfilter/helpers.py +75 -0
  19. jevfilter-0.1.0/src/jevfilter/judges/__init__.py +25 -0
  20. jevfilter-0.1.0/src/jevfilter/judges/base.py +98 -0
  21. jevfilter-0.1.0/src/jevfilter/judges/fake.py +106 -0
  22. jevfilter-0.1.0/src/jevfilter/judges/jev.py +90 -0
  23. jevfilter-0.1.0/src/jevfilter/policy.py +71 -0
  24. jevfilter-0.1.0/src/jevfilter/py.typed +0 -0
  25. jevfilter-0.1.0/src/jevfilter/registry.py +68 -0
  26. jevfilter-0.1.0/src/jevfilter/result.py +197 -0
  27. jevfilter-0.1.0/src/jevfilter/topic.py +598 -0
  28. jevfilter-0.1.0/src/jevfilter/version.py +6 -0
  29. jevfilter-0.1.0/src/jevfilter/wording.py +129 -0
  30. jevfilter-0.1.0/tests/conftest.py +17 -0
  31. jevfilter-0.1.0/tests/live/test_live.py +144 -0
  32. jevfilter-0.1.0/tests/test_content.py +24 -0
  33. jevfilter-0.1.0/tests/test_filter.py +236 -0
  34. jevfilter-0.1.0/tests/test_helpers.py +42 -0
  35. jevfilter-0.1.0/tests/test_judges.py +132 -0
  36. jevfilter-0.1.0/tests/test_logic.py +160 -0
  37. jevfilter-0.1.0/tests/test_policy.py +81 -0
  38. jevfilter-0.1.0/tests/test_topic.py +255 -0
  39. jevfilter-0.1.0/tests/test_wording.py +116 -0
  40. jevfilter-0.1.0/tests/topics/jobs.yaml +42 -0
  41. jevfilter-0.1.0/tests/topics/receipts.json +1 -0
  42. jevfilter-0.1.0/uv.lock +525 -0
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ .env
8
+ .coverage
9
+ htmlcov/
@@ -0,0 +1,47 @@
1
+ # jevfilter
2
+
3
+ Open-source (MIT) Python library for checking a message against
4
+ plain-English **topics** with TypeSafe's **Jev**: membership, category,
5
+ field selection from candidates, item matching, tracking rules,
6
+ thresholds, budget and failure policy. Stateless — no storage, cache,
7
+ hooks, sources or UI; callers persist the plain results. Published to
8
+ PyPI as `jevfilter`. First consumer: `jev-gmail-filter`, a Gmail app.
9
+
10
+ ## Context
11
+
12
+ - **Model:** Jev via `typesafe-sdk` (`client.system_one`). Typed answers
13
+ with calibrated probabilities; input tokens billed ($0.042/Mtok), output
14
+ free.
15
+ - **Docs:** <https://docs.typesafe.ai> — append `.md` to any page path for
16
+ its Markdown source.
17
+ - Pattern origin: <https://github.com/damiensmith1/semantic-pubsub-jev> (Go).
18
+ - Project docs: everything under `docs/` — background, requirements,
19
+ design, `topic-format.md` (language-neutral definition schema),
20
+ `api.md` (usage by example; see design.md "Status" for what's built).
21
+ - Goals: **easy** (three-line start), **extensible** (every part a small
22
+ protocol), **powerful** (fan-out, hierarchies, scores, items, eval).
23
+
24
+ ## Conventions
25
+
26
+ - Python ≥ 3.10 (matches `typesafe-sdk`), src layout, typed public API.
27
+ - Tooling: `uv sync`, `uv run pytest`, `uv run ruff check . && uv run ruff format .`.
28
+ Check the floor with `uv run --python 3.10 --isolated --with pytest --with pyyaml pytest`.
29
+ - Keep it stateless and source-agnostic. Nothing Gmail-specific here.
30
+ - `TYPESAFE_API_KEY` lives in `.env` (gitignored). Never log or commit it.
31
+ - Tests stub Jev. Live tests are opt-in and never run in CI.
32
+ - Commits are atomic and explain *why*. No co-author trailers.
33
+
34
+ ## Keeping docs in sync
35
+
36
+ Everything under docs/ is this project's source of truth, not a one-time
37
+ snapshot — including any file added there after initial setup, not just
38
+ background.md/requirements.md/design.md. In the SAME turn as a code
39
+ change (not a followup), update the relevant doc when you:
40
+ - resolve or add an open question in design.md
41
+ - make or change an architecture/approach decision
42
+ - add, change, or drop a requirement or non-goal
43
+ - learn something that changes the "why" in background.md
44
+ - create a new doc under docs/ for a topic that doesn't fit the above
45
+
46
+ Don't fabricate a decision that wasn't actually made. If it's unclear
47
+ whether something is doc-worthy, ask instead of guessing.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Damien Smith
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.
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.5
2
+ Name: jevfilter
3
+ Version: 0.1.0
4
+ Summary: Judge content against plain-English topic definitions with TypeSafe's Jev.
5
+ Project-URL: Repository, https://github.com/damiensmith1/jevfilter
6
+ Author: Damien Smith
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: classification,filter,jev,llm,typesafe
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: typesafe-sdk<0.8,>=0.7.1
16
+ Provides-Extra: yaml
17
+ Requires-Dist: pyyaml>=6; extra == 'yaml'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # jevfilter
21
+
22
+ [![CI](https://github.com/damiensmith1/jevfilter/actions/workflows/ci.yml/badge.svg)](https://github.com/damiensmith1/jevfilter/actions/workflows/ci.yml)
23
+
24
+ Judge content against plain-English definitions with TypeSafe's
25
+ [Jev](https://docs.typesafe.ai). Describe what you care about in a few
26
+ words; get back typed answers with calibrated probabilities: which topics
27
+ match, which category, which company, how urgent.
28
+
29
+ jevfilter is stateless: no storage, no cache and no network except Jev.
30
+ You get plain results back and store them however you like.
31
+
32
+ > Pre-alpha. The API may change before 1.0.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ pip install "jevfilter[yaml]"
38
+ export TYPESAFE_API_KEY=...
39
+ ```
40
+
41
+ Python 3.10+. You can also set the key in code with
42
+ `jf.configure(api_key=..., model="jev-1.13.0")`. jevfilter never reads
43
+ `.env` files, so load one yourself if you use it.
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ import jevfilter as jf
49
+
50
+ jf.choose("I was charged twice", ["billing", "bug", "feature request"])
51
+ # Choice(value='billing', confidence=0.96, ...)
52
+
53
+ jf.check("Can you send the form by Friday?", {"needs_reply": "The sender wants a reply"})
54
+ # {'needs_reply': 0.98}
55
+
56
+ jf.rate("The site is down for everyone", "severity", ["cosmetic", "degraded", "blocking"])
57
+ # Score(value=2.0, level='blocking', ...)
58
+ ```
59
+
60
+ ## Topics
61
+
62
+ A topic only needs a name and a description. Categories, fields, scores
63
+ and flags are optional.
64
+
65
+ ```yaml
66
+ # topics/jobs.yaml
67
+ name: Jobs
68
+ description: Applications I submitted, and recruiters contacting me about a role.
69
+ exclude: Job alerts, digests, newsletters.
70
+ categories:
71
+ applied: Confirms I submitted an application.
72
+ interview: Invites me to an interview.
73
+ rejection: Tells me I'm not moving forward.
74
+ fields:
75
+ company: {about: The hiring company, required: true}
76
+ flags:
77
+ needs_reply: The sender is asking me to reply.
78
+ ```
79
+
80
+ ```python
81
+ f = jf.Filter(jf.Topic.load("topics/"))
82
+
83
+ email = {"from": "...", "subject": "...", "body": "..."}
84
+ r = f.judge(jf.Content(email, candidates={"Jobs": {"company": ["Acme", "Initech"]}}))
85
+
86
+ for t in r.matches:
87
+ print(t.topic, t.p, t.category.value, t.fields["company"].value, t.flags)
88
+ for t in r.review: # uncertain: let a person decide
89
+ print(t.topic, t.reasons)
90
+
91
+ r.cost_usd # every result reports its cost
92
+ db.save(r.to_dict()) # plain JSON; restore with jf.Result.from_dict
93
+ ```
94
+
95
+ Each topic comes back as `match`, `review` or `no`. All questions for one
96
+ piece of content go to Jev in a single request. Field values are picked
97
+ from the candidates you pass in, never generated. `f.explain(email)` shows
98
+ the exact request and its estimated cost without sending it.
99
+
100
+ See [docs/topic-format.md](docs/topic-format.md) for every topic option
101
+ (scores, composites, `when`, thresholds, `meta`) and [docs/api.md](docs/api.md)
102
+ for the rest of the API.
103
+
104
+ ## Testing without an API key
105
+
106
+ ```python
107
+ from jevfilter.judges import FakeJudge
108
+
109
+ fake = FakeJudge({"Jobs/membership": 0.95, "Jobs/categories": "applied"})
110
+ r = jf.Filter(topics, judge=fake).judge("...")
111
+ ```
112
+
113
+ ## Development
114
+
115
+ ```sh
116
+ uv sync
117
+ uv run pytest # unit tests, no network
118
+ JEVFILTER_LIVE=1 uv run pytest tests/live -s # real Jev, prints spend
119
+ uv run ruff check . && uv run ruff format .
120
+ ```
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,105 @@
1
+ # jevfilter
2
+
3
+ [![CI](https://github.com/damiensmith1/jevfilter/actions/workflows/ci.yml/badge.svg)](https://github.com/damiensmith1/jevfilter/actions/workflows/ci.yml)
4
+
5
+ Judge content against plain-English definitions with TypeSafe's
6
+ [Jev](https://docs.typesafe.ai). Describe what you care about in a few
7
+ words; get back typed answers with calibrated probabilities: which topics
8
+ match, which category, which company, how urgent.
9
+
10
+ jevfilter is stateless: no storage, no cache and no network except Jev.
11
+ You get plain results back and store them however you like.
12
+
13
+ > Pre-alpha. The API may change before 1.0.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pip install "jevfilter[yaml]"
19
+ export TYPESAFE_API_KEY=...
20
+ ```
21
+
22
+ Python 3.10+. You can also set the key in code with
23
+ `jf.configure(api_key=..., model="jev-1.13.0")`. jevfilter never reads
24
+ `.env` files, so load one yourself if you use it.
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ import jevfilter as jf
30
+
31
+ jf.choose("I was charged twice", ["billing", "bug", "feature request"])
32
+ # Choice(value='billing', confidence=0.96, ...)
33
+
34
+ jf.check("Can you send the form by Friday?", {"needs_reply": "The sender wants a reply"})
35
+ # {'needs_reply': 0.98}
36
+
37
+ jf.rate("The site is down for everyone", "severity", ["cosmetic", "degraded", "blocking"])
38
+ # Score(value=2.0, level='blocking', ...)
39
+ ```
40
+
41
+ ## Topics
42
+
43
+ A topic only needs a name and a description. Categories, fields, scores
44
+ and flags are optional.
45
+
46
+ ```yaml
47
+ # topics/jobs.yaml
48
+ name: Jobs
49
+ description: Applications I submitted, and recruiters contacting me about a role.
50
+ exclude: Job alerts, digests, newsletters.
51
+ categories:
52
+ applied: Confirms I submitted an application.
53
+ interview: Invites me to an interview.
54
+ rejection: Tells me I'm not moving forward.
55
+ fields:
56
+ company: {about: The hiring company, required: true}
57
+ flags:
58
+ needs_reply: The sender is asking me to reply.
59
+ ```
60
+
61
+ ```python
62
+ f = jf.Filter(jf.Topic.load("topics/"))
63
+
64
+ email = {"from": "...", "subject": "...", "body": "..."}
65
+ r = f.judge(jf.Content(email, candidates={"Jobs": {"company": ["Acme", "Initech"]}}))
66
+
67
+ for t in r.matches:
68
+ print(t.topic, t.p, t.category.value, t.fields["company"].value, t.flags)
69
+ for t in r.review: # uncertain: let a person decide
70
+ print(t.topic, t.reasons)
71
+
72
+ r.cost_usd # every result reports its cost
73
+ db.save(r.to_dict()) # plain JSON; restore with jf.Result.from_dict
74
+ ```
75
+
76
+ Each topic comes back as `match`, `review` or `no`. All questions for one
77
+ piece of content go to Jev in a single request. Field values are picked
78
+ from the candidates you pass in, never generated. `f.explain(email)` shows
79
+ the exact request and its estimated cost without sending it.
80
+
81
+ See [docs/topic-format.md](docs/topic-format.md) for every topic option
82
+ (scores, composites, `when`, thresholds, `meta`) and [docs/api.md](docs/api.md)
83
+ for the rest of the API.
84
+
85
+ ## Testing without an API key
86
+
87
+ ```python
88
+ from jevfilter.judges import FakeJudge
89
+
90
+ fake = FakeJudge({"Jobs/membership": 0.95, "Jobs/categories": "applied"})
91
+ r = jf.Filter(topics, judge=fake).judge("...")
92
+ ```
93
+
94
+ ## Development
95
+
96
+ ```sh
97
+ uv sync
98
+ uv run pytest # unit tests, no network
99
+ JEVFILTER_LIVE=1 uv run pytest tests/live -s # real Jev, prints spend
100
+ uv run ruff check . && uv run ruff format .
101
+ ```
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,209 @@
1
+ ---
2
+ title: API
3
+ tags: [jevfilter, api]
4
+ status: draft — partly implemented (see design → Status)
5
+ ---
6
+
7
+ # API by example
8
+
9
+ Proposed public API, from simplest to most advanced. See [design](design.md) for
10
+ the internals and [topic-format](topic-format.md) for definitions.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pip install jevfilter # core
16
+ pip install "jevfilter[yaml,cli,eval]"
17
+ export TYPESAFE_API_KEY=...
18
+ ```
19
+
20
+ ## API key and model
21
+
22
+ jevfilter uses `TYPESAFE_API_KEY` from the environment by default. It
23
+ never reads `.env` files; load one yourself (e.g. `python-dotenv`) if you
24
+ keep the key there. To set it in code:
25
+
26
+ ```python
27
+ import jevfilter as jf
28
+ from jevfilter.judges import JevJudge
29
+
30
+ jf.configure(api_key=key, model="jev-1.13.0") # default for helpers and filters
31
+ f = Filter(topics, judge=JevJudge(api_key=key)) # or per filter
32
+ jf.choose("...", ["a", "b"], judge=my_judge) # or per call
33
+ ```
34
+
35
+ ## Level 1 — helpers
36
+
37
+ ```python
38
+ import jevfilter as jf
39
+
40
+ jf.choose("I was charged twice", ["billing", "bug", "feature request"])
41
+ # Choice(value='billing', confidence=0.97, probabilities={...})
42
+
43
+ jf.check("Can you send the signed form by Friday?", {
44
+ "needs_reply": "The sender is asking me to reply or send something",
45
+ "has_deadline": "A specific deadline is mentioned",
46
+ })
47
+ # {'needs_reply': 0.98, 'has_deadline': 0.95}
48
+
49
+ jf.rate("The site is down for all customers", "severity",
50
+ ["cosmetic", "degraded", "blocking"])
51
+ # Score(value=1.96, level='blocking', confidence=0.93, ...)
52
+ ```
53
+
54
+ Descriptions can be dicts (`{"billing": "Charges, refunds…"}`) for
55
+ clarity.
56
+
57
+ ## Level 2 — topics
58
+
59
+ ```python
60
+ from jevfilter import Filter, Topic
61
+
62
+ topics = Topic.load("topics/") # dir, file, or list of dicts
63
+ f = Filter(topics)
64
+
65
+ r = f.judge({"from": "...", "subject": "...", "body": "..."})
66
+
67
+ for t in r.matches: # outcome == "match"
68
+ print(t.topic, t.p, t.category, t.flags, t.scores)
69
+ for t in r.review: # needs a person
70
+ print(t.topic, t.reasons)
71
+
72
+ r.cost_usd, r.input_tokens, r.model
73
+ ```
74
+
75
+ Topics in code:
76
+
77
+ ```python
78
+ Topic(
79
+ name="Receipts",
80
+ description="Receipts and order confirmations for things I bought",
81
+ fields={"merchant": {"kind": "org"}},
82
+ )
83
+ ```
84
+
85
+ ### Fields and candidates
86
+
87
+ ```python
88
+ from jevfilter import Content
89
+
90
+ r = f.judge(Content(email, candidates={"Jobs": {"company": ["Acme", "Initech"]}}))
91
+ r["Jobs"].fields["company"] # Field(value='Acme', confidence=0.99)
92
+ ```
93
+
94
+ Omit `candidates` to use the extractor registered for each field's
95
+ `kind`.
96
+
97
+ ### Items and tracking
98
+
99
+ ```python
100
+ from jevfilter import track
101
+
102
+ jobs = topics["Jobs"]
103
+ m = f.match_item(email, jobs, items=[
104
+ {"id": 3, "fields": {"company": "Acme", "role": "Backend Engineer"}, "status": "applied"},
105
+ {"id": 7, "fields": {"company": "Acme", "role": "Data Engineer"}, "status": "contacted"},
106
+ ])
107
+ m.item_id # 3, or None for a new item
108
+ m.outcome # "match" | "review"
109
+
110
+ track.next_status(jobs, current="applied", category="interview") # "interviewing"
111
+ track.is_stale(jobs, last_activity=dt, now=now) # bool
112
+ ```
113
+
114
+ ### Many items
115
+
116
+ ```python
117
+ results = await AsyncFilter(topics).judge_many(emails, concurrency=8)
118
+ ```
119
+
120
+ ### Inspect before sending
121
+
122
+ ```python
123
+ plan = f.explain(email)
124
+ plan.requests # exact payloads
125
+ plan.cost_usd # estimate
126
+ ```
127
+
128
+ ## Level 3 — engine
129
+
130
+ ```python
131
+ from jevfilter import Filter, Budget, ThresholdPolicy
132
+ from jevfilter.judges import JevJudge, KeywordJudge
133
+
134
+ budget = Budget(usd=1.00, per_minute=60) # share across filters
135
+
136
+ f = Filter(
137
+ topics,
138
+ judge=JevJudge(model="jev-1.13.0"), # pin the version
139
+ policy=ThresholdPolicy(accept=0.8, reject=0.2, min_confidence=0.6),
140
+ budget=budget,
141
+ on_error=KeywordJudge(), # or "raise" / "review"
142
+ )
143
+ ```
144
+
145
+ ### Saving results (your code)
146
+
147
+ ```python
148
+ r = f.judge(email)
149
+ db.insert(r.to_dict()) # plain, JSON-able; restore with Result.from_dict
150
+ ```
151
+
152
+ ### Editing topics (e.g. from a UI)
153
+
154
+ ```python
155
+ t = Topic.from_dict(form_data) # validates, raises with clear messages
156
+ t.to_yaml("topics/jobs.yaml") # round-trips unchanged
157
+ t.version # changes → offer a rescan
158
+ ```
159
+
160
+ ### Custom facet
161
+
162
+ ```python
163
+ import jevfilter as jf
164
+ from jevfilter.facets import NoulFacet
165
+
166
+ @jf.facet("pii")
167
+ class ContainsPII(NoulFacet):
168
+ instructions = "Does `content` contain personal data such as addresses or ID numbers?"
169
+ ```
170
+
171
+ ```yaml
172
+ # usable from topic files
173
+ name: Support
174
+ description: Customer support requests.
175
+ pii: {}
176
+ ```
177
+
178
+ ### Custom extractor
179
+
180
+ ```python
181
+ @jf.extractor("order_number")
182
+ def order_numbers(content, field):
183
+ return re.findall(r"#\d{5,}", content.text)
184
+ ```
185
+
186
+ ### Testing without the API
187
+
188
+ ```python
189
+ from jevfilter.judges import FakeJudge
190
+
191
+ fake = FakeJudge({"Jobs/membership": 0.95, "Jobs/categories": "applied"})
192
+ r = Filter(topics, judge=fake).judge("...")
193
+ ```
194
+
195
+ Record once, replay forever:
196
+
197
+ ```python
198
+ judge = RecordingJudge(JevJudge(), path="tests/cassettes/jobs.jsonl")
199
+ judge = ReplayJudge("tests/cassettes/jobs.jsonl")
200
+ ```
201
+
202
+ ## CLI
203
+
204
+ ```sh
205
+ jevfilter try topics/ "Your application was sent to Acme"
206
+ jevfilter explain topics/ email.txt
207
+ jevfilter lint topics/
208
+ jevfilter eval topics/ labelled.jsonl --sweep
209
+ ```
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: Background
3
+ tags: [jevfilter, jev, typesafe, library]
4
+ status: draft
5
+ ---
6
+
7
+ # Background
8
+
9
+ ## The recurring problem
10
+
11
+ A piece of content arrives — an email, an event, a ticket, a document —
12
+ and code needs to decide things about it that only a person could
13
+ previously judge:
14
+
15
+ - Does it belong to any of these user-defined topics? (several may apply)
16
+ - Which category is it?
17
+ - Which company / person / order is it about?
18
+ - Which thing we're already tracking does it concern?
19
+ - How urgent / relevant / severe is it?
20
+
21
+ Keyword rules can't express meaning. Generative LLM prompts can, but
22
+ return prose to parse, drift between runs, and give no usable confidence.
23
+
24
+ ## Why Jev
25
+
26
+ TypeSafe's **Jev** is a System One model: it takes a *state* and a set of
27
+ *questions* and returns typed answers with calibrated probabilities —
28
+ **Noul** (probability of yes), **Choice** (one of a set, with a
29
+ distribution), **Score** (a position on ordered levels). All questions in
30
+ a request see the same state and are evaluated independently and in
31
+ parallel, so asking many questions at once costs little extra latency.
32
+ Input tokens are billed ($0.042 per million at the time of writing),
33
+ output is free — an estimated few cents per thousand typical emails.
34
+
35
+ That's the right primitive, but it's low level. Every project ends up
36
+ writing the same layer on top of it.
37
+
38
+ ## Where the pattern came from
39
+
40
+ - [semantic-pubsub-jev](https://github.com/damiensmith1/semantic-pubsub-jev) (Go) — subscribers state interests in plain
41
+ English; Jev decides who gets each message. Measured and settled:
42
+ - batch all conditions into one request (1 → 100 questions: 193ms →
43
+ 224ms)
44
+ - one Noul per condition, because several can match
45
+ - keep raw probabilities, not just the thresholded boolean
46
+ - pin the model version when measuring; record which version answered
47
+ - two spend ceilings (rate and total) that refuse rather than block
48
+ - an explicit failure policy (fail open vs closed)
49
+ - `jev-gmail-filter` (a Gmail app built on this library) (Python prototype) — user-defined topics filter
50
+ Gmail, with categories, tracked items and status pipelines. Added:
51
+ topics as files, selecting field values from code-extracted candidates
52
+ rather than generating them, a review band for uncertain answers, and
53
+ matching an email to an existing tracked item. A live run on four
54
+ synthetic job emails classified all of them correctly, including
55
+ rejecting a job-alert digest.
56
+
57
+ ## What jevfilter is
58
+
59
+ An open-source Python library that turns "judge this content against
60
+ these plain-English definitions" into a few lines of code, and scales up
61
+ to categories, fields, scores, tracked items and hierarchies without
62
+ changing tools. It owns the layer both projects rebuilt: turning
63
+ definitions into questions, packing them into requests, applying
64
+ decision policy, controlling spend, handling failure, and making results
65
+ inspectable and testable.
66
+
67
+ It is not an app, and it doesn't know about email.
68
+
69
+ ## Goals
70
+
71
+ - **Easy** — a first result in three lines; definitions in plain English.
72
+ - **Extensible** — every moving part (backend, extractors, facets,
73
+ policy) is a small protocol you can replace.
74
+ - **Powerful** — speculative fan-out, many topics per request,
75
+ hierarchical categories, composite scores, entity/item matching,
76
+ tracking rules, batch processing, evaluation against labelled data.
77
+
78
+ ## Prior art
79
+
80
+ - `typesafe-sdk` — the official client. jevfilter builds on it.
81
+ - Gmail filters / rule engines — keyword and header matching, no meaning.
82
+ - LLM classification prompts — generated text, uncalibrated.
83
+ - TypeSafe cookbooks (fan-out, composite scoring, hierarchical
84
+ classification, value extraction) — patterns jevfilter packages as
85
+ reusable building blocks.