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 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
+ )