complyedge 0.2.1__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.
- complyedge/__init__.py +876 -0
- complyedge/agents.py +170 -0
- complyedge/cli.py +358 -0
- complyedge/decorators.py +335 -0
- complyedge-0.2.1.dist-info/METADATA +156 -0
- complyedge-0.2.1.dist-info/RECORD +10 -0
- complyedge-0.2.1.dist-info/WHEEL +5 -0
- complyedge-0.2.1.dist-info/entry_points.txt +2 -0
- complyedge-0.2.1.dist-info/licenses/LICENSE +216 -0
- complyedge-0.2.1.dist-info/top_level.txt +1 -0
complyedge/__init__.py
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ComplyEdge Python SDK
|
|
3
|
+
|
|
4
|
+
A Python client library for the ComplyEdge Compliance API.
|
|
5
|
+
Provides both simple and advanced interfaces for AI agent compliance checking.
|
|
6
|
+
|
|
7
|
+
Basic Usage (Simple Interface):
|
|
8
|
+
from complyedge import ComplyEdge, is_safe, check
|
|
9
|
+
|
|
10
|
+
# Option 1: Class instance
|
|
11
|
+
ce = ComplyEdge(api_key="your-key")
|
|
12
|
+
if ce.is_safe("Some text"):
|
|
13
|
+
print("Safe to use")
|
|
14
|
+
|
|
15
|
+
# Option 2: Global functions
|
|
16
|
+
if is_safe("Some text", api_key="your-key"):
|
|
17
|
+
print("Safe to use")
|
|
18
|
+
|
|
19
|
+
result = check("Some text", api_key="your-key")
|
|
20
|
+
print(f"Safe: {result.safe}, Reason: {result.reason}")
|
|
21
|
+
|
|
22
|
+
Advanced Usage (Full Interface):
|
|
23
|
+
from complyedge import ComplyEdgeClient, AsyncComplyEdgeClient
|
|
24
|
+
|
|
25
|
+
# Synchronous client
|
|
26
|
+
with ComplyEdgeClient(api_key="your-key") as client:
|
|
27
|
+
result = client.check_compliance(
|
|
28
|
+
text="Your AI agent output text",
|
|
29
|
+
agent_id="my-bot",
|
|
30
|
+
jurisdiction="EU"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Asynchronous client
|
|
34
|
+
async with AsyncComplyEdgeClient(api_key="your-key") as client:
|
|
35
|
+
result = await client.check_compliance(
|
|
36
|
+
text="Your AI agent output text",
|
|
37
|
+
agent_id="my-bot"
|
|
38
|
+
)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import os
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from enum import Enum
|
|
44
|
+
from typing import Any, Dict, List, Optional
|
|
45
|
+
|
|
46
|
+
import httpx
|
|
47
|
+
from tenacity import (
|
|
48
|
+
retry,
|
|
49
|
+
retry_if_exception_type,
|
|
50
|
+
stop_after_attempt,
|
|
51
|
+
wait_exponential,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__version__ = "0.2.0"
|
|
55
|
+
|
|
56
|
+
# Default API URL — set via COMPLYEDGE_API_URL env var or explicit config
|
|
57
|
+
DEFAULT_BASE_URL = os.getenv("COMPLYEDGE_API_URL")
|
|
58
|
+
|
|
59
|
+
# Decorator functionality will be imported at the end to avoid circular imports
|
|
60
|
+
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# CORE DATA MODELS
|
|
63
|
+
# =============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SeverityLevel(str, Enum):
|
|
67
|
+
"""Severity levels for compliance violations."""
|
|
68
|
+
|
|
69
|
+
LOW = "low"
|
|
70
|
+
MEDIUM = "medium"
|
|
71
|
+
HIGH = "high"
|
|
72
|
+
CRITICAL = "critical"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DirectionType(str, Enum):
|
|
76
|
+
"""Direction of text flow for compliance checking."""
|
|
77
|
+
|
|
78
|
+
PROMPT = "prompt" # Input from user to AI
|
|
79
|
+
OUTPUT = "output" # Output from AI to user
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ComplianceViolation:
|
|
84
|
+
"""A compliance violation detected in text."""
|
|
85
|
+
|
|
86
|
+
rule_id: str
|
|
87
|
+
rule_description: str
|
|
88
|
+
severity: SeverityLevel
|
|
89
|
+
reason: str
|
|
90
|
+
confidence: float
|
|
91
|
+
text_excerpt: Optional[str] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ComplianceResult:
|
|
96
|
+
"""Result of a compliance check."""
|
|
97
|
+
|
|
98
|
+
event_id: str
|
|
99
|
+
allowed: bool
|
|
100
|
+
violations: List[ComplianceViolation]
|
|
101
|
+
latency_ms: int
|
|
102
|
+
bundle_version: str
|
|
103
|
+
evaluated_rules: List[str]
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def safe(self) -> bool:
|
|
107
|
+
"""True if text is safe (no violations)."""
|
|
108
|
+
return self.allowed
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def blocked(self) -> bool:
|
|
112
|
+
"""True if text is blocked (has violations)."""
|
|
113
|
+
return not self.allowed
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def violation_count(self) -> int:
|
|
117
|
+
"""Number of violations found."""
|
|
118
|
+
return len(self.violations)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def reason(self) -> Optional[str]:
|
|
122
|
+
"""Human-readable reason for the decision."""
|
|
123
|
+
if self.allowed:
|
|
124
|
+
return None
|
|
125
|
+
if self.violations:
|
|
126
|
+
return self.violations[0].rule_description
|
|
127
|
+
return "Blocked by compliance system"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ComplianceError(Exception):
|
|
131
|
+
"""Exception raised when compliance checking fails."""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
message: str,
|
|
136
|
+
violations: Optional[List[ComplianceViolation]] = None,
|
|
137
|
+
event_id: Optional[str] = None,
|
|
138
|
+
):
|
|
139
|
+
super().__init__(message)
|
|
140
|
+
self.violations = violations or []
|
|
141
|
+
self.event_id = event_id
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# =============================================================================
|
|
145
|
+
# SIMPLE INTERFACE (recommended for most users)
|
|
146
|
+
# =============================================================================
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ComplyEdge:
|
|
150
|
+
"""
|
|
151
|
+
Simple synchronous client for ComplyEdge Compliance API.
|
|
152
|
+
|
|
153
|
+
This is the recommended interface for most users.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
ce = ComplyEdge(api_key="your-key")
|
|
157
|
+
|
|
158
|
+
if ce.is_safe("Some text"):
|
|
159
|
+
print("Safe to use")
|
|
160
|
+
else:
|
|
161
|
+
result = ce.check("Some text")
|
|
162
|
+
print(f"Blocked: {result.reason}")
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
api_key: str,
|
|
168
|
+
agent_id: str = "default",
|
|
169
|
+
jurisdiction: Optional[str] = None,
|
|
170
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
171
|
+
):
|
|
172
|
+
"""
|
|
173
|
+
Initialize ComplyEdge client.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
api_key: Your ComplyEdge API key
|
|
177
|
+
agent_id: Default agent identifier
|
|
178
|
+
jurisdiction: Regulatory jurisdiction (e.g., 'EU', 'US')
|
|
179
|
+
base_url: API base URL
|
|
180
|
+
"""
|
|
181
|
+
self.api_key = api_key
|
|
182
|
+
self.agent_id = agent_id
|
|
183
|
+
self.jurisdiction = jurisdiction
|
|
184
|
+
self.base_url = (base_url or DEFAULT_BASE_URL or "https://api.complyedge.io").rstrip("/")
|
|
185
|
+
|
|
186
|
+
self._client = httpx.Client(
|
|
187
|
+
base_url=self.base_url,
|
|
188
|
+
timeout=300,
|
|
189
|
+
headers={
|
|
190
|
+
"Authorization": f"Bearer {api_key}",
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
"User-Agent": f"complyedge-python-sdk/{__version__}",
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def is_safe(self, text: str) -> bool:
|
|
197
|
+
"""
|
|
198
|
+
Check if text is safe to use (no compliance violations).
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
text: Text to check
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if safe, False if blocked
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
if ce.is_safe("Hello world"):
|
|
208
|
+
print("Safe to use")
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
result = self.check(text)
|
|
212
|
+
return result.safe
|
|
213
|
+
except Exception:
|
|
214
|
+
# Conservative: assume unsafe if check fails
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
def check(
|
|
218
|
+
self,
|
|
219
|
+
text: str,
|
|
220
|
+
agent_id: Optional[str] = None,
|
|
221
|
+
jurisdiction: Optional[str] = None,
|
|
222
|
+
) -> ComplianceResult:
|
|
223
|
+
"""
|
|
224
|
+
Check text for compliance violations.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
text: Text to check
|
|
228
|
+
agent_id: Agent identifier (uses default if not provided)
|
|
229
|
+
jurisdiction: Regulatory jurisdiction (uses default if not provided)
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
ComplianceResult with detailed information
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
result = ce.check("Some text")
|
|
236
|
+
if result.safe:
|
|
237
|
+
print("Safe")
|
|
238
|
+
else:
|
|
239
|
+
print(f"Blocked: {result.reason}")
|
|
240
|
+
"""
|
|
241
|
+
request_data = {
|
|
242
|
+
"text": text,
|
|
243
|
+
"jurisdiction": jurisdiction or self.jurisdiction or "EU",
|
|
244
|
+
"agent_id": agent_id or self.agent_id,
|
|
245
|
+
"direction": "output",
|
|
246
|
+
"use_semantic_fallback": True,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
response = self._client.post("/v1/check", json=request_data)
|
|
251
|
+
response.raise_for_status()
|
|
252
|
+
data = response.json()
|
|
253
|
+
|
|
254
|
+
violations = [
|
|
255
|
+
ComplianceViolation(
|
|
256
|
+
rule_id=v.get("rule_id", "UNKNOWN"),
|
|
257
|
+
rule_description=v.get("rule_description", ""),
|
|
258
|
+
severity=SeverityLevel(v.get("severity", "medium").lower()),
|
|
259
|
+
reason=v.get("reason", ""),
|
|
260
|
+
confidence=v.get("confidence", 1.0),
|
|
261
|
+
text_excerpt=v.get("text_excerpt"),
|
|
262
|
+
)
|
|
263
|
+
for v in data.get("violations", [])
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
return ComplianceResult(
|
|
267
|
+
event_id=data["event_id"],
|
|
268
|
+
allowed=data.get("allowed", True),
|
|
269
|
+
violations=violations,
|
|
270
|
+
latency_ms=data.get("latency_ms", 0),
|
|
271
|
+
bundle_version=data.get("bundle_version", "opa-rego-v1"),
|
|
272
|
+
evaluated_rules=data.get("evaluated_rules", []),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
except httpx.HTTPStatusError as e:
|
|
276
|
+
try:
|
|
277
|
+
error_data = e.response.json()
|
|
278
|
+
error_detail = error_data.get("detail", str(e))
|
|
279
|
+
except Exception:
|
|
280
|
+
error_detail = str(e)
|
|
281
|
+
|
|
282
|
+
raise ComplianceError(
|
|
283
|
+
f"API error ({e.response.status_code}): {error_detail}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
except httpx.RequestError as e:
|
|
287
|
+
raise ComplianceError(f"Request failed: {str(e)}")
|
|
288
|
+
|
|
289
|
+
def assess_pre_deployment(
|
|
290
|
+
self,
|
|
291
|
+
system_prompt: str,
|
|
292
|
+
model_config: Optional[Dict[str, Any]] = None,
|
|
293
|
+
agent_pipeline: Optional[Dict[str, Any]] = None,
|
|
294
|
+
jurisdiction: str = "EU",
|
|
295
|
+
) -> Dict[str, Any]:
|
|
296
|
+
"""
|
|
297
|
+
Assess an AI system configuration against EU AI Act requirements
|
|
298
|
+
BEFORE deployment.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
system_prompt: The system prompt of the AI agent
|
|
302
|
+
model_config: Model configuration (provider, model_id, temperature)
|
|
303
|
+
agent_pipeline: Pipeline configuration (tools, memory, autonomy_level)
|
|
304
|
+
jurisdiction: Regulatory jurisdiction (default: EU)
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Dict with compliance_score, risk_tier, violations, required_disclosures,
|
|
308
|
+
eu_ai_act_category, estimated_deadline
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
result = ce.assess_pre_deployment(
|
|
312
|
+
system_prompt="You are a hiring assistant...",
|
|
313
|
+
model_config={"provider": "openai", "model_id": "gpt-4"},
|
|
314
|
+
agent_pipeline={"tools": ["resume_scorer"], "autonomy_level": "full"},
|
|
315
|
+
)
|
|
316
|
+
print(f"Risk: {result['risk_tier']}, Score: {result['compliance_score']}")
|
|
317
|
+
"""
|
|
318
|
+
request_data = {
|
|
319
|
+
"system_prompt": system_prompt,
|
|
320
|
+
"jurisdiction": jurisdiction,
|
|
321
|
+
}
|
|
322
|
+
if model_config:
|
|
323
|
+
request_data["model_config"] = model_config
|
|
324
|
+
if agent_pipeline:
|
|
325
|
+
request_data["agent_pipeline"] = agent_pipeline
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
response = self._client.post(
|
|
329
|
+
"/v1/assessment/pre-deployment", json=request_data
|
|
330
|
+
)
|
|
331
|
+
response.raise_for_status()
|
|
332
|
+
return response.json()
|
|
333
|
+
except httpx.HTTPStatusError as e:
|
|
334
|
+
try:
|
|
335
|
+
error_detail = e.response.json().get("detail", str(e))
|
|
336
|
+
except Exception:
|
|
337
|
+
error_detail = str(e)
|
|
338
|
+
raise ComplianceError(
|
|
339
|
+
f"API error ({e.response.status_code}): {error_detail}"
|
|
340
|
+
)
|
|
341
|
+
except httpx.RequestError as e:
|
|
342
|
+
raise ComplianceError(f"Request failed: {str(e)}")
|
|
343
|
+
|
|
344
|
+
def close(self):
|
|
345
|
+
"""Close the HTTP client."""
|
|
346
|
+
self._client.close()
|
|
347
|
+
|
|
348
|
+
def __enter__(self):
|
|
349
|
+
return self
|
|
350
|
+
|
|
351
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
352
|
+
self.close()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# Global convenience functions
|
|
356
|
+
def is_safe(
|
|
357
|
+
text: str,
|
|
358
|
+
api_key: str,
|
|
359
|
+
agent_id: str = "default",
|
|
360
|
+
jurisdiction: Optional[str] = None,
|
|
361
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
362
|
+
) -> bool:
|
|
363
|
+
"""
|
|
364
|
+
Global convenience function to check if text is safe.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
text: Text to check
|
|
368
|
+
api_key: ComplyEdge API key
|
|
369
|
+
agent_id: Agent identifier
|
|
370
|
+
jurisdiction: Regulatory jurisdiction
|
|
371
|
+
base_url: API base URL
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
True if safe, False if blocked
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
from complyedge import is_safe
|
|
378
|
+
|
|
379
|
+
if is_safe("Hello world", api_key="your-key"):
|
|
380
|
+
print("Safe to use")
|
|
381
|
+
"""
|
|
382
|
+
client = ComplyEdge(
|
|
383
|
+
api_key=api_key, agent_id=agent_id, jurisdiction=jurisdiction, base_url=base_url
|
|
384
|
+
)
|
|
385
|
+
try:
|
|
386
|
+
return client.is_safe(text)
|
|
387
|
+
finally:
|
|
388
|
+
client.close()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def check(
|
|
392
|
+
text: str,
|
|
393
|
+
api_key: str,
|
|
394
|
+
agent_id: str = "default",
|
|
395
|
+
jurisdiction: Optional[str] = None,
|
|
396
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
397
|
+
) -> ComplianceResult:
|
|
398
|
+
"""
|
|
399
|
+
Global convenience function to check text compliance.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
text: Text to check
|
|
403
|
+
api_key: ComplyEdge API key
|
|
404
|
+
agent_id: Agent identifier
|
|
405
|
+
jurisdiction: Regulatory jurisdiction
|
|
406
|
+
base_url: API base URL
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
ComplianceResult with detailed information
|
|
410
|
+
|
|
411
|
+
Example:
|
|
412
|
+
from complyedge import check
|
|
413
|
+
|
|
414
|
+
result = check("Some text", api_key="your-key")
|
|
415
|
+
if result.safe:
|
|
416
|
+
print("Safe")
|
|
417
|
+
else:
|
|
418
|
+
print(f"Blocked: {result.reason}")
|
|
419
|
+
"""
|
|
420
|
+
client = ComplyEdge(
|
|
421
|
+
api_key=api_key, agent_id=agent_id, jurisdiction=jurisdiction, base_url=base_url
|
|
422
|
+
)
|
|
423
|
+
try:
|
|
424
|
+
return client.check(text)
|
|
425
|
+
finally:
|
|
426
|
+
client.close()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# =============================================================================
|
|
430
|
+
# ADVANCED INTERFACE (for power users and backward compatibility)
|
|
431
|
+
# =============================================================================
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class ComplyEdgeClient:
|
|
435
|
+
"""
|
|
436
|
+
Synchronous ComplyEdge client for compliance checking.
|
|
437
|
+
|
|
438
|
+
This client provides a simple, synchronous interface to the ComplyEdge API.
|
|
439
|
+
Perfect for quick integrations, testing, and scenarios where you don't need
|
|
440
|
+
async/await patterns.
|
|
441
|
+
|
|
442
|
+
Example:
|
|
443
|
+
Basic usage:
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from complyedge import ComplyEdgeClient
|
|
447
|
+
|
|
448
|
+
client = ComplyEdgeClient(api_key="your-api-key")
|
|
449
|
+
|
|
450
|
+
result = client.check_compliance(
|
|
451
|
+
text="We expect revenue to increase by 25% next quarter",
|
|
452
|
+
agent_id="financial-agent"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if result.allowed:
|
|
456
|
+
print("Text is compliant")
|
|
457
|
+
else:
|
|
458
|
+
print(f"Found {len(result.violations)} violations")
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
With context manager:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
with ComplyEdgeClient(api_key="your-api-key") as client:
|
|
465
|
+
result = client.check_compliance(
|
|
466
|
+
text="Check this message",
|
|
467
|
+
agent_id="my-agent"
|
|
468
|
+
)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
except ComplianceError as e:
|
|
472
|
+
print(f"Compliance check failed: {e}")
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
def __init__(
|
|
476
|
+
self,
|
|
477
|
+
api_key: str,
|
|
478
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
479
|
+
timeout: int = 300,
|
|
480
|
+
max_retries: int = 3,
|
|
481
|
+
verify_ssl: bool = True,
|
|
482
|
+
):
|
|
483
|
+
"""
|
|
484
|
+
Initialize the ComplyEdge client.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
api_key: Your ComplyEdge API key
|
|
488
|
+
base_url: Base URL for the ComplyEdge API
|
|
489
|
+
timeout: Request timeout in seconds
|
|
490
|
+
max_retries: Maximum number of retry attempts
|
|
491
|
+
verify_ssl: Whether to verify SSL certificates
|
|
492
|
+
"""
|
|
493
|
+
self.api_key = api_key
|
|
494
|
+
self.base_url = (base_url or DEFAULT_BASE_URL or "https://api.complyedge.io").rstrip("/")
|
|
495
|
+
|
|
496
|
+
self.client = httpx.Client(
|
|
497
|
+
base_url=self.base_url,
|
|
498
|
+
timeout=timeout,
|
|
499
|
+
verify=verify_ssl,
|
|
500
|
+
headers={
|
|
501
|
+
"Authorization": f"Bearer {api_key}",
|
|
502
|
+
"Content-Type": "application/json",
|
|
503
|
+
"User-Agent": f"complyedge-python-sdk/{__version__}",
|
|
504
|
+
},
|
|
505
|
+
)
|
|
506
|
+
self.max_retries = max_retries
|
|
507
|
+
|
|
508
|
+
def __enter__(self):
|
|
509
|
+
return self
|
|
510
|
+
|
|
511
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
512
|
+
self.close()
|
|
513
|
+
|
|
514
|
+
def close(self):
|
|
515
|
+
"""Close the HTTP client."""
|
|
516
|
+
self.client.close()
|
|
517
|
+
|
|
518
|
+
@retry(
|
|
519
|
+
stop=stop_after_attempt(3),
|
|
520
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
521
|
+
retry=retry_if_exception_type((httpx.RequestError, httpx.HTTPStatusError)),
|
|
522
|
+
)
|
|
523
|
+
def check_compliance(
|
|
524
|
+
self,
|
|
525
|
+
text: str,
|
|
526
|
+
agent_id: str,
|
|
527
|
+
jurisdiction: Optional[str] = None,
|
|
528
|
+
direction: DirectionType = DirectionType.OUTPUT,
|
|
529
|
+
context: Optional[Dict[str, Any]] = None,
|
|
530
|
+
use_semantic_fallback: bool = True,
|
|
531
|
+
raise_on_violation: bool = False,
|
|
532
|
+
) -> ComplianceResult:
|
|
533
|
+
"""
|
|
534
|
+
Check text for compliance violations.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
text: Text to evaluate for compliance
|
|
538
|
+
agent_id: Identifier of the AI agent
|
|
539
|
+
jurisdiction: Regulatory jurisdiction (e.g., 'EU', 'US', 'US-CA')
|
|
540
|
+
direction: Whether this is a prompt or output
|
|
541
|
+
context: Additional context for evaluation
|
|
542
|
+
use_semantic_fallback: Use LLM-based evaluation for ambiguous cases
|
|
543
|
+
raise_on_violation: Raise ComplianceError if violations are found
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
ComplianceResult with decision and any violations
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
ComplianceError: If compliance check fails or violations found
|
|
550
|
+
(when raise_on_violation=True)
|
|
551
|
+
"""
|
|
552
|
+
try:
|
|
553
|
+
request_data = {
|
|
554
|
+
"text": text,
|
|
555
|
+
"jurisdiction": jurisdiction or "EU",
|
|
556
|
+
"agent_id": agent_id,
|
|
557
|
+
"direction": direction.value if hasattr(direction, "value") else str(direction),
|
|
558
|
+
"use_semantic_fallback": use_semantic_fallback,
|
|
559
|
+
}
|
|
560
|
+
if context:
|
|
561
|
+
request_data["context"] = context
|
|
562
|
+
|
|
563
|
+
response = self.client.post("/v1/check", json=request_data)
|
|
564
|
+
response.raise_for_status()
|
|
565
|
+
data = response.json()
|
|
566
|
+
|
|
567
|
+
violations = [
|
|
568
|
+
ComplianceViolation(
|
|
569
|
+
rule_id=v.get("rule_id", "UNKNOWN"),
|
|
570
|
+
rule_description=v.get("rule_description", ""),
|
|
571
|
+
severity=SeverityLevel(v.get("severity", "medium").lower()),
|
|
572
|
+
reason=v.get("reason", ""),
|
|
573
|
+
confidence=v.get("confidence", 1.0),
|
|
574
|
+
text_excerpt=v.get("text_excerpt"),
|
|
575
|
+
)
|
|
576
|
+
for v in data.get("violations", [])
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
result = ComplianceResult(
|
|
580
|
+
event_id=data["event_id"],
|
|
581
|
+
allowed=data.get("allowed", True),
|
|
582
|
+
violations=violations,
|
|
583
|
+
latency_ms=data.get("latency_ms", 0),
|
|
584
|
+
bundle_version=data.get("bundle_version", "opa-rego-v1"),
|
|
585
|
+
evaluated_rules=data.get("evaluated_rules", []),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
if raise_on_violation and not result.allowed:
|
|
589
|
+
raise ComplianceError(
|
|
590
|
+
f"Compliance violations detected: "
|
|
591
|
+
f"{len(violations)} rules violated",
|
|
592
|
+
violations=violations,
|
|
593
|
+
event_id=result.event_id,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
return result
|
|
597
|
+
|
|
598
|
+
except httpx.HTTPStatusError as e:
|
|
599
|
+
error_detail = "Unknown error"
|
|
600
|
+
try:
|
|
601
|
+
error_data = e.response.json()
|
|
602
|
+
error_detail = error_data.get("detail", str(e))
|
|
603
|
+
except Exception:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
raise ComplianceError(
|
|
607
|
+
f"API error ({e.response.status_code}): {error_detail}"
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
except httpx.RequestError as e:
|
|
611
|
+
raise ComplianceError(f"Request failed: {str(e)}")
|
|
612
|
+
|
|
613
|
+
def get_rules_info(self) -> Dict[str, Any]:
|
|
614
|
+
"""Get information about the current rule bundle."""
|
|
615
|
+
try:
|
|
616
|
+
response = self.client.get("/v1/rules/info")
|
|
617
|
+
response.raise_for_status()
|
|
618
|
+
return response.json()
|
|
619
|
+
|
|
620
|
+
except httpx.HTTPStatusError as e:
|
|
621
|
+
raise ComplianceError(f"Failed to get rules info: {e}")
|
|
622
|
+
|
|
623
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
624
|
+
"""Get compliance metrics for your tenant."""
|
|
625
|
+
try:
|
|
626
|
+
response = self.client.get("/v1/metrics")
|
|
627
|
+
response.raise_for_status()
|
|
628
|
+
return response.json()
|
|
629
|
+
|
|
630
|
+
except httpx.HTTPStatusError as e:
|
|
631
|
+
raise ComplianceError(f"Failed to get metrics: {e}")
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class AsyncComplyEdgeClient:
|
|
635
|
+
"""
|
|
636
|
+
Async client for ComplyEdge Compliance API.
|
|
637
|
+
|
|
638
|
+
This is the advanced async interface for power users.
|
|
639
|
+
For simple use cases, use the ComplyEdge class instead.
|
|
640
|
+
|
|
641
|
+
Example:
|
|
642
|
+
async with AsyncComplyEdgeClient(api_key="your-key") as client:
|
|
643
|
+
result = await client.check_compliance(
|
|
644
|
+
text="Your AI agent output text",
|
|
645
|
+
agent_id="my-bot"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
if result.allowed:
|
|
649
|
+
print("Text is compliant")
|
|
650
|
+
else:
|
|
651
|
+
print(f"Found {len(result.violations)} violations")
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
def __init__(
|
|
655
|
+
self,
|
|
656
|
+
api_key: str,
|
|
657
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
658
|
+
timeout: int = 300,
|
|
659
|
+
max_retries: int = 3,
|
|
660
|
+
verify_ssl: bool = True,
|
|
661
|
+
):
|
|
662
|
+
"""Initialize the async ComplyEdge client."""
|
|
663
|
+
self.api_key = api_key
|
|
664
|
+
self.base_url = (base_url or DEFAULT_BASE_URL or "https://api.complyedge.io").rstrip("/")
|
|
665
|
+
|
|
666
|
+
self.client = httpx.AsyncClient(
|
|
667
|
+
base_url=self.base_url,
|
|
668
|
+
timeout=timeout,
|
|
669
|
+
verify=verify_ssl,
|
|
670
|
+
headers={
|
|
671
|
+
"Authorization": f"Bearer {api_key}",
|
|
672
|
+
"Content-Type": "application/json",
|
|
673
|
+
"User-Agent": f"complyedge-python-sdk/{__version__}",
|
|
674
|
+
},
|
|
675
|
+
)
|
|
676
|
+
self.max_retries = max_retries
|
|
677
|
+
|
|
678
|
+
async def __aenter__(self):
|
|
679
|
+
return self
|
|
680
|
+
|
|
681
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
682
|
+
await self.close()
|
|
683
|
+
|
|
684
|
+
async def close(self):
|
|
685
|
+
"""Close the HTTP client."""
|
|
686
|
+
await self.client.aclose()
|
|
687
|
+
|
|
688
|
+
@retry(
|
|
689
|
+
stop=stop_after_attempt(3),
|
|
690
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
691
|
+
retry=retry_if_exception_type((httpx.RequestError, httpx.HTTPStatusError)),
|
|
692
|
+
)
|
|
693
|
+
async def check_compliance(
|
|
694
|
+
self,
|
|
695
|
+
text: str,
|
|
696
|
+
agent_id: str,
|
|
697
|
+
jurisdiction: Optional[str] = None,
|
|
698
|
+
direction: DirectionType = DirectionType.OUTPUT,
|
|
699
|
+
context: Optional[Dict[str, Any]] = None,
|
|
700
|
+
use_semantic_fallback: bool = True,
|
|
701
|
+
raise_on_violation: bool = False,
|
|
702
|
+
) -> ComplianceResult:
|
|
703
|
+
"""Async version of check_compliance."""
|
|
704
|
+
try:
|
|
705
|
+
request_data = {
|
|
706
|
+
"text": text,
|
|
707
|
+
"jurisdiction": jurisdiction or "EU",
|
|
708
|
+
"agent_id": agent_id,
|
|
709
|
+
"direction": direction.value if hasattr(direction, "value") else str(direction),
|
|
710
|
+
"use_semantic_fallback": use_semantic_fallback,
|
|
711
|
+
}
|
|
712
|
+
if context:
|
|
713
|
+
request_data["context"] = context
|
|
714
|
+
|
|
715
|
+
response = await self.client.post("/v1/check", json=request_data)
|
|
716
|
+
response.raise_for_status()
|
|
717
|
+
data = response.json()
|
|
718
|
+
|
|
719
|
+
violations = [
|
|
720
|
+
ComplianceViolation(
|
|
721
|
+
rule_id=v.get("rule_id", "UNKNOWN"),
|
|
722
|
+
rule_description=v.get("rule_description", ""),
|
|
723
|
+
severity=SeverityLevel(v.get("severity", "medium").lower()),
|
|
724
|
+
reason=v.get("reason", ""),
|
|
725
|
+
confidence=v.get("confidence", 1.0),
|
|
726
|
+
text_excerpt=v.get("text_excerpt"),
|
|
727
|
+
)
|
|
728
|
+
for v in data.get("violations", [])
|
|
729
|
+
]
|
|
730
|
+
|
|
731
|
+
result = ComplianceResult(
|
|
732
|
+
event_id=data["event_id"],
|
|
733
|
+
allowed=data.get("allowed", True),
|
|
734
|
+
violations=violations,
|
|
735
|
+
latency_ms=data.get("latency_ms", 0),
|
|
736
|
+
bundle_version=data.get("bundle_version", "opa-rego-v1"),
|
|
737
|
+
evaluated_rules=data.get("evaluated_rules", []),
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
if raise_on_violation and not result.allowed:
|
|
741
|
+
raise ComplianceError(
|
|
742
|
+
f"Compliance violations detected: "
|
|
743
|
+
f"{len(violations)} rules violated",
|
|
744
|
+
violations=violations,
|
|
745
|
+
event_id=result.event_id,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
return result
|
|
749
|
+
|
|
750
|
+
except httpx.HTTPStatusError as e:
|
|
751
|
+
error_detail = "Unknown error"
|
|
752
|
+
try:
|
|
753
|
+
error_data = e.response.json()
|
|
754
|
+
error_detail = error_data.get("detail", str(e))
|
|
755
|
+
except Exception:
|
|
756
|
+
pass
|
|
757
|
+
|
|
758
|
+
raise ComplianceError(
|
|
759
|
+
f"API error ({e.response.status_code}): {error_detail}"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
except httpx.RequestError as e:
|
|
763
|
+
raise ComplianceError(f"Request failed: {str(e)}")
|
|
764
|
+
|
|
765
|
+
async def get_rules_info(self) -> Dict[str, Any]:
|
|
766
|
+
"""Get information about the current rule bundle."""
|
|
767
|
+
try:
|
|
768
|
+
response = await self.client.get("/v1/rules/info")
|
|
769
|
+
response.raise_for_status()
|
|
770
|
+
return response.json()
|
|
771
|
+
|
|
772
|
+
except httpx.HTTPStatusError as e:
|
|
773
|
+
raise ComplianceError(f"Failed to get rules info: {e}")
|
|
774
|
+
|
|
775
|
+
async def get_metrics(self) -> Dict[str, Any]:
|
|
776
|
+
"""Get compliance metrics for your tenant."""
|
|
777
|
+
try:
|
|
778
|
+
response = await self.client.get("/v1/metrics")
|
|
779
|
+
response.raise_for_status()
|
|
780
|
+
return response.json()
|
|
781
|
+
|
|
782
|
+
except httpx.HTTPStatusError as e:
|
|
783
|
+
raise ComplianceError(f"Failed to get metrics: {e}")
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
# =============================================================================
|
|
787
|
+
# BACKWARD COMPATIBILITY
|
|
788
|
+
# =============================================================================
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def check_compliance(
|
|
792
|
+
text: str, agent_id: str, api_key: str, jurisdiction: Optional[str] = None, **kwargs
|
|
793
|
+
) -> ComplianceResult:
|
|
794
|
+
"""
|
|
795
|
+
Quick compliance check without creating a client instance.
|
|
796
|
+
|
|
797
|
+
DEPRECATED: Use ComplyEdge class or global check() function instead.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
text: Text to check
|
|
801
|
+
agent_id: Agent identifier
|
|
802
|
+
api_key: ComplyEdge API key
|
|
803
|
+
jurisdiction: Regulatory jurisdiction
|
|
804
|
+
**kwargs: Additional arguments passed to check_compliance
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
ComplianceResult
|
|
808
|
+
"""
|
|
809
|
+
with ComplyEdgeClient(api_key=api_key) as client:
|
|
810
|
+
return client.check_compliance(
|
|
811
|
+
text=text, agent_id=agent_id, jurisdiction=jurisdiction, **kwargs
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# =============================================================================
|
|
816
|
+
# AUTO-CONFIGURATION FROM ENVIRONMENT
|
|
817
|
+
# =============================================================================
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def get_api_key() -> Optional[str]:
|
|
821
|
+
"""Get API key from environment variables."""
|
|
822
|
+
return os.environ.get("COMPLYEDGE_API_KEY")
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
# Convenience instance with environment configuration
|
|
826
|
+
if get_api_key():
|
|
827
|
+
default_client = ComplyEdge(api_key=get_api_key())
|
|
828
|
+
|
|
829
|
+
def safe(text: str) -> bool:
|
|
830
|
+
"""Check if text is safe using environment-configured client."""
|
|
831
|
+
return default_client.is_safe(text)
|
|
832
|
+
|
|
833
|
+
def compliance_check(text: str) -> ComplianceResult:
|
|
834
|
+
"""Check compliance using environment-configured client."""
|
|
835
|
+
return default_client.check(text)
|
|
836
|
+
|
|
837
|
+
else:
|
|
838
|
+
default_client = None
|
|
839
|
+
safe = None
|
|
840
|
+
compliance_check = None
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
# Export public API
|
|
844
|
+
__all__ = [
|
|
845
|
+
# Simple interface (recommended)
|
|
846
|
+
"ComplyEdge",
|
|
847
|
+
"is_safe",
|
|
848
|
+
"check",
|
|
849
|
+
# Advanced interface
|
|
850
|
+
"ComplyEdgeClient",
|
|
851
|
+
"AsyncComplyEdgeClient",
|
|
852
|
+
# Data models
|
|
853
|
+
"ComplianceResult",
|
|
854
|
+
"ComplianceViolation",
|
|
855
|
+
"ComplianceError",
|
|
856
|
+
"SeverityLevel",
|
|
857
|
+
"DirectionType",
|
|
858
|
+
# Decorator interface
|
|
859
|
+
"compliance_check",
|
|
860
|
+
"ComplianceConfig",
|
|
861
|
+
"default_violation_handler",
|
|
862
|
+
# Backward compatibility
|
|
863
|
+
"check_compliance",
|
|
864
|
+
# Environment helpers
|
|
865
|
+
"get_api_key",
|
|
866
|
+
"safe",
|
|
867
|
+
# Agent integration module (import separately)
|
|
868
|
+
# from complyedge.agents import create_compliance_guardrail
|
|
869
|
+
]
|
|
870
|
+
|
|
871
|
+
# Import decorator functionality after all classes are defined to avoid circular imports
|
|
872
|
+
from .decorators import ( # noqa: E402
|
|
873
|
+
ComplianceConfig,
|
|
874
|
+
compliance_check,
|
|
875
|
+
default_violation_handler,
|
|
876
|
+
)
|