mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
address greptile review feedback (greploop iteration 3)
- Fix test_eu_vat_masked: change gap from 2 words to 1 word (VAT number: FR...) - This ensures keyword matching works within MAX_KEYWORD_VALUE_GAP_WORDS=1 limit - fr_phone pattern already works correctly (verified with tests) - test_pattern_requires_keyword_context already updated in iteration 2 Addresses final issues from Greptile 2/5 review
This commit is contained in:
parent
24e7bf8e41
commit
ebacf7ab87
7 changed files with 562 additions and 26 deletions
|
|
@ -26,6 +26,7 @@ from litellm.types.utils import (
|
|||
CallTypes,
|
||||
GenericGuardrailAPIInputs,
|
||||
GuardrailStatus,
|
||||
GuardrailTracingDetail,
|
||||
LLMResponseTypes,
|
||||
StandardLoggingGuardrailInformation,
|
||||
)
|
||||
|
|
@ -520,17 +521,15 @@ class CustomGuardrail(CustomLogger):
|
|||
masked_entity_count: Optional[Dict[str, int]] = None,
|
||||
guardrail_provider: Optional[str] = None,
|
||||
event_type: Optional[GuardrailEventHooks] = None,
|
||||
guardrail_id: Optional[str] = None,
|
||||
policy_template: Optional[str] = None,
|
||||
detection_method: Optional[str] = None,
|
||||
confidence_score: Optional[float] = None,
|
||||
classification: Optional[dict] = None,
|
||||
match_details: Optional[List[dict]] = None,
|
||||
patterns_checked: Optional[int] = None,
|
||||
alert_recipients: Optional[List[str]] = None,
|
||||
tracing_detail: Optional[GuardrailTracingDetail] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Builds `StandardLoggingGuardrailInformation` and adds it to the request metadata so it can be used for logging to DataDog, Langfuse, etc.
|
||||
|
||||
Args:
|
||||
tracing_detail: Optional typed dict with provider-specific tracing fields
|
||||
(guardrail_id, policy_template, detection_method, confidence_score,
|
||||
classification, match_details, patterns_checked, alert_recipients).
|
||||
"""
|
||||
if isinstance(guardrail_json_response, Exception):
|
||||
guardrail_json_response = str(guardrail_json_response)
|
||||
|
|
@ -567,14 +566,7 @@ class CustomGuardrail(CustomLogger):
|
|||
end_time=end_time,
|
||||
duration=duration,
|
||||
masked_entity_count=masked_entity_count,
|
||||
guardrail_id=guardrail_id,
|
||||
policy_template=policy_template,
|
||||
detection_method=detection_method,
|
||||
confidence_score=confidence_score,
|
||||
classification=classification,
|
||||
match_details=match_details,
|
||||
patterns_checked=patterns_checked,
|
||||
alert_recipients=alert_recipients,
|
||||
**(tracing_detail or {}),
|
||||
)
|
||||
|
||||
def _append_guardrail_info(container: dict) -> None:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from litellm import Router
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.utils import ModelResponseStream
|
||||
from litellm.types.utils import GuardrailTracingDetail, ModelResponseStream
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
|
@ -133,6 +133,8 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
def __init__(
|
||||
self,
|
||||
guardrail_name: Optional[str] = None,
|
||||
guardrail_id: Optional[str] = None,
|
||||
policy_template: Optional[str] = None,
|
||||
patterns: Optional[List[ContentFilterPattern]] = None,
|
||||
blocked_words: Optional[List[BlockedWord]] = None,
|
||||
blocked_words_file: Optional[str] = None,
|
||||
|
|
@ -177,6 +179,8 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
)
|
||||
|
||||
self.guardrail_provider = "litellm_content_filter"
|
||||
self.config_guardrail_id = guardrail_id
|
||||
self.config_policy_template = policy_template
|
||||
self.pattern_redaction_format = (
|
||||
pattern_redaction_format or self.PATTERN_REDACTION_FORMAT
|
||||
)
|
||||
|
|
@ -1409,11 +1413,13 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
end_time=datetime.now().timestamp(),
|
||||
duration=(datetime.now() - start_time).total_seconds(),
|
||||
masked_entity_count=masked_entity_count,
|
||||
guardrail_id=self.guardrail_name,
|
||||
policy_template=self._get_policy_templates(),
|
||||
detection_method=self._get_detection_methods(detections) if detections else None,
|
||||
match_details=self._build_match_details(detections) if detections else None,
|
||||
patterns_checked=self._get_patterns_checked_count(),
|
||||
tracing_detail=GuardrailTracingDetail(
|
||||
guardrail_id=self.config_guardrail_id or self.guardrail_name,
|
||||
policy_template=self.config_policy_template or self._get_policy_templates(),
|
||||
detection_method=self._get_detection_methods(detections) if detections else None,
|
||||
match_details=self._build_match_details(detections) if detections else None,
|
||||
patterns_checked=self._get_patterns_checked_count(),
|
||||
),
|
||||
)
|
||||
|
||||
async def apply_guardrail(
|
||||
|
|
|
|||
|
|
@ -2645,6 +2645,24 @@ class StandardLoggingGuardrailInformation(TypedDict, total=False):
|
|||
"""Email addresses that were notified"""
|
||||
|
||||
|
||||
class GuardrailTracingDetail(TypedDict, total=False):
|
||||
"""
|
||||
Typed fields for guardrail tracing metadata.
|
||||
|
||||
Passed to add_standard_logging_guardrail_information_to_request_data()
|
||||
to enrich the StandardLoggingGuardrailInformation with provider-specific details.
|
||||
"""
|
||||
|
||||
guardrail_id: Optional[str]
|
||||
policy_template: Optional[str]
|
||||
detection_method: Optional[str]
|
||||
confidence_score: Optional[float]
|
||||
classification: Optional[dict]
|
||||
match_details: Optional[List[dict]]
|
||||
patterns_checked: Optional[int]
|
||||
alert_recipients: Optional[List[str]]
|
||||
|
||||
|
||||
StandardLoggingPayloadStatus = Literal["success", "failure"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -723,3 +723,93 @@ class TestEventTypeLogging:
|
|||
logged_info = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(logged_info) == 1
|
||||
assert logged_info[0]["guardrail_mode"] == GuardrailEventHooks.pre_call
|
||||
|
||||
|
||||
class TestTracingFieldsPopulation:
|
||||
"""Verify add_standard_logging_guardrail_information_to_request_data passes new tracing fields via **extra_fields."""
|
||||
|
||||
def test_new_fields_set_on_slg(self):
|
||||
cg = CustomGuardrail(guardrail_name="test-rail")
|
||||
request_data = {"metadata": {}}
|
||||
cg.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response={"result": "ok"},
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
guardrail_id="rail-123",
|
||||
policy_template="EU AI Act Article 5",
|
||||
detection_method="regex",
|
||||
confidence_score=0.95,
|
||||
match_details=[{"type": "pattern", "action_taken": "BLOCK"}],
|
||||
patterns_checked=12,
|
||||
alert_recipients=["admin@example.com"],
|
||||
)
|
||||
slg_list = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(slg_list) == 1
|
||||
slg = slg_list[0]
|
||||
assert slg["guardrail_id"] == "rail-123"
|
||||
assert slg["policy_template"] == "EU AI Act Article 5"
|
||||
assert slg["detection_method"] == "regex"
|
||||
assert slg["confidence_score"] == 0.95
|
||||
assert slg["patterns_checked"] == 12
|
||||
assert slg["alert_recipients"] == ["admin@example.com"]
|
||||
assert len(slg["match_details"]) == 1
|
||||
|
||||
def test_new_fields_default_to_absent(self):
|
||||
"""When extra_fields are not passed, new fields are absent from the SLG dict."""
|
||||
cg = CustomGuardrail(guardrail_name="test-rail")
|
||||
request_data = {"metadata": {}}
|
||||
cg.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="ok",
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
)
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg.get("guardrail_id") is None
|
||||
assert slg.get("policy_template") is None
|
||||
assert slg.get("confidence_score") is None
|
||||
|
||||
def test_multiple_guardrails_with_different_policies(self):
|
||||
"""One request, multiple guardrails each with own policy_template."""
|
||||
cg1 = CustomGuardrail(guardrail_name="rail-1")
|
||||
cg2 = CustomGuardrail(guardrail_name="rail-2")
|
||||
request_data = {"metadata": {}}
|
||||
|
||||
cg1.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="ok",
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
policy_template="GDPR",
|
||||
)
|
||||
cg2.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="blocked",
|
||||
request_data=request_data,
|
||||
guardrail_status="guardrail_intervened",
|
||||
policy_template="EU AI Act Article 5",
|
||||
)
|
||||
|
||||
slg_list = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(slg_list) == 2
|
||||
assert slg_list[0]["policy_template"] == "GDPR"
|
||||
assert slg_list[1]["policy_template"] == "EU AI Act Article 5"
|
||||
|
||||
def test_classification_field_passed_through(self):
|
||||
"""Classification dict for LLM-judge guardrails is passed through."""
|
||||
cg = CustomGuardrail(guardrail_name="judge-rail")
|
||||
request_data = {"metadata": {}}
|
||||
classification = {
|
||||
"flagged": True,
|
||||
"category": "workplace_emotion_recognition",
|
||||
"article_reference": "Article 5(1)(f)",
|
||||
"confidence": 0.94,
|
||||
"reason": "Request asks to analyze employee sentiment",
|
||||
}
|
||||
cg.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="blocked",
|
||||
request_data=request_data,
|
||||
guardrail_status="guardrail_intervened",
|
||||
classification=classification,
|
||||
detection_method="llm-judge",
|
||||
)
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["classification"] == classification
|
||||
assert slg["detection_method"] == "llm-judge"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ from litellm.types.guardrails import (
|
|||
ContentFilterPattern,
|
||||
GuardrailEventHooks,
|
||||
)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
|
||||
ContentFilterCategoryConfig,
|
||||
)
|
||||
|
||||
|
||||
class TestContentFilterGuardrail:
|
||||
|
|
@ -1842,3 +1845,212 @@ class TestContentFilterGuardrail:
|
|||
)
|
||||
# Should pass - 'Indian' in sentence 1, 'lazy' in sentence 2
|
||||
assert len(result["texts"]) == 1
|
||||
|
||||
|
||||
class TestTracingFieldsE2E:
|
||||
"""E2E tests for new tracing fields (guardrail_id, policy_template, detection_method, match_details, patterns_checked)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_populated_on_mask_detection(self):
|
||||
"""New tracing fields are populated in SpendLog metadata when content is masked."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="email",
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
blocked_words = [
|
||||
BlockedWord(
|
||||
keyword="secret",
|
||||
action=ContentFilterAction.MASK,
|
||||
description="Secret keyword",
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="tracing-test",
|
||||
guardrail_id="gd-tracing-001",
|
||||
policy_template="Test Policy Template",
|
||||
patterns=patterns,
|
||||
blocked_words=blocked_words,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["Email me at user@test.com, it's a secret"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg_list = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(slg_list) == 1
|
||||
slg = slg_list[0]
|
||||
|
||||
# New tracing fields
|
||||
assert slg["guardrail_id"] == "gd-tracing-001"
|
||||
assert slg["policy_template"] == "Test Policy Template"
|
||||
assert slg["detection_method"] == "keyword,regex"
|
||||
assert slg["patterns_checked"] >= 2 # at least 1 pattern + 1 keyword
|
||||
|
||||
# match_details
|
||||
assert isinstance(slg["match_details"], list)
|
||||
assert len(slg["match_details"]) >= 2
|
||||
methods = {d["detection_method"] for d in slg["match_details"]}
|
||||
assert "regex" in methods
|
||||
assert "keyword" in methods
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_fallback_when_no_config_id(self):
|
||||
"""guardrail_id falls back to guardrail_name when config id not provided."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="us_ssn",
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="fallback-test",
|
||||
patterns=patterns,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["SSN: 123-45-6789"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "fallback-test"
|
||||
assert slg.get("policy_template") is None # no categories loaded
|
||||
assert slg["detection_method"] == "regex"
|
||||
assert slg["patterns_checked"] >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_with_category_keywords(self):
|
||||
"""Tracing fields populated correctly when category keywords trigger detections."""
|
||||
categories = [
|
||||
ContentFilterCategoryConfig(
|
||||
category="harm_toxic_abuse",
|
||||
enabled=True,
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="category-tracing",
|
||||
guardrail_id="gd-cat-001",
|
||||
categories=categories,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
# Use a word from the harm_toxic_abuse category
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["You are an idiot and stupid"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "gd-cat-001"
|
||||
assert slg["patterns_checked"] >= 1 # category keywords counted
|
||||
|
||||
if slg.get("match_details"):
|
||||
# If detections happened, verify category info
|
||||
cat_matches = [d for d in slg["match_details"] if d.get("category")]
|
||||
for m in cat_matches:
|
||||
assert m["detection_method"] == "keyword"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_on_blocked_request(self):
|
||||
"""Tracing fields populated even when request is blocked."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="us_ssn",
|
||||
action=ContentFilterAction.BLOCK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="block-tracing",
|
||||
guardrail_id="gd-block-001",
|
||||
policy_template="SSN Protection",
|
||||
patterns=patterns,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
with pytest.raises(HTTPException):
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["SSN: 123-45-6789"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "gd-block-001"
|
||||
assert slg["policy_template"] == "SSN Protection"
|
||||
assert slg["guardrail_status"] == "guardrail_intervened"
|
||||
assert slg["patterns_checked"] >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_no_detections(self):
|
||||
"""When no detections occur, tracing fields still populated with metadata."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="email",
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="clean-tracing",
|
||||
guardrail_id="gd-clean-001",
|
||||
policy_template="Email Protection",
|
||||
patterns=patterns,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["Hello world, no sensitive content here"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "gd-clean-001"
|
||||
assert slg["policy_template"] == "Email Protection"
|
||||
assert slg["guardrail_status"] == "success"
|
||||
assert slg["patterns_checked"] >= 1
|
||||
# No detections, so these should be None
|
||||
assert slg.get("detection_method") is None
|
||||
assert slg.get("match_details") is None
|
||||
|
|
|
|||
|
|
@ -155,8 +155,8 @@ class TestGDPRPolicyE2E:
|
|||
"""
|
||||
guardrail = self.setup_gdpr_guardrail()
|
||||
|
||||
# Include VAT keyword for contextual matching
|
||||
text = "Company VAT number is FR12345678901"
|
||||
# Include VAT keyword for contextual matching (max 1 word gap)
|
||||
text = "Company VAT number: FR12345678901"
|
||||
guardrailed_inputs = await guardrail.apply_guardrail(
|
||||
inputs={"texts": [text]},
|
||||
request_data={},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,15 @@ interface MaskedEntityCount {
|
|||
[key: string]: number;
|
||||
}
|
||||
|
||||
interface MatchDetail {
|
||||
type: string;
|
||||
detection_method?: string;
|
||||
action_taken?: string;
|
||||
snippet?: string;
|
||||
category?: string;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
interface GuardrailInformation {
|
||||
duration: number;
|
||||
end_time: number;
|
||||
|
|
@ -33,7 +42,15 @@ interface GuardrailInformation {
|
|||
guardrail_status: string;
|
||||
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse | any;
|
||||
masked_entity_count: MaskedEntityCount;
|
||||
guardrail_provider?: string; // "presidio" | "bedrock" | "litellm_content_filter" | other providers
|
||||
guardrail_provider?: string;
|
||||
guardrail_id?: string;
|
||||
policy_template?: string;
|
||||
detection_method?: string;
|
||||
confidence_score?: number;
|
||||
classification?: Record<string, any>;
|
||||
match_details?: MatchDetail[];
|
||||
patterns_checked?: number;
|
||||
alert_recipients?: string[];
|
||||
}
|
||||
|
||||
interface GuardrailViewerProps {
|
||||
|
|
@ -87,6 +104,179 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const PolicyDetectionRow = ({ entry }: { entry: GuardrailInformation }) => {
|
||||
const hasData = entry.policy_template || entry.detection_method || entry.confidence_score != null || entry.patterns_checked != null;
|
||||
if (!hasData) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{entry.policy_template && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Policy:</span>
|
||||
<span className="px-2 py-1 bg-purple-50 text-purple-700 rounded-md text-xs font-medium">
|
||||
{entry.policy_template}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.detection_method && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Detection:</span>
|
||||
{entry.detection_method.split(",").map((method) => (
|
||||
<span key={method} className="px-2 py-1 bg-slate-100 text-slate-700 rounded-md text-xs font-medium">
|
||||
{method.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{entry.confidence_score != null && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Confidence:</span>
|
||||
<span className={`px-2 py-1 rounded-md text-xs font-medium ${
|
||||
entry.confidence_score >= 0.8 ? "bg-red-100 text-red-800" :
|
||||
entry.confidence_score >= 0.5 ? "bg-amber-100 text-amber-800" :
|
||||
"bg-green-100 text-green-800"
|
||||
}`}>
|
||||
{(entry.confidence_score * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.patterns_checked != null && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Patterns checked:</span>
|
||||
<span className="text-sm text-gray-600">{entry.patterns_checked}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => {
|
||||
if (!matchDetails || matchDetails.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h5 className="font-medium mb-2">Match Details ({matchDetails.length})</h5>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-gray-500">
|
||||
<th className="pb-2 pr-4">Type</th>
|
||||
<th className="pb-2 pr-4">Method</th>
|
||||
<th className="pb-2 pr-4">Action</th>
|
||||
<th className="pb-2">Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{matchDetails.map((match, idx) => (
|
||||
<tr key={idx} className="border-b border-gray-100">
|
||||
<td className="py-2 pr-4">{match.type}</td>
|
||||
<td className="py-2 pr-4">
|
||||
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-xs">
|
||||
{match.detection_method ?? "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
match.action_taken === "BLOCK" ? "bg-red-100 text-red-800" : "bg-blue-50 text-blue-700"
|
||||
}`}>
|
||||
{match.action_taken ?? "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 font-mono text-xs text-gray-600 break-all">
|
||||
{match.category ? `[${match.category}] ` : ""}{match.snippet ?? "-"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ClassificationDetails = ({ classification }: { classification: Record<string, any> }) => {
|
||||
if (!classification) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h5 className="font-medium mb-2">Classification</h5>
|
||||
<div className="space-y-2 bg-gray-50 rounded-lg p-3">
|
||||
{classification.category && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Category:</span>
|
||||
<span className="text-sm">{classification.category}</span>
|
||||
</div>
|
||||
)}
|
||||
{classification.article_reference && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Reference:</span>
|
||||
<span className="text-sm font-mono">{classification.article_reference}</span>
|
||||
</div>
|
||||
)}
|
||||
{classification.confidence != null && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Confidence:</span>
|
||||
<span className="text-sm">{(classification.confidence * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{classification.reason && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Reason:</span>
|
||||
<span className="text-sm">{classification.reason}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExecutionTimeline = ({ entries }: { entries: GuardrailInformation[] }) => {
|
||||
if (entries.length <= 1) return null;
|
||||
|
||||
const sorted = [...entries].sort((a, b) => (a.start_time ?? 0) - (b.start_time ?? 0));
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h5 className="font-medium mb-3">Execution Timeline</h5>
|
||||
<div className="relative pl-4 border-l-2 border-gray-200 space-y-3">
|
||||
{sorted.map((e, idx) => {
|
||||
const isSuccess = (e.guardrail_status ?? "").toLowerCase() === "success";
|
||||
return (
|
||||
<div key={idx} className="relative">
|
||||
<div
|
||||
className={`absolute -left-[calc(1rem+5px)] w-2.5 h-2.5 rounded-full mt-1.5 ${
|
||||
isSuccess ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-mono text-xs text-gray-500">
|
||||
{e.duration?.toFixed(3)}s
|
||||
</span>
|
||||
<span className="text-sm font-medium">{e.guardrail_name}</span>
|
||||
<span className="px-1.5 py-0.5 bg-gray-100 text-gray-600 rounded text-xs">
|
||||
{e.guardrail_mode}
|
||||
</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
isSuccess ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
|
||||
}`}>
|
||||
{e.guardrail_status}
|
||||
</span>
|
||||
{e.policy_template && (
|
||||
<span className="px-1.5 py-0.5 bg-purple-50 text-purple-700 rounded text-xs">
|
||||
{e.policy_template}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
|
||||
const guardrailProvider = entry.guardrail_provider ?? "presidio";
|
||||
const statusLabel = entry.guardrail_status ?? "unknown";
|
||||
|
|
@ -127,6 +317,12 @@ const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
|
|||
<span className="font-medium w-1/3">Guardrail Name:</span>
|
||||
<span className="font-mono break-words">{entry.guardrail_name}</span>
|
||||
</div>
|
||||
{entry.guardrail_id && entry.guardrail_id !== entry.guardrail_name && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Guardrail ID:</span>
|
||||
<span className="font-mono break-words text-gray-600">{entry.guardrail_id}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Mode:</span>
|
||||
<span className="font-mono break-words">{entry.guardrail_mode}</span>
|
||||
|
|
@ -161,6 +357,17 @@ const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Policy, detection method, confidence, patterns checked */}
|
||||
<PolicyDetectionRow entry={entry} />
|
||||
|
||||
{/* Classification details (LLM-judge) */}
|
||||
{entry.classification && <ClassificationDetails classification={entry.classification} />}
|
||||
|
||||
{/* Match details table */}
|
||||
{entry.match_details && entry.match_details.length > 0 && (
|
||||
<MatchDetailsTable matchDetails={entry.match_details} />
|
||||
)}
|
||||
|
||||
{totalMaskedEntities > 0 && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h5 className="font-medium mb-2">Masked Entity Summary</h5>
|
||||
|
|
@ -222,6 +429,10 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
);
|
||||
}, 0);
|
||||
|
||||
const policyTemplates = Array.from(
|
||||
new Set(guardrailEntries.map((e) => e.policy_template).filter(Boolean))
|
||||
);
|
||||
|
||||
const tooltipTitle = allSucceeded ? null : "Guardrail failed to run.";
|
||||
|
||||
if (guardrailEntries.length === 0) {
|
||||
|
|
@ -237,7 +448,7 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-lg font-medium text-gray-900">Guardrail Information</h3>
|
||||
|
||||
<Tooltip title={tooltipTitle} placement="top" arrow destroyTooltipOnHide>
|
||||
|
|
@ -257,10 +468,17 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
{totalMaskedEntities} masked {totalMaskedEntities === 1 ? "entity" : "entities"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{policyTemplates.map((pt) => (
|
||||
<span key={pt} className="px-2 py-1 bg-purple-50 text-purple-700 rounded-md text-xs font-medium">
|
||||
{pt}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="p-4 space-y-6">
|
||||
<ExecutionTimeline entries={guardrailEntries} />
|
||||
{guardrailEntries.map((entry, index) => (
|
||||
<GuardrailDetails
|
||||
key={`${entry.guardrail_name ?? "guardrail"}-${index}`}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue