drex-policy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- drex_policy-0.1.0/LICENSE +21 -0
- drex_policy-0.1.0/PKG-INFO +190 -0
- drex_policy-0.1.0/README.md +173 -0
- drex_policy-0.1.0/drex_policy/__init__.py +3 -0
- drex_policy-0.1.0/drex_policy/main.py +172 -0
- drex_policy-0.1.0/drex_policy.egg-info/PKG-INFO +190 -0
- drex_policy-0.1.0/drex_policy.egg-info/SOURCES.txt +11 -0
- drex_policy-0.1.0/drex_policy.egg-info/dependency_links.txt +1 -0
- drex_policy-0.1.0/drex_policy.egg-info/requires.txt +7 -0
- drex_policy-0.1.0/drex_policy.egg-info/top_level.txt +1 -0
- drex_policy-0.1.0/pyproject.toml +29 -0
- drex_policy-0.1.0/setup.cfg +4 -0
- drex_policy-0.1.0/tests/test_validator.py +161 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nace.AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drex-policy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Guardrails validator that checks text, or text with images, against a Drex noul or score question.
|
|
5
|
+
Author-email: "Nace.AI" <alish@nace.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: <4,>=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: guardrails-ai>=0.4.0
|
|
11
|
+
Requires-Dist: nace-sdk>=0.6.2
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pyright; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# drex-policy
|
|
19
|
+
|
|
20
|
+
A [Guardrails](https://github.com/guardrails-ai/guardrails) validator that asks Drex, Nace.AI's decision model, one question about a string.
|
|
21
|
+
|
|
22
|
+
| Developed by | Nace.AI |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Date of development | Oct 29, 2026 |
|
|
25
|
+
| Validator type | Moderation, policy |
|
|
26
|
+
| License | MIT |
|
|
27
|
+
| Input/Output | Input, Output |
|
|
28
|
+
|
|
29
|
+
## What it checks
|
|
30
|
+
|
|
31
|
+
`DrexPolicy` asks [Drex](https://console.nace.ai/docs) one question about a string and fails when the answer crosses a threshold you set. Use it for checks such as PII present, off-topic, toxicity level, jailbreak attempt, or competitor mention.
|
|
32
|
+
|
|
33
|
+
The question is a `nace_sdk.Noul` (probability of a yes answer) or a `nace_sdk.Score` (expected index over criteria you list). Drex returns the probability. You choose the threshold with `fail_at`, preferably from labeled examples from your application.
|
|
34
|
+
|
|
35
|
+
- Noul: fails when P(yes) is at or above `fail_at`. With `fail_when="no"`, fails when P(yes) is below `fail_at`.
|
|
36
|
+
- Score: fails when the score is at or above `fail_at`. With `fail_when="below"`, fails when the score is below `fail_at`.
|
|
37
|
+
|
|
38
|
+
The Drex answer is in the result metadata on pass and on fail.
|
|
39
|
+
|
|
40
|
+
## Models
|
|
41
|
+
|
|
42
|
+
The default model is `drex-v2.0`. Text-only checks can use any Drex model id, which is sent unchanged. `drex-latest` is sent as `drex-latest`. It is not `drex-max`.
|
|
43
|
+
|
|
44
|
+
To check images with the text, pass `model="drex-max"` and put the images in `metadata["images"]`. Each image is a URL or a data URL (PNG, JPEG, or WebP). `drex-max` accepts at most 8 images. If images are present and the model is not `drex-max`, or there are more than 8 images, the validator raises `DrexPolicyError` before it calls Drex. It does not drop the images.
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
* Dependencies:
|
|
49
|
+
- guardrails-ai>=0.4.0
|
|
50
|
+
- nace-sdk>=0.6.2
|
|
51
|
+
|
|
52
|
+
* Access keys:
|
|
53
|
+
- `NACE_API_KEY`: a key from [console.nace.ai](https://console.nace.ai)
|
|
54
|
+
- `NACE_BASE_URL` (optional): defaults to `https://console.nace.ai`
|
|
55
|
+
|
|
56
|
+
When the validator builds its own client, each HTTP attempt times out after 60 seconds, the total retry budget is 180 seconds, and requests carry `X-Nace-Client: drex-policy/0.1.0`.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
$ guardrails hub install hub://nace/drex_policy
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You can also install the package from PyPI:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
$ pip install drex-policy
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
With a PyPI install, import the validator from `drex_policy` instead of `guardrails.hub`:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from drex_policy import DrexPolicy
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The examples below use the hub import. The class and its arguments are the same.
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
### Check a string
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from guardrails import Guard
|
|
84
|
+
from guardrails.hub import DrexPolicy
|
|
85
|
+
from nace_sdk import Noul
|
|
86
|
+
|
|
87
|
+
guard = Guard().use(
|
|
88
|
+
DrexPolicy(
|
|
89
|
+
question=Noul(instructions="Does the text contain a personal phone number or email address?"),
|
|
90
|
+
fail_at=0.5,
|
|
91
|
+
on_fail="exception",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
guard.validate("Our office opens at 9am.") # Validator passes
|
|
96
|
+
guard.validate("Call me on +1 415 555 0100.") # Validator fails
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Check text with images
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from guardrails import Guard
|
|
103
|
+
from guardrails.hub import DrexPolicy
|
|
104
|
+
from nace_sdk import Score
|
|
105
|
+
|
|
106
|
+
guard = Guard().use(
|
|
107
|
+
DrexPolicy(
|
|
108
|
+
question=Score(
|
|
109
|
+
instructions="How unsafe is this listing for a general audience?",
|
|
110
|
+
criteria=["safe", "borderline", "unsafe"],
|
|
111
|
+
),
|
|
112
|
+
fail_at=1.5,
|
|
113
|
+
model="drex-max",
|
|
114
|
+
on_fail="exception",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
guard.validate(
|
|
119
|
+
"Vintage camera, works well.",
|
|
120
|
+
metadata={"images": ["https://example.com/listing.jpg"]},
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Check one field of structured output
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from pydantic import BaseModel, Field
|
|
128
|
+
from guardrails import Guard
|
|
129
|
+
from guardrails.hub import DrexPolicy
|
|
130
|
+
from nace_sdk import Noul
|
|
131
|
+
|
|
132
|
+
off_topic = DrexPolicy(
|
|
133
|
+
question=Noul(instructions="Is this reply about something other than our billing product?"),
|
|
134
|
+
fail_at=0.5,
|
|
135
|
+
on_fail="exception",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Reply(BaseModel):
|
|
140
|
+
answer: str = Field(validators=[off_topic])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
guard = Guard.for_pydantic(output_class=Reply)
|
|
144
|
+
guard.parse('{"answer": "Your invoice is in Settings > Billing."}')
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## API reference
|
|
148
|
+
|
|
149
|
+
**`__init__(self, question, fail_at, fail_when=None, question_id="policy", model=None, client=None, on_fail=None)`**
|
|
150
|
+
<ul>
|
|
151
|
+
|
|
152
|
+
**Parameters**
|
|
153
|
+
- **`question`** *(nace_sdk.Noul | nace_sdk.Score)*: The question Drex answers about the value. If `instructions` is omitted, it stays `None` and is not sent.
|
|
154
|
+
- **`fail_at`** *(float)*: For a noul, a P(yes) threshold between 0 and 1. For a score, a finite expected-index threshold.
|
|
155
|
+
- **`fail_when`** *(str)*: `"yes"` (default) or `"no"` for a noul. `"at_or_above"` (default) or `"below"` for a score.
|
|
156
|
+
- **`question_id`** *(str)*: Key of the question in the Drex request and response. Default `"policy"`.
|
|
157
|
+
- **`model`** *(str)*: Drex model id. Default `drex-v2.0`. Images need `drex-max`.
|
|
158
|
+
- **`client`** *(nace_sdk.NaceClient)*: An existing client. If omitted, the validator builds one that reads `NACE_API_KEY` and `NACE_BASE_URL`.
|
|
159
|
+
- **`on_fail`** *(str, Callable)*: What Guardrails does on a fail: one of `exception`, `noop`, `reask`, `fix`, `filter`, `refrain` or `fix_reask`, or a function that Guardrails calls on a fail.
|
|
160
|
+
</ul>
|
|
161
|
+
<br/>
|
|
162
|
+
|
|
163
|
+
**`validate(self, value, metadata) -> ValidationResult`**
|
|
164
|
+
<ul>
|
|
165
|
+
Asks Drex the question about `value` and compares the answer with `fail_at`. Do not call it directly. `guard.parse(...)` and `guard.validate(...)` call it.
|
|
166
|
+
|
|
167
|
+
If Drex cannot answer (a network error, an API error, or a response without the expected answer), the exception propagates. The validator does not turn it into a pass or a fail.
|
|
168
|
+
|
|
169
|
+
**Parameters**
|
|
170
|
+
- **`value`** *(str)*: The text to check.
|
|
171
|
+
- **`metadata`** *(dict)*: Optional.
|
|
172
|
+
|
|
173
|
+
| Key | Type | Description | Default |
|
|
174
|
+
| --- | --- | --- | --- |
|
|
175
|
+
| `images` | list[str] | Image URLs or data URLs. Needs `model="drex-max"`; at most 8. | None |
|
|
176
|
+
|
|
177
|
+
**Result metadata**
|
|
178
|
+
|
|
179
|
+
| Key | Description |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `drex_model` | Model id sent to Drex |
|
|
182
|
+
| `drex_question_id` | The `question_id` |
|
|
183
|
+
| `drex_answer` | The Drex answer, for example `{"type": "noul", "noul": 0.93}` |
|
|
184
|
+
</ul>
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT. Copyright (c) 2026 Nace.AI. See [LICENSE](LICENSE).
|
|
189
|
+
|
|
190
|
+
Scaffolding from [guardrails-ai/validator-template](https://github.com/guardrails-ai/validator-template) (Apache-2.0).
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# drex-policy
|
|
2
|
+
|
|
3
|
+
A [Guardrails](https://github.com/guardrails-ai/guardrails) validator that asks Drex, Nace.AI's decision model, one question about a string.
|
|
4
|
+
|
|
5
|
+
| Developed by | Nace.AI |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Date of development | Oct 29, 2026 |
|
|
8
|
+
| Validator type | Moderation, policy |
|
|
9
|
+
| License | MIT |
|
|
10
|
+
| Input/Output | Input, Output |
|
|
11
|
+
|
|
12
|
+
## What it checks
|
|
13
|
+
|
|
14
|
+
`DrexPolicy` asks [Drex](https://console.nace.ai/docs) one question about a string and fails when the answer crosses a threshold you set. Use it for checks such as PII present, off-topic, toxicity level, jailbreak attempt, or competitor mention.
|
|
15
|
+
|
|
16
|
+
The question is a `nace_sdk.Noul` (probability of a yes answer) or a `nace_sdk.Score` (expected index over criteria you list). Drex returns the probability. You choose the threshold with `fail_at`, preferably from labeled examples from your application.
|
|
17
|
+
|
|
18
|
+
- Noul: fails when P(yes) is at or above `fail_at`. With `fail_when="no"`, fails when P(yes) is below `fail_at`.
|
|
19
|
+
- Score: fails when the score is at or above `fail_at`. With `fail_when="below"`, fails when the score is below `fail_at`.
|
|
20
|
+
|
|
21
|
+
The Drex answer is in the result metadata on pass and on fail.
|
|
22
|
+
|
|
23
|
+
## Models
|
|
24
|
+
|
|
25
|
+
The default model is `drex-v2.0`. Text-only checks can use any Drex model id, which is sent unchanged. `drex-latest` is sent as `drex-latest`. It is not `drex-max`.
|
|
26
|
+
|
|
27
|
+
To check images with the text, pass `model="drex-max"` and put the images in `metadata["images"]`. Each image is a URL or a data URL (PNG, JPEG, or WebP). `drex-max` accepts at most 8 images. If images are present and the model is not `drex-max`, or there are more than 8 images, the validator raises `DrexPolicyError` before it calls Drex. It does not drop the images.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
* Dependencies:
|
|
32
|
+
- guardrails-ai>=0.4.0
|
|
33
|
+
- nace-sdk>=0.6.2
|
|
34
|
+
|
|
35
|
+
* Access keys:
|
|
36
|
+
- `NACE_API_KEY`: a key from [console.nace.ai](https://console.nace.ai)
|
|
37
|
+
- `NACE_BASE_URL` (optional): defaults to `https://console.nace.ai`
|
|
38
|
+
|
|
39
|
+
When the validator builds its own client, each HTTP attempt times out after 60 seconds, the total retry budget is 180 seconds, and requests carry `X-Nace-Client: drex-policy/0.1.0`.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
$ guardrails hub install hub://nace/drex_policy
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You can also install the package from PyPI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
$ pip install drex-policy
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
With a PyPI install, import the validator from `drex_policy` instead of `guardrails.hub`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from drex_policy import DrexPolicy
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The examples below use the hub import. The class and its arguments are the same.
|
|
60
|
+
|
|
61
|
+
## Examples
|
|
62
|
+
|
|
63
|
+
### Check a string
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from guardrails import Guard
|
|
67
|
+
from guardrails.hub import DrexPolicy
|
|
68
|
+
from nace_sdk import Noul
|
|
69
|
+
|
|
70
|
+
guard = Guard().use(
|
|
71
|
+
DrexPolicy(
|
|
72
|
+
question=Noul(instructions="Does the text contain a personal phone number or email address?"),
|
|
73
|
+
fail_at=0.5,
|
|
74
|
+
on_fail="exception",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
guard.validate("Our office opens at 9am.") # Validator passes
|
|
79
|
+
guard.validate("Call me on +1 415 555 0100.") # Validator fails
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Check text with images
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from guardrails import Guard
|
|
86
|
+
from guardrails.hub import DrexPolicy
|
|
87
|
+
from nace_sdk import Score
|
|
88
|
+
|
|
89
|
+
guard = Guard().use(
|
|
90
|
+
DrexPolicy(
|
|
91
|
+
question=Score(
|
|
92
|
+
instructions="How unsafe is this listing for a general audience?",
|
|
93
|
+
criteria=["safe", "borderline", "unsafe"],
|
|
94
|
+
),
|
|
95
|
+
fail_at=1.5,
|
|
96
|
+
model="drex-max",
|
|
97
|
+
on_fail="exception",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
guard.validate(
|
|
102
|
+
"Vintage camera, works well.",
|
|
103
|
+
metadata={"images": ["https://example.com/listing.jpg"]},
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Check one field of structured output
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from pydantic import BaseModel, Field
|
|
111
|
+
from guardrails import Guard
|
|
112
|
+
from guardrails.hub import DrexPolicy
|
|
113
|
+
from nace_sdk import Noul
|
|
114
|
+
|
|
115
|
+
off_topic = DrexPolicy(
|
|
116
|
+
question=Noul(instructions="Is this reply about something other than our billing product?"),
|
|
117
|
+
fail_at=0.5,
|
|
118
|
+
on_fail="exception",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Reply(BaseModel):
|
|
123
|
+
answer: str = Field(validators=[off_topic])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
guard = Guard.for_pydantic(output_class=Reply)
|
|
127
|
+
guard.parse('{"answer": "Your invoice is in Settings > Billing."}')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## API reference
|
|
131
|
+
|
|
132
|
+
**`__init__(self, question, fail_at, fail_when=None, question_id="policy", model=None, client=None, on_fail=None)`**
|
|
133
|
+
<ul>
|
|
134
|
+
|
|
135
|
+
**Parameters**
|
|
136
|
+
- **`question`** *(nace_sdk.Noul | nace_sdk.Score)*: The question Drex answers about the value. If `instructions` is omitted, it stays `None` and is not sent.
|
|
137
|
+
- **`fail_at`** *(float)*: For a noul, a P(yes) threshold between 0 and 1. For a score, a finite expected-index threshold.
|
|
138
|
+
- **`fail_when`** *(str)*: `"yes"` (default) or `"no"` for a noul. `"at_or_above"` (default) or `"below"` for a score.
|
|
139
|
+
- **`question_id`** *(str)*: Key of the question in the Drex request and response. Default `"policy"`.
|
|
140
|
+
- **`model`** *(str)*: Drex model id. Default `drex-v2.0`. Images need `drex-max`.
|
|
141
|
+
- **`client`** *(nace_sdk.NaceClient)*: An existing client. If omitted, the validator builds one that reads `NACE_API_KEY` and `NACE_BASE_URL`.
|
|
142
|
+
- **`on_fail`** *(str, Callable)*: What Guardrails does on a fail: one of `exception`, `noop`, `reask`, `fix`, `filter`, `refrain` or `fix_reask`, or a function that Guardrails calls on a fail.
|
|
143
|
+
</ul>
|
|
144
|
+
<br/>
|
|
145
|
+
|
|
146
|
+
**`validate(self, value, metadata) -> ValidationResult`**
|
|
147
|
+
<ul>
|
|
148
|
+
Asks Drex the question about `value` and compares the answer with `fail_at`. Do not call it directly. `guard.parse(...)` and `guard.validate(...)` call it.
|
|
149
|
+
|
|
150
|
+
If Drex cannot answer (a network error, an API error, or a response without the expected answer), the exception propagates. The validator does not turn it into a pass or a fail.
|
|
151
|
+
|
|
152
|
+
**Parameters**
|
|
153
|
+
- **`value`** *(str)*: The text to check.
|
|
154
|
+
- **`metadata`** *(dict)*: Optional.
|
|
155
|
+
|
|
156
|
+
| Key | Type | Description | Default |
|
|
157
|
+
| --- | --- | --- | --- |
|
|
158
|
+
| `images` | list[str] | Image URLs or data URLs. Needs `model="drex-max"`; at most 8. | None |
|
|
159
|
+
|
|
160
|
+
**Result metadata**
|
|
161
|
+
|
|
162
|
+
| Key | Description |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `drex_model` | Model id sent to Drex |
|
|
165
|
+
| `drex_question_id` | The `question_id` |
|
|
166
|
+
| `drex_answer` | The Drex answer, for example `{"type": "noul", "noul": 0.93}` |
|
|
167
|
+
</ul>
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT. Copyright (c) 2026 Nace.AI. See [LICENSE](LICENSE).
|
|
172
|
+
|
|
173
|
+
Scaffolding from [guardrails-ai/validator-template](https://github.com/guardrails-ai/validator-template) (Apache-2.0).
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
from guardrails.validator_base import (
|
|
6
|
+
FailResult,
|
|
7
|
+
PassResult,
|
|
8
|
+
ValidationResult,
|
|
9
|
+
Validator,
|
|
10
|
+
register_validator,
|
|
11
|
+
)
|
|
12
|
+
from nace_sdk import NaceClient, Noul, NoulAnswer, RetryPolicy, Score, ScoreAnswer
|
|
13
|
+
|
|
14
|
+
VERSION = "0.1.0"
|
|
15
|
+
CLIENT_INFO = f"drex-policy/{VERSION}"
|
|
16
|
+
DEFAULT_MODEL = "drex-v2.0"
|
|
17
|
+
IMAGE_MODEL = "drex-max"
|
|
18
|
+
MAX_IMAGES = 8
|
|
19
|
+
|
|
20
|
+
Question = Noul | Score
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DrexPolicyError(Exception):
|
|
24
|
+
"""Drex could not decide the check. This is not a policy fail."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SystemOneCaller(Protocol):
|
|
28
|
+
def system_one(
|
|
29
|
+
self,
|
|
30
|
+
state: str,
|
|
31
|
+
questions: Mapping[str, Question],
|
|
32
|
+
*,
|
|
33
|
+
model: str | None = None,
|
|
34
|
+
images: Sequence[str] | None = None,
|
|
35
|
+
) -> Any: ...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@register_validator(name="nace/drex_policy", data_type="string")
|
|
39
|
+
class DrexPolicy(Validator):
|
|
40
|
+
"""Validates a string by asking Drex one `noul` or `score` question about it.
|
|
41
|
+
|
|
42
|
+
Registered as `nace/drex_policy` for `string` values. It returns no fix value.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
question (Noul | Score): The Drex question asked about the value.
|
|
46
|
+
fail_at (float): For a noul, the P(yes) threshold in [0, 1]. For a score,
|
|
47
|
+
the expected-index threshold.
|
|
48
|
+
fail_when (str): "yes" (default) or "no" for a noul. "at_or_above"
|
|
49
|
+
(default) or "below" for a score.
|
|
50
|
+
question_id (str): Key of the question in the Drex request. Default "policy".
|
|
51
|
+
model (str): Drex model id. Default "drex-v2.0". Images need "drex-max".
|
|
52
|
+
client (NaceClient): An existing client. Omit it to build one from
|
|
53
|
+
`NACE_API_KEY` and `NACE_BASE_URL`.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
question: Question,
|
|
59
|
+
fail_at: float,
|
|
60
|
+
fail_when: str | None = None,
|
|
61
|
+
question_id: str = "policy",
|
|
62
|
+
model: str | None = None,
|
|
63
|
+
client: SystemOneCaller | None = None,
|
|
64
|
+
on_fail: Callable | None = None,
|
|
65
|
+
**kwargs: Any,
|
|
66
|
+
):
|
|
67
|
+
if not isinstance(question, (Noul, Score)):
|
|
68
|
+
raise TypeError("question must be a nace_sdk.Noul or nace_sdk.Score")
|
|
69
|
+
if not isinstance(question_id, str) or not question_id:
|
|
70
|
+
raise ValueError("question_id must be a non-empty string")
|
|
71
|
+
if isinstance(fail_at, bool) or not isinstance(fail_at, (int, float)):
|
|
72
|
+
raise TypeError("fail_at must be a number")
|
|
73
|
+
if model is not None and (not isinstance(model, str) or not model):
|
|
74
|
+
raise ValueError("model must be a non-empty string")
|
|
75
|
+
if isinstance(question, Noul):
|
|
76
|
+
fail_when = "yes" if fail_when is None else fail_when
|
|
77
|
+
if fail_when not in ("yes", "no"):
|
|
78
|
+
raise ValueError("noul fail_when must be 'yes' or 'no'")
|
|
79
|
+
if not 0.0 <= fail_at <= 1.0:
|
|
80
|
+
raise ValueError("noul fail_at must be between 0 and 1")
|
|
81
|
+
else:
|
|
82
|
+
fail_when = "at_or_above" if fail_when is None else fail_when
|
|
83
|
+
if fail_when not in ("at_or_above", "below"):
|
|
84
|
+
raise ValueError("score fail_when must be 'at_or_above' or 'below'")
|
|
85
|
+
if not math.isfinite(fail_at):
|
|
86
|
+
raise ValueError("score fail_at must be finite")
|
|
87
|
+
super().__init__(
|
|
88
|
+
on_fail=on_fail,
|
|
89
|
+
question=question.model_dump(mode="json"),
|
|
90
|
+
fail_at=fail_at,
|
|
91
|
+
fail_when=fail_when,
|
|
92
|
+
question_id=question_id,
|
|
93
|
+
model=model,
|
|
94
|
+
**kwargs,
|
|
95
|
+
)
|
|
96
|
+
self._question = question
|
|
97
|
+
self._fail_at = float(fail_at)
|
|
98
|
+
self._fail_when = fail_when
|
|
99
|
+
self._question_id = question_id
|
|
100
|
+
self._model = model
|
|
101
|
+
self._client = client
|
|
102
|
+
|
|
103
|
+
def _get_client(self) -> SystemOneCaller:
|
|
104
|
+
if self._client is None:
|
|
105
|
+
self._client = NaceClient(
|
|
106
|
+
client_info=CLIENT_INFO,
|
|
107
|
+
timeout=60,
|
|
108
|
+
retry=RetryPolicy(timeout=180),
|
|
109
|
+
)
|
|
110
|
+
return self._client
|
|
111
|
+
|
|
112
|
+
def _model_for(self, images: Sequence[str] | None) -> str:
|
|
113
|
+
if not images:
|
|
114
|
+
return self._model or DEFAULT_MODEL
|
|
115
|
+
if self._model != IMAGE_MODEL:
|
|
116
|
+
requested = self._model or DEFAULT_MODEL
|
|
117
|
+
raise DrexPolicyError(f"images require model {IMAGE_MODEL!r}, not {requested!r}")
|
|
118
|
+
if len(images) > MAX_IMAGES:
|
|
119
|
+
raise DrexPolicyError(f"{IMAGE_MODEL} accepts at most {MAX_IMAGES} images")
|
|
120
|
+
return IMAGE_MODEL
|
|
121
|
+
|
|
122
|
+
def validate(self, value: Any, metadata: dict[str, Any] | None = None) -> ValidationResult:
|
|
123
|
+
"""Asks Drex the question about `value` and compares the answer with `fail_at`."""
|
|
124
|
+
if not isinstance(value, str):
|
|
125
|
+
raise TypeError("drex_policy validates a string")
|
|
126
|
+
images = (metadata or {}).get("images")
|
|
127
|
+
if images is not None and (
|
|
128
|
+
isinstance(images, str)
|
|
129
|
+
or not isinstance(images, (list, tuple))
|
|
130
|
+
or not all(isinstance(image, str) and image for image in images)
|
|
131
|
+
):
|
|
132
|
+
raise TypeError("metadata['images'] must be a list of non-empty strings")
|
|
133
|
+
model = self._model_for(images)
|
|
134
|
+
response = self._get_client().system_one(
|
|
135
|
+
value,
|
|
136
|
+
{self._question_id: self._question},
|
|
137
|
+
model=model,
|
|
138
|
+
images=list(images) if images else None,
|
|
139
|
+
)
|
|
140
|
+
answer = _answer(response, self._question_id, self._question)
|
|
141
|
+
if isinstance(answer, NoulAnswer):
|
|
142
|
+
label, measured = "P(yes)", float(answer.noul)
|
|
143
|
+
fail_low = self._fail_when == "no"
|
|
144
|
+
else:
|
|
145
|
+
label, measured = "score", float(answer.score)
|
|
146
|
+
fail_low = self._fail_when == "below"
|
|
147
|
+
failed = measured < self._fail_at if fail_low else measured >= self._fail_at
|
|
148
|
+
result_metadata = {
|
|
149
|
+
"drex_model": model,
|
|
150
|
+
"drex_question_id": self._question_id,
|
|
151
|
+
"drex_answer": answer.model_dump(mode="json"),
|
|
152
|
+
}
|
|
153
|
+
if failed:
|
|
154
|
+
relation = "<" if fail_low else ">="
|
|
155
|
+
return FailResult(
|
|
156
|
+
error_message=f"Drex {label}={measured} {relation} fail_at={self._fail_at}",
|
|
157
|
+
metadata=result_metadata,
|
|
158
|
+
)
|
|
159
|
+
return PassResult(metadata=result_metadata)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _answer(response: Any, question_id: str, question: Question) -> NoulAnswer | ScoreAnswer:
|
|
163
|
+
answers = getattr(response, "answers", None)
|
|
164
|
+
if not isinstance(answers, Mapping) or question_id not in answers:
|
|
165
|
+
raise DrexPolicyError(f"Drex response has no answer for {question_id!r}")
|
|
166
|
+
answer = answers[question_id]
|
|
167
|
+
expected = NoulAnswer if isinstance(question, Noul) else ScoreAnswer
|
|
168
|
+
if not isinstance(answer, expected):
|
|
169
|
+
raise DrexPolicyError(
|
|
170
|
+
f"Drex answer for {question_id!r} is {type(answer).__name__}, expected {expected.__name__}"
|
|
171
|
+
)
|
|
172
|
+
return answer
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drex-policy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Guardrails validator that checks text, or text with images, against a Drex noul or score question.
|
|
5
|
+
Author-email: "Nace.AI" <alish@nace.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: <4,>=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: guardrails-ai>=0.4.0
|
|
11
|
+
Requires-Dist: nace-sdk>=0.6.2
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pyright; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# drex-policy
|
|
19
|
+
|
|
20
|
+
A [Guardrails](https://github.com/guardrails-ai/guardrails) validator that asks Drex, Nace.AI's decision model, one question about a string.
|
|
21
|
+
|
|
22
|
+
| Developed by | Nace.AI |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Date of development | Oct 29, 2026 |
|
|
25
|
+
| Validator type | Moderation, policy |
|
|
26
|
+
| License | MIT |
|
|
27
|
+
| Input/Output | Input, Output |
|
|
28
|
+
|
|
29
|
+
## What it checks
|
|
30
|
+
|
|
31
|
+
`DrexPolicy` asks [Drex](https://console.nace.ai/docs) one question about a string and fails when the answer crosses a threshold you set. Use it for checks such as PII present, off-topic, toxicity level, jailbreak attempt, or competitor mention.
|
|
32
|
+
|
|
33
|
+
The question is a `nace_sdk.Noul` (probability of a yes answer) or a `nace_sdk.Score` (expected index over criteria you list). Drex returns the probability. You choose the threshold with `fail_at`, preferably from labeled examples from your application.
|
|
34
|
+
|
|
35
|
+
- Noul: fails when P(yes) is at or above `fail_at`. With `fail_when="no"`, fails when P(yes) is below `fail_at`.
|
|
36
|
+
- Score: fails when the score is at or above `fail_at`. With `fail_when="below"`, fails when the score is below `fail_at`.
|
|
37
|
+
|
|
38
|
+
The Drex answer is in the result metadata on pass and on fail.
|
|
39
|
+
|
|
40
|
+
## Models
|
|
41
|
+
|
|
42
|
+
The default model is `drex-v2.0`. Text-only checks can use any Drex model id, which is sent unchanged. `drex-latest` is sent as `drex-latest`. It is not `drex-max`.
|
|
43
|
+
|
|
44
|
+
To check images with the text, pass `model="drex-max"` and put the images in `metadata["images"]`. Each image is a URL or a data URL (PNG, JPEG, or WebP). `drex-max` accepts at most 8 images. If images are present and the model is not `drex-max`, or there are more than 8 images, the validator raises `DrexPolicyError` before it calls Drex. It does not drop the images.
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
* Dependencies:
|
|
49
|
+
- guardrails-ai>=0.4.0
|
|
50
|
+
- nace-sdk>=0.6.2
|
|
51
|
+
|
|
52
|
+
* Access keys:
|
|
53
|
+
- `NACE_API_KEY`: a key from [console.nace.ai](https://console.nace.ai)
|
|
54
|
+
- `NACE_BASE_URL` (optional): defaults to `https://console.nace.ai`
|
|
55
|
+
|
|
56
|
+
When the validator builds its own client, each HTTP attempt times out after 60 seconds, the total retry budget is 180 seconds, and requests carry `X-Nace-Client: drex-policy/0.1.0`.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
$ guardrails hub install hub://nace/drex_policy
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You can also install the package from PyPI:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
$ pip install drex-policy
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
With a PyPI install, import the validator from `drex_policy` instead of `guardrails.hub`:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from drex_policy import DrexPolicy
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The examples below use the hub import. The class and its arguments are the same.
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
### Check a string
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from guardrails import Guard
|
|
84
|
+
from guardrails.hub import DrexPolicy
|
|
85
|
+
from nace_sdk import Noul
|
|
86
|
+
|
|
87
|
+
guard = Guard().use(
|
|
88
|
+
DrexPolicy(
|
|
89
|
+
question=Noul(instructions="Does the text contain a personal phone number or email address?"),
|
|
90
|
+
fail_at=0.5,
|
|
91
|
+
on_fail="exception",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
guard.validate("Our office opens at 9am.") # Validator passes
|
|
96
|
+
guard.validate("Call me on +1 415 555 0100.") # Validator fails
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Check text with images
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from guardrails import Guard
|
|
103
|
+
from guardrails.hub import DrexPolicy
|
|
104
|
+
from nace_sdk import Score
|
|
105
|
+
|
|
106
|
+
guard = Guard().use(
|
|
107
|
+
DrexPolicy(
|
|
108
|
+
question=Score(
|
|
109
|
+
instructions="How unsafe is this listing for a general audience?",
|
|
110
|
+
criteria=["safe", "borderline", "unsafe"],
|
|
111
|
+
),
|
|
112
|
+
fail_at=1.5,
|
|
113
|
+
model="drex-max",
|
|
114
|
+
on_fail="exception",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
guard.validate(
|
|
119
|
+
"Vintage camera, works well.",
|
|
120
|
+
metadata={"images": ["https://example.com/listing.jpg"]},
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Check one field of structured output
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from pydantic import BaseModel, Field
|
|
128
|
+
from guardrails import Guard
|
|
129
|
+
from guardrails.hub import DrexPolicy
|
|
130
|
+
from nace_sdk import Noul
|
|
131
|
+
|
|
132
|
+
off_topic = DrexPolicy(
|
|
133
|
+
question=Noul(instructions="Is this reply about something other than our billing product?"),
|
|
134
|
+
fail_at=0.5,
|
|
135
|
+
on_fail="exception",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Reply(BaseModel):
|
|
140
|
+
answer: str = Field(validators=[off_topic])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
guard = Guard.for_pydantic(output_class=Reply)
|
|
144
|
+
guard.parse('{"answer": "Your invoice is in Settings > Billing."}')
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## API reference
|
|
148
|
+
|
|
149
|
+
**`__init__(self, question, fail_at, fail_when=None, question_id="policy", model=None, client=None, on_fail=None)`**
|
|
150
|
+
<ul>
|
|
151
|
+
|
|
152
|
+
**Parameters**
|
|
153
|
+
- **`question`** *(nace_sdk.Noul | nace_sdk.Score)*: The question Drex answers about the value. If `instructions` is omitted, it stays `None` and is not sent.
|
|
154
|
+
- **`fail_at`** *(float)*: For a noul, a P(yes) threshold between 0 and 1. For a score, a finite expected-index threshold.
|
|
155
|
+
- **`fail_when`** *(str)*: `"yes"` (default) or `"no"` for a noul. `"at_or_above"` (default) or `"below"` for a score.
|
|
156
|
+
- **`question_id`** *(str)*: Key of the question in the Drex request and response. Default `"policy"`.
|
|
157
|
+
- **`model`** *(str)*: Drex model id. Default `drex-v2.0`. Images need `drex-max`.
|
|
158
|
+
- **`client`** *(nace_sdk.NaceClient)*: An existing client. If omitted, the validator builds one that reads `NACE_API_KEY` and `NACE_BASE_URL`.
|
|
159
|
+
- **`on_fail`** *(str, Callable)*: What Guardrails does on a fail: one of `exception`, `noop`, `reask`, `fix`, `filter`, `refrain` or `fix_reask`, or a function that Guardrails calls on a fail.
|
|
160
|
+
</ul>
|
|
161
|
+
<br/>
|
|
162
|
+
|
|
163
|
+
**`validate(self, value, metadata) -> ValidationResult`**
|
|
164
|
+
<ul>
|
|
165
|
+
Asks Drex the question about `value` and compares the answer with `fail_at`. Do not call it directly. `guard.parse(...)` and `guard.validate(...)` call it.
|
|
166
|
+
|
|
167
|
+
If Drex cannot answer (a network error, an API error, or a response without the expected answer), the exception propagates. The validator does not turn it into a pass or a fail.
|
|
168
|
+
|
|
169
|
+
**Parameters**
|
|
170
|
+
- **`value`** *(str)*: The text to check.
|
|
171
|
+
- **`metadata`** *(dict)*: Optional.
|
|
172
|
+
|
|
173
|
+
| Key | Type | Description | Default |
|
|
174
|
+
| --- | --- | --- | --- |
|
|
175
|
+
| `images` | list[str] | Image URLs or data URLs. Needs `model="drex-max"`; at most 8. | None |
|
|
176
|
+
|
|
177
|
+
**Result metadata**
|
|
178
|
+
|
|
179
|
+
| Key | Description |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `drex_model` | Model id sent to Drex |
|
|
182
|
+
| `drex_question_id` | The `question_id` |
|
|
183
|
+
| `drex_answer` | The Drex answer, for example `{"type": "noul", "noul": 0.93}` |
|
|
184
|
+
</ul>
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT. Copyright (c) 2026 Nace.AI. See [LICENSE](LICENSE).
|
|
189
|
+
|
|
190
|
+
Scaffolding from [guardrails-ai/validator-template](https://github.com/guardrails-ai/validator-template) (Apache-2.0).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
drex_policy/__init__.py
|
|
5
|
+
drex_policy/main.py
|
|
6
|
+
drex_policy.egg-info/PKG-INFO
|
|
7
|
+
drex_policy.egg-info/SOURCES.txt
|
|
8
|
+
drex_policy.egg-info/dependency_links.txt
|
|
9
|
+
drex_policy.egg-info/requires.txt
|
|
10
|
+
drex_policy.egg-info/top_level.txt
|
|
11
|
+
tests/test_validator.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
drex_policy
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "drex-policy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Guardrails validator that checks text, or text with images, against a Drex noul or score question."
|
|
9
|
+
authors = [{ name = "Nace.AI", email = "alish@nace.ai" }]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10,<4"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"guardrails-ai>=0.4.0",
|
|
16
|
+
"nace-sdk>=0.6.2",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = ["pyright", "pytest", "ruff"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
packages = ["drex_policy"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
|
|
28
|
+
[tool.pyright]
|
|
29
|
+
include = ["drex_policy"]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from guardrails import Guard
|
|
6
|
+
from nace_sdk import Noul, RetryPolicy, Score, SystemOneResponse
|
|
7
|
+
|
|
8
|
+
from drex_policy import DrexPolicy
|
|
9
|
+
from drex_policy.main import CLIENT_INFO, DrexPolicyError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FakeClient:
|
|
13
|
+
def __init__(self, answer: dict) -> None:
|
|
14
|
+
self.answer = answer
|
|
15
|
+
self.calls: list[Any] = []
|
|
16
|
+
|
|
17
|
+
def system_one(self, state, questions, *, model=None, images=None):
|
|
18
|
+
self.calls.append((state, questions, model, images))
|
|
19
|
+
return SystemOneResponse.model_validate_json(
|
|
20
|
+
json.dumps(
|
|
21
|
+
{
|
|
22
|
+
"model": model,
|
|
23
|
+
"usage": {"input_tokens": 4, "image_tokens": 0, "output_tokens": 1},
|
|
24
|
+
"answers": {"policy": self.answer},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def noul(probability: float) -> FakeClient:
|
|
31
|
+
return FakeClient({"type": "noul", "noul": probability})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def score(value: float) -> FakeClient:
|
|
35
|
+
return FakeClient(
|
|
36
|
+
{
|
|
37
|
+
"type": "score",
|
|
38
|
+
"score": value,
|
|
39
|
+
"confidence": 0.8,
|
|
40
|
+
"legend": ["none", "low", "high"],
|
|
41
|
+
"probabilities": {"0": 0.1, "1": 0.2, "2": 0.7},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
TOXIC = Noul(instructions="Is this text toxic?")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def guard(client: FakeClient, model: str | None = None) -> Guard:
|
|
50
|
+
return Guard().use(
|
|
51
|
+
DrexPolicy(question=TOXIC, fail_at=0.5, model=model, client=client, on_fail="exception")
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_pass():
|
|
56
|
+
client = noul(0.1)
|
|
57
|
+
result = guard(client).validate("hello")
|
|
58
|
+
assert result.validation_passed is True
|
|
59
|
+
assert result.validated_output == "hello"
|
|
60
|
+
assert client.calls == [("hello", {"policy": TOXIC}, "drex-v2.0", None)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_fail():
|
|
64
|
+
with pytest.raises(Exception) as exc_info:
|
|
65
|
+
guard(noul(0.9)).validate("you idiot")
|
|
66
|
+
assert str(exc_info.value) == (
|
|
67
|
+
"Validation failed for field with errors: Drex P(yes)=0.9 >= fail_at=0.5"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_fail_when_no():
|
|
72
|
+
validator = DrexPolicy(question=TOXIC, fail_at=0.5, fail_when="no", client=noul(0.1))
|
|
73
|
+
result = validator.validate("hello", {})
|
|
74
|
+
assert result.outcome == "fail"
|
|
75
|
+
assert result.error_message == "Drex P(yes)=0.1 < fail_at=0.5"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_fail_keeps_answer_in_metadata():
|
|
79
|
+
validator = DrexPolicy(question=TOXIC, fail_at=0.5, client=noul(0.9))
|
|
80
|
+
result = validator.validate("you idiot", {})
|
|
81
|
+
assert result.outcome == "fail"
|
|
82
|
+
assert result.metadata == {
|
|
83
|
+
"drex_model": "drex-v2.0",
|
|
84
|
+
"drex_question_id": "policy",
|
|
85
|
+
"drex_answer": {"type": "noul", "noul": 0.9},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_score_at_or_above_and_below():
|
|
90
|
+
question = Score(instructions="How toxic?", criteria=["none", "low", "high"])
|
|
91
|
+
high = DrexPolicy(question=question, fail_at=1.5, client=score(2.0))
|
|
92
|
+
assert high.validate("x", {}).outcome == "fail"
|
|
93
|
+
below = DrexPolicy(question=question, fail_at=1.5, fail_when="below", client=score(2.0))
|
|
94
|
+
assert below.validate("x", {}).outcome == "pass"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_drex_latest_is_forwarded_unchanged():
|
|
98
|
+
client = noul(0.1)
|
|
99
|
+
guard(client, model="drex-latest").validate("hello")
|
|
100
|
+
assert client.calls[0][2] == "drex-latest"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_images_go_to_drex_max():
|
|
104
|
+
client = noul(0.1)
|
|
105
|
+
validator = DrexPolicy(question=TOXIC, fail_at=0.5, model="drex-max", client=client)
|
|
106
|
+
validator.validate("caption", {"images": ["data:image/png;base64,AAAA"]})
|
|
107
|
+
assert client.calls[0][2:] == ("drex-max", ["data:image/png;base64,AAAA"])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.parametrize("model", [None, "drex-v2.0", "drex-latest"])
|
|
111
|
+
def test_images_without_drex_max_are_refused(model):
|
|
112
|
+
client = noul(0.1)
|
|
113
|
+
validator = DrexPolicy(question=TOXIC, fail_at=0.5, model=model, client=client)
|
|
114
|
+
with pytest.raises(DrexPolicyError, match="images require model 'drex-max'"):
|
|
115
|
+
validator.validate("caption", {"images": ["data:image/png;base64,AAAA"]})
|
|
116
|
+
assert client.calls == []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_more_than_eight_images_are_refused():
|
|
120
|
+
client = noul(0.1)
|
|
121
|
+
validator = DrexPolicy(question=TOXIC, fail_at=0.5, model="drex-max", client=client)
|
|
122
|
+
with pytest.raises(DrexPolicyError, match="at most 8 images"):
|
|
123
|
+
validator.validate("caption", {"images": ["data:image/png;base64,AAAA"] * 9})
|
|
124
|
+
assert client.calls == []
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_omitted_instructions_stay_none():
|
|
128
|
+
client = noul(0.1)
|
|
129
|
+
DrexPolicy(question=Noul(), fail_at=0.5, client=client).validate("hello", {})
|
|
130
|
+
sent = client.calls[0][1]["policy"]
|
|
131
|
+
assert sent.instructions is None
|
|
132
|
+
assert sent.model_dump(exclude_none=True) == {"type": "noul"}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_built_client_settings(monkeypatch):
|
|
136
|
+
seen = {}
|
|
137
|
+
|
|
138
|
+
class Recorder:
|
|
139
|
+
def __init__(self, **kwargs):
|
|
140
|
+
seen.update(kwargs)
|
|
141
|
+
|
|
142
|
+
monkeypatch.setattr("drex_policy.main.NaceClient", Recorder)
|
|
143
|
+
DrexPolicy(question=TOXIC, fail_at=0.5)._get_client()
|
|
144
|
+
assert seen["client_info"] == CLIENT_INFO == "drex-policy/0.1.0"
|
|
145
|
+
assert seen["timeout"] == 60
|
|
146
|
+
assert seen["retry"] == RetryPolicy(timeout=180)
|
|
147
|
+
assert "model" not in seen
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.parametrize(
|
|
151
|
+
"kwargs",
|
|
152
|
+
[
|
|
153
|
+
{"fail_at": 1.5},
|
|
154
|
+
{"fail_at": 0.5, "fail_when": "below"},
|
|
155
|
+
{"fail_at": 0.5, "question_id": ""},
|
|
156
|
+
{"fail_at": 0.5, "model": ""},
|
|
157
|
+
],
|
|
158
|
+
)
|
|
159
|
+
def test_bad_config(kwargs):
|
|
160
|
+
with pytest.raises(ValueError):
|
|
161
|
+
DrexPolicy(question=TOXIC, **kwargs)
|