mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-24 02:25:29 +00:00
Add compliance checker endpoints + UI panel (#21432)
* eu_ai_act_article5_prohibited_practices_fr * add backend for checkers * test checker * checkEuAiActCompliance * register compliance router in proxy_server.py * add compliance check functions to networking.tsx * fix useEffect dependency array in CompliancePanel * ui fixes
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Compliance checker for EU AI Act and GDPR regulations.
|
||||
|
||||
Provides guardrail-agnostic compliance validation based on guardrail modes
|
||||
and execution results rather than specific guardrail names.
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from litellm.types.proxy.compliance_endpoints import (
|
||||
ComplianceCheckRequest,
|
||||
ComplianceCheckResult,
|
||||
)
|
||||
|
||||
|
||||
class ComplianceChecker:
|
||||
"""
|
||||
Validates compliance with EU AI Act and GDPR regulations.
|
||||
|
||||
Uses guardrail-agnostic checks based on:
|
||||
- Whether any guardrails ran
|
||||
- Guardrail execution mode (pre-call, post-call, etc.)
|
||||
- Whether guardrails intervened/blocked content
|
||||
- Completeness of audit records
|
||||
"""
|
||||
|
||||
def __init__(self, data: ComplianceCheckRequest):
|
||||
self.data = data
|
||||
self.guardrails = data.guardrail_information or []
|
||||
|
||||
def _get_guardrails_by_mode(self, mode: str) -> List[Dict]:
|
||||
"""
|
||||
Get all guardrails that ran in a specific mode.
|
||||
|
||||
If a guardrail doesn't have a mode specified, it's treated as pre-call
|
||||
(the most common case).
|
||||
"""
|
||||
result = []
|
||||
for g in self.guardrails:
|
||||
g_mode = g.get("guardrail_mode")
|
||||
# If no mode specified, default to pre_call
|
||||
if g_mode is None and mode == "pre_call":
|
||||
result.append(g)
|
||||
elif g_mode == mode:
|
||||
result.append(g)
|
||||
return result
|
||||
|
||||
def _has_guardrail_intervention(self, guardrails: List[Dict]) -> bool:
|
||||
"""Check if any guardrail intervened (blocked/masked content)."""
|
||||
for g in guardrails:
|
||||
status = g.get("guardrail_status", "")
|
||||
if status in ["guardrail_intervened", "failed", "blocked"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _all_guardrails_passed(self, guardrails: List[Dict]) -> bool:
|
||||
"""Check if all guardrails passed (no issues detected)."""
|
||||
if not guardrails:
|
||||
return False
|
||||
return all(g.get("guardrail_status") == "success" for g in guardrails)
|
||||
|
||||
# ── EU AI Act Helper Methods ────────────────────────────────────────────
|
||||
|
||||
def _check_art_9_guardrails_applied(self) -> ComplianceCheckResult:
|
||||
"""Art. 9: Check if any guardrails were applied."""
|
||||
has_guardrails = len(self.guardrails) > 0
|
||||
return ComplianceCheckResult(
|
||||
check_name="Guardrails applied",
|
||||
article="Art. 9",
|
||||
passed=has_guardrails,
|
||||
detail=(
|
||||
f"{len(self.guardrails)} guardrail(s) applied"
|
||||
if has_guardrails
|
||||
else "No guardrails applied"
|
||||
),
|
||||
)
|
||||
|
||||
def _check_art_5_content_screened(self) -> ComplianceCheckResult:
|
||||
"""Art. 5: Check if content was screened before LLM (pre-call)."""
|
||||
pre_call_guardrails = self._get_guardrails_by_mode("pre_call")
|
||||
has_pre_call = len(pre_call_guardrails) > 0
|
||||
return ComplianceCheckResult(
|
||||
check_name="Content screened before LLM",
|
||||
article="Art. 5",
|
||||
passed=has_pre_call,
|
||||
detail=(
|
||||
f"{len(pre_call_guardrails)} pre-call guardrail(s) screened content"
|
||||
if has_pre_call
|
||||
else "No pre-call screening applied"
|
||||
),
|
||||
)
|
||||
|
||||
def _check_art_12_audit_complete(self) -> ComplianceCheckResult:
|
||||
"""Art. 12: Check if audit record is complete."""
|
||||
has_user = bool(self.data.user_id)
|
||||
has_model = bool(self.data.model)
|
||||
has_timestamp = bool(self.data.timestamp)
|
||||
has_guardrails = len(self.guardrails) > 0
|
||||
audit_complete = has_user and has_model and has_timestamp and has_guardrails
|
||||
|
||||
missing = []
|
||||
if not has_user:
|
||||
missing.append("user_id")
|
||||
if not has_model:
|
||||
missing.append("model")
|
||||
if not has_timestamp:
|
||||
missing.append("timestamp")
|
||||
if not has_guardrails:
|
||||
missing.append("guardrail_results")
|
||||
|
||||
return ComplianceCheckResult(
|
||||
check_name="Audit record complete",
|
||||
article="Art. 12",
|
||||
passed=audit_complete,
|
||||
detail=(
|
||||
"All required audit fields present"
|
||||
if audit_complete
|
||||
else f"Missing: {', '.join(missing)}"
|
||||
),
|
||||
)
|
||||
|
||||
# ── GDPR Helper Methods ──────────────────────────────────────────────────
|
||||
|
||||
def _check_art_32_data_protection(self) -> ComplianceCheckResult:
|
||||
"""Art. 32: Check if data protection was applied (pre-call)."""
|
||||
pre_call_guardrails = self._get_guardrails_by_mode("pre_call")
|
||||
has_pre_call = len(pre_call_guardrails) > 0
|
||||
return ComplianceCheckResult(
|
||||
check_name="Data protection applied",
|
||||
article="Art. 32",
|
||||
passed=has_pre_call,
|
||||
detail=(
|
||||
f"{len(pre_call_guardrails)} pre-call guardrail(s) protect data"
|
||||
if has_pre_call
|
||||
else "No pre-call data protection applied"
|
||||
),
|
||||
)
|
||||
|
||||
def _check_art_5_1c_sensitive_data_protected(self) -> ComplianceCheckResult:
|
||||
"""Art. 5(1)(c): Check if sensitive data was protected."""
|
||||
pre_call_guardrails = self._get_guardrails_by_mode("pre_call")
|
||||
has_intervention = self._has_guardrail_intervention(pre_call_guardrails)
|
||||
all_passed = self._all_guardrails_passed(pre_call_guardrails)
|
||||
data_protected = has_intervention or all_passed
|
||||
|
||||
if has_intervention:
|
||||
detail = "Guardrail intervened to protect sensitive data"
|
||||
elif all_passed:
|
||||
detail = "No sensitive data detected"
|
||||
else:
|
||||
detail = "No pre-call guardrails to protect sensitive data"
|
||||
|
||||
return ComplianceCheckResult(
|
||||
check_name="Sensitive data protected",
|
||||
article="Art. 5(1)(c)",
|
||||
passed=data_protected,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
def _check_art_30_audit_complete(self) -> ComplianceCheckResult:
|
||||
"""Art. 30: Check if audit record is complete."""
|
||||
has_user = bool(self.data.user_id)
|
||||
has_model = bool(self.data.model)
|
||||
has_timestamp = bool(self.data.timestamp)
|
||||
has_guardrails = len(self.guardrails) > 0
|
||||
audit_complete = has_user and has_model and has_timestamp and has_guardrails
|
||||
|
||||
missing = []
|
||||
if not has_user:
|
||||
missing.append("user_id")
|
||||
if not has_model:
|
||||
missing.append("model")
|
||||
if not has_timestamp:
|
||||
missing.append("timestamp")
|
||||
if not has_guardrails:
|
||||
missing.append("guardrail_results")
|
||||
|
||||
return ComplianceCheckResult(
|
||||
check_name="Audit record complete",
|
||||
article="Art. 30",
|
||||
passed=audit_complete,
|
||||
detail=(
|
||||
"All required audit fields present"
|
||||
if audit_complete
|
||||
else f"Missing: {', '.join(missing)}"
|
||||
),
|
||||
)
|
||||
|
||||
# ── Main Compliance Check Methods ────────────────────────────────────────
|
||||
|
||||
def check_eu_ai_act(self) -> List[ComplianceCheckResult]:
|
||||
"""
|
||||
Check EU AI Act compliance.
|
||||
|
||||
Returns:
|
||||
List of compliance check results for:
|
||||
- Art. 9: Guardrails applied
|
||||
- Art. 5: Content screened before LLM (pre-call screening)
|
||||
- Art. 12: Audit record complete
|
||||
"""
|
||||
return [
|
||||
self._check_art_9_guardrails_applied(),
|
||||
self._check_art_5_content_screened(),
|
||||
self._check_art_12_audit_complete(),
|
||||
]
|
||||
|
||||
def check_gdpr(self) -> List[ComplianceCheckResult]:
|
||||
"""
|
||||
Check GDPR compliance.
|
||||
|
||||
Returns:
|
||||
List of compliance check results for:
|
||||
- Art. 32: Data protection applied (pre-call screening)
|
||||
- Art. 5(1)(c): Sensitive data protected
|
||||
- Art. 30: Audit record complete
|
||||
"""
|
||||
return [
|
||||
self._check_art_32_data_protection(),
|
||||
self._check_art_5_1c_sensitive_data_protected(),
|
||||
self._check_art_30_audit_complete(),
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
COMPLIANCE CHECK ENDPOINTS
|
||||
|
||||
Endpoints for checking regulatory compliance of LLM request logs.
|
||||
|
||||
/compliance/eu-ai-act - Check EU AI Act compliance
|
||||
/compliance/gdpr - Check GDPR compliance
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.compliance_checks import ComplianceChecker
|
||||
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
|
||||
from litellm.types.proxy.compliance_endpoints import (
|
||||
ComplianceCheckRequest,
|
||||
ComplianceResponse,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/compliance/eu-ai-act",
|
||||
tags=["compliance"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=ComplianceResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def check_eu_ai_act_compliance(
|
||||
data: ComplianceCheckRequest,
|
||||
http_request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> ComplianceResponse:
|
||||
"""
|
||||
Check EU AI Act compliance for a spend log entry.
|
||||
|
||||
Checks:
|
||||
- Art. 9: Guardrails applied (any guardrail)
|
||||
- Art. 5: Content screened before LLM (pre-call guardrails)
|
||||
- Art. 12: Audit record complete (user_id, model, timestamp, guardrail_results)
|
||||
"""
|
||||
checker = ComplianceChecker(data)
|
||||
checks = checker.check_eu_ai_act()
|
||||
return ComplianceResponse(
|
||||
compliant=all(c.passed for c in checks),
|
||||
regulation="EU AI Act",
|
||||
checks=checks,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/compliance/gdpr",
|
||||
tags=["compliance"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=ComplianceResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def check_gdpr_compliance(
|
||||
data: ComplianceCheckRequest,
|
||||
http_request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> ComplianceResponse:
|
||||
"""
|
||||
Check GDPR compliance for a spend log entry.
|
||||
|
||||
Checks:
|
||||
- Art. 32: Data protection applied (pre-call guardrails)
|
||||
- Art. 5(1)(c): Sensitive data protected (masked/blocked or no issues)
|
||||
- Art. 30: Audit record complete (user_id, model, timestamp, guardrail_results)
|
||||
"""
|
||||
checker = ComplianceChecker(data)
|
||||
checks = checker.check_gdpr()
|
||||
return ComplianceResponse(
|
||||
compliant=all(c.passed for c in checks),
|
||||
regulation="GDPR",
|
||||
checks=checks,
|
||||
)
|
||||
@@ -345,6 +345,9 @@ from litellm.proxy.management_endpoints.common_utils import (
|
||||
_user_has_admin_privileges,
|
||||
admin_can_invite_user,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.compliance_endpoints import (
|
||||
router as compliance_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.cost_tracking_settings import (
|
||||
router as cost_tracking_settings_router,
|
||||
)
|
||||
@@ -12467,6 +12470,7 @@ app.include_router(user_agent_analytics_router)
|
||||
app.include_router(enterprise_router)
|
||||
app.include_router(ui_discovery_endpoints_router)
|
||||
app.include_router(agent_endpoints_router)
|
||||
app.include_router(compliance_router)
|
||||
app.include_router(a2a_router)
|
||||
app.include_router(access_group_router)
|
||||
########################################################
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ComplianceCheckResult(BaseModel):
|
||||
"""Result of a single compliance check."""
|
||||
|
||||
check_name: str
|
||||
article: str
|
||||
passed: bool
|
||||
detail: str
|
||||
|
||||
|
||||
class ComplianceResponse(BaseModel):
|
||||
"""Response from a compliance check endpoint."""
|
||||
|
||||
compliant: bool
|
||||
regulation: str
|
||||
checks: List[ComplianceCheckResult]
|
||||
|
||||
|
||||
class ComplianceCheckRequest(BaseModel):
|
||||
"""Request payload for compliance check endpoints.
|
||||
|
||||
Mirrors the spend log fields needed for compliance evaluation.
|
||||
"""
|
||||
|
||||
request_id: str
|
||||
user_id: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
timestamp: Optional[str] = None
|
||||
guardrail_information: Optional[List[dict]] = None
|
||||
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
Unit tests for compliance check endpoints (EU AI Act and GDPR).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.proxy.compliance_checks import ComplianceChecker
|
||||
from litellm.types.proxy.compliance_endpoints import ComplianceCheckRequest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EU AI Act — Non-compliant cases (Task #3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEuAiActNonCompliant:
|
||||
"""Requests that should NOT be EU AI Act compliant."""
|
||||
|
||||
def test_no_guardrails_applied(self):
|
||||
"""Request with no guardrail information at all."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-001",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=None,
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Guardrails applied"] is False
|
||||
assert results["Content screened before LLM"] is False
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_empty_guardrails_list(self):
|
||||
"""Request with an empty guardrail list."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-002",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Guardrails applied"] is False
|
||||
assert results["Content screened before LLM"] is False
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_no_prohibited_practices_screening(self):
|
||||
"""Guardrails exist but only post-call (no pre-call screening)."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-003",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "content_filter",
|
||||
"guardrail_mode": "post_call",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Guardrails applied"] is True
|
||||
assert results["Content screened before LLM"] is False
|
||||
|
||||
def test_incomplete_audit_missing_user_id(self):
|
||||
"""Audit record missing user_id."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-004",
|
||||
user_id=None,
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "prohibited_practices",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_incomplete_audit_missing_model(self):
|
||||
"""Audit record missing model."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-005",
|
||||
user_id="user-1",
|
||||
model=None,
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "prohibited_practices",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_incomplete_audit_missing_timestamp(self):
|
||||
"""Audit record missing timestamp."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-006",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp=None,
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "prohibited_practices",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_incomplete_audit_missing_guardrails(self):
|
||||
"""Audit record has user/model/timestamp but no guardrails."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-007",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GDPR — Non-compliant cases (Task #3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGdprNonCompliant:
|
||||
"""Requests that should NOT be GDPR compliant."""
|
||||
|
||||
def test_no_pii_detection(self):
|
||||
"""Guardrails exist but only post-call (no pre-call data protection)."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-101",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "content_filter",
|
||||
"guardrail_mode": "post_call",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Data protection applied"] is False
|
||||
assert results["Sensitive data protected"] is False
|
||||
|
||||
def test_empty_guardrails(self):
|
||||
"""Empty guardrail list — no PII scan."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-102",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Data protection applied"] is False
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_pii_sent_in_plaintext(self):
|
||||
"""PII detection ran but status indicates PII was passed through."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-103",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "pii_detection",
|
||||
"guardrail_status": "pii_detected_not_blocked",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Data protection applied"] is True
|
||||
assert results["Sensitive data protected"] is False
|
||||
|
||||
def test_gdpr_audit_missing_user_id(self):
|
||||
"""GDPR audit missing user_id."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-104",
|
||||
user_id=None,
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "pii_detection",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_gdpr_audit_missing_model(self):
|
||||
"""GDPR audit missing model."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-105",
|
||||
user_id="user-1",
|
||||
model=None,
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "pii_detection",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
def test_no_guardrails_at_all(self):
|
||||
"""None guardrail_information."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-106",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=None,
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Data protection applied"] is False
|
||||
assert results["Audit record complete"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EU AI Act — Compliant cases (Task #4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEuAiActCompliant:
|
||||
"""Requests that SHOULD be EU AI Act compliant."""
|
||||
|
||||
def test_fully_compliant(self):
|
||||
"""All checks pass: guardrails, prohibited_practices, full audit."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-201",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "content_filter",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
{
|
||||
"guardrail_name": "prohibited_practices",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Guardrails applied"] is True
|
||||
assert results["Content screened before LLM"] is True
|
||||
assert results["Audit record complete"] is True
|
||||
assert all(c.passed for c in checks)
|
||||
|
||||
def test_compliant_with_multiple_guardrails(self):
|
||||
"""Multiple guardrails including prohibited_practices."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-202",
|
||||
user_id="user-2",
|
||||
model="claude-3",
|
||||
timestamp="2026-02-17T12:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "pii_detection",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
{
|
||||
"guardrail_name": "prohibited_practices",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
{
|
||||
"guardrail_name": "content_filter",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_eu_ai_act()
|
||||
assert all(c.passed for c in checks)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GDPR — Compliant cases (Task #4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGdprCompliant:
|
||||
"""Requests that SHOULD be GDPR compliant."""
|
||||
|
||||
def test_fully_compliant_pii_no_issues(self):
|
||||
"""PII scan ran, found nothing (status=success), full audit."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-301",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "pii_detection",
|
||||
"guardrail_status": "success",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Data protection applied"] is True
|
||||
assert results["Sensitive data protected"] is True
|
||||
assert results["Audit record complete"] is True
|
||||
assert all(c.passed for c in checks)
|
||||
|
||||
def test_compliant_pii_masked(self):
|
||||
"""PII detected and masked (guardrail_intervened) — still compliant."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-302",
|
||||
user_id="user-1",
|
||||
model="gpt-4",
|
||||
timestamp="2026-02-17T00:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "pii_detection",
|
||||
"guardrail_status": "guardrail_intervened",
|
||||
}
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
results = {c.check_name: c.passed for c in checks}
|
||||
assert results["Data protection applied"] is True
|
||||
assert results["Sensitive data protected"] is True
|
||||
assert results["Audit record complete"] is True
|
||||
assert all(c.passed for c in checks)
|
||||
|
||||
def test_compliant_with_other_guardrails(self):
|
||||
"""PII detection plus other guardrails — still compliant."""
|
||||
data = ComplianceCheckRequest(
|
||||
request_id="req-303",
|
||||
user_id="user-2",
|
||||
model="claude-3",
|
||||
timestamp="2026-02-17T12:00:00Z",
|
||||
guardrail_information=[
|
||||
{
|
||||
"guardrail_name": "content_filter",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
{
|
||||
"guardrail_name": "pii_detection",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
{
|
||||
"guardrail_name": "prohibited_practices",
|
||||
"guardrail_status": "success",
|
||||
},
|
||||
],
|
||||
)
|
||||
checks = ComplianceChecker(data).check_gdpr()
|
||||
assert all(c.passed for c in checks)
|
||||
@@ -9235,3 +9235,70 @@ export const deleteClaudeCodePlugin = async (accessToken: string, pluginName: st
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Compliance check types and functions
|
||||
|
||||
export interface ComplianceCheckResult {
|
||||
check_name: string;
|
||||
article: string;
|
||||
passed: boolean;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface ComplianceResponse {
|
||||
compliant: boolean;
|
||||
regulation: string;
|
||||
checks: ComplianceCheckResult[];
|
||||
}
|
||||
|
||||
export interface ComplianceCheckRequest {
|
||||
request_id: string;
|
||||
user_id?: string;
|
||||
model?: string;
|
||||
timestamp?: string;
|
||||
guardrail_information?: Record<string, any>[];
|
||||
}
|
||||
|
||||
export const checkEuAiActCompliance = async (
|
||||
accessToken: string,
|
||||
payload: ComplianceCheckRequest
|
||||
): Promise<ComplianceResponse> => {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/compliance/eu-ai-act`
|
||||
: `/compliance/eu-ai-act`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
throw new Error(errorData);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const checkGdprCompliance = async (
|
||||
accessToken: string,
|
||||
payload: ComplianceCheckRequest
|
||||
): Promise<ComplianceResponse> => {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/compliance/gdpr`
|
||||
: `/compliance/gdpr`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
throw new Error(errorData);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Tooltip } from "antd";
|
||||
import {
|
||||
checkEuAiActCompliance,
|
||||
checkGdprCompliance,
|
||||
ComplianceResponse,
|
||||
ComplianceCheckRequest,
|
||||
} from "@/components/networking";
|
||||
|
||||
interface CompliancePanelProps {
|
||||
accessToken: string | null;
|
||||
logEntry: {
|
||||
request_id: string;
|
||||
user?: string;
|
||||
model?: string;
|
||||
startTime?: string;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
// -- Icons --
|
||||
|
||||
const CheckIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="7" stroke="#16A34A" strokeWidth="1.5" fill="#F0FDF4" />
|
||||
<path d="M5 8l2 2 4-4" stroke="#16A34A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CrossIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="7" stroke="#DC2626" strokeWidth="1.5" fill="#FEF2F2" />
|
||||
<path d="M6 6l4 4M10 6l-4 4" stroke="#DC2626" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SpinnerIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="animate-spin">
|
||||
<circle cx="8" cy="8" r="6" stroke="#D1D5DB" strokeWidth="2" />
|
||||
<path d="M8 2a6 6 0 0 1 6 6" stroke="#6366F1" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// -- Sub-components --
|
||||
|
||||
const ComplianceCard = ({
|
||||
title,
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
}: {
|
||||
title: string;
|
||||
data: ComplianceResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg bg-white">
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{loading ? (
|
||||
<SpinnerIcon />
|
||||
) : error ? (
|
||||
<Tooltip title={error}>
|
||||
<span className="text-gray-400 text-sm">--</span>
|
||||
</Tooltip>
|
||||
) : data?.compliant ? (
|
||||
<CheckIcon />
|
||||
) : (
|
||||
<CrossIcon />
|
||||
)}
|
||||
<span className="font-medium text-sm text-gray-900">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!loading && !error && data && (
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-[11px] font-semibold uppercase ${
|
||||
data.compliant
|
||||
? "bg-green-100 text-green-700 border border-green-200"
|
||||
: "bg-red-100 text-red-700 border border-red-200"
|
||||
}`}
|
||||
>
|
||||
{data.compliant ? "COMPLIANT" : "NON-COMPLIANT"}
|
||||
</span>
|
||||
)}
|
||||
{error && (
|
||||
<span className="px-2 py-0.5 rounded text-[11px] font-medium bg-gray-100 text-gray-500 border border-gray-200">
|
||||
UNAVAILABLE
|
||||
</span>
|
||||
)}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className={`transition-transform ${expanded ? "rotate-180" : ""}`}
|
||||
>
|
||||
<path d="M6 8l4 4 4-4" stroke="#6B7280" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t border-gray-100 px-4 py-3">
|
||||
{loading && <p className="text-sm text-gray-500">Checking compliance...</p>}
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
{data && (
|
||||
<div className="space-y-2">
|
||||
{data.checks.map((check, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{check.passed ? <CheckIcon /> : <CrossIcon />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">{check.check_name}</span>
|
||||
<span className="text-[10px] font-mono text-gray-400">{check.article}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{check.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// -- Main Component --
|
||||
|
||||
const CompliancePanel: React.FC<CompliancePanelProps> = ({ accessToken, logEntry }) => {
|
||||
const [euAiActData, setEuAiActData] = useState<ComplianceResponse | null>(null);
|
||||
const [gdprData, setGdprData] = useState<ComplianceResponse | null>(null);
|
||||
const [euAiActLoading, setEuAiActLoading] = useState(false);
|
||||
const [gdprLoading, setGdprLoading] = useState(false);
|
||||
const [euAiActError, setEuAiActError] = useState<string | null>(null);
|
||||
const [gdprError, setGdprError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || !logEntry.request_id) return;
|
||||
|
||||
const payload: ComplianceCheckRequest = {
|
||||
request_id: logEntry.request_id,
|
||||
user_id: logEntry.user,
|
||||
model: logEntry.model,
|
||||
timestamp: logEntry.startTime,
|
||||
guardrail_information: logEntry.metadata?.guardrail_information,
|
||||
};
|
||||
|
||||
setEuAiActLoading(true);
|
||||
setEuAiActError(null);
|
||||
checkEuAiActCompliance(accessToken, payload)
|
||||
.then(setEuAiActData)
|
||||
.catch((err) => setEuAiActError(err.message || "Failed to check EU AI Act compliance"))
|
||||
.finally(() => setEuAiActLoading(false));
|
||||
|
||||
setGdprLoading(true);
|
||||
setGdprError(null);
|
||||
checkGdprCompliance(accessToken, payload)
|
||||
.then(setGdprData)
|
||||
.catch((err) => setGdprError(err.message || "Failed to check GDPR compliance"))
|
||||
.finally(() => setGdprLoading(false));
|
||||
}, [accessToken, logEntry]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">
|
||||
Regulatory Compliance
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<ComplianceCard
|
||||
title="EU AI Act"
|
||||
data={euAiActData}
|
||||
loading={euAiActLoading}
|
||||
error={euAiActError}
|
||||
/>
|
||||
<ComplianceCard
|
||||
title="GDPR"
|
||||
data={gdprData}
|
||||
loading={gdprLoading}
|
||||
error={gdprError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompliancePanel;
|
||||
@@ -5,6 +5,7 @@ import BedrockGuardrailDetails, {
|
||||
BedrockGuardrailResponse,
|
||||
} from "@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails";
|
||||
import ContentFilterDetails from "./ContentFilterDetails";
|
||||
import CompliancePanel from "./CompliancePanel";
|
||||
|
||||
// ── Interfaces ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -58,6 +59,14 @@ interface GuardrailInformation {
|
||||
|
||||
interface GuardrailViewerProps {
|
||||
data: GuardrailInformation | GuardrailInformation[];
|
||||
accessToken?: string | null;
|
||||
logEntry?: {
|
||||
request_id: string;
|
||||
user?: string;
|
||||
model?: string;
|
||||
startTime?: string;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
@@ -585,7 +594,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
||||
|
||||
// ── Main Component ──────────────────────────────────────────────────────────
|
||||
|
||||
const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
||||
const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps) => {
|
||||
const guardrailEntries = useMemo(() => {
|
||||
return Array.isArray(data)
|
||||
? data.filter((entry): entry is GuardrailInformation => Boolean(entry))
|
||||
@@ -659,11 +668,6 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
Total: {totalOverheadMs}ms overhead
|
||||
</div>
|
||||
{policyTemplates.length > 0 && (
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
Policy: {policyTemplates.join(" / ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -676,6 +680,13 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Compliance Panel ──────────────────────────────────── */}
|
||||
{accessToken && logEntry && (
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<CompliancePanel accessToken={accessToken} logEntry={logEntry} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Body: two columns ──────────────────────────────────── */}
|
||||
<div className="flex">
|
||||
{/* Left column: Request Lifecycle */}
|
||||
|
||||
+14
-2
@@ -4,6 +4,7 @@ import moment from "moment";
|
||||
import { LogEntry } from "../columns";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import GuardrailViewer from "../GuardrailViewer/GuardrailViewer";
|
||||
import CompliancePanel from "../GuardrailViewer/CompliancePanel";
|
||||
import { CostBreakdownViewer } from "../CostBreakdownViewer";
|
||||
import { ConfigInfoMessage } from "../ConfigInfoMessage";
|
||||
import { VectorStoreViewer } from "../VectorStoreViewer";
|
||||
@@ -40,6 +41,7 @@ export interface LogDetailContentProps {
|
||||
onOpenSettings?: () => void;
|
||||
/** When true, log details (messages/response) are still being lazy-loaded. */
|
||||
isLoadingDetails?: boolean;
|
||||
accessToken?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +52,7 @@ export interface LogDetailContentProps {
|
||||
* Designed to be placed inside LogDetailsDrawer's right panel so it can
|
||||
* be reused for both single-log and session-mode views.
|
||||
*/
|
||||
export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails = false }: LogDetailContentProps) {
|
||||
export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails = false, accessToken }: LogDetailContentProps) {
|
||||
const metadata = logEntry.metadata || {};
|
||||
const hasError = metadata.status === "failure";
|
||||
const errorInfo = hasError ? metadata.error_information : null;
|
||||
@@ -166,7 +168,17 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
|
||||
{/* Guardrail Data */}
|
||||
{hasGuardrailData && (
|
||||
<div id="guardrail-section">
|
||||
<GuardrailViewer data={guardrailInfo} />
|
||||
<GuardrailViewer
|
||||
data={guardrailInfo}
|
||||
accessToken={accessToken ?? null}
|
||||
logEntry={{
|
||||
request_id: logEntry.request_id,
|
||||
user: logEntry.user,
|
||||
model: logEntry.model,
|
||||
startTime: logEntry.startTime,
|
||||
metadata: logEntry.metadata,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -384,6 +384,7 @@ export function LogDetailsDrawer({
|
||||
logEntry={enrichedLog}
|
||||
onOpenSettings={onOpenSettings}
|
||||
isLoadingDetails={isLoadingDetails}
|
||||
accessToken={accessToken ?? null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user