From 78cb8c71fda021929043c0ade8b1684c7956fa50 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 18 Sep 2025 15:07:34 -0700 Subject: [PATCH] prelim changes for new Bedrock guardrail info view --- litellm/integrations/custom_guardrail.py | 2 + .../guardrail_hooks/bedrock_guardrails.py | 2 + .../guardrails/guardrail_hooks/presidio.py | 2 + litellm/types/utils.py | 1 + .../components/view_logs/GuardrailViewer.tsx | 257 --------- .../BedrockGuardrailDetails.tsx | 508 ++++++++++++++++++ .../GuardrailViewer/GuardrailViewer.tsx | 163 ++++++ .../PresidioDetectedEntities.tsx | 136 +++++ .../src/components/view_logs/index.tsx | 2 +- 9 files changed, 815 insertions(+), 258 deletions(-) delete mode 100644 ui/litellm-dashboard/src/components/view_logs/GuardrailViewer.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/BedrockGuardrailDetails.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/PresidioDetectedEntities.tsx diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index e4d16160357..6b77557cd3d 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -357,6 +357,7 @@ class CustomGuardrail(CustomLogger): end_time: Optional[float] = None, duration: Optional[float] = None, masked_entity_count: Optional[Dict[str, int]] = None, + guardrail_provider: Optional[str] = None, ) -> None: """ Builds `StandardLoggingGuardrailInformation` and adds it to the request metadata so it can be used for logging to DataDog, Langfuse, etc. @@ -367,6 +368,7 @@ class CustomGuardrail(CustomLogger): slg = StandardLoggingGuardrailInformation( guardrail_name=self.guardrail_name, + guardrail_provider=guardrail_provider, guardrail_mode=( GuardrailMode(**self.event_hook.model_dump()) # type: ignore if isinstance(self.event_hook, Mode) diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index dfb01a7cd0c..6dfde92e5c9 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -111,6 +111,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): ) self.guardrailIdentifier = guardrailIdentifier self.guardrailVersion = guardrailVersion + self.guardrail_provider = "bedrock" # store kwargs as optional_params self.optional_params = kwargs @@ -372,6 +373,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): # Add guardrail information to request trace ######################################################### self.add_standard_logging_guardrail_information_to_request_data( + guardrail_provider=self.guardrail_provider, guardrail_json_response=response.json(), request_data=request_data or {}, guardrail_status=self._get_bedrock_guardrail_response_status( diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 3e40f33d167..8dbcf77a844 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -76,6 +76,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): self.logging_only = True kwargs["event_hook"] = GuardrailEventHooks.logging_only super().__init__(**kwargs) + self.guardrail_provider = "presidio" self.pii_tokens: dict = ( {} ) # mapping of PII token to original text - only used with Presidio `replace` operation @@ -369,6 +370,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): else: guardrail_json_response = exception_str self.add_standard_logging_guardrail_information_to_request_data( + guardrail_provider=self.guardrail_provider, guardrail_json_response=guardrail_json_response, request_data=request_data, guardrail_status=status, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index fbf2bad98ba..a240b674580 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2015,6 +2015,7 @@ class GuardrailMode(TypedDict, total=False): class StandardLoggingGuardrailInformation(TypedDict, total=False): guardrail_name: Optional[str] + guardrail_provider: Optional[str] guardrail_mode: Optional[ Union[GuardrailEventHooks, List[GuardrailEventHooks], GuardrailMode] ] diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer.tsx deleted file mode 100644 index a2909e28d4d..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import React, { useState } from "react"; - -interface RecognitionMetadata { - recognizer_name: string; - recognizer_identifier: string; -} - -interface GuardrailEntity { - end: number; - score: number; - start: number; - entity_type: string; - analysis_explanation: string | null; - recognition_metadata: RecognitionMetadata; -} - -interface MaskedEntityCount { - [key: string]: number; -} - -interface GuardrailInformation { - duration: number; - end_time: number; - start_time: number; - guardrail_mode: string; - guardrail_name: string; - guardrail_status: string; - guardrail_response: GuardrailEntity[]; - masked_entity_count: MaskedEntityCount; -} - -interface GuardrailViewerProps { - data: GuardrailInformation; -} - -export function GuardrailViewer({ data }: GuardrailViewerProps) { - const [sectionExpanded, setSectionExpanded] = useState(true); - const [entityListExpanded, setEntityListExpanded] = useState(true); - const [expandedEntities, setExpandedEntities] = useState>({}); - - if (!data) { - return null; - } - - // Calculate total masked entities - const totalMaskedEntities = data.masked_entity_count ? - Object.values(data.masked_entity_count).reduce((sum, count) => sum + count, 0) : 0; - - const formatTime = (timestamp: number): string => { - const date = new Date(timestamp * 1000); - return date.toLocaleString(); - }; - - const toggleEntity = (index: number) => { - setExpandedEntities(prev => ({ - ...prev, - [index]: !prev[index] - })); - }; - - const getScoreColor = (score: number): string => { - if (score >= 0.8) return "text-green-600"; - return "text-yellow-600"; - }; - - return ( -
-
setSectionExpanded(!sectionExpanded)} - > -
- - - -

Guardrail Information

- - {data.guardrail_status} - - {totalMaskedEntities > 0 && ( - - {totalMaskedEntities} masked {totalMaskedEntities === 1 ? 'entity' : 'entities'} - - )} -
- {sectionExpanded ? 'Click to collapse' : 'Click to expand'} -
- - {sectionExpanded && ( -
-
-
-
-
- Guardrail Name: - {data.guardrail_name} -
-
- Mode: - {data.guardrail_mode} -
-
- Status: - - {data.guardrail_status} - -
-
-
-
- Start Time: - {formatTime(data.start_time)} -
-
- End Time: - {formatTime(data.end_time)} -
-
- Duration: - {data.duration.toFixed(4)}s -
-
-
- - {/* Masked Entity Summary */} - {data.masked_entity_count && Object.keys(data.masked_entity_count).length > 0 && ( -
-

Masked Entity Summary

-
- {Object.entries(data.masked_entity_count).map(([entityType, count]) => ( - - {entityType}: {count} - - ))} -
-
- )} -
- - {/* Detected Entities Section */} - {data.guardrail_response && data.guardrail_response.length > 0 && ( -
-
setEntityListExpanded(!entityListExpanded)} - > - - - -

Detected Entities ({data.guardrail_response.length})

-
- - {entityListExpanded && ( -
- {data.guardrail_response.map((entity, index) => { - const isExpanded = expandedEntities[index] || false; - - return ( -
-
toggleEntity(index)} - > -
- - - - {entity.entity_type} - - Score: {entity.score.toFixed(2)} - -
- - Position: {entity.start}-{entity.end} - -
- - {isExpanded && ( -
-
-
-
- Entity Type: - {entity.entity_type} -
-
- Position: - Characters {entity.start}-{entity.end} -
-
- Confidence: - - {entity.score.toFixed(2)} - -
-
- -
- {entity.recognition_metadata && ( - <> -
- Recognizer: - {entity.recognition_metadata.recognizer_name} -
-
- Identifier: - - {entity.recognition_metadata.recognizer_identifier} - -
- - )} - {entity.analysis_explanation && ( -
- Explanation: - {entity.analysis_explanation} -
- )} -
-
-
- )} -
- ); - })} -
- )} -
- )} -
- )} -
- ); -} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/BedrockGuardrailDetails.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/BedrockGuardrailDetails.tsx new file mode 100644 index 00000000000..e7b041b93b4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/BedrockGuardrailDetails.tsx @@ -0,0 +1,508 @@ +import React, { useState } from "react"; + +export type BedrockGuardrailAction = "NONE" | "GUARDRAIL_INTERVENED"; + +export interface BedrockGuardrailUsage { + wordPolicyUnits?: number; + topicPolicyUnits?: number; + contentPolicyUnits?: number; + contentPolicyImageUnits?: number; + automatedReasoningPolicies?: number; + automatedReasoningPolicyUnits?: number; + contextualGroundingPolicyUnits?: number; + sensitiveInformationPolicyUnits?: number; + sensitiveInformationPolicyFreeUnits?: number; +} + +export interface BedrockGuardrailCoverageDim { + total?: number; + guarded?: number; +} + +export interface BedrockGuardrailCoverage { + images?: BedrockGuardrailCoverageDim; + textCharacters?: BedrockGuardrailCoverageDim; +} + +export interface BedrockWordItem { + action?: string; // e.g., "BLOCKED" or "ALLOWED" + detected?: boolean; + match?: string; + type?: string; // present on managedWordLists entries +} + +export interface BedrockContentFilter { + type?: string; // e.g. "HATE", etc. + action?: string; // "BLOCKED" | "NONE" (varies by config) + detected?: boolean; + filterStrength?: string; // e.g., "HIGH"/"MEDIUM"/"LOW" + confidence?: string; // e.g., "HIGH"/"MEDIUM"/"LOW" +} + +export interface BedrockContextualGroundingFilter { + type?: string; // "GROUNDING" | "RELEVANCE" + action?: string; // "BLOCKED" | ... + detected?: boolean; + score?: number; // model score + threshold?: number; // configured threshold +} + +export interface BedrockPiiEntity { + type?: string; // e.g., "ADDRESS", "EMAIL", etc. + match?: string; // the matched text (may be masked) + detected?: boolean; + action?: string; // "BLOCKED" | "ANONYMIZED" | ... +} + +export interface BedrockRegexFinding { + name?: string; + regex?: string; + match?: string; + detected?: boolean; + action?: string; +} + +export interface BedrockTopic { + name?: string; // topic list entry name + type?: string; // "DENY" | "ALLOW" depending on list + detected?: boolean; + action?: string; // "BLOCKED" | "NONE" +} + +export interface BedrockInvocationMetrics { + usage?: BedrockGuardrailUsage; + guardrailCoverage?: BedrockGuardrailCoverage; + guardrailProcessingLatency?: number; // in ms +} + +export interface BedrockAssessment { + wordPolicy?: { customWords?: BedrockWordItem[]; managedWordLists?: BedrockWordItem[] }; + contentPolicy?: { filters?: BedrockContentFilter[] }; + topicPolicy?: { topics?: BedrockTopic[] }; + sensitiveInformationPolicy?: { piiEntities?: BedrockPiiEntity[]; regexes?: BedrockRegexFinding[] }; + contextualGroundingPolicy?: { filters?: BedrockContextualGroundingFilter[] }; + automatedReasoningPolicy?: { findings?: any[] }; + invocationMetrics?: BedrockInvocationMetrics; +} + +export interface BedrockOutputContent { + text?: string; +} + +export interface BedrockGuardrailResponse { + action?: BedrockGuardrailAction; + actionReason?: string | null; + outputs?: BedrockOutputContent[]; + output?: BedrockOutputContent[]; + usage?: BedrockGuardrailUsage; + guardrailCoverage?: BedrockGuardrailCoverage; + assessments?: BedrockAssessment[]; + blockedResponse?: string; +} + +/** ====== UI helpers ====== */ +type ChipTone = "green" | "red" | "blue" | "slate" | "amber"; + +const chip = ( + text: React.ReactNode, + tone: ChipTone = "slate" +) => { + const map: Record = { + green: "bg-green-100 text-green-800", + red: "bg-red-100 text-red-800", + blue: "bg-blue-50 text-blue-700", + slate: "bg-slate-100 text-slate-800", + amber: "bg-amber-100 text-amber-800", + }; + return {text}; +}; + +const boolPill = (b?: boolean) => (b ? chip("detected", "red") : chip("not detected", "slate")); + +interface SectionProps { + title: string; + count?: number; + defaultOpen?: boolean; + right?: React.ReactNode; + children?: React.ReactNode; +} + +const Section: React.FC = ({ + title, + count, + defaultOpen = true, + right, + children, + }) => { + const [open, setOpen] = useState(defaultOpen); + return ( +
+
setOpen((v) => !v)} + > +
+ + + +
+ {title} {typeof count === "number" && ({count})} +
+
+
{right}
+
+ {open &&
{children}
} +
+ ); +}; + +interface KVProps { + label: string; + children?: React.ReactNode; + mono?: boolean; +} + +const KV: React.FC = ({ label, children, mono }) => ( +
+ {label} + {children} +
+); + +const Divider: React.FC = () =>
; + +/** ====== Main component ====== */ +export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailResponse }> = ({ response }) => { + + if (!response) return null; + + const outputs: BedrockOutputContent[] = (response.outputs ?? response.output ?? []) as BedrockOutputContent[]; + + const actionTone: "green" | "red" = response.action === "GUARDRAIL_INTERVENED" ? "red" : "green"; + + const coverageChips = ( +
+ {response.guardrailCoverage?.textCharacters && ( + chip( + `text guarded ${response.guardrailCoverage.textCharacters.guarded ?? 0}/${response.guardrailCoverage.textCharacters.total ?? 0}`, + "blue" + ) + )} + {response.guardrailCoverage?.images && ( + chip( + `images guarded ${response.guardrailCoverage.images.guarded ?? 0}/${response.guardrailCoverage.images.total ?? 0}`, + "blue" + ) + )} +
+ ); + + const usagePills = response.usage && ( +
+ {Object.entries(response.usage).map(([k, v]) => + typeof v === "number" ? ( + + {k}: {v} + + ) : null + )} +
+ ); + + return ( +
+ {/* Top summary card */} +
+
+
+ + {chip(response.action ?? "N/A", actionTone)} + + {response.actionReason && {response.actionReason}} + {response.blockedResponse && ( + + {response.blockedResponse} + + )} +
+
+ {coverageChips} + {usagePills} +
+
+ + {/* Outputs */} + {outputs.length > 0 && ( + <> + +

Outputs

+
+ {outputs.map((o, i) => ( +
+
{o.text ?? (non-text output)}
+
+ ))} +
+ + )} +
+ + {/* Assessments */} + {response.assessments?.length ? ( +
+ {response.assessments.map((assess, idx) => { + const policyBadges = ( +
+ {assess.wordPolicy && chip("word", "slate")} + {assess.contentPolicy && chip("content", "slate")} + {assess.topicPolicy && chip("topic", "slate")} + {assess.sensitiveInformationPolicy && chip("sensitive-info", "slate")} + {assess.contextualGroundingPolicy && chip("contextual-grounding", "slate")} + {assess.automatedReasoningPolicy && chip("automated-reasoning", "slate")} +
+ ); + + return ( +
+ {assess.invocationMetrics?.guardrailProcessingLatency != null && + chip(`${assess.invocationMetrics.guardrailProcessingLatency} ms`, "amber")} + {policyBadges} +
+ } + > + {/* Word policy */} + {assess.wordPolicy && ( +
+
Word Policy
+ {(assess.wordPolicy.customWords?.length ?? 0) > 0 && ( +
+
+ {assess.wordPolicy.customWords!.map((w, i) => ( +
+
+ {chip(w.action ?? "N/A", w.detected ? "red" : "slate")} + {w.match} +
+ {boolPill(w.detected)} +
+ ))} +
+
+ )} + {(assess.wordPolicy.managedWordLists?.length ?? 0) > 0 && ( +
+
+ {assess.wordPolicy.managedWordLists!.map((w, i) => ( +
+
+ {chip(w.action ?? "N/A", w.detected ? "red" : "slate")} + {w.match} + {w.type && chip(w.type, "slate")} +
+ {boolPill(w.detected)} +
+ ))} +
+
+ )} +
+ )} + + {/* Content policy */} + {assess.contentPolicy?.filters?.length ? ( +
+
Content Policy
+
+ + + + + + + + + + + + {assess.contentPolicy.filters!.map((f, i) => ( + + + + + + + + ))} + +
TypeActionDetectedStrengthConfidence
{f.type ?? "—"}{chip(f.action ?? "—", f.detected ? "red" : "slate")}{boolPill(f.detected)}{f.filterStrength ?? "—"}{f.confidence ?? "—"}
+
+
+ ) : null} + + {/* Contextual grounding */} + {assess.contextualGroundingPolicy?.filters?.length ? ( +
+
Contextual Grounding
+
+ + + + + + + + + + + + {assess.contextualGroundingPolicy.filters!.map((f, i) => ( + + + + + + + + ))} + +
TypeActionDetectedScoreThreshold
{f.type ?? "—"}{chip(f.action ?? "—", f.detected ? "red" : "slate")}{boolPill(f.detected)}{f.score ?? "—"}{f.threshold ?? "—"}
+
+
+ ) : null} + + {/* Sensitive Information */} + {assess.sensitiveInformationPolicy && ( +
+
Sensitive Information
+ {(assess.sensitiveInformationPolicy.piiEntities?.length ?? 0) > 0 && ( +
+
+ {assess.sensitiveInformationPolicy.piiEntities!.map((p, i) => ( +
+
+ {chip(p.action ?? "N/A", p.detected ? "red" : "slate")} + {p.type && chip(p.type, "slate")} + {p.match} +
+ {boolPill(p.detected)} +
+ ))} +
+
+ )} + {(assess.sensitiveInformationPolicy.regexes?.length ?? 0) > 0 && ( +
+
+ {assess.sensitiveInformationPolicy.regexes!.map((r, i) => ( +
+
+ {chip(r.action ?? "N/A", r.detected ? "red" : "slate")} + {r.name ?? "regex"} + {r.regex} +
+
+ {boolPill(r.detected)} + {r.match && {r.match}} +
+
+ ))} +
+
+ )} +
+ )} + + {/* Topic policy */} + {assess.topicPolicy?.topics?.length ? ( +
+
Topic Policy
+
+ {assess.topicPolicy.topics!.map((t, i) => ( +
+
+ {chip(t.action ?? "N/A", t.detected ? "red" : "slate")} + {t.name ?? "topic"} + {t.type && chip(t.type, "slate")} + {boolPill(t.detected)} +
+
+ ))} +
+
+ ) : null} + + {/* Invocation metrics */} + {assess.invocationMetrics && ( +
+
+
+ {assess.invocationMetrics.guardrailProcessingLatency ?? "—"} + +
+ {assess.invocationMetrics.guardrailCoverage?.textCharacters && + chip( + `text ${assess.invocationMetrics.guardrailCoverage.textCharacters.guarded ?? 0}/${ + assess.invocationMetrics.guardrailCoverage.textCharacters.total ?? 0 + }`, + "blue" + )} + {assess.invocationMetrics.guardrailCoverage?.images && + chip( + `images ${assess.invocationMetrics.guardrailCoverage.images.guarded ?? 0}/${ + assess.invocationMetrics.guardrailCoverage.images.total ?? 0 + }`, + "blue" + )} +
+
+
+
+ +
+ {assess.invocationMetrics.usage && + Object.entries(assess.invocationMetrics.usage).map(([k, v]) => + typeof v === "number" ? ( + + {k}: {v} + + ) : null + )} +
+
+
+
+
+ )} + + {/* Automated reasoning (fallback render) */} + {assess.automatedReasoningPolicy?.findings?.length ? ( +
+
+ {assess.automatedReasoningPolicy.findings!.map((f, i) => ( +
+                          {JSON.stringify(f, null, 2)}
+                        
+ ))} +
+
+ ) : null} + + ); + })} +
+ ) : null} + + {/* Raw JSON (for debugging / completeness) */} +
+
{JSON.stringify(response, null, 2)}
+
+
+ ); +}; + +export default BedrockGuardrailDetails; diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx new file mode 100644 index 00000000000..96d1da91627 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx @@ -0,0 +1,163 @@ +import React, { useState } from "react"; +import PresidioDetectedEntities from "./PresidioDetectedEntities"; +import BedrockGuardrailDetails, { + BedrockGuardrailResponse, +} from "@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails" + +interface RecognitionMetadata { + recognizer_name: string; + recognizer_identifier: string; +} + +interface GuardrailEntity { + end: number; + score: number; + start: number; + entity_type: string; + analysis_explanation: string | null; + recognition_metadata: RecognitionMetadata; +} + +interface MaskedEntityCount { + [key: string]: number; +} + +interface GuardrailInformation { + duration: number; + end_time: number; + start_time: number; + guardrail_mode: string; + guardrail_name: string; + guardrail_status: string; + guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse; + masked_entity_count: MaskedEntityCount; + guardrail_provider?: string; // "presidio" | other providers +} + +interface GuardrailViewerProps { + data: GuardrailInformation; +} + +const GuardrailViewer = ({ data }: GuardrailViewerProps) => { + const [sectionExpanded, setSectionExpanded] = useState(true); + + // Default to presidio for backwards compatibility + const guardrailProvider = data.guardrail_provider ?? "presidio"; + + if (!data) { + return null; + } + + // Calculate total masked entities + const totalMaskedEntities = data.masked_entity_count ? + Object.values(data.masked_entity_count).reduce((sum, count) => sum + count, 0) : 0; + + const formatTime = (timestamp: number): string => { + const date = new Date(timestamp * 1000); + return date.toLocaleString(); + }; + + return ( +
+
setSectionExpanded(!sectionExpanded)} + > +
+ + + +

Guardrail Information

+ + {data.guardrail_status} + + {totalMaskedEntities > 0 && ( + + {totalMaskedEntities} masked {totalMaskedEntities === 1 ? 'entity' : 'entities'} + + )} +
+ {sectionExpanded ? 'Click to collapse' : 'Click to expand'} +
+ + {sectionExpanded && ( +
+
+
+
+
+ Guardrail Name: + {data.guardrail_name} +
+
+ Mode: + {data.guardrail_mode} +
+
+ Status: + + {data.guardrail_status} + +
+
+
+
+ Start Time: + {formatTime(data.start_time)} +
+
+ End Time: + {formatTime(data.end_time)} +
+
+ Duration: + {data.duration.toFixed(4)}s +
+
+
+ + {/* Masked Entity Summary */} + {data.masked_entity_count && Object.keys(data.masked_entity_count).length > 0 && ( +
+

Masked Entity Summary

+
+ {Object.entries(data.masked_entity_count).map(([entityType, count]) => ( + + {entityType}: {count} + + ))} +
+
+ )} +
+ + {/* Provider-specific Detected Entities */} + {guardrailProvider === "presidio" && (data.guardrail_response as GuardrailEntity[])?.length > 0 && ( + + )} + + {guardrailProvider === "bedrock" && data.guardrail_response && ( +
+ +
+ )} +
+ )} +
+ ); +} + +export default GuardrailViewer; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/PresidioDetectedEntities.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/PresidioDetectedEntities.tsx new file mode 100644 index 00000000000..d03478a569e --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/PresidioDetectedEntities.tsx @@ -0,0 +1,136 @@ +import React, { useState } from "react"; + +interface RecognitionMetadata { + recognizer_name: string; + recognizer_identifier: string; +} + +export interface GuardrailEntity { + end: number; + score: number; + start: number; + entity_type: string; + analysis_explanation: string | null; + recognition_metadata: RecognitionMetadata; +} + +interface PresidioDetectedEntitiesProps { + entities: GuardrailEntity[]; +} + +const getScoreColor = (score: number): string => { + if (score >= 0.8) return "text-green-600"; + return "text-yellow-600"; +}; + +const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) => { + const [entityListExpanded, setEntityListExpanded] = useState(true); + const [expandedEntities, setExpandedEntities] = useState>({}); + + const toggleEntity = (index: number) => { + setExpandedEntities((prev) => ({ + ...prev, + [index]: !prev[index], + })); + }; + + if (!entities || entities.length === 0) return null; + + return ( +
+
setEntityListExpanded(!entityListExpanded)} + > + + + +

Detected Entities ({entities.length})

+
+ + {entityListExpanded && ( +
+ {entities.map((entity, index) => { + const isExpanded = expandedEntities[index] || false; + + return ( +
+
toggleEntity(index)} + > +
+ + + + {entity.entity_type} + + Score: {entity.score.toFixed(2)} + +
+ Position: {entity.start}-{entity.end} +
+ + {isExpanded && ( +
+
+
+
+ Entity Type: + {entity.entity_type} +
+
+ Position: + Characters {entity.start}-{entity.end} +
+
+ Confidence: + {entity.score.toFixed(2)} +
+
+ +
+ {entity.recognition_metadata && ( + <> +
+ Recognizer: + {entity.recognition_metadata.recognizer_name} +
+
+ Identifier: + + {entity.recognition_metadata.recognizer_identifier} + +
+ + )} + {entity.analysis_explanation && ( +
+ Explanation: + {entity.analysis_explanation} +
+ )} +
+
+
+ )} +
+ ); + })} +
+ )} +
+ ); +} + +export default PresidioDetectedEntities; diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 4552870ae9b..2a916db7f0f 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -17,7 +17,7 @@ import { KeyResponse, Team } from "../key_team_helpers/key_list" import KeyInfoView from "../templates/key_info_view" import { SessionView } from "./SessionView" import { VectorStoreViewer } from "./VectorStoreViewer" -import { GuardrailViewer } from "./GuardrailViewer" +import GuardrailViewer from "@/components/view_logs/GuardrailViewer/GuardrailViewer" import FilterComponent from "../molecules/filter" import { FilterOption } from "../molecules/filter" import { useLogFilterLogic } from "./log_filter_logic"