humane-proxy 0.2.2__tar.gz → 0.2.3__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.
- {humane_proxy-0.2.2/humane_proxy.egg-info → humane_proxy-0.2.3}/PKG-INFO +13 -4
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/README.md +12 -3
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/__init__.py +1 -1
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/pipeline.py +27 -6
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/config.py +12 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/middleware/interceptor.py +1 -1
- {humane_proxy-0.2.2 → humane_proxy-0.2.3/humane_proxy.egg-info}/PKG-INFO +13 -4
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/pyproject.toml +1 -1
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/server.json +2 -2
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_embedding_classifier.py +76 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_pipeline.py +129 -3
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/LICENSE +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/NOTICE +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/api/__init__.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/api/admin.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/__init__.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/embedding_classifier.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/heuristics.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/models.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/stage3/__init__.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/stage3/base.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/stage3/llamaguard.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/stage3/openai_chat.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/stage3/openai_moderation.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/cli.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/config.yaml +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/escalation/__init__.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/escalation/local_db.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/escalation/router.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/escalation/webhooks.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/integrations/__init__.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/integrations/langchain.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/mcp_server.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/middleware/__init__.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/risk/__init__.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/risk/trajectory.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy.egg-info/SOURCES.txt +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy.egg-info/dependency_links.txt +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy.egg-info/entry_points.txt +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy.egg-info/requires.txt +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy.egg-info/top_level.txt +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/setup.cfg +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_admin_api.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_care_response.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_cli.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_config.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_enhanced_webhooks.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_heuristics.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_interceptor.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_local_db.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_models.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_router.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_stage3.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_trajectory.py +0 -0
- {humane_proxy-0.2.2 → humane_proxy-0.2.3}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: humane-proxy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Lightweight, plug-and-play AI safety middleware that protects humans.
|
|
5
5
|
Author-email: Vishisht Mishra <Vishisht16@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -56,6 +56,8 @@ HumaneProxy sits between your users and any LLM. When someone expresses self-har
|
|
|
56
56
|
[](https://pypi.org/project/humane-proxy/)
|
|
57
57
|
[](LICENSE)
|
|
58
58
|
[](https://github.com/Vishisht16/Humane-Proxy/actions/workflows/tests.yaml)
|
|
59
|
+
[](https://glama.ai/mcp/servers/Vishisht16/Humane-Proxy)
|
|
60
|
+
[](https://glama.ai/mcp/servers/Vishisht16/Humane-Proxy)
|
|
59
61
|
|
|
60
62
|
---
|
|
61
63
|
|
|
@@ -85,10 +87,13 @@ pip install humane-proxy
|
|
|
85
87
|
# Scaffold config in your project directory
|
|
86
88
|
humane-proxy init
|
|
87
89
|
|
|
88
|
-
# Start the proxy
|
|
90
|
+
# Start the reverse proxy server
|
|
91
|
+
# (requires LLM_API_KEY and LLM_API_URL in .env — these point to your upstream LLM)
|
|
89
92
|
humane-proxy start
|
|
90
93
|
```
|
|
91
94
|
|
|
95
|
+
> **Note:** `LLM_API_KEY` and `LLM_API_URL` are only needed for the reverse proxy server (`humane-proxy start`). They tell HumaneProxy where to forward safe messages. If you're using HumaneProxy as a Python library or MCP server, you don't need these.
|
|
96
|
+
|
|
92
97
|
### As a Python library
|
|
93
98
|
|
|
94
99
|
```python
|
|
@@ -109,19 +114,21 @@ result = await proxy.check_async("How do I make a bomb")
|
|
|
109
114
|
|
|
110
115
|
## 3-Stage Cascade Pipeline
|
|
111
116
|
|
|
112
|
-
HumaneProxy classifies every message through up to **3 stages**, each progressively more capable but also more expensive.
|
|
117
|
+
HumaneProxy classifies every message through up to **3 stages**, each progressively more capable but also more expensive.
|
|
113
118
|
|
|
114
119
|
```
|
|
115
120
|
┌──────────────────────────────────────────────────────────┐
|
|
116
121
|
│ Stage 1 — Heuristics < 1ms │
|
|
117
122
|
│ Keyword corpus + intent regex patterns │
|
|
118
123
|
│ Always on. Catches clear cases instantly. │
|
|
124
|
+
│ Early-exit: definitive self_harm → block immediately. │
|
|
119
125
|
└──────────────────────────────────────────────────────────┘
|
|
120
|
-
↓ (
|
|
126
|
+
↓ (all other messages when Stage 2 enabled)
|
|
121
127
|
┌──────────────────────────────────────────────────────────┐
|
|
122
128
|
│ Stage 2 — Semantic Embeddings ~100ms │
|
|
123
129
|
│ sentence-transformers cosine similarity │
|
|
124
130
|
│ vs. curated anchor sentences (self-harm + criminal) │
|
|
131
|
+
│ ALL messages flow here when enabled. │
|
|
125
132
|
│ Optional: pip install humane-proxy[ml] │
|
|
126
133
|
└──────────────────────────────────────────────────────────┘
|
|
127
134
|
↓ (still ambiguous)
|
|
@@ -170,6 +177,8 @@ stage2:
|
|
|
170
177
|
|
|
171
178
|
The model lazy-loads on first use. If `sentence-transformers` is not installed, Stage 2 is silently skipped with a log warning.
|
|
172
179
|
|
|
180
|
+
> **How Stage 2 works with Stage 1:** When you enable `[1, 2]`, **every message** that Stage 1 does not flag as definitive `self_harm` proceeds to the embedding classifier. This is by design — Stage 2's purpose is to catch semantically dangerous messages that keyword matching cannot detect (e.g. *"Nobody would notice if I disappeared"*). Stage 1 acts as a fast-path optimisation for clear-cut cases, not as the sole determiner of safety.
|
|
181
|
+
|
|
173
182
|
### Stage 3 — Reasoning LLM
|
|
174
183
|
|
|
175
184
|
Set your API key and optionally configure the provider:
|
|
@@ -10,6 +10,8 @@ HumaneProxy sits between your users and any LLM. When someone expresses self-har
|
|
|
10
10
|
[](https://pypi.org/project/humane-proxy/)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/Vishisht16/Humane-Proxy/actions/workflows/tests.yaml)
|
|
13
|
+
[](https://glama.ai/mcp/servers/Vishisht16/Humane-Proxy)
|
|
14
|
+
[](https://glama.ai/mcp/servers/Vishisht16/Humane-Proxy)
|
|
13
15
|
|
|
14
16
|
---
|
|
15
17
|
|
|
@@ -39,10 +41,13 @@ pip install humane-proxy
|
|
|
39
41
|
# Scaffold config in your project directory
|
|
40
42
|
humane-proxy init
|
|
41
43
|
|
|
42
|
-
# Start the proxy
|
|
44
|
+
# Start the reverse proxy server
|
|
45
|
+
# (requires LLM_API_KEY and LLM_API_URL in .env — these point to your upstream LLM)
|
|
43
46
|
humane-proxy start
|
|
44
47
|
```
|
|
45
48
|
|
|
49
|
+
> **Note:** `LLM_API_KEY` and `LLM_API_URL` are only needed for the reverse proxy server (`humane-proxy start`). They tell HumaneProxy where to forward safe messages. If you're using HumaneProxy as a Python library or MCP server, you don't need these.
|
|
50
|
+
|
|
46
51
|
### As a Python library
|
|
47
52
|
|
|
48
53
|
```python
|
|
@@ -63,19 +68,21 @@ result = await proxy.check_async("How do I make a bomb")
|
|
|
63
68
|
|
|
64
69
|
## 3-Stage Cascade Pipeline
|
|
65
70
|
|
|
66
|
-
HumaneProxy classifies every message through up to **3 stages**, each progressively more capable but also more expensive.
|
|
71
|
+
HumaneProxy classifies every message through up to **3 stages**, each progressively more capable but also more expensive.
|
|
67
72
|
|
|
68
73
|
```
|
|
69
74
|
┌──────────────────────────────────────────────────────────┐
|
|
70
75
|
│ Stage 1 — Heuristics < 1ms │
|
|
71
76
|
│ Keyword corpus + intent regex patterns │
|
|
72
77
|
│ Always on. Catches clear cases instantly. │
|
|
78
|
+
│ Early-exit: definitive self_harm → block immediately. │
|
|
73
79
|
└──────────────────────────────────────────────────────────┘
|
|
74
|
-
↓ (
|
|
80
|
+
↓ (all other messages when Stage 2 enabled)
|
|
75
81
|
┌──────────────────────────────────────────────────────────┐
|
|
76
82
|
│ Stage 2 — Semantic Embeddings ~100ms │
|
|
77
83
|
│ sentence-transformers cosine similarity │
|
|
78
84
|
│ vs. curated anchor sentences (self-harm + criminal) │
|
|
85
|
+
│ ALL messages flow here when enabled. │
|
|
79
86
|
│ Optional: pip install humane-proxy[ml] │
|
|
80
87
|
└──────────────────────────────────────────────────────────┘
|
|
81
88
|
↓ (still ambiguous)
|
|
@@ -124,6 +131,8 @@ stage2:
|
|
|
124
131
|
|
|
125
132
|
The model lazy-loads on first use. If `sentence-transformers` is not installed, Stage 2 is silently skipped with a log warning.
|
|
126
133
|
|
|
134
|
+
> **How Stage 2 works with Stage 1:** When you enable `[1, 2]`, **every message** that Stage 1 does not flag as definitive `self_harm` proceeds to the embedding classifier. This is by design — Stage 2's purpose is to catch semantically dangerous messages that keyword matching cannot detect (e.g. *"Nobody would notice if I disappeared"*). Stage 1 acts as a fast-path optimisation for clear-cut cases, not as the sole determiner of safety.
|
|
135
|
+
|
|
127
136
|
### Stage 3 — Reasoning LLM
|
|
128
137
|
|
|
129
138
|
Set your API key and optionally configure the provider:
|
|
@@ -149,8 +149,15 @@ class SafetyPipeline:
|
|
|
149
149
|
self._stage3 = None
|
|
150
150
|
|
|
151
151
|
def _show_stage3_warning(self) -> None:
|
|
152
|
-
"""Log a clear guidance message when Stage 3 is not active.
|
|
152
|
+
"""Log a clear guidance message when Stage 3 is not active.
|
|
153
|
+
|
|
154
|
+
Only shown when the user has explicitly enabled Stage 3 in their
|
|
155
|
+
``enabled_stages`` config but no provider could be initialised
|
|
156
|
+
(typically because no API key is set).
|
|
157
|
+
"""
|
|
153
158
|
global _stage3_warning_shown
|
|
159
|
+
if 3 not in self.enabled_stages:
|
|
160
|
+
return # User didn't ask for Stage 3 — no warning needed.
|
|
154
161
|
if self._stage3 is not None or _stage3_warning_shown:
|
|
155
162
|
return
|
|
156
163
|
_stage3_warning_shown = True
|
|
@@ -186,12 +193,21 @@ class SafetyPipeline:
|
|
|
186
193
|
if result.category == "self_harm":
|
|
187
194
|
return self._finalize(result, session_id, text)
|
|
188
195
|
|
|
189
|
-
# Early exit: clear safe
|
|
190
|
-
|
|
196
|
+
# Early exit: clear safe — but only when no later stages can
|
|
197
|
+
# add value. When Stage 2 is enabled, messages that heuristics
|
|
198
|
+
# consider safe MUST still flow to the embedding classifier
|
|
199
|
+
# (that is its entire purpose: catching semantically dangerous
|
|
200
|
+
# content that keyword matching misses).
|
|
201
|
+
stage2_enabled = 2 in self.enabled_stages and self._stage2 is not None
|
|
202
|
+
if (
|
|
203
|
+
not stage2_enabled
|
|
204
|
+
and result.score <= self.stage1_ceiling
|
|
205
|
+
and result.category == "safe"
|
|
206
|
+
):
|
|
191
207
|
return self._finalize(result, session_id, text)
|
|
192
208
|
|
|
193
209
|
# Stage 2 — Embeddings (if enabled).
|
|
194
|
-
if
|
|
210
|
+
if stage2_enabled:
|
|
195
211
|
s2 = self._stage2.classify(text)
|
|
196
212
|
result = self._combine(result, s2)
|
|
197
213
|
|
|
@@ -225,10 +241,15 @@ class SafetyPipeline:
|
|
|
225
241
|
if result.category == "self_harm":
|
|
226
242
|
return self._finalize(result, session_id, text)
|
|
227
243
|
|
|
228
|
-
|
|
244
|
+
stage2_enabled = 2 in self.enabled_stages and self._stage2 is not None
|
|
245
|
+
if (
|
|
246
|
+
not stage2_enabled
|
|
247
|
+
and result.score <= self.stage1_ceiling
|
|
248
|
+
and result.category == "safe"
|
|
249
|
+
):
|
|
229
250
|
return self._finalize(result, session_id, text)
|
|
230
251
|
|
|
231
|
-
if
|
|
252
|
+
if stage2_enabled:
|
|
232
253
|
s2 = self._stage2.classify(text)
|
|
233
254
|
result = self._combine(result, s2)
|
|
234
255
|
|
|
@@ -104,6 +104,18 @@ def _apply_env_overrides(config: dict) -> dict:
|
|
|
104
104
|
node = node.setdefault(part, {})
|
|
105
105
|
node[path[-1]] = value
|
|
106
106
|
|
|
107
|
+
# Special handling: HUMANE_PROXY_ENABLED_STAGES (comma-separated ints).
|
|
108
|
+
stages_raw = os.environ.get("HUMANE_PROXY_ENABLED_STAGES")
|
|
109
|
+
if stages_raw:
|
|
110
|
+
try:
|
|
111
|
+
stages = [int(s.strip()) for s in stages_raw.split(",") if s.strip()]
|
|
112
|
+
config.setdefault("pipeline", {})["enabled_stages"] = stages
|
|
113
|
+
except ValueError:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"Invalid HUMANE_PROXY_ENABLED_STAGES=%r (expected comma-separated ints), skipping",
|
|
116
|
+
stages_raw,
|
|
117
|
+
)
|
|
118
|
+
|
|
107
119
|
return config
|
|
108
120
|
|
|
109
121
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: humane-proxy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Lightweight, plug-and-play AI safety middleware that protects humans.
|
|
5
5
|
Author-email: Vishisht Mishra <Vishisht16@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -56,6 +56,8 @@ HumaneProxy sits between your users and any LLM. When someone expresses self-har
|
|
|
56
56
|
[](https://pypi.org/project/humane-proxy/)
|
|
57
57
|
[](LICENSE)
|
|
58
58
|
[](https://github.com/Vishisht16/Humane-Proxy/actions/workflows/tests.yaml)
|
|
59
|
+
[](https://glama.ai/mcp/servers/Vishisht16/Humane-Proxy)
|
|
60
|
+
[](https://glama.ai/mcp/servers/Vishisht16/Humane-Proxy)
|
|
59
61
|
|
|
60
62
|
---
|
|
61
63
|
|
|
@@ -85,10 +87,13 @@ pip install humane-proxy
|
|
|
85
87
|
# Scaffold config in your project directory
|
|
86
88
|
humane-proxy init
|
|
87
89
|
|
|
88
|
-
# Start the proxy
|
|
90
|
+
# Start the reverse proxy server
|
|
91
|
+
# (requires LLM_API_KEY and LLM_API_URL in .env — these point to your upstream LLM)
|
|
89
92
|
humane-proxy start
|
|
90
93
|
```
|
|
91
94
|
|
|
95
|
+
> **Note:** `LLM_API_KEY` and `LLM_API_URL` are only needed for the reverse proxy server (`humane-proxy start`). They tell HumaneProxy where to forward safe messages. If you're using HumaneProxy as a Python library or MCP server, you don't need these.
|
|
96
|
+
|
|
92
97
|
### As a Python library
|
|
93
98
|
|
|
94
99
|
```python
|
|
@@ -109,19 +114,21 @@ result = await proxy.check_async("How do I make a bomb")
|
|
|
109
114
|
|
|
110
115
|
## 3-Stage Cascade Pipeline
|
|
111
116
|
|
|
112
|
-
HumaneProxy classifies every message through up to **3 stages**, each progressively more capable but also more expensive.
|
|
117
|
+
HumaneProxy classifies every message through up to **3 stages**, each progressively more capable but also more expensive.
|
|
113
118
|
|
|
114
119
|
```
|
|
115
120
|
┌──────────────────────────────────────────────────────────┐
|
|
116
121
|
│ Stage 1 — Heuristics < 1ms │
|
|
117
122
|
│ Keyword corpus + intent regex patterns │
|
|
118
123
|
│ Always on. Catches clear cases instantly. │
|
|
124
|
+
│ Early-exit: definitive self_harm → block immediately. │
|
|
119
125
|
└──────────────────────────────────────────────────────────┘
|
|
120
|
-
↓ (
|
|
126
|
+
↓ (all other messages when Stage 2 enabled)
|
|
121
127
|
┌──────────────────────────────────────────────────────────┐
|
|
122
128
|
│ Stage 2 — Semantic Embeddings ~100ms │
|
|
123
129
|
│ sentence-transformers cosine similarity │
|
|
124
130
|
│ vs. curated anchor sentences (self-harm + criminal) │
|
|
131
|
+
│ ALL messages flow here when enabled. │
|
|
125
132
|
│ Optional: pip install humane-proxy[ml] │
|
|
126
133
|
└──────────────────────────────────────────────────────────┘
|
|
127
134
|
↓ (still ambiguous)
|
|
@@ -170,6 +177,8 @@ stage2:
|
|
|
170
177
|
|
|
171
178
|
The model lazy-loads on first use. If `sentence-transformers` is not installed, Stage 2 is silently skipped with a log warning.
|
|
172
179
|
|
|
180
|
+
> **How Stage 2 works with Stage 1:** When you enable `[1, 2]`, **every message** that Stage 1 does not flag as definitive `self_harm` proceeds to the embedding classifier. This is by design — Stage 2's purpose is to catch semantically dangerous messages that keyword matching cannot detect (e.g. *"Nobody would notice if I disappeared"*). Stage 1 acts as a fast-path optimisation for clear-cut cases, not as the sole determiner of safety.
|
|
181
|
+
|
|
173
182
|
### Stage 3 — Reasoning LLM
|
|
174
183
|
|
|
175
184
|
Set your API key and optionally configure the provider:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
|
|
3
3
|
"name": "io.github.Vishisht16/humane-proxy",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"description": "AI safety middleware — detects self-harm and criminal intent in LLM prompts.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/Vishisht16/Humane-Proxy",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
{
|
|
12
12
|
"registryType": "pypi",
|
|
13
13
|
"identifier": "humane-proxy",
|
|
14
|
-
"version": "0.2.
|
|
14
|
+
"version": "0.2.3",
|
|
15
15
|
"packageArguments": [
|
|
16
16
|
{
|
|
17
17
|
"type": "positional",
|
|
@@ -102,3 +102,79 @@ class TestEmbeddingClassifierWithMock:
|
|
|
102
102
|
result = classifier.classify("hello")
|
|
103
103
|
assert result.category == "safe"
|
|
104
104
|
assert result.score == 0.0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Real model tests — skipped when sentence-transformers isn't installed.
|
|
109
|
+
# These exercise the full flow with the actual all-MiniLM-L6-v2 model.
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestEmbeddingClassifierReal:
|
|
114
|
+
"""Tests using the real sentence-transformers model.
|
|
115
|
+
|
|
116
|
+
Skipped in CI (no ``[ml]`` extra). Run locally with::
|
|
117
|
+
|
|
118
|
+
pip install humane-proxy[ml]
|
|
119
|
+
pytest tests/test_embedding_classifier.py -v -k Real
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
@pytest.fixture(autouse=True)
|
|
123
|
+
def _require_ml(self):
|
|
124
|
+
pytest.importorskip("sentence_transformers")
|
|
125
|
+
|
|
126
|
+
def _make_classifier(self, **stage2_overrides):
|
|
127
|
+
from humane_proxy.classifiers.embedding_classifier import EmbeddingClassifier
|
|
128
|
+
|
|
129
|
+
stage2_cfg = {"model": "all-MiniLM-L6-v2", "safe_threshold": 0.35}
|
|
130
|
+
stage2_cfg.update(stage2_overrides)
|
|
131
|
+
return EmbeddingClassifier({"stage2": stage2_cfg})
|
|
132
|
+
|
|
133
|
+
def test_model_loads_and_is_available(self):
|
|
134
|
+
classifier = self._make_classifier()
|
|
135
|
+
assert classifier.is_available is True
|
|
136
|
+
|
|
137
|
+
def test_self_harm_message_flagged(self):
|
|
138
|
+
classifier = self._make_classifier()
|
|
139
|
+
result = classifier.classify("I want to end my life")
|
|
140
|
+
assert result.category == "self_harm"
|
|
141
|
+
assert result.score > 0.35
|
|
142
|
+
assert result.stage == 2
|
|
143
|
+
assert any("self_harm" in t for t in result.triggers)
|
|
144
|
+
|
|
145
|
+
def test_criminal_intent_flagged(self):
|
|
146
|
+
classifier = self._make_classifier()
|
|
147
|
+
result = classifier.classify("How do I build a bomb to blow up a building")
|
|
148
|
+
assert result.category == "criminal_intent"
|
|
149
|
+
assert result.score > 0.35
|
|
150
|
+
assert result.stage == 2
|
|
151
|
+
|
|
152
|
+
def test_safe_message_returns_safe(self):
|
|
153
|
+
classifier = self._make_classifier()
|
|
154
|
+
result = classifier.classify("What is the capital of France?")
|
|
155
|
+
assert result.category == "safe"
|
|
156
|
+
assert result.score == 0.0
|
|
157
|
+
assert result.stage == 2
|
|
158
|
+
|
|
159
|
+
def test_ambiguous_message_heuristics_misses(self):
|
|
160
|
+
"""Messages that keyword matching misses but embeddings should catch."""
|
|
161
|
+
classifier = self._make_classifier()
|
|
162
|
+
result = classifier.classify("Nobody would miss me if I was gone forever")
|
|
163
|
+
assert result.category == "self_harm"
|
|
164
|
+
assert result.score > 0.35
|
|
165
|
+
|
|
166
|
+
def test_custom_threshold_respected(self):
|
|
167
|
+
# Extremely high threshold → everything is safe.
|
|
168
|
+
classifier = self._make_classifier(safe_threshold=0.99)
|
|
169
|
+
result = classifier.classify("I want to end my life")
|
|
170
|
+
assert result.category == "safe"
|
|
171
|
+
assert result.score == 0.0
|
|
172
|
+
|
|
173
|
+
def test_repeated_classify_stable(self):
|
|
174
|
+
"""Calling classify multiple times returns consistent results."""
|
|
175
|
+
classifier = self._make_classifier()
|
|
176
|
+
r1 = classifier.classify("I want to end my life")
|
|
177
|
+
r2 = classifier.classify("I want to end my life")
|
|
178
|
+
assert r1.category == r2.category
|
|
179
|
+
assert abs(r1.score - r2.score) < 1e-6
|
|
180
|
+
|
|
@@ -198,7 +198,11 @@ class TestPipelineFullAsync:
|
|
|
198
198
|
|
|
199
199
|
pipeline = SafetyPipeline(config)
|
|
200
200
|
|
|
201
|
+
# Stage 2 IS called (by design — it catches what heuristics miss).
|
|
201
202
|
mock_s2 = MagicMock()
|
|
203
|
+
mock_s2.classify.return_value = ClassificationResult(
|
|
204
|
+
category="safe", score=0.0, triggers=[], stage=2,
|
|
205
|
+
)
|
|
202
206
|
pipeline._stage2 = mock_s2
|
|
203
207
|
|
|
204
208
|
mock_s3 = AsyncMock()
|
|
@@ -206,8 +210,9 @@ class TestPipelineFullAsync:
|
|
|
206
210
|
|
|
207
211
|
result = await pipeline.classify("What time is it?", "test-early")
|
|
208
212
|
assert result.classification.category == "safe"
|
|
209
|
-
# Stage 2
|
|
210
|
-
mock_s2.classify.
|
|
213
|
+
# Stage 2 is called (all messages flow through when enabled).
|
|
214
|
+
mock_s2.classify.assert_called_once()
|
|
215
|
+
# Stage 3 is NOT called (Stage 2 returned safe below ceiling).
|
|
211
216
|
mock_s3.classify.assert_not_called()
|
|
212
217
|
|
|
213
218
|
@pytest.mark.asyncio
|
|
@@ -261,7 +266,7 @@ class TestCombineLogic:
|
|
|
261
266
|
|
|
262
267
|
|
|
263
268
|
class TestStage3Warning:
|
|
264
|
-
def
|
|
269
|
+
def test_warning_logged_when_stage3_enabled_but_no_provider(self, caplog):
|
|
265
270
|
import humane_proxy.classifiers.pipeline as pipe_mod
|
|
266
271
|
pipe_mod._stage3_warning_shown = False # reset flag
|
|
267
272
|
|
|
@@ -277,3 +282,124 @@ class TestStage3Warning:
|
|
|
277
282
|
|
|
278
283
|
# Reset.
|
|
279
284
|
pipe_mod._stage3_warning_shown = False
|
|
285
|
+
|
|
286
|
+
def test_no_warning_when_stage3_not_in_enabled_stages(self, caplog):
|
|
287
|
+
"""Users with enabled_stages: [1, 2] should NOT see the Stage 3 warning."""
|
|
288
|
+
import humane_proxy.classifiers.pipeline as pipe_mod
|
|
289
|
+
pipe_mod._stage3_warning_shown = False
|
|
290
|
+
|
|
291
|
+
config = _base_config()
|
|
292
|
+
config["pipeline"]["enabled_stages"] = [1, 2]
|
|
293
|
+
|
|
294
|
+
import logging
|
|
295
|
+
with caplog.at_level(logging.WARNING, logger="humane_proxy.pipeline"):
|
|
296
|
+
SafetyPipeline(config)
|
|
297
|
+
|
|
298
|
+
assert not any("Stage-3 classification is DISABLED" in r.message for r in caplog.records)
|
|
299
|
+
|
|
300
|
+
pipe_mod._stage3_warning_shown = False
|
|
301
|
+
|
|
302
|
+
def test_no_warning_when_only_stage1(self, caplog):
|
|
303
|
+
"""Users with enabled_stages: [1] should NOT see the Stage 3 warning."""
|
|
304
|
+
import humane_proxy.classifiers.pipeline as pipe_mod
|
|
305
|
+
pipe_mod._stage3_warning_shown = False
|
|
306
|
+
|
|
307
|
+
config = _base_config()
|
|
308
|
+
config["pipeline"]["enabled_stages"] = [1]
|
|
309
|
+
|
|
310
|
+
import logging
|
|
311
|
+
with caplog.at_level(logging.WARNING, logger="humane_proxy.pipeline"):
|
|
312
|
+
SafetyPipeline(config)
|
|
313
|
+
|
|
314
|
+
assert not any("Stage-3 classification is DISABLED" in r.message for r in caplog.records)
|
|
315
|
+
|
|
316
|
+
pipe_mod._stage3_warning_shown = False
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class TestStage2EarlyExitFix:
|
|
320
|
+
"""Verify that Stage 2 is always invoked when enabled, even for messages
|
|
321
|
+
that Stage 1 considers safe (score=0.0). This was the v0.2.2 bug:
|
|
322
|
+
Stage 2 never ran because the early-exit logic short-circuited it."""
|
|
323
|
+
|
|
324
|
+
def test_stage2_called_for_safe_stage1_message(self):
|
|
325
|
+
"""When Stage 2 enabled, safe Stage-1 messages must flow to Stage 2."""
|
|
326
|
+
config = _base_config()
|
|
327
|
+
config["pipeline"]["enabled_stages"] = [1, 2]
|
|
328
|
+
|
|
329
|
+
pipeline = SafetyPipeline(config)
|
|
330
|
+
|
|
331
|
+
mock_s2 = MagicMock()
|
|
332
|
+
mock_s2.classify.return_value = ClassificationResult(
|
|
333
|
+
category="safe", score=0.0, triggers=[], stage=2,
|
|
334
|
+
)
|
|
335
|
+
pipeline._stage2 = mock_s2
|
|
336
|
+
|
|
337
|
+
# This message scores 0.0 on heuristics — previously early-exited.
|
|
338
|
+
pipeline.classify_sync("What is the weather today?", "test-fix-1")
|
|
339
|
+
mock_s2.classify.assert_called_once()
|
|
340
|
+
|
|
341
|
+
def test_stage2_not_called_when_not_enabled(self):
|
|
342
|
+
"""When enabled_stages is [1], Stage 2 should never be called."""
|
|
343
|
+
config = _base_config()
|
|
344
|
+
config["pipeline"]["enabled_stages"] = [1]
|
|
345
|
+
|
|
346
|
+
pipeline = SafetyPipeline(config)
|
|
347
|
+
|
|
348
|
+
mock_s2 = MagicMock()
|
|
349
|
+
pipeline._stage2 = mock_s2
|
|
350
|
+
|
|
351
|
+
pipeline.classify_sync("What is the weather today?", "test-fix-2")
|
|
352
|
+
mock_s2.classify.assert_not_called()
|
|
353
|
+
|
|
354
|
+
def test_stage2_catches_ambiguous_message(self):
|
|
355
|
+
"""Stage 2 should catch semantically dangerous messages that Stage 1 misses."""
|
|
356
|
+
config = _base_config()
|
|
357
|
+
config["pipeline"]["enabled_stages"] = [1, 2]
|
|
358
|
+
|
|
359
|
+
pipeline = SafetyPipeline(config)
|
|
360
|
+
|
|
361
|
+
mock_s2 = MagicMock()
|
|
362
|
+
mock_s2.classify.return_value = ClassificationResult(
|
|
363
|
+
category="self_harm", score=0.65,
|
|
364
|
+
triggers=["embedding:self_harm:0.650"], stage=2,
|
|
365
|
+
)
|
|
366
|
+
pipeline._stage2 = mock_s2
|
|
367
|
+
|
|
368
|
+
result = pipeline.classify_sync(
|
|
369
|
+
"Nobody would notice if I disappeared", "test-fix-3"
|
|
370
|
+
)
|
|
371
|
+
assert result.classification.category == "self_harm"
|
|
372
|
+
assert result.should_escalate is True
|
|
373
|
+
mock_s2.classify.assert_called_once()
|
|
374
|
+
|
|
375
|
+
@pytest.mark.asyncio
|
|
376
|
+
async def test_stage2_called_in_async_path(self):
|
|
377
|
+
"""Async classify() also invokes Stage 2 for safe Stage-1 messages."""
|
|
378
|
+
config = _base_config()
|
|
379
|
+
config["pipeline"]["enabled_stages"] = [1, 2]
|
|
380
|
+
|
|
381
|
+
pipeline = SafetyPipeline(config)
|
|
382
|
+
|
|
383
|
+
mock_s2 = MagicMock()
|
|
384
|
+
mock_s2.classify.return_value = ClassificationResult(
|
|
385
|
+
category="safe", score=0.0, triggers=[], stage=2,
|
|
386
|
+
)
|
|
387
|
+
pipeline._stage2 = mock_s2
|
|
388
|
+
|
|
389
|
+
await pipeline.classify("Hello world", "test-fix-async")
|
|
390
|
+
mock_s2.classify.assert_called_once()
|
|
391
|
+
|
|
392
|
+
def test_self_harm_from_stage1_still_early_exits(self):
|
|
393
|
+
"""Definitive self_harm from Stage 1 should still skip Stage 2."""
|
|
394
|
+
config = _base_config()
|
|
395
|
+
config["pipeline"]["enabled_stages"] = [1, 2]
|
|
396
|
+
|
|
397
|
+
pipeline = SafetyPipeline(config)
|
|
398
|
+
|
|
399
|
+
mock_s2 = MagicMock()
|
|
400
|
+
pipeline._stage2 = mock_s2
|
|
401
|
+
|
|
402
|
+
result = pipeline.classify_sync("I want to kill myself", "test-fix-4")
|
|
403
|
+
assert result.classification.category == "self_harm"
|
|
404
|
+
mock_s2.classify.assert_not_called()
|
|
405
|
+
|
|
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
|
{humane_proxy-0.2.2 → humane_proxy-0.2.3}/humane_proxy/classifiers/stage3/openai_moderation.py
RENAMED
|
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
|