openlayer-guardrails 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
|
|
1
|
+
"""OpenLayer Guardrails - Open source guardrail implementations."""
|
2
|
+
|
3
|
+
from openlayer.lib.guardrails import (
|
4
|
+
BaseGuardrail,
|
5
|
+
GuardrailAction,
|
6
|
+
GuardrailResult,
|
7
|
+
BlockStrategy,
|
8
|
+
GuardrailBlockedException,
|
9
|
+
)
|
10
|
+
|
11
|
+
from .pii import PIIGuardrail
|
12
|
+
|
13
|
+
__version__ = "0.1.0"
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"BaseGuardrail",
|
17
|
+
"GuardrailAction",
|
18
|
+
"GuardrailResult",
|
19
|
+
"BlockStrategy",
|
20
|
+
"GuardrailBlockedException",
|
21
|
+
"PIIGuardrail",
|
22
|
+
]
|
@@ -0,0 +1,317 @@
|
|
1
|
+
"""PII (Personally Identifiable Information) guardrail using Presidio."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
|
5
|
+
|
6
|
+
from openlayer.lib.guardrails import (
|
7
|
+
BaseGuardrail,
|
8
|
+
GuardrailAction,
|
9
|
+
GuardrailResult,
|
10
|
+
BlockStrategy,
|
11
|
+
)
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
try:
|
15
|
+
from presidio_analyzer import AnalyzerEngine, RecognizerResult
|
16
|
+
from presidio_anonymizer import AnonymizerEngine
|
17
|
+
from presidio_anonymizer.entities import OperatorConfig
|
18
|
+
except ImportError:
|
19
|
+
# When presidio isn't available, we'll use string literals for type annotations
|
20
|
+
pass
|
21
|
+
|
22
|
+
try:
|
23
|
+
from presidio_analyzer import AnalyzerEngine, RecognizerResult
|
24
|
+
from presidio_anonymizer import AnonymizerEngine
|
25
|
+
from presidio_anonymizer.entities import OperatorConfig
|
26
|
+
|
27
|
+
HAVE_PRESIDIO = True
|
28
|
+
except ImportError:
|
29
|
+
HAVE_PRESIDIO = False
|
30
|
+
AnalyzerEngine = None
|
31
|
+
AnonymizerEngine = None
|
32
|
+
RecognizerResult = None
|
33
|
+
OperatorConfig = None
|
34
|
+
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
37
|
+
|
38
|
+
class PIIGuardrail(BaseGuardrail):
|
39
|
+
"""PII detection and protection guardrail using Microsoft Presidio."""
|
40
|
+
|
41
|
+
# Default entity types that trigger blocking (high-risk PII)
|
42
|
+
DEFAULT_BLOCK_ENTITIES = {
|
43
|
+
"CREDIT_CARD",
|
44
|
+
"CRYPTO",
|
45
|
+
"IBAN_CODE",
|
46
|
+
"IP_ADDRESS",
|
47
|
+
"US_SSN",
|
48
|
+
"US_BANK_NUMBER",
|
49
|
+
"US_DRIVER_LICENSE",
|
50
|
+
"US_PASSPORT",
|
51
|
+
}
|
52
|
+
|
53
|
+
# Default entity types that get redacted/modified (medium-risk PII)
|
54
|
+
DEFAULT_REDACT_ENTITIES = {
|
55
|
+
"PHONE_NUMBER",
|
56
|
+
"EMAIL_ADDRESS",
|
57
|
+
"PERSON",
|
58
|
+
"LOCATION",
|
59
|
+
"DATE_TIME",
|
60
|
+
"NRP",
|
61
|
+
"MEDICAL_LICENSE",
|
62
|
+
"URL",
|
63
|
+
}
|
64
|
+
|
65
|
+
def __init__(
|
66
|
+
self,
|
67
|
+
name: str = "PII Guardrail",
|
68
|
+
enabled: bool = True,
|
69
|
+
block_entities: Optional[Set[str]] = None,
|
70
|
+
redact_entities: Optional[Set[str]] = None,
|
71
|
+
confidence_threshold: float = 0.7,
|
72
|
+
language: str = "en",
|
73
|
+
block_strategy: BlockStrategy = BlockStrategy.RETURN_ERROR_MESSAGE,
|
74
|
+
block_message: str = "Request blocked due to sensitive information",
|
75
|
+
**config,
|
76
|
+
):
|
77
|
+
"""Initialize PII guardrail.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
name: Human-readable name for this guardrail
|
81
|
+
enabled: Whether this guardrail is active
|
82
|
+
block_entities: Set of entity types that should block execution
|
83
|
+
redact_entities: Set of entity types that should be redacted
|
84
|
+
confidence_threshold: Minimum confidence score to trigger action (0.0-1.0)
|
85
|
+
language: Language code for analysis (default: "en")
|
86
|
+
block_strategy: How to handle blocked requests (graceful vs exception)
|
87
|
+
block_message: Custom message for blocked requests
|
88
|
+
**config: Additional configuration
|
89
|
+
"""
|
90
|
+
if not HAVE_PRESIDIO:
|
91
|
+
raise ImportError(
|
92
|
+
"Presidio is required for PII guardrail. "
|
93
|
+
"Install with: pip install openlayer-guardrails[pii]"
|
94
|
+
)
|
95
|
+
|
96
|
+
super().__init__(name=name, enabled=enabled, **config)
|
97
|
+
|
98
|
+
self.block_entities = block_entities or self.DEFAULT_BLOCK_ENTITIES.copy()
|
99
|
+
self.redact_entities = redact_entities or self.DEFAULT_REDACT_ENTITIES.copy()
|
100
|
+
self.confidence_threshold = confidence_threshold
|
101
|
+
self.language = language
|
102
|
+
self.block_strategy = block_strategy
|
103
|
+
self.block_message = block_message
|
104
|
+
|
105
|
+
# Initialize Presidio engines
|
106
|
+
self.analyzer = AnalyzerEngine()
|
107
|
+
self.anonymizer = AnonymizerEngine()
|
108
|
+
|
109
|
+
logger.debug(
|
110
|
+
f"Initialized PII guardrail with block_entities={self.block_entities}, "
|
111
|
+
f"redact_entities={self.redact_entities}, threshold={confidence_threshold}"
|
112
|
+
)
|
113
|
+
|
114
|
+
def check_input(self, inputs: Dict[str, Any]) -> GuardrailResult:
|
115
|
+
"""Check function inputs for PII."""
|
116
|
+
return self._check_data(inputs, data_type="input")
|
117
|
+
|
118
|
+
def check_output(self, output: Any, inputs: Dict[str, Any]) -> GuardrailResult:
|
119
|
+
"""Check function output for PII."""
|
120
|
+
return self._check_data(output, data_type="output")
|
121
|
+
|
122
|
+
def _check_data(self, data: Any, data_type: str) -> GuardrailResult:
|
123
|
+
"""Check arbitrary data for PII."""
|
124
|
+
if not self.enabled:
|
125
|
+
return GuardrailResult(
|
126
|
+
action=GuardrailAction.ALLOW,
|
127
|
+
metadata={
|
128
|
+
"guardrail": self.name,
|
129
|
+
"action": "allow",
|
130
|
+
"reason": "disabled",
|
131
|
+
},
|
132
|
+
)
|
133
|
+
|
134
|
+
# Extract text content from data
|
135
|
+
text_content = self._extract_text(data)
|
136
|
+
if not text_content:
|
137
|
+
return GuardrailResult(
|
138
|
+
action=GuardrailAction.ALLOW,
|
139
|
+
metadata={
|
140
|
+
"guardrail": self.name,
|
141
|
+
"action": "allow",
|
142
|
+
"reason": "no_text_content",
|
143
|
+
},
|
144
|
+
)
|
145
|
+
|
146
|
+
# Analyze for PII and map results to their source text
|
147
|
+
detected_entities = set()
|
148
|
+
self._text_to_results = {} # Store mapping for redaction phase
|
149
|
+
|
150
|
+
for text in text_content:
|
151
|
+
results = self.analyzer.analyze(
|
152
|
+
text=text,
|
153
|
+
language=self.language,
|
154
|
+
entities=list(self.block_entities | self.redact_entities),
|
155
|
+
)
|
156
|
+
|
157
|
+
# Filter by confidence threshold
|
158
|
+
filtered_results = [
|
159
|
+
result
|
160
|
+
for result in results
|
161
|
+
if result.score >= self.confidence_threshold
|
162
|
+
]
|
163
|
+
|
164
|
+
detected_entities.update(result.entity_type for result in filtered_results)
|
165
|
+
self._text_to_results[text] = (
|
166
|
+
filtered_results # Map text to its specific results
|
167
|
+
)
|
168
|
+
|
169
|
+
# Determine action based on detected entities
|
170
|
+
blocked_entities = detected_entities & self.block_entities
|
171
|
+
redacted_entities = detected_entities & self.redact_entities
|
172
|
+
|
173
|
+
metadata = {
|
174
|
+
"guardrail": self.name,
|
175
|
+
"detected_entities": list(detected_entities),
|
176
|
+
"blocked_entities": list(blocked_entities),
|
177
|
+
"redacted_entities": list(redacted_entities),
|
178
|
+
"confidence_threshold": self.confidence_threshold,
|
179
|
+
"data_type": data_type,
|
180
|
+
}
|
181
|
+
|
182
|
+
if blocked_entities:
|
183
|
+
return GuardrailResult(
|
184
|
+
action=GuardrailAction.BLOCK,
|
185
|
+
metadata={**metadata, "action": "blocked"},
|
186
|
+
reason=f"Detected high-risk PII entities: {', '.join(blocked_entities)}",
|
187
|
+
block_strategy=self.block_strategy,
|
188
|
+
error_message=self.block_message,
|
189
|
+
)
|
190
|
+
|
191
|
+
elif redacted_entities:
|
192
|
+
# Redact the sensitive information using our stored mapping
|
193
|
+
modified_data = self._redact_data(
|
194
|
+
data, None
|
195
|
+
) # analysis_results not needed anymore
|
196
|
+
return GuardrailResult(
|
197
|
+
action=GuardrailAction.MODIFY,
|
198
|
+
modified_data=modified_data,
|
199
|
+
metadata={**metadata, "action": "redacted"},
|
200
|
+
reason=f"Redacted PII entities: {', '.join(redacted_entities)}",
|
201
|
+
)
|
202
|
+
|
203
|
+
else:
|
204
|
+
return GuardrailResult(
|
205
|
+
action=GuardrailAction.ALLOW,
|
206
|
+
metadata={**metadata, "action": "allow", "reason": "no_pii_detected"},
|
207
|
+
)
|
208
|
+
|
209
|
+
def _extract_text(self, data: Any) -> List[str]:
|
210
|
+
"""Extract text content from various data types.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
data: Data to extract text from
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
List of text strings found in the data
|
217
|
+
"""
|
218
|
+
texts = []
|
219
|
+
|
220
|
+
if isinstance(data, str):
|
221
|
+
texts.append(data)
|
222
|
+
elif isinstance(data, dict):
|
223
|
+
for value in data.values():
|
224
|
+
texts.extend(self._extract_text(value))
|
225
|
+
elif isinstance(data, (list, tuple)):
|
226
|
+
for item in data:
|
227
|
+
texts.extend(self._extract_text(item))
|
228
|
+
elif hasattr(data, "__str__") and not isinstance(data, (int, float, bool)):
|
229
|
+
# Convert other types to string, but skip basic numeric/boolean types
|
230
|
+
text_repr = str(data)
|
231
|
+
if text_repr and text_repr not in ("True", "False", "None"):
|
232
|
+
texts.append(text_repr)
|
233
|
+
|
234
|
+
return [text for text in texts if text and len(text.strip()) > 0]
|
235
|
+
|
236
|
+
def _redact_data(
|
237
|
+
self, data: Any, analysis_results: List["RecognizerResult"] = None
|
238
|
+
) -> Any:
|
239
|
+
"""Redact PII from data using pre-computed text-to-results mapping."""
|
240
|
+
if isinstance(data, str):
|
241
|
+
return self._redact_text_with_mapping(data)
|
242
|
+
elif isinstance(data, dict):
|
243
|
+
return {
|
244
|
+
key: self._redact_data(value, analysis_results)
|
245
|
+
for key, value in data.items()
|
246
|
+
}
|
247
|
+
elif isinstance(data, list):
|
248
|
+
return [self._redact_data(item, analysis_results) for item in data]
|
249
|
+
elif isinstance(data, tuple):
|
250
|
+
return tuple(self._redact_data(item, analysis_results) for item in data)
|
251
|
+
else:
|
252
|
+
# For other types, convert to string, redact, and return as string
|
253
|
+
if hasattr(data, "__str__"):
|
254
|
+
text_repr = str(data)
|
255
|
+
if text_repr and text_repr not in ("True", "False", "None"):
|
256
|
+
return self._redact_text_with_mapping(text_repr)
|
257
|
+
return data
|
258
|
+
|
259
|
+
def _redact_text_with_mapping(self, text: str) -> str:
|
260
|
+
"""Redact PII from text using pre-computed analysis results."""
|
261
|
+
if not text or not hasattr(self, "_text_to_results"):
|
262
|
+
return text
|
263
|
+
|
264
|
+
# Get the analysis results specific to this text
|
265
|
+
relevant_results = self._text_to_results.get(text, [])
|
266
|
+
|
267
|
+
# Filter to only redaction entities (not blocking entities)
|
268
|
+
redact_results = [
|
269
|
+
result
|
270
|
+
for result in relevant_results
|
271
|
+
if result.entity_type in self.redact_entities
|
272
|
+
and result.score >= self.confidence_threshold
|
273
|
+
]
|
274
|
+
|
275
|
+
if not redact_results:
|
276
|
+
return text
|
277
|
+
|
278
|
+
# Use Presidio anonymizer to redact
|
279
|
+
try:
|
280
|
+
anonymized_result = self.anonymizer.anonymize(
|
281
|
+
text=text,
|
282
|
+
analyzer_results=redact_results,
|
283
|
+
operators={
|
284
|
+
"DEFAULT": OperatorConfig("replace", {"new_value": "[REDACTED]"}),
|
285
|
+
"PHONE_NUMBER": OperatorConfig(
|
286
|
+
"replace", {"new_value": "[PHONE-REDACTED]"}
|
287
|
+
),
|
288
|
+
"EMAIL_ADDRESS": OperatorConfig(
|
289
|
+
"replace", {"new_value": "[EMAIL-REDACTED]"}
|
290
|
+
),
|
291
|
+
"PERSON": OperatorConfig(
|
292
|
+
"replace", {"new_value": "[NAME-REDACTED]"}
|
293
|
+
),
|
294
|
+
"LOCATION": OperatorConfig(
|
295
|
+
"replace", {"new_value": "[LOCATION-REDACTED]"}
|
296
|
+
),
|
297
|
+
},
|
298
|
+
)
|
299
|
+
return anonymized_result.text
|
300
|
+
except Exception as e:
|
301
|
+
logger.warning(f"Failed to anonymize text '{text[:50]}...': {e}")
|
302
|
+
# Fallback to simple replacement
|
303
|
+
return "[REDACTED]"
|
304
|
+
|
305
|
+
def get_metadata(self) -> Dict[str, Any]:
|
306
|
+
"""Get metadata about this guardrail for trace logging."""
|
307
|
+
base_metadata = super().get_metadata()
|
308
|
+
base_metadata.update(
|
309
|
+
{
|
310
|
+
"block_entities": list(self.block_entities),
|
311
|
+
"redact_entities": list(self.redact_entities),
|
312
|
+
"confidence_threshold": self.confidence_threshold,
|
313
|
+
"language": self.language,
|
314
|
+
"presidio_available": HAVE_PRESIDIO,
|
315
|
+
}
|
316
|
+
)
|
317
|
+
return base_metadata
|
@@ -0,0 +1,63 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: openlayer-guardrails
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Guardrails that can be used to check inputs and outputs of functions and works well with Openlayer tracing.
|
5
|
+
Requires-Python: >=3.10
|
6
|
+
Requires-Dist: openlayer>=0.2.0a89
|
7
|
+
Provides-Extra: pii
|
8
|
+
Requires-Dist: presidio-analyzer>=2.2.0; extra == 'pii'
|
9
|
+
Requires-Dist: presidio-anonymizer>=2.2.0; extra == 'pii'
|
10
|
+
Description-Content-Type: text/markdown
|
11
|
+
|
12
|
+
# Openlayer Guardrails
|
13
|
+
|
14
|
+
Open source guardrail implementations that work with Openlayer tracing.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
```bash
|
19
|
+
pip install openlayer-guardrails[pii]
|
20
|
+
```
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
### Standalone Usage
|
25
|
+
|
26
|
+
```python
|
27
|
+
from openlayer_guardrails import PIIGuardrail
|
28
|
+
|
29
|
+
# Create guardrail
|
30
|
+
pii_guard = PIIGuardrail(
|
31
|
+
block_entities={"CREDIT_CARD", "US_SSN"},
|
32
|
+
redact_entities={"EMAIL_ADDRESS", "PHONE_NUMBER"}
|
33
|
+
)
|
34
|
+
|
35
|
+
# Check inputs manually
|
36
|
+
data = {"message": "My email is john@example.com and SSN is 123-45-6789"}
|
37
|
+
result = pii_guard.check_input(data)
|
38
|
+
|
39
|
+
if result.action.value == "block":
|
40
|
+
print(f"Blocked: {result.reason}")
|
41
|
+
elif result.action.value == "modify":
|
42
|
+
print(f"Modified data: {result.modified_data}")
|
43
|
+
```
|
44
|
+
|
45
|
+
### With Openlayer Tracing
|
46
|
+
|
47
|
+
```python
|
48
|
+
from openlayer_guardrails import PIIGuardrail
|
49
|
+
from openlayer.lib.tracing import trace
|
50
|
+
|
51
|
+
# Create guardrail
|
52
|
+
pii_guard = PIIGuardrail()
|
53
|
+
|
54
|
+
# Apply to traced functions
|
55
|
+
@trace(guardrails=[pii_guard])
|
56
|
+
def process_user_data(user_input: str):
|
57
|
+
return f"Processed: {user_input}"
|
58
|
+
|
59
|
+
# PII is automatically detected and handled
|
60
|
+
result = process_user_data("My email is john@example.com")
|
61
|
+
# Output: "Processed: My email is [EMAIL-REDACTED]"
|
62
|
+
```
|
63
|
+
|
@@ -0,0 +1,5 @@
|
|
1
|
+
openlayer_guardrails/__init__.py,sha256=s30XRwaczzs1TNuZfspn7EkP7cJYvhQWGM_tT4mwh3E,431
|
2
|
+
openlayer_guardrails/pii.py,sha256=av5VqNyR-fuEUdlQBbJ9C4j4t9ra0fu0Pag1hsHqf1w,11794
|
3
|
+
openlayer_guardrails-0.1.0.dist-info/METADATA,sha256=8IASuuVgRWq-fEuIqdH-zPIGHG6oD3ObSfQxTHYxm0s,1592
|
4
|
+
openlayer_guardrails-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
openlayer_guardrails-0.1.0.dist-info/RECORD,,
|