jevmod 0.2.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.
- jevmod-0.2.0/LICENSE +21 -0
- jevmod-0.2.0/PKG-INFO +293 -0
- jevmod-0.2.0/README.md +249 -0
- jevmod-0.2.0/jevmod/__init__.py +67 -0
- jevmod-0.2.0/jevmod/__main__.py +50 -0
- jevmod-0.2.0/jevmod/adapters/__init__.py +0 -0
- jevmod-0.2.0/jevmod/adapters/discord_bot.py +324 -0
- jevmod-0.2.0/jevmod/adapters/reddit_bot.py +91 -0
- jevmod-0.2.0/jevmod/adapters/telegram_bot.py +206 -0
- jevmod-0.2.0/jevmod/api/__init__.py +0 -0
- jevmod-0.2.0/jevmod/api/demo.py +199 -0
- jevmod-0.2.0/jevmod/api/server.py +185 -0
- jevmod-0.2.0/jevmod/categories.json +69 -0
- jevmod-0.2.0/jevmod/cli.py +97 -0
- jevmod-0.2.0/jevmod/core/__init__.py +17 -0
- jevmod-0.2.0/jevmod/core/policy.py +155 -0
- jevmod-0.2.0/jevmod/core/service.py +117 -0
- jevmod-0.2.0/jevmod/core/store.py +215 -0
- jevmod-0.2.0/jevmod/judge.py +189 -0
- jevmod-0.2.0/jevmod/keys.py +146 -0
- jevmod-0.2.0/jevmod/mcp_server.py +76 -0
- jevmod-0.2.0/jevmod.egg-info/PKG-INFO +293 -0
- jevmod-0.2.0/jevmod.egg-info/SOURCES.txt +36 -0
- jevmod-0.2.0/jevmod.egg-info/dependency_links.txt +1 -0
- jevmod-0.2.0/jevmod.egg-info/entry_points.txt +2 -0
- jevmod-0.2.0/jevmod.egg-info/requires.txt +36 -0
- jevmod-0.2.0/jevmod.egg-info/top_level.txt +1 -0
- jevmod-0.2.0/pyproject.toml +52 -0
- jevmod-0.2.0/setup.cfg +4 -0
- jevmod-0.2.0/tests/test_api.py +77 -0
- jevmod-0.2.0/tests/test_cli.py +43 -0
- jevmod-0.2.0/tests/test_demo.py +63 -0
- jevmod-0.2.0/tests/test_examples.py +104 -0
- jevmod-0.2.0/tests/test_judge.py +71 -0
- jevmod-0.2.0/tests/test_keys.py +97 -0
- jevmod-0.2.0/tests/test_mcp.py +69 -0
- jevmod-0.2.0/tests/test_offline.py +97 -0
- jevmod-0.2.0/tests/test_redteam.py +137 -0
jevmod-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omar Hernandez
|
|
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.
|
jevmod-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jevmod
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Moderation for communities and apps, powered by Jev: spam, scams, harassment, adult, off-topic and your own plain-language rules, with a probability per category and thresholds you own.
|
|
5
|
+
Author: Omar Hernandez
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ohernandezdev/jevmod
|
|
8
|
+
Keywords: moderation,discord,telegram,reddit,content-moderation,typesafe,jev
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Communications :: Chat
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: typesafe-sdk>=0.6
|
|
16
|
+
Requires-Dist: fastapi>=0.110
|
|
17
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
18
|
+
Requires-Dist: pydantic>=2
|
|
19
|
+
Provides-Extra: discord
|
|
20
|
+
Requires-Dist: discord.py>=2.3; extra == "discord"
|
|
21
|
+
Provides-Extra: telegram
|
|
22
|
+
Requires-Dist: python-telegram-bot>=21; extra == "telegram"
|
|
23
|
+
Provides-Extra: reddit
|
|
24
|
+
Requires-Dist: praw>=7.7; extra == "reddit"
|
|
25
|
+
Provides-Extra: mcp
|
|
26
|
+
Requires-Dist: mcp>=2; extra == "mcp"
|
|
27
|
+
Provides-Extra: keys
|
|
28
|
+
Requires-Dist: keyring>=25; extra == "keys"
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: discord.py>=2.3; extra == "all"
|
|
31
|
+
Requires-Dist: python-telegram-bot>=21; extra == "all"
|
|
32
|
+
Requires-Dist: praw>=7.7; extra == "all"
|
|
33
|
+
Requires-Dist: mcp>=2; extra == "all"
|
|
34
|
+
Requires-Dist: keyring>=25; extra == "all"
|
|
35
|
+
Provides-Extra: examples
|
|
36
|
+
Requires-Dist: langchain-core>=0.3; extra == "examples"
|
|
37
|
+
Requires-Dist: httpx; extra == "examples"
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest; extra == "dev"
|
|
40
|
+
Requires-Dist: ruff; extra == "dev"
|
|
41
|
+
Requires-Dist: mypy; extra == "dev"
|
|
42
|
+
Requires-Dist: httpx; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# jevmod
|
|
46
|
+
|
|
47
|
+
Moderation for communities and apps: every message gets a probability for **spam, scam, harassment, nsfw,
|
|
48
|
+
off-topic, self-harm, doxxing, sexual content involving minors**, and for **rules you write in plain English**.
|
|
49
|
+
You set the thresholds and the actions. Every decision is logged with its numbers.
|
|
50
|
+
|
|
51
|
+
It runs on [Jev](https://typesafe.ai), TypeSafe's System One model: you ask yes/no questions about a message and
|
|
52
|
+
get probabilities back, no text generation. About **$0.04 per 1,000 messages** with all categories on.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
$ jevmod check "FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"
|
|
58
|
+
scam 0.99 'FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro' [scam 0.99, spam 0.98, harassment 0.02]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Flag-only by default: nothing is deleted until you turn that on. Fails open: if Jev is unreachable, messages are
|
|
62
|
+
left alone and the failure is logged. Self-harm is flag-only by design so a moderator can reach out.
|
|
63
|
+
|
|
64
|
+
| you are | you get | start |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| a community owner, not technical | a Discord bot you tune with commands (Telegram with fewer commands, Reddit by env vars) | [Run the bot](#run-the-bot) |
|
|
67
|
+
| a developer | a CLI, a Python package, an npm package, or one HTTP call | [Developer](#developer) |
|
|
68
|
+
| a coding agent, or someone using one | an MCP server and a Claude Code skill that wires jevmod into a codebase | [Agents](#agents) |
|
|
69
|
+
|
|
70
|
+
On the [benchmark](BENCHMARK.md) (2,531 messages from OpenAI's moderation eval, Jigsaw and YouTube spam) jevmod
|
|
71
|
+
had the best AUROC in every category it was compared on in OpenAI's human-labelled set: harassment 0.93 and
|
|
72
|
+
sexual 0.98 against Llama Guard 3 8B, ShieldGemma 2B and toxic-bert; self-harm 0.99 and minors 0.98 against Llama
|
|
73
|
+
Guard, the only other system with those labels. Calibration was measured too; caveats are in the same file.
|
|
74
|
+
|
|
75
|
+
## The key, once
|
|
76
|
+
|
|
77
|
+
Every surface needs a TypeSafe API key (free tier at [console.typesafe.ai](https://console.typesafe.ai)).
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install "jevmod[keys]"
|
|
81
|
+
jevmod init # asks for the key without echo, verifies it with one call, stores it in the OS keyring
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Resolution order everywhere (CLI, API, bots, MCP, `Moderator`): `TYPESAFE_API_KEY` in the environment, then the
|
|
85
|
+
keyring, then a `.env` in the current directory. In a container or a headless server `jevmod init --env-file`
|
|
86
|
+
writes `.env` with mode 600. `jevmod init --forget` removes the keyring entry (`pip uninstall` does not). Keys never
|
|
87
|
+
go into git; `.env` is ignored.
|
|
88
|
+
|
|
89
|
+
## Run the bot
|
|
90
|
+
|
|
91
|
+
### Discord
|
|
92
|
+
|
|
93
|
+
1. [Developer Portal](https://discord.com/developers/applications) → New Application → Bot → **Reset Token** →
|
|
94
|
+
enable **Message Content Intent** (the only privileged intent used).
|
|
95
|
+
2. OAuth2 → URL Generator → scopes `bot` + `applications.commands`; permissions: Read Messages, Send Messages,
|
|
96
|
+
Manage Messages, Moderate Members, Manage Channels, Embed Links, Add Reactions. Open the URL, add it to your server.
|
|
97
|
+
3. `pip install "jevmod[discord]"`, set `DISCORD_TOKEN`, run `jevmod discord`.
|
|
98
|
+
|
|
99
|
+
The bot creates a private `#jevmod-log` channel and starts flagging there.
|
|
100
|
+
|
|
101
|
+
| command (server managers only) | what |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `/mod status` | settings and this month's usage |
|
|
104
|
+
| `/mod set <category> <action> [threshold]` | any category → `off`, `flag`, `delete`, `timeout` |
|
|
105
|
+
| `/mod rule <name> <text> [action] [threshold]` | a rule in your words: "No politics. News about the game is fine." (max 5) |
|
|
106
|
+
| `/mod trust <role>` | messages from that role are never judged |
|
|
107
|
+
| `/mod topic <text>` | what the current channel is for (turns on `offtopic`) |
|
|
108
|
+
| `/mod log`, `/mod recent` | choose the log channel; last decisions with probabilities |
|
|
109
|
+
| `/mod forget`, `/mod forget_user @member` | delete everything stored about the server, or one member |
|
|
110
|
+
|
|
111
|
+
React ❌ on a log entry to mark a false positive (that category's threshold goes up a notch), ✅ to confirm a
|
|
112
|
+
correct call (down a notch, floor 0.5).
|
|
113
|
+
|
|
114
|
+
### Telegram and Reddit
|
|
115
|
+
|
|
116
|
+
Telegram: [@BotFather](https://t.me/BotFather) → `/newbot`, make the bot a group admin, `pip install
|
|
117
|
+
"jevmod[telegram]"`, set `TELEGRAM_TOKEN`, run `jevmod telegram`. Admin commands: `/mod_status`, `/mod_set`,
|
|
118
|
+
`/mod_rule`, `/mod_topic`, `/mod_log`.
|
|
119
|
+
|
|
120
|
+
Reddit: for your own subreddit with your own "script" app credentials, non-commercial (Reddit's API terms).
|
|
121
|
+
`pip install "jevmod[reddit]"`, fill the `REDDIT_*` variables from `.env.example`, run `jevmod reddit`. Reports by
|
|
122
|
+
default; removal and bans are opt-in.
|
|
123
|
+
|
|
124
|
+
### What the bots send where
|
|
125
|
+
|
|
126
|
+
Only the **message text** and the **channel topic** go to TypeSafe. Author names and ids never do. Locally,
|
|
127
|
+
jevmod keeps a decision log (category, probabilities, action, first 300 characters); rows older than 30 days are
|
|
128
|
+
purged on every batch.
|
|
129
|
+
`/mod forget` deletes everything; leaving the server does the same. Members whose message is removed get a direct
|
|
130
|
+
message saying an automated system did it and how to appeal.
|
|
131
|
+
|
|
132
|
+
## Developer
|
|
133
|
+
|
|
134
|
+
### CLI
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pip install jevmod
|
|
138
|
+
jevmod check "some text" # exit 0 clean, 1 something triggered, 2 error
|
|
139
|
+
cat comments.txt | jevmod check --json --rule "No politics. Game news is fine." -
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
One message per line on stdin, one Jev request per 50. `--topic` turns on the off-topic check, `--threshold` sets
|
|
143
|
+
one for every category, `--json` prints one object per line with every probability. `examples/cli/` has a file
|
|
144
|
+
screener and a pre-commit hook.
|
|
145
|
+
|
|
146
|
+
### Python
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from jevmod import Moderator, Policy
|
|
150
|
+
|
|
151
|
+
d = Moderator().check("FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro", channel_topic="gaming")
|
|
152
|
+
d.action, d.category, d.probability # ('flag', 'scam', 0.99)
|
|
153
|
+
d.scores # {'spam': 0.98, 'scam': 0.99, 'harassment': 0.02, 'nsfw': 0.01, ...}
|
|
154
|
+
|
|
155
|
+
p = Policy()
|
|
156
|
+
p.set_category("scam", "delete", 0.7)
|
|
157
|
+
p.set_rule("no_politics", "No political discussion. Game news is fine.", action="flag", threshold=0.8)
|
|
158
|
+
Moderator(policy=p).check_many(["...", "..."], channel_topic="support") # one request for the batch
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### npm
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { Moderator, Policy } from "jevmod";
|
|
165
|
+
const d = await new Moderator().check("FREE NITRO ...", { channelTopic: "gaming" });
|
|
166
|
+
d.action, d.category, d.scores // same shape as Python and the HTTP API
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Node 20+. Same questions (`jevmod/categories.json` is copied byte for byte and CI fails if it drifts), same
|
|
170
|
+
policy, same decision. `JevmodClient` talks to a deployed HTTP API instead, so browsers and edge functions never
|
|
171
|
+
hold the TypeSafe key. Express middleware in `packages/jevmod-js/examples/`. Details in
|
|
172
|
+
[packages/jevmod-js/README.md](packages/jevmod-js/README.md).
|
|
173
|
+
|
|
174
|
+
### HTTP API
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
JEVMOD_ADMIN_TOKEN=... jevmod api # or: docker compose up -d
|
|
178
|
+
curl -X POST localhost:8080/v1/keys -H "Authorization: Bearer $JEVMOD_ADMIN_TOKEN" \
|
|
179
|
+
-H "Content-Type: application/json" -d '{"tenant":"my-app"}' # {"api_key":"jm_...", shown once}
|
|
180
|
+
curl -X POST localhost:8080/v1/moderate -H "Authorization: Bearer jm_..." -H "Content-Type: application/json" \
|
|
181
|
+
-d '{"messages":[{"id":"a","text":"FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"}]}'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
| endpoint | what |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `POST /v1/moderate` | up to 50 messages → decisions; the `X-Request-Id` you send comes back as `request_id` and as a response header |
|
|
187
|
+
| `GET/PUT /v1/policy` | thresholds, actions, rules for this tenant |
|
|
188
|
+
| `GET /v1/decisions` | the audit log |
|
|
189
|
+
| `DELETE /v1/tenant` | forget this tenant |
|
|
190
|
+
| `POST /v1/keys` (admin) | mint a tenant key, stored hashed |
|
|
191
|
+
| `GET /v1/health`, `GET /metrics` | liveness, Prometheus counters |
|
|
192
|
+
|
|
193
|
+
OpenAPI at `/docs`, a Postman collection in `postman/`. Any chatbot, forum or comment system that can make an HTTP
|
|
194
|
+
call can use it; the bots are adapters over the same service.
|
|
195
|
+
|
|
196
|
+
### Examples, one folder per surface
|
|
197
|
+
|
|
198
|
+
| folder | what |
|
|
199
|
+
|---|---|
|
|
200
|
+
| [`examples/sdk/`](examples/sdk) | `Moderator` basics, a custom policy with a rule, batching 50 per request |
|
|
201
|
+
| [`examples/cli/`](examples/cli) | `screen_file.sh` exits 1 on hits; `pre-commit.sh` blocks flagged text files |
|
|
202
|
+
| [`examples/api/`](examples/api) | curl, a stdlib Python client, a Node client against a local `jevmod api` |
|
|
203
|
+
| [`examples/discord/`](examples/discord) | run the bot; `custom_adapter.py` puts any chat platform on `ModerationService` in 20 lines |
|
|
204
|
+
| [`examples/agent_harness/`](examples/agent_harness) | `@guarded` decorator, Claude Agent SDK `PreToolUse`/`PostToolUse` hooks, LangChain callback |
|
|
205
|
+
| [`examples/input_validation/`](examples/input_validation) | FastAPI dependency that answers 422, a pydantic `ModeratedText` field |
|
|
206
|
+
| [`packages/jevmod-js/examples/`](packages/jevmod-js/examples) | Express middleware and a plain Node script |
|
|
207
|
+
|
|
208
|
+
## Agents
|
|
209
|
+
|
|
210
|
+
**MCP server.** `pip install "jevmod[mcp]"` then `jevmod mcp` (stdio). Tools: `moderate(texts, channel_topic?,
|
|
211
|
+
rules?)` returns one decision per text; `categories()` describes each category and its default threshold.
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
claude mcp add jevmod -- jevmod mcp # Claude Code
|
|
215
|
+
{"mcpServers": {"jevmod": {"command": "jevmod", "args": ["mcp"]}}} # Cursor, Codex, others
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Claude Code plugin.** This repository is its own marketplace:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
/plugin marketplace add ohernandezdev/jevmod
|
|
222
|
+
/plugin install jevmod@jevmod
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`jevmod-integrate` adds moderation to an existing codebase (detects the stack, picks SDK/npm/HTTP/MCP, wires the
|
|
226
|
+
key, inserts the call where it belongs, adds a real test). `jevmod-moderate` screens text or datasets from the
|
|
227
|
+
terminal while working. [AGENTS.md](AGENTS.md) has every signature; [docs/llms.txt](docs/llms.txt) indexes the docs.
|
|
228
|
+
|
|
229
|
+
## Categories
|
|
230
|
+
|
|
231
|
+
| category | true when | default |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `spam` | unsolicited promotion, invite farming, bare link drops, mass mentions | flag ≥ 0.85 |
|
|
234
|
+
| `scam` | fake giveaways, phishing domains, impersonated support, "DM me for a deal" | flag ≥ 0.75 |
|
|
235
|
+
| `harassment` | insults, slurs, threats, targeted abuse, in any language | flag ≥ 0.75 |
|
|
236
|
+
| `nsfw` | sexual or gore content for a general audience (below the threshold means SFW) | flag ≥ 0.80 |
|
|
237
|
+
| `offtopic` | unrelated to `channel_topic`; needs a topic to mean anything | off, 0.90 |
|
|
238
|
+
| `selfharm` | the author is in crisis or considering self-harm; alert moderators, never punish | flag ≥ 0.80 |
|
|
239
|
+
| `doxxing` | reveals or hunts private data about a real person | flag ≥ 0.80 |
|
|
240
|
+
| `minors` | sexualises a minor or shows grooming behaviour | flag ≥ 0.70 |
|
|
241
|
+
| `rule:<name>` | your rule in plain language, exceptions included, up to 5 | flag ≥ 0.80 |
|
|
242
|
+
|
|
243
|
+
Every check returns all enabled categories at once, in one request. The questions are in
|
|
244
|
+
[`jevmod/categories.json`](jevmod/categories.json): one yes/no question per category with explicit true/false
|
|
245
|
+
criteria, the pattern of TypeSafe's guardrails cookbook. Jev's probabilities move about ±0.03 between runs, so
|
|
246
|
+
anything within that band of a threshold will flip; the ❌/✅ feedback and `PUT /v1/policy` exist to move the line.
|
|
247
|
+
|
|
248
|
+
## How it works
|
|
249
|
+
|
|
250
|
+

|
|
251
|
+
|
|
252
|
+
Message → pre-filter (trusted authors, under eight letters without a link, repeats of judged text never reach
|
|
253
|
+
Jev) → batch for 2 s per community → **one** Jev request for the batch → probabilities → policy → action → audit
|
|
254
|
+
log. Batched messages are sent as a dict keyed by position; as a list, probabilities leaked between neighbours,
|
|
255
|
+
which the 98-message adversarial [red team](tests/data/redteam.csv) caught and which now runs as a regression
|
|
256
|
+
suite in CI. More in [docs/diagrams/](docs/diagrams) and [PLAN.md](PLAN.md).
|
|
257
|
+
|
|
258
|
+
## Self-host
|
|
259
|
+
|
|
260
|
+
One image, one variable picks the role: `api`, `discord`, `telegram`, `reddit`. SQLite on a volume.
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
cp .env.example .env && docker compose up -d # API on :8080
|
|
264
|
+
docker compose --profile discord up -d # add the Discord bot
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A $4/month VM, Fly.io or Railway with a volume is enough. Failure policy: Jev unreachable → decisions come back
|
|
268
|
+
`reason="error_open"` and nothing is acted on. There is no quota by default; `JEVMOD_MONTHLY_QUOTA=5000` pauses
|
|
269
|
+
judging for a tenant after 5,000 judged messages in a month as a cost guard, tells the owner once, deletes nothing.
|
|
270
|
+
|
|
271
|
+
## Public demo
|
|
272
|
+
|
|
273
|
+
The landing page's live check talks to `jevmod demo`, a separate role that keeps the key on the server and stops
|
|
274
|
+
at a monthly budget (`JEVMOD_DEMO_BUDGET_USD`, default $0.50), with per-visitor limits and a CORS allow-list. It
|
|
275
|
+
logs what visitors try (text, scores, hashed IP) for the operator. `deploy/demo/` has a Caddy + Docker compose
|
|
276
|
+
for a small VPS with HTTPS in two commands.
|
|
277
|
+
|
|
278
|
+
## Development
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
git clone https://github.com/ohernandezdev/jevmod && cd jevmod
|
|
282
|
+
python -m venv .venv && .venv/bin/pip install -e ".[all,dev,examples]" # Windows: .venv\Scripts\pip
|
|
283
|
+
ruff check . && mypy jevmod && pytest # offline tests run without a key; the rest hit the real API
|
|
284
|
+
cd packages/jevmod-js && npm ci && npm test # same for the npm package
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Tests never mock Jev. `tests/test_redteam.py` is the adversarial regression set; `benchmark/` reproduces
|
|
288
|
+
[BENCHMARK.md](BENCHMARK.md).
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT, © Omar Hernandez. See [DISCLAIMER.md](DISCLAIMER.md): decisions are probabilistic, the operator owns the
|
|
293
|
+
thresholds, the actions and legal compliance; not affiliated with TypeSafe, Discord, Telegram or Reddit.
|
jevmod-0.2.0/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# jevmod
|
|
2
|
+
|
|
3
|
+
Moderation for communities and apps: every message gets a probability for **spam, scam, harassment, nsfw,
|
|
4
|
+
off-topic, self-harm, doxxing, sexual content involving minors**, and for **rules you write in plain English**.
|
|
5
|
+
You set the thresholds and the actions. Every decision is logged with its numbers.
|
|
6
|
+
|
|
7
|
+
It runs on [Jev](https://typesafe.ai), TypeSafe's System One model: you ask yes/no questions about a message and
|
|
8
|
+
get probabilities back, no text generation. About **$0.04 per 1,000 messages** with all categories on.
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
$ jevmod check "FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"
|
|
14
|
+
scam 0.99 'FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro' [scam 0.99, spam 0.98, harassment 0.02]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Flag-only by default: nothing is deleted until you turn that on. Fails open: if Jev is unreachable, messages are
|
|
18
|
+
left alone and the failure is logged. Self-harm is flag-only by design so a moderator can reach out.
|
|
19
|
+
|
|
20
|
+
| you are | you get | start |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| a community owner, not technical | a Discord bot you tune with commands (Telegram with fewer commands, Reddit by env vars) | [Run the bot](#run-the-bot) |
|
|
23
|
+
| a developer | a CLI, a Python package, an npm package, or one HTTP call | [Developer](#developer) |
|
|
24
|
+
| a coding agent, or someone using one | an MCP server and a Claude Code skill that wires jevmod into a codebase | [Agents](#agents) |
|
|
25
|
+
|
|
26
|
+
On the [benchmark](BENCHMARK.md) (2,531 messages from OpenAI's moderation eval, Jigsaw and YouTube spam) jevmod
|
|
27
|
+
had the best AUROC in every category it was compared on in OpenAI's human-labelled set: harassment 0.93 and
|
|
28
|
+
sexual 0.98 against Llama Guard 3 8B, ShieldGemma 2B and toxic-bert; self-harm 0.99 and minors 0.98 against Llama
|
|
29
|
+
Guard, the only other system with those labels. Calibration was measured too; caveats are in the same file.
|
|
30
|
+
|
|
31
|
+
## The key, once
|
|
32
|
+
|
|
33
|
+
Every surface needs a TypeSafe API key (free tier at [console.typesafe.ai](https://console.typesafe.ai)).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install "jevmod[keys]"
|
|
37
|
+
jevmod init # asks for the key without echo, verifies it with one call, stores it in the OS keyring
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Resolution order everywhere (CLI, API, bots, MCP, `Moderator`): `TYPESAFE_API_KEY` in the environment, then the
|
|
41
|
+
keyring, then a `.env` in the current directory. In a container or a headless server `jevmod init --env-file`
|
|
42
|
+
writes `.env` with mode 600. `jevmod init --forget` removes the keyring entry (`pip uninstall` does not). Keys never
|
|
43
|
+
go into git; `.env` is ignored.
|
|
44
|
+
|
|
45
|
+
## Run the bot
|
|
46
|
+
|
|
47
|
+
### Discord
|
|
48
|
+
|
|
49
|
+
1. [Developer Portal](https://discord.com/developers/applications) → New Application → Bot → **Reset Token** →
|
|
50
|
+
enable **Message Content Intent** (the only privileged intent used).
|
|
51
|
+
2. OAuth2 → URL Generator → scopes `bot` + `applications.commands`; permissions: Read Messages, Send Messages,
|
|
52
|
+
Manage Messages, Moderate Members, Manage Channels, Embed Links, Add Reactions. Open the URL, add it to your server.
|
|
53
|
+
3. `pip install "jevmod[discord]"`, set `DISCORD_TOKEN`, run `jevmod discord`.
|
|
54
|
+
|
|
55
|
+
The bot creates a private `#jevmod-log` channel and starts flagging there.
|
|
56
|
+
|
|
57
|
+
| command (server managers only) | what |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `/mod status` | settings and this month's usage |
|
|
60
|
+
| `/mod set <category> <action> [threshold]` | any category → `off`, `flag`, `delete`, `timeout` |
|
|
61
|
+
| `/mod rule <name> <text> [action] [threshold]` | a rule in your words: "No politics. News about the game is fine." (max 5) |
|
|
62
|
+
| `/mod trust <role>` | messages from that role are never judged |
|
|
63
|
+
| `/mod topic <text>` | what the current channel is for (turns on `offtopic`) |
|
|
64
|
+
| `/mod log`, `/mod recent` | choose the log channel; last decisions with probabilities |
|
|
65
|
+
| `/mod forget`, `/mod forget_user @member` | delete everything stored about the server, or one member |
|
|
66
|
+
|
|
67
|
+
React ❌ on a log entry to mark a false positive (that category's threshold goes up a notch), ✅ to confirm a
|
|
68
|
+
correct call (down a notch, floor 0.5).
|
|
69
|
+
|
|
70
|
+
### Telegram and Reddit
|
|
71
|
+
|
|
72
|
+
Telegram: [@BotFather](https://t.me/BotFather) → `/newbot`, make the bot a group admin, `pip install
|
|
73
|
+
"jevmod[telegram]"`, set `TELEGRAM_TOKEN`, run `jevmod telegram`. Admin commands: `/mod_status`, `/mod_set`,
|
|
74
|
+
`/mod_rule`, `/mod_topic`, `/mod_log`.
|
|
75
|
+
|
|
76
|
+
Reddit: for your own subreddit with your own "script" app credentials, non-commercial (Reddit's API terms).
|
|
77
|
+
`pip install "jevmod[reddit]"`, fill the `REDDIT_*` variables from `.env.example`, run `jevmod reddit`. Reports by
|
|
78
|
+
default; removal and bans are opt-in.
|
|
79
|
+
|
|
80
|
+
### What the bots send where
|
|
81
|
+
|
|
82
|
+
Only the **message text** and the **channel topic** go to TypeSafe. Author names and ids never do. Locally,
|
|
83
|
+
jevmod keeps a decision log (category, probabilities, action, first 300 characters); rows older than 30 days are
|
|
84
|
+
purged on every batch.
|
|
85
|
+
`/mod forget` deletes everything; leaving the server does the same. Members whose message is removed get a direct
|
|
86
|
+
message saying an automated system did it and how to appeal.
|
|
87
|
+
|
|
88
|
+
## Developer
|
|
89
|
+
|
|
90
|
+
### CLI
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install jevmod
|
|
94
|
+
jevmod check "some text" # exit 0 clean, 1 something triggered, 2 error
|
|
95
|
+
cat comments.txt | jevmod check --json --rule "No politics. Game news is fine." -
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
One message per line on stdin, one Jev request per 50. `--topic` turns on the off-topic check, `--threshold` sets
|
|
99
|
+
one for every category, `--json` prints one object per line with every probability. `examples/cli/` has a file
|
|
100
|
+
screener and a pre-commit hook.
|
|
101
|
+
|
|
102
|
+
### Python
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from jevmod import Moderator, Policy
|
|
106
|
+
|
|
107
|
+
d = Moderator().check("FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro", channel_topic="gaming")
|
|
108
|
+
d.action, d.category, d.probability # ('flag', 'scam', 0.99)
|
|
109
|
+
d.scores # {'spam': 0.98, 'scam': 0.99, 'harassment': 0.02, 'nsfw': 0.01, ...}
|
|
110
|
+
|
|
111
|
+
p = Policy()
|
|
112
|
+
p.set_category("scam", "delete", 0.7)
|
|
113
|
+
p.set_rule("no_politics", "No political discussion. Game news is fine.", action="flag", threshold=0.8)
|
|
114
|
+
Moderator(policy=p).check_many(["...", "..."], channel_topic="support") # one request for the batch
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### npm
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { Moderator, Policy } from "jevmod";
|
|
121
|
+
const d = await new Moderator().check("FREE NITRO ...", { channelTopic: "gaming" });
|
|
122
|
+
d.action, d.category, d.scores // same shape as Python and the HTTP API
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Node 20+. Same questions (`jevmod/categories.json` is copied byte for byte and CI fails if it drifts), same
|
|
126
|
+
policy, same decision. `JevmodClient` talks to a deployed HTTP API instead, so browsers and edge functions never
|
|
127
|
+
hold the TypeSafe key. Express middleware in `packages/jevmod-js/examples/`. Details in
|
|
128
|
+
[packages/jevmod-js/README.md](packages/jevmod-js/README.md).
|
|
129
|
+
|
|
130
|
+
### HTTP API
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
JEVMOD_ADMIN_TOKEN=... jevmod api # or: docker compose up -d
|
|
134
|
+
curl -X POST localhost:8080/v1/keys -H "Authorization: Bearer $JEVMOD_ADMIN_TOKEN" \
|
|
135
|
+
-H "Content-Type: application/json" -d '{"tenant":"my-app"}' # {"api_key":"jm_...", shown once}
|
|
136
|
+
curl -X POST localhost:8080/v1/moderate -H "Authorization: Bearer jm_..." -H "Content-Type: application/json" \
|
|
137
|
+
-d '{"messages":[{"id":"a","text":"FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"}]}'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
| endpoint | what |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `POST /v1/moderate` | up to 50 messages → decisions; the `X-Request-Id` you send comes back as `request_id` and as a response header |
|
|
143
|
+
| `GET/PUT /v1/policy` | thresholds, actions, rules for this tenant |
|
|
144
|
+
| `GET /v1/decisions` | the audit log |
|
|
145
|
+
| `DELETE /v1/tenant` | forget this tenant |
|
|
146
|
+
| `POST /v1/keys` (admin) | mint a tenant key, stored hashed |
|
|
147
|
+
| `GET /v1/health`, `GET /metrics` | liveness, Prometheus counters |
|
|
148
|
+
|
|
149
|
+
OpenAPI at `/docs`, a Postman collection in `postman/`. Any chatbot, forum or comment system that can make an HTTP
|
|
150
|
+
call can use it; the bots are adapters over the same service.
|
|
151
|
+
|
|
152
|
+
### Examples, one folder per surface
|
|
153
|
+
|
|
154
|
+
| folder | what |
|
|
155
|
+
|---|---|
|
|
156
|
+
| [`examples/sdk/`](examples/sdk) | `Moderator` basics, a custom policy with a rule, batching 50 per request |
|
|
157
|
+
| [`examples/cli/`](examples/cli) | `screen_file.sh` exits 1 on hits; `pre-commit.sh` blocks flagged text files |
|
|
158
|
+
| [`examples/api/`](examples/api) | curl, a stdlib Python client, a Node client against a local `jevmod api` |
|
|
159
|
+
| [`examples/discord/`](examples/discord) | run the bot; `custom_adapter.py` puts any chat platform on `ModerationService` in 20 lines |
|
|
160
|
+
| [`examples/agent_harness/`](examples/agent_harness) | `@guarded` decorator, Claude Agent SDK `PreToolUse`/`PostToolUse` hooks, LangChain callback |
|
|
161
|
+
| [`examples/input_validation/`](examples/input_validation) | FastAPI dependency that answers 422, a pydantic `ModeratedText` field |
|
|
162
|
+
| [`packages/jevmod-js/examples/`](packages/jevmod-js/examples) | Express middleware and a plain Node script |
|
|
163
|
+
|
|
164
|
+
## Agents
|
|
165
|
+
|
|
166
|
+
**MCP server.** `pip install "jevmod[mcp]"` then `jevmod mcp` (stdio). Tools: `moderate(texts, channel_topic?,
|
|
167
|
+
rules?)` returns one decision per text; `categories()` describes each category and its default threshold.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
claude mcp add jevmod -- jevmod mcp # Claude Code
|
|
171
|
+
{"mcpServers": {"jevmod": {"command": "jevmod", "args": ["mcp"]}}} # Cursor, Codex, others
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Claude Code plugin.** This repository is its own marketplace:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
/plugin marketplace add ohernandezdev/jevmod
|
|
178
|
+
/plugin install jevmod@jevmod
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`jevmod-integrate` adds moderation to an existing codebase (detects the stack, picks SDK/npm/HTTP/MCP, wires the
|
|
182
|
+
key, inserts the call where it belongs, adds a real test). `jevmod-moderate` screens text or datasets from the
|
|
183
|
+
terminal while working. [AGENTS.md](AGENTS.md) has every signature; [docs/llms.txt](docs/llms.txt) indexes the docs.
|
|
184
|
+
|
|
185
|
+
## Categories
|
|
186
|
+
|
|
187
|
+
| category | true when | default |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `spam` | unsolicited promotion, invite farming, bare link drops, mass mentions | flag ≥ 0.85 |
|
|
190
|
+
| `scam` | fake giveaways, phishing domains, impersonated support, "DM me for a deal" | flag ≥ 0.75 |
|
|
191
|
+
| `harassment` | insults, slurs, threats, targeted abuse, in any language | flag ≥ 0.75 |
|
|
192
|
+
| `nsfw` | sexual or gore content for a general audience (below the threshold means SFW) | flag ≥ 0.80 |
|
|
193
|
+
| `offtopic` | unrelated to `channel_topic`; needs a topic to mean anything | off, 0.90 |
|
|
194
|
+
| `selfharm` | the author is in crisis or considering self-harm; alert moderators, never punish | flag ≥ 0.80 |
|
|
195
|
+
| `doxxing` | reveals or hunts private data about a real person | flag ≥ 0.80 |
|
|
196
|
+
| `minors` | sexualises a minor or shows grooming behaviour | flag ≥ 0.70 |
|
|
197
|
+
| `rule:<name>` | your rule in plain language, exceptions included, up to 5 | flag ≥ 0.80 |
|
|
198
|
+
|
|
199
|
+
Every check returns all enabled categories at once, in one request. The questions are in
|
|
200
|
+
[`jevmod/categories.json`](jevmod/categories.json): one yes/no question per category with explicit true/false
|
|
201
|
+
criteria, the pattern of TypeSafe's guardrails cookbook. Jev's probabilities move about ±0.03 between runs, so
|
|
202
|
+
anything within that band of a threshold will flip; the ❌/✅ feedback and `PUT /v1/policy` exist to move the line.
|
|
203
|
+
|
|
204
|
+
## How it works
|
|
205
|
+
|
|
206
|
+

|
|
207
|
+
|
|
208
|
+
Message → pre-filter (trusted authors, under eight letters without a link, repeats of judged text never reach
|
|
209
|
+
Jev) → batch for 2 s per community → **one** Jev request for the batch → probabilities → policy → action → audit
|
|
210
|
+
log. Batched messages are sent as a dict keyed by position; as a list, probabilities leaked between neighbours,
|
|
211
|
+
which the 98-message adversarial [red team](tests/data/redteam.csv) caught and which now runs as a regression
|
|
212
|
+
suite in CI. More in [docs/diagrams/](docs/diagrams) and [PLAN.md](PLAN.md).
|
|
213
|
+
|
|
214
|
+
## Self-host
|
|
215
|
+
|
|
216
|
+
One image, one variable picks the role: `api`, `discord`, `telegram`, `reddit`. SQLite on a volume.
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
cp .env.example .env && docker compose up -d # API on :8080
|
|
220
|
+
docker compose --profile discord up -d # add the Discord bot
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
A $4/month VM, Fly.io or Railway with a volume is enough. Failure policy: Jev unreachable → decisions come back
|
|
224
|
+
`reason="error_open"` and nothing is acted on. There is no quota by default; `JEVMOD_MONTHLY_QUOTA=5000` pauses
|
|
225
|
+
judging for a tenant after 5,000 judged messages in a month as a cost guard, tells the owner once, deletes nothing.
|
|
226
|
+
|
|
227
|
+
## Public demo
|
|
228
|
+
|
|
229
|
+
The landing page's live check talks to `jevmod demo`, a separate role that keeps the key on the server and stops
|
|
230
|
+
at a monthly budget (`JEVMOD_DEMO_BUDGET_USD`, default $0.50), with per-visitor limits and a CORS allow-list. It
|
|
231
|
+
logs what visitors try (text, scores, hashed IP) for the operator. `deploy/demo/` has a Caddy + Docker compose
|
|
232
|
+
for a small VPS with HTTPS in two commands.
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
git clone https://github.com/ohernandezdev/jevmod && cd jevmod
|
|
238
|
+
python -m venv .venv && .venv/bin/pip install -e ".[all,dev,examples]" # Windows: .venv\Scripts\pip
|
|
239
|
+
ruff check . && mypy jevmod && pytest # offline tests run without a key; the rest hit the real API
|
|
240
|
+
cd packages/jevmod-js && npm ci && npm test # same for the npm package
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Tests never mock Jev. `tests/test_redteam.py` is the adversarial regression set; `benchmark/` reproduces
|
|
244
|
+
[BENCHMARK.md](BENCHMARK.md).
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT, © Omar Hernandez. See [DISCLAIMER.md](DISCLAIMER.md): decisions are probabilistic, the operator owns the
|
|
249
|
+
thresholds, the actions and legal compliance; not affiliated with TypeSafe, Discord, Telegram or Reddit.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""jevmod: moderation for communities and apps, powered by Jev.
|
|
2
|
+
|
|
3
|
+
Developer API, three lines:
|
|
4
|
+
|
|
5
|
+
from jevmod import Moderator
|
|
6
|
+
mod = Moderator() # TYPESAFE_API_KEY in the environment
|
|
7
|
+
d = mod.check("FREE NITRO click discord-gifts.ru") # -> Decision(action="flag", category="scam", probability=0.97)
|
|
8
|
+
|
|
9
|
+
`check_many([...])` judges a batch in one request. Thresholds and actions come from a `Policy` you can pass in.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
from .core import ACTIONS, Decision, ModerationService, Policy, Store, decide
|
|
17
|
+
from .judge import CATEGORIES, Judge, Message, Verdict
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ACTIONS",
|
|
21
|
+
"CATEGORIES",
|
|
22
|
+
"Decision",
|
|
23
|
+
"Judge",
|
|
24
|
+
"Message",
|
|
25
|
+
"ModerationService",
|
|
26
|
+
"Moderator",
|
|
27
|
+
"Policy",
|
|
28
|
+
"Store",
|
|
29
|
+
"Verdict",
|
|
30
|
+
"decide",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from importlib.metadata import version as _v
|
|
35
|
+
|
|
36
|
+
__version__ = _v("jevmod")
|
|
37
|
+
except Exception: # pragma: no cover
|
|
38
|
+
__version__ = "0.0.0+local"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Moderator:
|
|
42
|
+
"""Stateless convenience wrapper for developers: no SQLite, no tenants, just judge + policy."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, policy: Policy | None = None, judge: Judge | None = None) -> None:
|
|
45
|
+
self.policy = policy or Policy()
|
|
46
|
+
self.judge = judge or Judge()
|
|
47
|
+
|
|
48
|
+
def check(self, text: str, *, author: str = "", channel_topic: str = "", author_trusted: bool = False) -> Decision:
|
|
49
|
+
return self.check_many([text], author=author, channel_topic=channel_topic, author_trusted=author_trusted)[0]
|
|
50
|
+
|
|
51
|
+
def check_many(
|
|
52
|
+
self,
|
|
53
|
+
texts: Sequence[str],
|
|
54
|
+
*,
|
|
55
|
+
author: str = "",
|
|
56
|
+
channel_topic: str = "",
|
|
57
|
+
author_trusted: bool = False,
|
|
58
|
+
ids: Sequence[str] | None = None,
|
|
59
|
+
) -> list[Decision]:
|
|
60
|
+
msgs = [
|
|
61
|
+
Message(
|
|
62
|
+
ids[i] if ids else str(i), t, author=author, channel_topic=channel_topic, author_trusted=author_trusted
|
|
63
|
+
)
|
|
64
|
+
for i, t in enumerate(texts)
|
|
65
|
+
]
|
|
66
|
+
verdicts = self.judge.judge(msgs, self.policy.enabled_categories(), self.policy.rules)
|
|
67
|
+
return [decide(self.policy, v) for v in verdicts]
|