spamkit-api 0.1.0__py3-none-any.whl

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.
spamkit/verdict.py ADDED
@@ -0,0 +1,46 @@
1
+ """Threshold policy: maps a raw spam score to clean, review, or spam."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+
10
+
11
+ class Verdict(str, Enum):
12
+ CLEAN = "clean"
13
+ REVIEW = "review"
14
+ SPAM = "spam"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Thresholds:
19
+ """Scores below ``clean`` are clean, scores above ``spam`` are spam, and everything between is review.
20
+
21
+ The defaults route every submission to review. A deployment has to choose thresholds
22
+ deliberately, based on its own evaluation results, before any automatic action is safe.
23
+ """
24
+
25
+ clean: float = 0.0
26
+ spam: float = 1.0
27
+
28
+ def __post_init__(self) -> None:
29
+ for name, value in (("clean", self.clean), ("spam", self.spam)):
30
+ if not 0.0 <= value <= 1.0:
31
+ raise ValueError(f"{name} threshold must be between 0 and 1")
32
+ if not self.clean < self.spam:
33
+ raise ValueError("clean threshold must be lower than spam threshold")
34
+
35
+ def verdict_for(self, score: float) -> Verdict:
36
+ if score < self.clean:
37
+ return Verdict.CLEAN
38
+ if score > self.spam:
39
+ return Verdict.SPAM
40
+ return Verdict.REVIEW
41
+
42
+ @property
43
+ def version(self) -> str:
44
+ """Short, deterministic fingerprint of the threshold values. Changes whenever they change."""
45
+ canonical = json.dumps({"clean": self.clean, "spam": self.spam}, sort_keys=True)
46
+ return hashlib.sha256(canonical.encode()).hexdigest()[:12]
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.5
2
+ Name: spamkit-api
3
+ Version: 0.1.0
4
+ Summary: Self-hostable spam-scoring service and client for user-submitted text.
5
+ Project-URL: Homepage, https://spamkit.pages.dev
6
+ License-Expression: AGPL-3.0-only
7
+ License-File: LICENSE
8
+ Keywords: classifier,moderation,self-hosted,spam
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: pydantic>=2.6
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
14
+ Requires-Dist: pytest>=8; extra == 'dev'
15
+ Provides-Extra: server
16
+ Requires-Dist: fastapi>=0.110; extra == 'server'
17
+ Requires-Dist: huggingface-hub>=0.22; extra == 'server'
18
+ Requires-Dist: torch>=2.2; extra == 'server'
19
+ Requires-Dist: transformers<5,>=4.48; extra == 'server'
20
+ Requires-Dist: uvicorn>=0.29; extra == 'server'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Spamkit
24
+
25
+ Spam scoring for user-submitted text, designed to run in your own environment.
26
+
27
+ Spamkit is a small HTTP service around the [spamton](https://huggingface.co/tiagozip/spamton) text classifier, plus Python and TypeScript clients and an offline evaluation CLI. It returns a score and a three-way verdict (`clean`, `review`, `spam`). Your application decides what to do with each verdict. Spamkit never deletes, blocks, or edits content.
28
+
29
+ > **Status: v0.1 prototype.** The product hypothesis is in [PRD.md](PRD.md). Model quality has **not** been validated on a labeled dataset for any target segment, so no accuracy or false-positive claim is made here. Use `spamkit evaluate` on your own labeled examples before you trust any threshold.
30
+
31
+ ## Quick start
32
+
33
+ ### Docker (recommended)
34
+
35
+ ```bash
36
+ docker compose up --build
37
+ curl -s http://127.0.0.1:8080/readyz
38
+ curl -s -X POST http://127.0.0.1:8080/v1/score \
39
+ -H 'content-type: application/json' \
40
+ -d '{"text":"I can improve your Shopify SEO. Reply for a free audit.","id":"submission_123"}'
41
+ ```
42
+
43
+ The image pins the model revision and fetches the model at build time, so the running container makes no network calls. The Dockerfile and compose file have not been built in this repository's CI yet. Verify with `docker compose build` before you rely on them.
44
+
45
+ ### Local (Python 3.10+)
46
+
47
+ ```bash
48
+ python -m venv .venv && . .venv/bin/activate
49
+ pip install --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple torch
50
+ pip install "spamkit-api[server]" # or: pip install -e ".[server]" from a checkout
51
+ spamkit fetch-model --dest models/spamton # downloads spamton at the pinned revision
52
+ spamkit serve # 127.0.0.1:8080
53
+ ```
54
+
55
+ ## API
56
+
57
+ Base path `/v1`. All request and response bodies are JSON.
58
+
59
+ ### `POST /v1/score`
60
+
61
+ ```json
62
+ { "text": "I can improve your Shopify SEO. Reply for a free audit.", "id": "submission_123", "context": "store_contact_form" }
63
+ ```
64
+
65
+ ```json
66
+ {
67
+ "id": "submission_123",
68
+ "score": 0.5499,
69
+ "verdict": "review",
70
+ "model": "spamton",
71
+ "model_version": "7aa94d049d24bbc302c5a69e63cafe6c44d49bc1",
72
+ "latency_ms": 18,
73
+ "truncated": false
74
+ }
75
+ ```
76
+
77
+ - `text` is required, must be non-empty UTF-8, and is limited to `SPAMKIT_MAX_TEXT_BYTES` (default 16,384 bytes).
78
+ - `id` is opaque and echoed back. It is never logged or used as a metric label.
79
+ - `context` is a short label (max 64 characters). It is logged and counted, and it does not condition the classifier.
80
+ - `score` is the raw spam-class output in [0, 1]. **It is not a calibrated probability.** Calibration has not been measured.
81
+ - `truncated` is `true` when the text exceeded the model's 512-token window. The model then scores only the leading part. The service first clips to 6,000 characters, matching the model author's reference preprocessing.
82
+
83
+ ### `POST /v1/score/batch`
84
+
85
+ ```json
86
+ { "items": [{ "text": "...", "id": "a" }, { "text": "", "id": "b" }] }
87
+ ```
88
+
89
+ Returns `results` in the same order as `items`. Each entry is `{"ok": true, "result": {...}}` or `{"ok": false, "id": ..., "error": {...}}`, so one bad item does not discard the others. Up to `SPAMKIT_MAX_BATCH_ITEMS` items (default 100).
90
+
91
+ ### Verdicts
92
+
93
+ | Score | Verdict |
94
+ |---|---|
95
+ | `score < clean` | `clean` |
96
+ | `score > spam` | `spam` |
97
+ | otherwise | `review` |
98
+
99
+ Defaults are `clean=0.0` and `spam=1.0`, so every submission is `review` until you choose thresholds from your own evaluation. Thresholds are returned by `GET /v1/info` with a `threshold_config_version` fingerprint.
100
+
101
+ ### Errors
102
+
103
+ Every error uses one envelope, and the message never includes submitted text:
104
+
105
+ ```json
106
+ { "error": { "code": "input_too_large", "message": "text must be at most 16384 bytes of UTF-8.", "request_id": "..." } }
107
+ ```
108
+
109
+ | HTTP | `code` | Meaning |
110
+ |---|---|---|
111
+ | 400 | `invalid_request` | Malformed JSON, missing or empty `text`, wrong field types |
112
+ | 401 | `unauthorized` | Bearer token required and missing or wrong (only when `SPAMKIT_API_TOKEN` is set) |
113
+ | 413 | `input_too_large` | Body, text, or batch item count over the limit |
114
+ | 422 | `unsupported_input` | Text contains NUL characters |
115
+ | 429 | `rate_limited` | Too many requests in flight (`SPAMKIT_MAX_INFLIGHT`). Retry after `Retry-After`. |
116
+ | 503 | `model_not_ready` | Model failed to load or is still loading |
117
+ | 500 | `internal_error` | Unexpected failure. Details are logged by type only. |
118
+
119
+ ### Operations
120
+
121
+ | Endpoint | Purpose |
122
+ |---|---|
123
+ | `GET /healthz` | Liveness. Always 200 while the process is up. |
124
+ | `GET /readyz` | 200 only when the model is loaded. Returns the model version. |
125
+ | `GET /v1/info` | Model ID, version, token limit, thresholds, and size limits. No secrets. |
126
+ | `GET /metrics` | Prometheus text: request counts, verdict counts, error codes, latency histogram. |
127
+
128
+ ## Evaluate on your own data
129
+
130
+ Put your labeled examples in a CSV or JSONL file with a `text` column and a `label` column (`spam` or `clean`). An optional `id` column is echoed in the output.
131
+
132
+ ```bash
133
+ spamkit evaluate ./samples.csv --threshold 0.82 --output report.json
134
+ ```
135
+
136
+ The command runs the model locally. It does not upload the file. By default it prints row numbers only (file line numbers, so they match your editor). Pass `--show-text` to print the text of false positives and false negatives. The output includes:
137
+
138
+ - dataset count and label balance,
139
+ - confusion matrix, precision, recall, specificity, FPR, FNR, and F1,
140
+ - a threshold sweep from 0.05 to 0.95 (or `--sweep ...`),
141
+ - the model version and software version,
142
+ - the note "Metrics describe only the supplied dataset."
143
+
144
+ Undefined ratios (for example precision when nothing was predicted as spam) print as `n/a`.
145
+
146
+ ## SDKs
147
+
148
+ Both SDKs use the same field names as the REST API, return the same verdicts, and map errors to the same codes. Both are tested against [`contract/fixtures.json`](contract/fixtures.json).
149
+
150
+ **Python** (`pip install -e .` from this repo until the package is published; the base install has no torch):
151
+
152
+ ```python
153
+ from spamkit import SpamkitClient, SpamkitError
154
+
155
+ client = SpamkitClient("http://127.0.0.1:8080", timeout=2.0, max_retries=1)
156
+ try:
157
+ result = client.score("Win a free cruise!!!", id="submission_123")
158
+ print(result.verdict, result.score)
159
+ except SpamkitError as error:
160
+ print(error.code, error.status, error.request_id)
161
+ ```
162
+
163
+ **TypeScript** (`sdk/typescript`, Node 18.17+):
164
+
165
+ ```ts
166
+ import { SpamkitClient, SpamkitError } from "spamkit";
167
+
168
+ const client = new SpamkitClient({ baseUrl: "http://127.0.0.1:8080", timeoutMs: 2000, maxRetries: 1 });
169
+ try {
170
+ const result = await client.score({ text: "Win a free cruise!!!", id: "submission_123" });
171
+ console.log(result.verdict, result.score);
172
+ } catch (error) {
173
+ if (error instanceof SpamkitError) console.error(error.code, error.status, error.requestId);
174
+ }
175
+ ```
176
+
177
+ Retries are off by default. When you turn them on, they cover transport errors, timeouts, 429, and 5xx. Scoring is read-only, so a retry cannot duplicate side effects. Samples for a Node server and a FastAPI app are in [`examples/`](examples/).
178
+
179
+ ## Configuration
180
+
181
+ | Variable | Default | Purpose |
182
+ |---|---|---|
183
+ | `SPAMKIT_MODEL_DIR` | `models/spamton` | Directory from `spamkit fetch-model` |
184
+ | `SPAMKIT_HOST` | `127.0.0.1` | Bind address. The container sets `0.0.0.0`. |
185
+ | `SPAMKIT_PORT` | `8080` | Listen port |
186
+ | `SPAMKIT_CLEAN_THRESHOLD` | `0.0` | Scores below this are `clean` |
187
+ | `SPAMKIT_SPAM_THRESHOLD` | `1.0` | Scores above this are `spam` |
188
+ | `SPAMKIT_MAX_TEXT_BYTES` | `16384` | Per-text byte limit |
189
+ | `SPAMKIT_MAX_BODY_BYTES` | `1048576` | Request body limit |
190
+ | `SPAMKIT_MAX_BATCH_ITEMS` | `100` | Batch item limit |
191
+ | `SPAMKIT_MAX_INFLIGHT` | `32` | Requests in flight (queued or running) before 429 |
192
+ | `SPAMKIT_API_TOKEN` | unset | When set, `/v1/*` and `/metrics` require `Authorization: Bearer <token>` |
193
+
194
+ Run one worker per process. Each worker loads its own copy of the model. `spamkit serve` already runs one.
195
+
196
+ ## Privacy and data handling
197
+
198
+ This describes what the code does today.
199
+
200
+ - Submitted text is held in process memory for the duration of scoring. It is not written to disk, a database, or a cache.
201
+ - Logs are JSON lines with request ID, route, status, latency, verdict, score band (for example `0.1-0.2`), model version, error code, and `context`. They never include `text` or `id`. The test suite checks this.
202
+ - Metrics contain only fixed labels: route, status, verdict, and error code.
203
+ - The `evaluate` command reads your file locally and writes only aggregate metrics and the error-row list. Text is printed only with `--show-text`.
204
+ - There is no telemetry and no outbound network call at runtime.
205
+ - Local deployment reduces data exposure. It does not make a deployment GDPR-compliant or "zero data processing". Your own operations still matter, including TLS, access control, and log retention.
206
+
207
+ ## Known gaps
208
+
209
+ - **No measured accuracy.** No labeled evaluation set has been run for any segment. PRD section 11 lists the release gates. Until they pass, do not present the service as production-ready.
210
+ - **Score is uncalibrated.** Do not treat it as a probability.
211
+ - **English only.** The model's training data is English.
212
+ - **Text only.** No image, link, sender, or account signals.
213
+ - **Rate limiting is only admission control.** `SPAMKIT_MAX_INFLIGHT` returns 429 when saturated. There is no per-client limit. Apply one at the reverse proxy.
214
+ - **Body size is enforced while streaming**, but there is no per-client limit. Put a proxy-level limit in front of the service for untrusted traffic.
215
+ - **No debug text logging, no auth beyond one shared bearer token, no multi-tenant support.**
216
+ - **Performance was measured only on Apple Silicon, not on the x86 target in the PRD.** On an M-series Mac with the pinned model, warm batch-size-1 latency was p50 9 ms and p95 24 ms over 30 runs. Startup includes one warm-up pass. Memory and concurrency have not been benchmarked on target hardware.
217
+ - **Batch latency** reported per item is the latency of the whole batch scoring call.
218
+
219
+ ## Model and license
220
+
221
+ - Model: `tiagozip/spamton` at revision `7aa94d049d24bbc302c5a69e63cafe6c44d49bc1`, a 32M-parameter ModernBERT classifier fine-tuned from `jhu-clsp/ettin-encoder-32m`.
222
+ - License: **AGPL-3.0** for the model. The `LICENSE` and `NOTICE` files are kept in the model directory and in the image. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
223
+ - PRD section 14 flags that AGPL obligations for hosted or redistributed use have not been reviewed by counsel. Resolve that before any commercial release.
224
+ - The license for this repository's own code has not been chosen.
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ # Python: unit tests run without the model; model tests run when models/spamton exists.
230
+ pip install -e ".[server,dev]" && pytest
231
+
232
+ # TypeScript SDK
233
+ cd sdk/typescript && npm ci && npm run typecheck && npm test
234
+ ```
235
+
236
+ Layout:
237
+
238
+ ```
239
+ src/spamkit/ server (app, classifier, settings), evaluation, client, CLI
240
+ sdk/typescript/ spamkit
241
+ contract/ shared request/response fixtures used by both SDK suites
242
+ examples/ Node and FastAPI integration samples
243
+ tests/ pytest suite
244
+ Dockerfile single-node image with the model pinned at build time
245
+ ```
@@ -0,0 +1,16 @@
1
+ spamkit/__init__.py,sha256=Bh6bJeaC_7OUTzqjsJIbi5I3ofgab-OQqrA-Lem9_zk,616
2
+ spamkit/app.py,sha256=DFtt75YAf6RMyxiBV3epo56BoTPzoru4OxLtB8kNrv8,12822
3
+ spamkit/classifier.py,sha256=T8Lftx1hbiwbjmYyEdTwlYo4PID5gH8QSFk8o-O272w,4638
4
+ spamkit/cli.py,sha256=RpH69OUjHOYhu3gixvbZk8kpGK24MGOXNFacR4NOLIA,7580
5
+ spamkit/client.py,sha256=Z-Vk1quiC6jWpESOv0yPwYs1Bll1UX3BKFQypNJgSHo,6162
6
+ spamkit/errors.py,sha256=eHa_0z4MeAw2cjykKbxDm1qObpkiA0MhwhhR_WOv1TI,1168
7
+ spamkit/evaluation.py,sha256=90Lk91k28YHJCeSvvGJeN4Gu8zhCl1hyLniTYssIAKw,6473
8
+ spamkit/observability.py,sha256=ERTVd7OdsO4WAjwAfnT8O4BqyA5529TIybTt6Iz3ido,3432
9
+ spamkit/schemas.py,sha256=d9AR4VrJmGvR6zy5sHR0e3vzIGqI6yoHaNjVlEDk-Nk,3205
10
+ spamkit/settings.py,sha256=rxa2rp_EGcJo54x7R73EJGn3c2AcieJH1Ig4xs1Dlkc,2220
11
+ spamkit/verdict.py,sha256=dDq8g-Fmd36ttgSBMDTG3mauVxN61QD6QbaAXx4Lido,1530
12
+ spamkit_api-0.1.0.dist-info/METADATA,sha256=3V2_r-JO_9dIuYhZv01JNMb9I4BIrv2S_DxioC-4FMY,12006
13
+ spamkit_api-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
14
+ spamkit_api-0.1.0.dist-info/entry_points.txt,sha256=iroCCVAdDu_JlOJJmml_xcgePgr3_ITEPhAb-Dlo9Ys,45
15
+ spamkit_api-0.1.0.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
16
+ spamkit_api-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spamkit = spamkit.cli:main