llm-entropy-filter 1.0.1 → 1.1.0
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.
- package/CHANGELOG.md +51 -0
- package/README.md +255 -98
- package/integrations/express.mjs +117 -0
- package/integrations/fastify.mjs +106 -0
- package/integrations/langchain.mjs +98 -0
- package/integrations/vercel-ai-sdk.mjs +44 -0
- package/package.json +37 -42
- package/rulesets/default.json +73 -0
- package/rulesets/public-api.json +27 -0
- package/rulesets/schema +24 -0
- package/rulesets/strict.json +25 -0
- package/rulesets/support.json +22 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on semantic versioning principles.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [1.1.0] - 2026-01-28
|
|
10
|
+
|
|
11
|
+
### 🚀 Added
|
|
12
|
+
|
|
13
|
+
- Introduced formal **ruleset architecture** (`default`, `strict`, `support`, `public-api`).
|
|
14
|
+
- Added `rulesets/` directory for configurable entropy presets.
|
|
15
|
+
- Added integration examples:
|
|
16
|
+
- Express middleware
|
|
17
|
+
- Fastify plugin
|
|
18
|
+
- Vercel AI SDK pre-gate wrapper
|
|
19
|
+
- Added reproducible benchmark scripts for spam dataset evaluation.
|
|
20
|
+
- Added economic & performance impact documentation.
|
|
21
|
+
- Added stability & hallucination mitigation section in README.
|
|
22
|
+
- Added production-readiness checklist.
|
|
23
|
+
|
|
24
|
+
### 🧪 Bench & Metrics
|
|
25
|
+
|
|
26
|
+
- Included reproducible SMS spam dataset benchmarking.
|
|
27
|
+
- Added support for generating precision / recall style reports.
|
|
28
|
+
- Added structured telemetry output for integration logging.
|
|
29
|
+
|
|
30
|
+
### 🛠 Internal
|
|
31
|
+
|
|
32
|
+
- No changes to core `gate()` logic.
|
|
33
|
+
- No breaking changes to public API.
|
|
34
|
+
- Existing behavior remains the default under `ruleset: "default"`.
|
|
35
|
+
|
|
36
|
+
### ⚠️ Breaking Changes
|
|
37
|
+
|
|
38
|
+
None.
|
|
39
|
+
|
|
40
|
+
This release focuses on infrastructure packaging, documentation clarity, and integration readiness without altering the deterministic entropy engine.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## [1.0.1] - 2026-01-27
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- Initial demo server (`/analyze`, `/triad`)
|
|
49
|
+
- Deterministic entropy scoring
|
|
50
|
+
- ALLOW / WARN / BLOCK verdict structure
|
|
51
|
+
- Performance benchmark documentation
|
package/README.md
CHANGED
|
@@ -1,195 +1,352 @@
|
|
|
1
|
-
|
|
1
|
+
# llm-entropy-filter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/llm-entropy-filter)
|
|
4
|
+
[](LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Minimal, fast **entropy + intent gate** for LLM inputs.
|
|
6
7
|
|
|
7
|
-
,
|
|
8
|
+
`llm-entropy-filter` is a deterministic, local middleware layer that filters high-entropy / low-signal inputs before they reach expensive LLM inference.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
It transforms your LLM from a generic processor into a **premium signal resource**.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
---
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
# 🚀 Why this exists
|
|
15
|
+
|
|
16
|
+
LLMs are powerful but:
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
- Expensive per token
|
|
19
|
+
- Latency-heavy (seconds vs milliseconds)
|
|
20
|
+
- Vulnerable to spam, coercion, broken causality, and noise
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
Most systems solve this with *more processing*.
|
|
23
|
+
|
|
24
|
+
`llm-entropy-filter` solves it with **criterion before processing**.
|
|
18
25
|
|
|
19
26
|
---
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
# 🧠 Architecture
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
- `gate(text)` → `{ action, entropy_score, flags, intention, confidence, rationale }`
|
|
25
|
-
- `gateLLM(text)` → alias of `gate(text)` (kept for compatibility)
|
|
26
|
-
- `runEntropyFilter(text)` → underlying entropy + intention analysis utilities
|
|
30
|
+
The system operates in two deterministic local layers:
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
- `POST /analyze` → runs local `gate(text)` + returns `meta.ts` + `meta.version`
|
|
30
|
-
- `POST /triad` → optional OpenAI analysis (only if `OPENAI_API_KEY` is set)
|
|
32
|
+
## Layer 1 — Hard Triggers (Deterministic Signals)
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
Immediate structural patterns:
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
- Shouting (ALL CAPS)
|
|
37
|
+
- Urgency markers
|
|
38
|
+
- Money / % signals
|
|
39
|
+
- Spam phrasing
|
|
40
|
+
- Conspiracy vagueness
|
|
41
|
+
- Broken causality structures
|
|
42
|
+
- Repetition anomalies
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
npm i llm-entropy-filter
|
|
44
|
+
These are language-light, low-cost, and capture obvious noise.
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
## Layer 2 — Thematic Scoring (Signal Accumulation)
|
|
47
|
+
|
|
48
|
+
If no hard block occurs, the input is evaluated by topic clusters:
|
|
49
|
+
|
|
50
|
+
- Marketing spam
|
|
51
|
+
- Conspiracy framing
|
|
52
|
+
- Coercive tone
|
|
53
|
+
- Pseudo-scientific structure
|
|
54
|
+
- Relativism / truth dilution
|
|
55
|
+
- Semantic incoherence
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
console.log(r);
|
|
57
|
+
Each topic contributes to an `entropy_score`.
|
|
44
58
|
|
|
59
|
+
Final verdict:
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
ALLOW | WARN | BLOCK
|
|
47
62
|
|
|
63
|
+
|
|
64
|
+
Returned with:
|
|
65
|
+
|
|
66
|
+
```json
|
|
48
67
|
{
|
|
49
68
|
"action": "BLOCK",
|
|
50
69
|
"entropy_score": 0.7,
|
|
51
|
-
"flags": [
|
|
52
|
-
"intention": "
|
|
70
|
+
"flags": [...],
|
|
71
|
+
"intention": "...",
|
|
53
72
|
"confidence": 0.85,
|
|
54
|
-
"rationale": "
|
|
73
|
+
"rationale": "..."
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
Demo server (Express)
|
|
58
76
|
|
|
59
|
-
|
|
77
|
+
No network calls. No embeddings. No remote inference.
|
|
60
78
|
|
|
61
|
-
|
|
79
|
+
## Rulesets
|
|
62
80
|
|
|
81
|
+
This project ships with preset rule packs:
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
- `default` (balanced)
|
|
84
|
+
- `strict` (aggressive blocking)
|
|
85
|
+
- `support` (fewer false positives)
|
|
86
|
+
- `public-api` (hardened for open endpoints)
|
|
65
87
|
|
|
66
|
-
|
|
88
|
+
Rulesets live in `rulesets/` and define:
|
|
89
|
+
- thresholds (WARN/BLOCK)
|
|
90
|
+
- hard triggers
|
|
91
|
+
- topic scoring weights
|
|
67
92
|
|
|
93
|
+
## Integrations (copy/paste)
|
|
68
94
|
|
|
69
|
-
|
|
95
|
+
This repo includes ready-to-use adapters under `integrations/`:
|
|
70
96
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
- `integrations/express.mjs` — Express middleware gate (ALLOW/WARN/BLOCK)
|
|
98
|
+
- `integrations/fastify.mjs` — Fastify plugin gate
|
|
99
|
+
- `integrations/vercel-ai-sdk.mjs` — pre-gate wrapper for `streamText()` / `generateText()`
|
|
100
|
+
- `integrations/langchain.mjs` — pre-gate + optional Runnable wrapper for LangChain
|
|
101
|
+
|
|
102
|
+
These integrations do **not** change core behavior. They only call `gate()` and route based on the verdict.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
📦 Installation
|
|
106
|
+
npm i llm-entropy-filter
|
|
107
|
+
|
|
108
|
+
⚡ Quickstart
|
|
109
|
+
import { gate } from "llm-entropy-filter";
|
|
74
110
|
|
|
111
|
+
const result = gate("¡¡COMPRA YA!! Oferta limitada 90% OFF $$$");
|
|
75
112
|
|
|
76
|
-
|
|
113
|
+
console.log(result);
|
|
77
114
|
|
|
78
|
-
|
|
115
|
+
🖥 Demo Server
|
|
79
116
|
|
|
80
|
-
|
|
117
|
+
The demo server wraps the local gate.
|
|
118
|
+
|
|
119
|
+
Start
|
|
120
|
+
npm run serve
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
(Ensure your package.json includes: "serve": "node demo/server.mjs")
|
|
124
|
+
|
|
125
|
+
Health
|
|
126
|
+
curl http://127.0.0.1:3000/health
|
|
127
|
+
|
|
128
|
+
Local gate
|
|
129
|
+
curl -X POST http://127.0.0.1:3000/analyze \
|
|
130
|
+
-H "Content-Type: application/json" \
|
|
131
|
+
-d '{"text":"FREE iPhone!!! Click now!!!"}'
|
|
132
|
+
|
|
133
|
+
Optional LLM Triad (Demo Only)
|
|
81
134
|
export OPENAI_API_KEY="YOUR_KEY"
|
|
82
135
|
export OPENAI_MODEL="gpt-4.1-mini"
|
|
83
136
|
|
|
84
|
-
curl -
|
|
137
|
+
curl -X POST http://127.0.0.1:3000/triad \
|
|
85
138
|
-H "Content-Type: application/json" \
|
|
86
|
-
-d '{"text":"Vivimos en una simulación y todos lo esconden."}'
|
|
139
|
+
-d '{"text":"Vivimos en una simulación y todos lo esconden."}'
|
|
87
140
|
|
|
88
141
|
|
|
89
|
-
|
|
142
|
+
If OPENAI_API_KEY is not set, /triad returns 503.
|
|
90
143
|
|
|
91
|
-
|
|
92
|
-
HTTP gate /analyze (local, deterministic)
|
|
144
|
+
⚡ Performance (Measured)
|
|
93
145
|
|
|
94
|
-
|
|
146
|
+
Environment:
|
|
147
|
+
GitHub Codespaces (Linux container), Node 24.x
|
|
95
148
|
|
|
96
|
-
|
|
97
|
-
http://127.0.0.1:3000/analyze \
|
|
98
|
-
-H "Content-Type: application/json" \
|
|
99
|
-
-b '{"text":"Congratulations. You won a FREE iPhone. Click here to claim now."}'
|
|
149
|
+
Local Gate — /analyze
|
|
100
150
|
|
|
151
|
+
Avg latency: 5.28 ms
|
|
101
152
|
|
|
102
|
-
|
|
153
|
+
p50: 4 ms
|
|
103
154
|
|
|
104
|
-
|
|
155
|
+
p99: 16 ms
|
|
105
156
|
|
|
106
|
-
~5
|
|
157
|
+
Throughput: ~5,118 req/sec
|
|
107
158
|
|
|
108
|
-
|
|
159
|
+
0 errors
|
|
109
160
|
|
|
110
|
-
|
|
161
|
+
LLM Roundtrip — /triad
|
|
111
162
|
|
|
112
|
-
|
|
113
|
-
http://127.0.0.1:3000/triad \
|
|
114
|
-
-H "Content-Type: application/json" \
|
|
115
|
-
-b '{"text":"Texto real de prueba (1-3 párrafos) ..."}'
|
|
163
|
+
Avg latency: 5,321 ms
|
|
116
164
|
|
|
165
|
+
p50: 5,030 ms
|
|
117
166
|
|
|
118
|
-
|
|
167
|
+
Throughput: ~0.34 req/sec
|
|
119
168
|
|
|
120
|
-
|
|
169
|
+
2 timeouts in 30s test
|
|
121
170
|
|
|
122
|
-
|
|
171
|
+
Note: These represent different pipeline layers (local deterministic vs external LLM API). The architectural gain comes from avoiding unnecessary LLM calls.
|
|
123
172
|
|
|
124
|
-
|
|
173
|
+
📉 Economic Impact (Projection)
|
|
174
|
+
Assumptions
|
|
125
175
|
|
|
126
|
-
|
|
176
|
+
300 tokens per request (150 in / 150 out)
|
|
127
177
|
|
|
128
|
-
|
|
178
|
+
gpt-4o-mini pricing baseline
|
|
129
179
|
|
|
130
|
-
|
|
180
|
+
30% traffic filtered locally
|
|
131
181
|
|
|
132
|
-
|
|
133
|
-
cat bench/reports/sms_spam_report.md
|
|
182
|
+
Effect
|
|
134
183
|
|
|
184
|
+
If 1M requests are received:
|
|
135
185
|
|
|
136
|
-
|
|
186
|
+
300,000 requests never hit the LLM
|
|
137
187
|
|
|
138
|
-
|
|
188
|
+
30% token cost avoided
|
|
139
189
|
|
|
140
|
-
|
|
190
|
+
30% rate-limit headroom gained
|
|
141
191
|
|
|
142
|
-
|
|
192
|
+
30% reduction in latency pressure
|
|
143
193
|
|
|
144
|
-
|
|
194
|
+
Savings scale linearly with volume and exponentially with higher-cost models.
|
|
145
195
|
|
|
146
|
-
|
|
196
|
+
Formula:
|
|
147
197
|
|
|
148
|
-
|
|
198
|
+
Savings =
|
|
199
|
+
(Filtered_Requests / Total_Requests)
|
|
200
|
+
× Avg_Tokens_Per_Request
|
|
201
|
+
× Token_Price
|
|
149
202
|
|
|
150
|
-
|
|
203
|
+
🛡 Stability & Hallucination Mitigation
|
|
151
204
|
|
|
152
|
-
|
|
205
|
+
High-entropy inputs increase:
|
|
153
206
|
|
|
154
|
-
|
|
207
|
+
Off-topic generation
|
|
155
208
|
|
|
156
|
-
|
|
209
|
+
Reasoning drift
|
|
157
210
|
|
|
158
|
-
|
|
211
|
+
Prompt injection exposure
|
|
159
212
|
|
|
160
|
-
|
|
213
|
+
Token expansion loops
|
|
161
214
|
|
|
162
|
-
|
|
215
|
+
By constraining input entropy before inference,
|
|
216
|
+
the downstream model operates in a narrower semantic bandwidth.
|
|
163
217
|
|
|
164
|
-
|
|
218
|
+
This improves stability without imposing moral or ideological constraints.
|
|
165
219
|
|
|
166
|
-
|
|
220
|
+
🧪 Dataset Benchmark
|
|
167
221
|
|
|
168
|
-
|
|
222
|
+
Included:
|
|
169
223
|
|
|
170
|
-
|
|
224
|
+
bench/sms_spam.csv
|
|
171
225
|
|
|
172
|
-
npm i llm-entropy-filter
|
|
173
226
|
|
|
227
|
+
Run:
|
|
174
228
|
|
|
175
|
-
|
|
229
|
+
node bench/sms_spam_bench.mjs bench/sms_spam.csv
|
|
176
230
|
|
|
177
|
-
import { gate } from "llm-entropy-filter";
|
|
178
|
-
console.log(gate("Congratulations. You won a FREE iPhone. Click here."));
|
|
179
231
|
|
|
232
|
+
Generates:
|
|
180
233
|
|
|
181
|
-
|
|
234
|
+
Precision / recall
|
|
182
235
|
|
|
183
|
-
|
|
236
|
+
Confusion matrix
|
|
184
237
|
|
|
185
|
-
|
|
238
|
+
Top flags
|
|
186
239
|
|
|
187
|
-
|
|
240
|
+
JSON + Markdown reports
|
|
188
241
|
|
|
189
|
-
|
|
242
|
+
🎯 Design Goals
|
|
190
243
|
|
|
191
|
-
|
|
244
|
+
Deterministic
|
|
192
245
|
|
|
246
|
+
Transparent
|
|
193
247
|
|
|
194
|
-
|
|
248
|
+
Fast
|
|
249
|
+
|
|
250
|
+
Composable
|
|
251
|
+
|
|
252
|
+
Observable
|
|
253
|
+
|
|
254
|
+
Economically rational
|
|
255
|
+
|
|
256
|
+
🗺 Roadmap
|
|
257
|
+
|
|
258
|
+
Multilingual rulesets
|
|
259
|
+
|
|
260
|
+
Configurable rule packs
|
|
261
|
+
|
|
262
|
+
Express / Fastify middleware exports
|
|
263
|
+
|
|
264
|
+
Suggested rewrite mode
|
|
265
|
+
|
|
266
|
+
Production case studies
|
|
267
|
+
|
|
268
|
+
👤 Attribution
|
|
269
|
+
|
|
270
|
+
Developed and maintained by Ernesto Rosati.
|
|
271
|
+
|
|
272
|
+
If this library creates value for your organization,
|
|
273
|
+
consider collaboration or sponsorship.
|
|
274
|
+
|
|
275
|
+
📜 License
|
|
276
|
+
|
|
277
|
+
Apache-2.0
|
|
278
|
+
Copyright (c) 2026 Ernesto Rosati
|
|
195
279
|
|
|
280
|
+
Use cases & integrations
|
|
281
|
+
## ✅ Where this fits in real systems
|
|
282
|
+
|
|
283
|
+
`llm-entropy-filter` is designed to sit **before** expensive inference. Common placements:
|
|
284
|
+
|
|
285
|
+
### 1) Public chat apps (startups)
|
|
286
|
+
Use as a first-line gate to block obvious spam/coercion before the LLM:
|
|
287
|
+
- faster UX for rejected traffic (<10ms)
|
|
288
|
+
- reduced token spend
|
|
289
|
+
- reduced prompt-abuse surface
|
|
290
|
+
|
|
291
|
+
### 2) Rate-limit protection
|
|
292
|
+
Acts as a semantic pre-filter that reduces:
|
|
293
|
+
- quota exhaustion
|
|
294
|
+
- burst abuse
|
|
295
|
+
- coordinated spam floods
|
|
296
|
+
|
|
297
|
+
It creates headroom by rejecting high-entropy traffic locally.
|
|
298
|
+
|
|
299
|
+
### 3) RAG pipelines (pre-retrieval gate)
|
|
300
|
+
Before retrieval:
|
|
301
|
+
- block low-signal queries that would waste retrieval + reranking
|
|
302
|
+
- normalize/clean input to improve recall precision
|
|
303
|
+
- prevent adversarial queries from polluting retrieval traces
|
|
304
|
+
|
|
305
|
+
### 4) Multi-agent systems
|
|
306
|
+
In agent loops:
|
|
307
|
+
- prevent “reasoning drift” from noisy inputs
|
|
308
|
+
- keep agents from spending cycles on incoherent or adversarial prompts
|
|
309
|
+
- add structured telemetry for agent decisions (`flags`, `intention`, `entropy_score`)
|
|
310
|
+
|
|
311
|
+
### 5) Tooling & SDK pre-gates (LangChain / Vercel AI SDK)
|
|
312
|
+
Drop in as a deterministic guard:
|
|
313
|
+
- before `callLLM()`
|
|
314
|
+
- before `streamText()`
|
|
315
|
+
- before tool selection / agent routing
|
|
316
|
+
|
|
317
|
+
The output can be used as:
|
|
318
|
+
- a routing signal (ALLOW/WARN/BLOCK)
|
|
319
|
+
- a logging payload for audits and dashboards
|
|
320
|
+
|
|
321
|
+
“What’s missing to be production-ready”
|
|
322
|
+
## Production readiness checklist
|
|
323
|
+
|
|
324
|
+
The core gate is stable, but “production-ready” requires:
|
|
325
|
+
|
|
326
|
+
### 1) Configurable rulesets
|
|
327
|
+
- `default` (balanced)
|
|
328
|
+
- `strict` (aggressive spam/coercion blocking)
|
|
329
|
+
- `support` (customer support / fewer false positives)
|
|
330
|
+
- `public-api` (open endpoints / hardened)
|
|
331
|
+
|
|
332
|
+
### 2) Reproducible metrics (precision / recall)
|
|
333
|
+
Bench scripts should emit:
|
|
334
|
+
- precision/recall/F1
|
|
335
|
+
- confusion matrix
|
|
336
|
+
- false-positive rate on normal conversations
|
|
337
|
+
- top flags per dataset
|
|
338
|
+
|
|
339
|
+
### 3) Copy-paste integrations
|
|
340
|
+
Provide ready-to-use adapters:
|
|
341
|
+
- Express middleware
|
|
342
|
+
- Fastify plugin
|
|
343
|
+
- Next.js / Vercel edge wrapper
|
|
344
|
+
- “pre-gate” helpers for LangChain-style pipelines
|
|
345
|
+
|
|
346
|
+
### 4) One real production example
|
|
347
|
+
A minimal public case study:
|
|
348
|
+
- traffic volume
|
|
349
|
+
- % blocked
|
|
350
|
+
- cost avoided
|
|
351
|
+
- rate-limit incidents reduced
|
|
352
|
+
- latency improvement for blocked traffic
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// integrations/express.mjs
|
|
2
|
+
import { gate } from "llm-entropy-filter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create an Express middleware that runs `gate()` before LLM calls.
|
|
6
|
+
*
|
|
7
|
+
* Design goals:
|
|
8
|
+
* - Zero behavior changes to core `gate()`
|
|
9
|
+
* - Drop-in for public chat endpoints
|
|
10
|
+
* - Deterministic: no external calls
|
|
11
|
+
*
|
|
12
|
+
* @param {object} [opts]
|
|
13
|
+
* @param {string} [opts.bodyField="text"] - Field name in req.body that contains user text.
|
|
14
|
+
* @param {string} [opts.queryField] - Optional query param fallback (e.g., ?text=...).
|
|
15
|
+
* @param {boolean} [opts.attachResult=true] - Attach result to req.entropyGate.
|
|
16
|
+
* @param {boolean} [opts.blockOn="BLOCK"] - Block when action matches this string ("BLOCK") or array of actions.
|
|
17
|
+
* @param {number} [opts.blockStatus=400] - HTTP status when blocked.
|
|
18
|
+
* @param {object|function} [opts.blockResponse] - Custom JSON response or function(req, res, result) => any
|
|
19
|
+
* @param {boolean} [opts.warnHeader=true] - If WARN, add response headers with gate metadata.
|
|
20
|
+
* @param {boolean} [opts.alwaysHeader=false] - If true, add headers for all actions.
|
|
21
|
+
* @param {function} [opts.onResult] - Hook: (req, result) => void
|
|
22
|
+
* @param {function} [opts.getText] - Hook: (req) => string (overrides bodyField/queryField)
|
|
23
|
+
*/
|
|
24
|
+
export function entropyGateMiddleware(opts = {}) {
|
|
25
|
+
const {
|
|
26
|
+
bodyField = "text",
|
|
27
|
+
queryField,
|
|
28
|
+
attachResult = true,
|
|
29
|
+
blockOn = "BLOCK",
|
|
30
|
+
blockStatus = 400,
|
|
31
|
+
blockResponse,
|
|
32
|
+
warnHeader = true,
|
|
33
|
+
alwaysHeader = false,
|
|
34
|
+
onResult,
|
|
35
|
+
getText,
|
|
36
|
+
} = opts;
|
|
37
|
+
|
|
38
|
+
const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
|
|
39
|
+
|
|
40
|
+
return function entropyGate(req, res, next) {
|
|
41
|
+
try {
|
|
42
|
+
// 1) Extract text
|
|
43
|
+
let text = "";
|
|
44
|
+
if (typeof getText === "function") {
|
|
45
|
+
text = String(getText(req) ?? "");
|
|
46
|
+
} else {
|
|
47
|
+
const bodyVal = req?.body?.[bodyField];
|
|
48
|
+
const queryVal = queryField ? req?.query?.[queryField] : undefined;
|
|
49
|
+
text = String(bodyVal ?? queryVal ?? "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2) Run deterministic gate
|
|
53
|
+
const result = gate(text);
|
|
54
|
+
|
|
55
|
+
// 3) Attach result for downstream routing/logging
|
|
56
|
+
if (attachResult) {
|
|
57
|
+
// Convention: req.entropyGate
|
|
58
|
+
req.entropyGate = result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Optional hook
|
|
62
|
+
if (typeof onResult === "function") {
|
|
63
|
+
onResult(req, result);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4) Telemetry headers (optional)
|
|
67
|
+
const shouldHeader = alwaysHeader || (warnHeader && result?.action === "WARN");
|
|
68
|
+
if (shouldHeader) {
|
|
69
|
+
// Keep headers small and stable
|
|
70
|
+
res.setHeader("x-entropy-action", String(result?.action ?? ""));
|
|
71
|
+
res.setHeader("x-entropy-score", String(result?.entropy_score ?? ""));
|
|
72
|
+
res.setHeader("x-entropy-intention", String(result?.intention ?? ""));
|
|
73
|
+
// Flags can be large; keep compact
|
|
74
|
+
if (Array.isArray(result?.flags)) {
|
|
75
|
+
res.setHeader("x-entropy-flags", result.flags.slice(0, 10).join(","));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 5) Block if configured
|
|
80
|
+
if (blockSet.has(result?.action)) {
|
|
81
|
+
res.status(blockStatus);
|
|
82
|
+
|
|
83
|
+
if (typeof blockResponse === "function") {
|
|
84
|
+
return res.json(blockResponse(req, res, result));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (blockResponse && typeof blockResponse === "object") {
|
|
88
|
+
return res.json(blockResponse);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Default response: transparent + actionable
|
|
92
|
+
return res.json({
|
|
93
|
+
ok: false,
|
|
94
|
+
blocked: true,
|
|
95
|
+
gate: result,
|
|
96
|
+
message:
|
|
97
|
+
"Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Otherwise proceed
|
|
102
|
+
return next();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Fail-open by default: do not block requests if the gate errors.
|
|
105
|
+
// You can change this behavior by wrapping with your own error handler.
|
|
106
|
+
return next(err);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Small helper for routing:
|
|
113
|
+
* If you prefer to run gate manually inside route handlers.
|
|
114
|
+
*/
|
|
115
|
+
export function runEntropyGate(text) {
|
|
116
|
+
return gate(String(text ?? ""));
|
|
117
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// integrations/fastify.mjs
|
|
2
|
+
import { gate } from "llm-entropy-filter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fastify plugin: adds a preHandler that runs `gate()` before your route handler.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* fastify.register(entropyGatePlugin, { bodyField: "text", blockOn: "BLOCK" })
|
|
9
|
+
*
|
|
10
|
+
* Design:
|
|
11
|
+
* - Deterministic, local
|
|
12
|
+
* - Fail-open by default (if gate throws, request continues unless you override)
|
|
13
|
+
*/
|
|
14
|
+
export async function entropyGatePlugin(fastify, opts = {}) {
|
|
15
|
+
const {
|
|
16
|
+
bodyField = "text",
|
|
17
|
+
queryField,
|
|
18
|
+
attachResult = true,
|
|
19
|
+
blockOn = "BLOCK",
|
|
20
|
+
blockStatus = 400,
|
|
21
|
+
blockResponse,
|
|
22
|
+
warnHeader = true,
|
|
23
|
+
alwaysHeader = false,
|
|
24
|
+
onResult,
|
|
25
|
+
getText,
|
|
26
|
+
failClosed = false, // if true: return 500 on errors instead of passing through
|
|
27
|
+
} = opts;
|
|
28
|
+
|
|
29
|
+
const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
|
|
30
|
+
|
|
31
|
+
fastify.decorateRequest("entropyGate", null);
|
|
32
|
+
|
|
33
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
34
|
+
try {
|
|
35
|
+
// 1) Extract text
|
|
36
|
+
let text = "";
|
|
37
|
+
if (typeof getText === "function") {
|
|
38
|
+
text = String(getText(request) ?? "");
|
|
39
|
+
} else {
|
|
40
|
+
const bodyVal = request?.body?.[bodyField];
|
|
41
|
+
const queryVal = queryField ? request?.query?.[queryField] : undefined;
|
|
42
|
+
text = String(bodyVal ?? queryVal ?? "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2) Run gate
|
|
46
|
+
const result = gate(text);
|
|
47
|
+
|
|
48
|
+
// 3) Attach
|
|
49
|
+
if (attachResult) {
|
|
50
|
+
request.entropyGate = result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof onResult === "function") {
|
|
54
|
+
onResult(request, result);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4) Headers
|
|
58
|
+
const shouldHeader =
|
|
59
|
+
alwaysHeader || (warnHeader && result?.action === "WARN");
|
|
60
|
+
if (shouldHeader) {
|
|
61
|
+
reply.header("x-entropy-action", String(result?.action ?? ""));
|
|
62
|
+
reply.header("x-entropy-score", String(result?.entropy_score ?? ""));
|
|
63
|
+
reply.header("x-entropy-intention", String(result?.intention ?? ""));
|
|
64
|
+
if (Array.isArray(result?.flags)) {
|
|
65
|
+
reply.header("x-entropy-flags", result.flags.slice(0, 10).join(","));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 5) Block
|
|
70
|
+
if (blockSet.has(result?.action)) {
|
|
71
|
+
reply.code(blockStatus);
|
|
72
|
+
|
|
73
|
+
if (typeof blockResponse === "function") {
|
|
74
|
+
return reply.send(blockResponse(request, reply, result));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (blockResponse && typeof blockResponse === "object") {
|
|
78
|
+
return reply.send(blockResponse);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return reply.send({
|
|
82
|
+
ok: false,
|
|
83
|
+
blocked: true,
|
|
84
|
+
gate: result,
|
|
85
|
+
message:
|
|
86
|
+
"Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (failClosed) {
|
|
91
|
+
reply.code(500);
|
|
92
|
+
return reply.send({
|
|
93
|
+
ok: false,
|
|
94
|
+
error: "entropy_gate_error",
|
|
95
|
+
message: String(err?.message ?? err),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// fail-open: continue request
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Manual helper if you prefer using it inside handlers */
|
|
104
|
+
export function runEntropyGate(text) {
|
|
105
|
+
return gate(String(text ?? ""));
|
|
106
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// integrations/langchain.mjs
|
|
2
|
+
import { gate } from "llm-entropy-filter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal pre-gate for LangChain flows.
|
|
6
|
+
* Use this before calling any LLM / chain / agent.
|
|
7
|
+
*/
|
|
8
|
+
export function entropyPreGate(input, opts = {}) {
|
|
9
|
+
const { blockOn = "BLOCK" } = opts;
|
|
10
|
+
const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
|
|
11
|
+
|
|
12
|
+
const text = typeof input === "string" ? input : String(input?.text ?? input ?? "");
|
|
13
|
+
const result = gate(text);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
gate: result,
|
|
17
|
+
inputText: text,
|
|
18
|
+
shouldBlock: blockSet.has(result?.action),
|
|
19
|
+
shouldWarn: result?.action === "WARN",
|
|
20
|
+
shouldAllow: result?.action === "ALLOW",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Standard error object you can throw in API routes.
|
|
26
|
+
*/
|
|
27
|
+
export function entropyBlockedError(gateResult, opts = {}) {
|
|
28
|
+
const { status = 400, code = "ENTROPY_BLOCKED" } = opts;
|
|
29
|
+
const err = new Error("Blocked by llm-entropy-filter (high-entropy / low-signal input).");
|
|
30
|
+
err.name = "EntropyBlockedError";
|
|
31
|
+
err.status = status;
|
|
32
|
+
err.code = code;
|
|
33
|
+
err.gate = gateResult;
|
|
34
|
+
return err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* LCEL wrapper: wrap a Runnable / function to enforce gate before execution.
|
|
39
|
+
* Works with `@langchain/core/runnables` (RunnableLambda).
|
|
40
|
+
*
|
|
41
|
+
* Usage:
|
|
42
|
+
* const safe = withEntropyGate(myRunnableOrFn, { pickText: (i)=> i.input })
|
|
43
|
+
* await safe.invoke({ input: "..." })
|
|
44
|
+
*/
|
|
45
|
+
export function withEntropyGate(target, opts = {}) {
|
|
46
|
+
const {
|
|
47
|
+
blockOn = "BLOCK",
|
|
48
|
+
pickText, // (input) => string
|
|
49
|
+
onWarn, // (gateResult, input) => void
|
|
50
|
+
} = opts;
|
|
51
|
+
|
|
52
|
+
const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
|
|
53
|
+
|
|
54
|
+
// lazy import to avoid forcing langchain deps
|
|
55
|
+
let RunnableLambda;
|
|
56
|
+
async function getRunnableLambda() {
|
|
57
|
+
if (RunnableLambda) return RunnableLambda;
|
|
58
|
+
const mod = await import("@langchain/core/runnables");
|
|
59
|
+
RunnableLambda = mod.RunnableLambda;
|
|
60
|
+
return RunnableLambda;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
async invoke(input, config) {
|
|
65
|
+
const text = typeof pickText === "function"
|
|
66
|
+
? String(pickText(input) ?? "")
|
|
67
|
+
: (typeof input === "string" ? input : String(input?.input ?? input?.text ?? ""));
|
|
68
|
+
|
|
69
|
+
const g = gate(text);
|
|
70
|
+
|
|
71
|
+
if (g?.action === "WARN" && typeof onWarn === "function") onWarn(g, input);
|
|
72
|
+
|
|
73
|
+
if (blockSet.has(g?.action)) {
|
|
74
|
+
throw entropyBlockedError(g, { status: 400 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If target is a Runnable with invoke()
|
|
78
|
+
if (target && typeof target.invoke === "function") {
|
|
79
|
+
return target.invoke(input, config);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If target is a function
|
|
83
|
+
if (typeof target === "function") {
|
|
84
|
+
return target(input, config);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error("withEntropyGate: target must be a Runnable (invoke) or a function.");
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Optional: make it LCEL-friendly by exposing `asRunnable()`
|
|
91
|
+
async asRunnable() {
|
|
92
|
+
const RL = await getRunnableLambda();
|
|
93
|
+
return new RL({
|
|
94
|
+
func: async (input, config) => this.invoke(input, config),
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// integrations/vercel-ai-sdk.mjs
|
|
2
|
+
import { gate } from "llm-entropy-filter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pre-gate helper for Vercel AI SDK style flows.
|
|
6
|
+
*
|
|
7
|
+
* Typical usage:
|
|
8
|
+
* - Compute gate result
|
|
9
|
+
* - If BLOCK: return early
|
|
10
|
+
* - If WARN: optionally add metadata/logging and continue
|
|
11
|
+
*/
|
|
12
|
+
export function entropyPreGate(input, opts = {}) {
|
|
13
|
+
const { blockOn = "BLOCK" } = opts;
|
|
14
|
+
const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
|
|
15
|
+
|
|
16
|
+
const result = gate(String(input ?? ""));
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
gate: result,
|
|
20
|
+
shouldBlock: blockSet.has(result?.action),
|
|
21
|
+
shouldWarn: result?.action === "WARN",
|
|
22
|
+
shouldAllow: result?.action === "ALLOW",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Helper to build a standard Response when blocked (Edge/Node compatible).
|
|
28
|
+
*/
|
|
29
|
+
export function blockedResponse(gateResult, opts = {}) {
|
|
30
|
+
const { status = 400 } = opts;
|
|
31
|
+
return new Response(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
ok: false,
|
|
34
|
+
blocked: true,
|
|
35
|
+
gate: gateResult,
|
|
36
|
+
message:
|
|
37
|
+
"Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
|
|
38
|
+
}),
|
|
39
|
+
{
|
|
40
|
+
status,
|
|
41
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-entropy-filter",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
5
|
-
"keywords": [
|
|
6
|
-
"llm",
|
|
7
|
-
"ai",
|
|
8
|
-
"prompt-filter",
|
|
9
|
-
"input-validation",
|
|
10
|
-
"entropy",
|
|
11
|
-
"spam-detection",
|
|
12
|
-
"content-moderation",
|
|
13
|
-
"heuristics",
|
|
14
|
-
"openai",
|
|
15
|
-
"guardrails"
|
|
16
|
-
],
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Deterministic entropy-based pre-gate for LLM pipelines. ALLOW / WARN / BLOCK high-entropy inputs before expensive model calls.",
|
|
17
5
|
"license": "Apache-2.0",
|
|
18
6
|
"type": "module",
|
|
19
|
-
"sideEffects": false,
|
|
20
|
-
"engines": {
|
|
21
|
-
"node": ">=18"
|
|
22
|
-
},
|
|
23
7
|
"main": "./dist/index.cjs",
|
|
24
8
|
"module": "./dist/index.js",
|
|
25
9
|
"types": "./dist/index.d.ts",
|
|
@@ -28,37 +12,48 @@
|
|
|
28
12
|
"types": "./dist/index.d.ts",
|
|
29
13
|
"import": "./dist/index.js",
|
|
30
14
|
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./integrations/express": {
|
|
17
|
+
"import": "./integrations/express.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./integrations/fastify": {
|
|
20
|
+
"import": "./integrations/fastify.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./integrations/vercel-ai-sdk": {
|
|
23
|
+
"import": "./integrations/vercel-ai-sdk.mjs"
|
|
24
|
+
},
|
|
25
|
+
"./integrations/langchain": {
|
|
26
|
+
"import": "./integrations/langchain.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./rulesets/default": {
|
|
29
|
+
"import": "./rulesets/default.js"
|
|
30
|
+
},
|
|
31
|
+
"./rulesets/strict": {
|
|
32
|
+
"import": "./rulesets/strict.js"
|
|
31
33
|
}
|
|
32
34
|
},
|
|
33
35
|
"files": [
|
|
34
36
|
"dist",
|
|
35
|
-
"
|
|
37
|
+
"integrations",
|
|
38
|
+
"rulesets",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md"
|
|
36
42
|
],
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"build": "tsup",
|
|
41
|
-
"demo": "npm run build && node demo/demo.mjs",
|
|
42
|
-
"bench": "npm run build && node bench/benchmark.mjs",
|
|
43
|
-
"serve": "npm run build && node demo/server.mjs",
|
|
44
|
-
"prepublishOnly": "npm run clean:dist && npm run build"
|
|
45
|
-
},
|
|
46
|
-
"repository": {
|
|
47
|
-
"type": "git",
|
|
48
|
-
"url": "git+https://github.com/rosatisoft/llm-entropy-filter.git"
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
49
46
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
49
|
+
"clean": "rimraf dist",
|
|
50
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
51
|
+
"bench:sms": "node bench/sms_benchmark.js",
|
|
52
|
+
"bench:report": "node bench/generate_report.js"
|
|
52
53
|
},
|
|
53
|
-
"homepage": "https://github.com/rosatisoft/llm-entropy-filter#readme",
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"minimist": "^1.2.8",
|
|
59
|
-
"tsup": "^8.5.1",
|
|
60
|
-
"typescript": "^5.9.3",
|
|
61
|
-
"express": "^5.2.1",
|
|
62
|
-
"openai": "^6.16.0"
|
|
55
|
+
"rimraf": "^5.0.10",
|
|
56
|
+
"tsup": "^8.0.1",
|
|
57
|
+
"typescript": "^5.4.0"
|
|
63
58
|
}
|
|
64
59
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "default",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "Balanced preset: safe defaults for general apps.",
|
|
5
|
+
"thresholds": { "warn": 0.45, "block": 0.65 },
|
|
6
|
+
"normalization": {
|
|
7
|
+
"lowercase": true,
|
|
8
|
+
"trim": true,
|
|
9
|
+
"collapse_whitespace": true,
|
|
10
|
+
"unicode_nfkc": true
|
|
11
|
+
},
|
|
12
|
+
"hard_triggers": [
|
|
13
|
+
{
|
|
14
|
+
"id": "shouting",
|
|
15
|
+
"type": "signal",
|
|
16
|
+
"weight": 0.18,
|
|
17
|
+
"notes": "Excess uppercase or repeated punctuation",
|
|
18
|
+
"patterns": ["[A-ZÁÉÍÓÚÑ]{6,}", "!!+", "\\?\\?+", "¡¡+", "…{3,}"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "urgency",
|
|
22
|
+
"type": "topic_hint",
|
|
23
|
+
"weight": 0.12,
|
|
24
|
+
"patterns": ["\\bnow\\b", "\\btoday\\b", "\\burgent\\b", "\\bya\\b", "\\bahora\\b", "\\bhoy\\b", "\\búltim[oa]s?\\b", "\\bsolo\\s+hoy\\b"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "money_signal",
|
|
28
|
+
"type": "topic_hint",
|
|
29
|
+
"weight": 0.12,
|
|
30
|
+
"patterns": ["\\$\\d+", "\\bUSD\\b", "\\bMXN\\b", "\\b%\\b", "\\b90%\\b", "\\bfree\\b", "\\bgratis\\b", "\\bpromo\\b", "\\bdiscount\\b", "\\boffer\\b", "\\boferta\\b"]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "conspiracy_vague",
|
|
34
|
+
"type": "topic_hint",
|
|
35
|
+
"weight": 0.14,
|
|
36
|
+
"patterns": ["\\ball\\s+hide\\b", "\\bthey\\s+hide\\b", "\\beveryone\\s+knows\\b", "\\btodos\\s+lo\\s+esconden\\b", "\\bla\\s+verdad\\s+oculta\\b", "\\bnadie\\s+quiere\\s+que\\s+sepas\\b"]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"topics": [
|
|
40
|
+
{
|
|
41
|
+
"id": "marketing_spam",
|
|
42
|
+
"weight": 0.35,
|
|
43
|
+
"signals": [
|
|
44
|
+
{ "id": "cta", "weight": 0.18, "patterns": ["\\bclick\\b", "\\bclaim\\b", "\\bbuy\\b", "\\bcompra\\b", "\\borden(a|e)\\b"] },
|
|
45
|
+
{ "id": "promo_terms", "weight": 0.15, "patterns": ["\\bfree\\b", "\\bgratis\\b", "\\bwin\\b", "\\bganaste\\b", "\\biphone\\b", "\\bpremio\\b"] },
|
|
46
|
+
{ "id": "links", "weight": 0.12, "patterns": ["https?://", "www\\.", "\\bbit\\.ly\\b"] }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "coercion",
|
|
51
|
+
"weight": 0.25,
|
|
52
|
+
"signals": [
|
|
53
|
+
{ "id": "threat", "weight": 0.18, "patterns": ["\\bor\\s+else\\b", "\\bsi\\s+no\\b", "\\bte\\s+voy\\s+a\\b", "\\bI\\s+will\\b"] },
|
|
54
|
+
{ "id": "forced_tone", "weight": 0.12, "patterns": ["\\bdo\\s+it\\b", "\\bhazlo\\b", "\\bahora\\b"] }
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "conspiracy",
|
|
59
|
+
"weight": 0.25,
|
|
60
|
+
"signals": [
|
|
61
|
+
{ "id": "vague_all", "weight": 0.18, "patterns": ["\\btodos\\b", "\\beveryone\\b", "\\bthey\\b", "\\bel\\s+sistema\\b"] },
|
|
62
|
+
{ "id": "hidden_truth", "weight": 0.14, "patterns": ["\\bocult\\w+\\b", "\\bhidden\\b", "\\bsecret\\b", "\\bcover\\s*up\\b"] }
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "broken_causality",
|
|
67
|
+
"weight": 0.15,
|
|
68
|
+
"signals": [
|
|
69
|
+
{ "id": "contradiction_markers", "weight": 0.12, "patterns": ["\\bpero\\s+entonces\\b", "\\btherefore\\b.*\\bnot\\b", "\\bsi\\s+A\\s+entonces\\s+no\\s+A\\b"] }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "public-api",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "Hardened preset for open/public APIs: reduces abuse and quota burn.",
|
|
5
|
+
"thresholds": { "warn": 0.40, "block": 0.60 },
|
|
6
|
+
"normalization": { "lowercase": true, "trim": true, "collapse_whitespace": true, "unicode_nfkc": true },
|
|
7
|
+
"hard_triggers": [
|
|
8
|
+
{ "id": "links", "type": "signal", "weight": 0.22, "patterns": ["https?://", "www\\.", "\\bbit\\.ly\\b", "\\bt\\.co\\b"] },
|
|
9
|
+
{ "id": "shouting", "type": "signal", "weight": 0.18, "patterns": ["[A-ZÁÉÍÓÚÑ]{5,}", "!!+", "\\?\\?+"] },
|
|
10
|
+
{ "id": "money_signal", "type": "topic_hint", "weight": 0.16, "patterns": ["\\$\\d+", "\\b%\\b", "\\bfree\\b", "\\bgratis\\b"] }
|
|
11
|
+
],
|
|
12
|
+
"topics": [
|
|
13
|
+
{ "id": "marketing_spam", "weight": 0.40, "signals": [
|
|
14
|
+
{ "id": "cta", "weight": 0.20, "patterns": ["\\bclick\\b", "\\bclaim\\b", "\\bbuy\\b", "\\bcompra\\b"] },
|
|
15
|
+
{ "id": "promo", "weight": 0.18, "patterns": ["\\bfree\\b", "\\bgratis\\b", "\\bwin\\b", "\\bganaste\\b"] }
|
|
16
|
+
]},
|
|
17
|
+
{ "id": "coercion", "weight": 0.25, "signals": [
|
|
18
|
+
{ "id": "threat", "weight": 0.18, "patterns": ["\\bor\\s+else\\b", "\\bsi\\s+no\\b", "\\bI\\s+will\\b"] }
|
|
19
|
+
]},
|
|
20
|
+
{ "id": "conspiracy", "weight": 0.20, "signals": [
|
|
21
|
+
{ "id": "hidden", "weight": 0.14, "patterns": ["\\bocult\\w+\\b", "\\bhidden\\b", "\\bsecret\\b"] }
|
|
22
|
+
]},
|
|
23
|
+
{ "id": "incoherence", "weight": 0.15, "signals": [
|
|
24
|
+
{ "id": "noise_markers", "weight": 0.10, "patterns": ["\\bqwerty\\b", "\\basdf\\b", "([!?.])\\1{4,}"] }
|
|
25
|
+
]}
|
|
26
|
+
]
|
|
27
|
+
}
|
package/rulesets/schema
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "default",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "Balanced preset",
|
|
5
|
+
"thresholds": { "warn": 0.45, "block": 0.65 },
|
|
6
|
+
"hard_triggers": [
|
|
7
|
+
{ "id": "shouting", "type": "pattern", "weight": 0.20, "patterns": ["..."] }
|
|
8
|
+
],
|
|
9
|
+
"topics": [
|
|
10
|
+
{
|
|
11
|
+
"id": "marketing_spam",
|
|
12
|
+
"weight": 0.25,
|
|
13
|
+
"signals": [
|
|
14
|
+
{ "id": "money_signal", "weight": 0.10, "patterns": ["..."] }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"normalization": {
|
|
19
|
+
"lowercase": true,
|
|
20
|
+
"trim": true,
|
|
21
|
+
"collapse_whitespace": true,
|
|
22
|
+
"unicode_nfkc": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "strict",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "Aggressive preset for public endpoints and high-abuse environments.",
|
|
5
|
+
"thresholds": { "warn": 0.35, "block": 0.55 },
|
|
6
|
+
"normalization": { "lowercase": true, "trim": true, "collapse_whitespace": true, "unicode_nfkc": true },
|
|
7
|
+
"hard_triggers": [
|
|
8
|
+
{ "id": "shouting", "type": "signal", "weight": 0.22, "patterns": ["[A-ZÁÉÍÓÚÑ]{5,}", "!!+", "\\?\\?+", "¡¡+"] },
|
|
9
|
+
{ "id": "money_signal", "type": "topic_hint", "weight": 0.18, "patterns": ["\\$\\d+", "\\b%\\b", "\\bfree\\b", "\\bgratis\\b"] },
|
|
10
|
+
{ "id": "links", "type": "signal", "weight": 0.20, "patterns": ["https?://", "www\\.", "\\bbit\\.ly\\b"] },
|
|
11
|
+
{ "id": "conspiracy_vague", "type": "topic_hint", "weight": 0.18, "patterns": ["\\btodos\\s+lo\\s+esconden\\b", "\\btruth\\s+is\\s+hidden\\b"] }
|
|
12
|
+
],
|
|
13
|
+
"topics": [
|
|
14
|
+
{ "id": "marketing_spam", "weight": 0.45, "signals": [
|
|
15
|
+
{ "id": "cta", "weight": 0.22, "patterns": ["\\bclick\\b", "\\bclaim\\b", "\\bbuy\\b", "\\bcompra\\b"] },
|
|
16
|
+
{ "id": "promo", "weight": 0.18, "patterns": ["\\bwin\\b", "\\bganaste\\b", "\\bgratis\\b", "\\bfree\\b"] }
|
|
17
|
+
]},
|
|
18
|
+
{ "id": "coercion", "weight": 0.30, "signals": [
|
|
19
|
+
{ "id": "threat", "weight": 0.22, "patterns": ["\\bor\\s+else\\b", "\\bsi\\s+no\\b", "\\bI\\s+will\\b", "\\bte\\s+voy\\s+a\\b"] }
|
|
20
|
+
]},
|
|
21
|
+
{ "id": "conspiracy", "weight": 0.25, "signals": [
|
|
22
|
+
{ "id": "hidden", "weight": 0.20, "patterns": ["\\bocult\\w+\\b", "\\bhidden\\b", "\\bsecret\\b"] }
|
|
23
|
+
]}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "support",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "Customer-support preset: minimizes false positives for normal conversation.",
|
|
5
|
+
"thresholds": { "warn": 0.55, "block": 0.75 },
|
|
6
|
+
"normalization": { "lowercase": true, "trim": true, "collapse_whitespace": true, "unicode_nfkc": true },
|
|
7
|
+
"hard_triggers": [
|
|
8
|
+
{ "id": "links", "type": "signal", "weight": 0.10, "patterns": ["https?://", "www\\."] }
|
|
9
|
+
],
|
|
10
|
+
"topics": [
|
|
11
|
+
{ "id": "marketing_spam", "weight": 0.25, "signals": [
|
|
12
|
+
{ "id": "cta", "weight": 0.12, "patterns": ["\\bclick\\b", "\\bclaim\\b"] },
|
|
13
|
+
{ "id": "promo", "weight": 0.10, "patterns": ["\\bfree\\b", "\\bgratis\\b"] }
|
|
14
|
+
]},
|
|
15
|
+
{ "id": "coercion", "weight": 0.20, "signals": [
|
|
16
|
+
{ "id": "threat", "weight": 0.12, "patterns": ["\\bor\\s+else\\b", "\\bI\\s+will\\b"] }
|
|
17
|
+
]},
|
|
18
|
+
{ "id": "conspiracy", "weight": 0.15, "signals": [
|
|
19
|
+
{ "id": "hidden", "weight": 0.10, "patterns": ["\\btruth\\s+hidden\\b", "\\bverdad\\s+oculta\\b"] }
|
|
20
|
+
]}
|
|
21
|
+
]
|
|
22
|
+
}
|