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:
Ishaan Jaff
2026-02-17 18:22:26 -08:00
committed by GitHub
parent 2517c069ca
commit 5946a933a0
10 changed files with 1018 additions and 8 deletions
+221
View File
@@ -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,
)
+4
View File
@@ -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 */}
@@ -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>