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.
- jevfilter-0.1.0/.gitignore +9 -0
- jevfilter-0.1.0/CLAUDE.md +47 -0
- jevfilter-0.1.0/LICENSE +21 -0
- jevfilter-0.1.0/PKG-INFO +124 -0
- jevfilter-0.1.0/README.md +105 -0
- jevfilter-0.1.0/docs/api.md +209 -0
- jevfilter-0.1.0/docs/background.md +85 -0
- jevfilter-0.1.0/docs/design.md +366 -0
- jevfilter-0.1.0/docs/requirements.md +122 -0
- jevfilter-0.1.0/docs/topic-format.md +154 -0
- jevfilter-0.1.0/pyproject.toml +44 -0
- jevfilter-0.1.0/src/jevfilter/__init__.py +50 -0
- jevfilter-0.1.0/src/jevfilter/content.py +46 -0
- jevfilter-0.1.0/src/jevfilter/defaults.py +52 -0
- jevfilter-0.1.0/src/jevfilter/engine.py +285 -0
- jevfilter-0.1.0/src/jevfilter/errors.py +19 -0
- jevfilter-0.1.0/src/jevfilter/facets/__init__.py +214 -0
- jevfilter-0.1.0/src/jevfilter/helpers.py +75 -0
- jevfilter-0.1.0/src/jevfilter/judges/__init__.py +25 -0
- jevfilter-0.1.0/src/jevfilter/judges/base.py +98 -0
- jevfilter-0.1.0/src/jevfilter/judges/fake.py +106 -0
- jevfilter-0.1.0/src/jevfilter/judges/jev.py +90 -0
- jevfilter-0.1.0/src/jevfilter/policy.py +71 -0
- jevfilter-0.1.0/src/jevfilter/py.typed +0 -0
- jevfilter-0.1.0/src/jevfilter/registry.py +68 -0
- jevfilter-0.1.0/src/jevfilter/result.py +197 -0
- jevfilter-0.1.0/src/jevfilter/topic.py +598 -0
- jevfilter-0.1.0/src/jevfilter/version.py +6 -0
- jevfilter-0.1.0/src/jevfilter/wording.py +129 -0
- jevfilter-0.1.0/tests/conftest.py +17 -0
- jevfilter-0.1.0/tests/live/test_live.py +144 -0
- jevfilter-0.1.0/tests/test_content.py +24 -0
- jevfilter-0.1.0/tests/test_filter.py +236 -0
- jevfilter-0.1.0/tests/test_helpers.py +42 -0
- jevfilter-0.1.0/tests/test_judges.py +132 -0
- jevfilter-0.1.0/tests/test_logic.py +160 -0
- jevfilter-0.1.0/tests/test_policy.py +81 -0
- jevfilter-0.1.0/tests/test_topic.py +255 -0
- jevfilter-0.1.0/tests/test_wording.py +116 -0
- jevfilter-0.1.0/tests/topics/jobs.yaml +42 -0
- jevfilter-0.1.0/tests/topics/receipts.json +1 -0
- jevfilter-0.1.0/uv.lock +525 -0
|
@@ -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.
|
jevfilter-0.1.0/LICENSE
ADDED
|
@@ -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.
|
jevfilter-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](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
|
+
[](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.
|