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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any