prometheusnn 0.2.1__tar.gz → 0.2.2__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.
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/CHANGELOG.md +12 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/PKG-INFO +1 -1
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/pyproject.toml +1 -1
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/novelty.py +17 -8
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/ui_server.py +1 -1
- prometheusnn-0.2.2/tests/test_novelty.py +100 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/uv.lock +1 -1
- prometheusnn-0.2.1/tests/test_novelty.py +0 -50
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/.github/workflows/ci.yml +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/.github/workflows/publish.yml +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/.gitignore +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/LICENSE +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/README.md +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/docs/beam-search-strategy.md +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/docs/roadmap-self-aware-server.md +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/scripts/parse_hts.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/scripts/parse_mesh.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/config.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/embedding.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/logging.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/lru.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/mcp_safety.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/core/responses.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/loader.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/splits.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/data/taxonomy.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/beam_search.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/predictor.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/router.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/inference/summary.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/insights.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/detector.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/models.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/profiler.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/recommender.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/intake/scoper.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/py.typed +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/INSTRUCTIONS.md +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/evaluation.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/inference.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/intake.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/management.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/server/tools/training.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/storage/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/storage/model_store.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/calibration.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/cascade.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/classifier.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/noise.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/src/prometheus/training/trainer.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/__init__.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/conftest.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/fixtures/naics_test.csv +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_beam_search.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_calibration.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_classifier.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_insights.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_intake.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_integration.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_noise.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_predictor.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_router.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_server.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_splits.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_storage.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_summary.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_taxonomy.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_trainer.py +0 -0
- {prometheusnn-0.2.1 → prometheusnn-0.2.2}/tests/test_ui_server.py +0 -0
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.2] - 2026-10-30
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- **Novelty data is loaded without pickle.** `novelty_centroids.npz` was loaded
|
|
12
|
+
with `allow_pickle=True`, so a crafted model directory could run code in the
|
|
13
|
+
server when any tool loaded that model. In 0.2.0 and earlier this was
|
|
14
|
+
reachable through the `model_name` path traversal fixed in 0.2.1; in 0.2.1 it
|
|
15
|
+
still applied to a malicious model placed in the models directory. Both
|
|
16
|
+
novelty files now load with `allow_pickle=False`. Models saved by
|
|
17
|
+
PrometheusNN contain only numeric and string arrays and load unchanged; a
|
|
18
|
+
file holding pickled objects is refused with an error asking for a retrain.
|
|
19
|
+
|
|
8
20
|
## [0.2.1] - 2026-10-30
|
|
9
21
|
|
|
10
22
|
### Security
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: prometheusnn
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Frictionless hierarchical text classification — an MCP server that guides you from CSV to trained classifier
|
|
5
5
|
Project-URL: Homepage, https://github.com/mfbaig35r/prometheusnn
|
|
6
6
|
Project-URL: Repository, https://github.com/mfbaig35r/prometheusnn
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prometheusnn"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Frictionless hierarchical text classification — an MCP server that guides you from CSV to trained classifier"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -116,17 +116,26 @@ class NoveltyDetector:
|
|
|
116
116
|
|
|
117
117
|
def load(self, path: Path):
|
|
118
118
|
path = Path(path)
|
|
119
|
-
data
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
# Never unpickle: a model directory is data, and a crafted .npz with object arrays would
|
|
120
|
+
# run code on load. save() writes only numeric and string arrays, which need no pickle.
|
|
121
|
+
data = np.load(path / "novelty_centroids.npz", allow_pickle=False)
|
|
122
|
+
try:
|
|
123
|
+
class_names = data["class_names"]
|
|
124
|
+
mean_dists = data["mean_dists"]
|
|
125
|
+
std_dists = data["std_dists"]
|
|
126
|
+
p95_dists = data["p95_dists"]
|
|
127
|
+
counts = data["counts"]
|
|
128
|
+
centroids = [data[f"centroid_{i}"] for i in range(len(class_names))]
|
|
129
|
+
except ValueError as exc:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"{path / 'novelty_centroids.npz'} contains pickled objects; refusing to load it. "
|
|
132
|
+
"Retrain the model to regenerate it."
|
|
133
|
+
) from exc
|
|
125
134
|
|
|
126
135
|
self.centroids = {}
|
|
127
136
|
for i, name in enumerate(class_names):
|
|
128
137
|
self.centroids[str(name)] = ClassCentroid(
|
|
129
|
-
centroid=
|
|
138
|
+
centroid=centroids[i],
|
|
130
139
|
mean_dist=float(mean_dists[i]),
|
|
131
140
|
std_dist=max(float(std_dists[i]), 0.1),
|
|
132
141
|
p95_dist=float(p95_dists[i]),
|
|
@@ -135,7 +144,7 @@ class NoveltyDetector:
|
|
|
135
144
|
|
|
136
145
|
emb_path = path / "novelty_embeddings.npy"
|
|
137
146
|
if emb_path.exists():
|
|
138
|
-
self.training_embeddings = np.load(emb_path)
|
|
147
|
+
self.training_embeddings = np.load(emb_path, allow_pickle=False)
|
|
139
148
|
|
|
140
149
|
def get_centroids_data(self) -> dict:
|
|
141
150
|
"""Return data dict suitable for np.savez."""
|
|
@@ -236,7 +236,7 @@ async def _handle_message(ws, msg: dict[str, Any]) -> None:
|
|
|
236
236
|
try:
|
|
237
237
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
238
238
|
|
|
239
|
-
app: FastAPI | None = FastAPI(title="prometheusnn-ui", version="0.2.
|
|
239
|
+
app: FastAPI | None = FastAPI(title="prometheusnn-ui", version="0.2.2")
|
|
240
240
|
|
|
241
241
|
@app.get("/health")
|
|
242
242
|
def _health() -> dict:
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Tests for novelty detection."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from prometheus.inference.novelty import NoveltyDetector
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_fit_and_score():
|
|
13
|
+
rng = np.random.default_rng(42)
|
|
14
|
+
n = 50
|
|
15
|
+
embs = rng.standard_normal((n, 16)).astype(np.float32)
|
|
16
|
+
labels = np.array([0] * 25 + [1] * 25)
|
|
17
|
+
classes = ["classA", "classB"]
|
|
18
|
+
|
|
19
|
+
det = NoveltyDetector(z_score_threshold=2.5)
|
|
20
|
+
det.fit(embs, labels, classes)
|
|
21
|
+
|
|
22
|
+
assert len(det.centroids) == 2
|
|
23
|
+
|
|
24
|
+
# Score an in-distribution sample
|
|
25
|
+
score = det.score(embs[0], "classA", confidence=0.9)
|
|
26
|
+
assert not score.is_novel
|
|
27
|
+
|
|
28
|
+
# Score a far-out sample
|
|
29
|
+
outlier = np.ones(16, dtype=np.float32) * 100
|
|
30
|
+
score = det.score(outlier, "classA", confidence=0.3)
|
|
31
|
+
assert score.centroid_z_score > 2.5
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_save_load_roundtrip():
|
|
35
|
+
rng = np.random.default_rng(42)
|
|
36
|
+
embs = rng.standard_normal((30, 8)).astype(np.float32)
|
|
37
|
+
labels = np.array([0] * 15 + [1] * 15)
|
|
38
|
+
classes = ["X", "Y"]
|
|
39
|
+
|
|
40
|
+
det = NoveltyDetector()
|
|
41
|
+
det.fit(embs, labels, classes)
|
|
42
|
+
|
|
43
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
44
|
+
det.save(Path(tmpdir))
|
|
45
|
+
|
|
46
|
+
det2 = NoveltyDetector()
|
|
47
|
+
det2.load(Path(tmpdir))
|
|
48
|
+
|
|
49
|
+
assert set(det2.centroids.keys()) == {"X", "Y"}
|
|
50
|
+
assert det2.training_embeddings is not None
|
|
51
|
+
assert det2.training_embeddings.shape == (30, 8)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _Payload:
|
|
55
|
+
"""Unpickling this creates a file: proof that loading ran attacker code."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, marker: Path):
|
|
58
|
+
self.marker = str(marker)
|
|
59
|
+
|
|
60
|
+
def __reduce__(self):
|
|
61
|
+
return (open, (self.marker, "w"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_load_never_unpickles_centroids(tmp_path):
|
|
65
|
+
marker = tmp_path / "executed"
|
|
66
|
+
np.savez(
|
|
67
|
+
tmp_path / "novelty_centroids.npz",
|
|
68
|
+
centroid_0=np.zeros(4),
|
|
69
|
+
class_names=np.array([_Payload(marker)], dtype=object),
|
|
70
|
+
mean_dists=[1.0],
|
|
71
|
+
std_dists=[1.0],
|
|
72
|
+
p95_dists=[1.0],
|
|
73
|
+
counts=[1],
|
|
74
|
+
)
|
|
75
|
+
with pytest.raises(ValueError, match="pickled objects"):
|
|
76
|
+
NoveltyDetector().load(tmp_path)
|
|
77
|
+
assert not marker.exists()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_load_never_unpickles_embeddings(tmp_path):
|
|
81
|
+
marker = tmp_path / "executed"
|
|
82
|
+
det = NoveltyDetector()
|
|
83
|
+
det.fit(np.eye(4, dtype=np.float32), np.array([0, 0, 1, 1]), ["a", "b"])
|
|
84
|
+
det.save(tmp_path)
|
|
85
|
+
np.save(tmp_path / "novelty_embeddings.npy", np.array([_Payload(marker)], dtype=object))
|
|
86
|
+
with pytest.raises(ValueError):
|
|
87
|
+
NoveltyDetector().load(tmp_path)
|
|
88
|
+
assert not marker.exists()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_saved_files_need_no_pickle(tmp_path):
|
|
92
|
+
rng = np.random.default_rng(0)
|
|
93
|
+
det = NoveltyDetector()
|
|
94
|
+
det.fit(rng.standard_normal((20, 4)), np.array([0] * 10 + [1] * 10), ["Café", "naïve/class"])
|
|
95
|
+
det.save(tmp_path)
|
|
96
|
+
with np.load(tmp_path / "novelty_centroids.npz", allow_pickle=False) as data:
|
|
97
|
+
assert all(data[k].dtype != object for k in data.files)
|
|
98
|
+
det2 = NoveltyDetector()
|
|
99
|
+
det2.load(tmp_path)
|
|
100
|
+
assert set(det2.centroids) == {"Café", "naïve/class"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Tests for novelty detection."""
|
|
2
|
-
|
|
3
|
-
import tempfile
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
7
|
-
|
|
8
|
-
from prometheus.inference.novelty import NoveltyDetector
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_fit_and_score():
|
|
12
|
-
rng = np.random.default_rng(42)
|
|
13
|
-
n = 50
|
|
14
|
-
embs = rng.standard_normal((n, 16)).astype(np.float32)
|
|
15
|
-
labels = np.array([0] * 25 + [1] * 25)
|
|
16
|
-
classes = ["classA", "classB"]
|
|
17
|
-
|
|
18
|
-
det = NoveltyDetector(z_score_threshold=2.5)
|
|
19
|
-
det.fit(embs, labels, classes)
|
|
20
|
-
|
|
21
|
-
assert len(det.centroids) == 2
|
|
22
|
-
|
|
23
|
-
# Score an in-distribution sample
|
|
24
|
-
score = det.score(embs[0], "classA", confidence=0.9)
|
|
25
|
-
assert not score.is_novel
|
|
26
|
-
|
|
27
|
-
# Score a far-out sample
|
|
28
|
-
outlier = np.ones(16, dtype=np.float32) * 100
|
|
29
|
-
score = det.score(outlier, "classA", confidence=0.3)
|
|
30
|
-
assert score.centroid_z_score > 2.5
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_save_load_roundtrip():
|
|
34
|
-
rng = np.random.default_rng(42)
|
|
35
|
-
embs = rng.standard_normal((30, 8)).astype(np.float32)
|
|
36
|
-
labels = np.array([0] * 15 + [1] * 15)
|
|
37
|
-
classes = ["X", "Y"]
|
|
38
|
-
|
|
39
|
-
det = NoveltyDetector()
|
|
40
|
-
det.fit(embs, labels, classes)
|
|
41
|
-
|
|
42
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
|
43
|
-
det.save(Path(tmpdir))
|
|
44
|
-
|
|
45
|
-
det2 = NoveltyDetector()
|
|
46
|
-
det2.load(Path(tmpdir))
|
|
47
|
-
|
|
48
|
-
assert set(det2.centroids.keys()) == {"X", "Y"}
|
|
49
|
-
assert det2.training_embeddings is not None
|
|
50
|
-
assert det2.training_embeddings.shape == (30, 8)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|