mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Add aviation and UAE policy templates with tag-based filtering (#21518)
* add aviation and UAE regex patterns (PNR, Skywards, Emirates ID, UAE phone, flight number) * add aviation safety topics category yaml * add airline brand protection category yaml * add UAE cultural sensitivity category yaml * add UAE anti-discrimination category yaml * add aviation, UAE, and competitor policy templates + migrate region/type to tags * sync policy_templates_backup.json * add /policy/templates/enrich endpoint for LLM competitor discovery * add enrichPolicyTemplate networking call * add TemplateParameterModal component * handle parameterized templates and LLM enrichment in policies index * replace region/type radio filters with tag sidebar checkboxes
This commit is contained in:
parent
a385126a7c
commit
91fd2b4621
12 changed files with 2798 additions and 364 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -452,6 +452,47 @@
|
|||
"pattern": "\\b\\d{1,6}\\s+[A-Za-z0-9][A-Za-z0-9\\s.'-]*\\s+(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Court|Ct|Place|Pl|Circle|Cir)\\b",
|
||||
"category": "PII Patterns",
|
||||
"description": "Detects street addresses (number + street name + street type)"
|
||||
},
|
||||
{
|
||||
"name": "airline_pnr",
|
||||
"display_name": "Airline PNR / Booking Reference",
|
||||
"pattern": "\\b[A-Z]{6}\\b",
|
||||
"category": "Aviation PII Patterns",
|
||||
"description": "Detects airline PNR / booking references (6 uppercase alpha characters) when near booking context",
|
||||
"keyword_pattern": "\\b(?:PNR|booking\\s*(?:reference|ref|code|number|confirmation)|reservation\\s*(?:code|number|ref)|record\\s*locator|confirmation\\s*(?:code|number)|itinerary\\s*(?:number|ref))\\b",
|
||||
"allow_word_numbers": false
|
||||
},
|
||||
{
|
||||
"name": "skywards_number",
|
||||
"display_name": "Emirates Skywards / Frequent Flyer Number",
|
||||
"pattern": "\\b(?:EK\\s?)?\\d{9,10}\\b",
|
||||
"category": "Aviation PII Patterns",
|
||||
"description": "Detects Emirates Skywards frequent flyer numbers (9-10 digits, optional EK prefix) when near loyalty/frequent flyer context",
|
||||
"keyword_pattern": "\\b(?:[Ss]kywards|frequent\\s*flyer|FF\\s*(?:number|no|#)|loyalty\\s*(?:number|id|member)|member\\s*(?:number|id|#)|miles\\s*(?:account|number)|tier\\s*(?:number|status))\\b",
|
||||
"allow_word_numbers": false
|
||||
},
|
||||
{
|
||||
"name": "uae_emirates_id",
|
||||
"display_name": "UAE Emirates ID",
|
||||
"pattern": "\\b784-?\\d{4}-?\\d{7}-?\\d\\b",
|
||||
"category": "UAE PII Patterns",
|
||||
"description": "Detects UAE Emirates ID numbers (784-YYYY-NNNNNNN-C format, 15 digits starting with 784)"
|
||||
},
|
||||
{
|
||||
"name": "uae_phone",
|
||||
"display_name": "Phone Number (UAE)",
|
||||
"pattern": "(?<!\\d)(?:\\+971|00971|0)\\s?(?:2|3|4|6|7|9|50|52|54|55|56|58)\\s?\\d{3}\\s?\\d{4}\\b",
|
||||
"category": "UAE PII Patterns",
|
||||
"description": "Detects UAE phone numbers (+971/00971/0 prefix, landline and mobile)"
|
||||
},
|
||||
{
|
||||
"name": "flight_number",
|
||||
"display_name": "Airline Flight Number",
|
||||
"pattern": "\\b(?:EK|QF|BA|AA|LH|AF|SQ|CX|QR|EY|FZ|WY|GF|KU|SV|MS|RJ|ME|PK|AI|UL|WS|9W|6E)\\s?\\d{1,4}\\b",
|
||||
"category": "Aviation PII Patterns",
|
||||
"description": "Detects airline flight numbers (major IATA 2-letter codes + 1-4 digit flight number) when near flight context",
|
||||
"keyword_pattern": "\\b(?:flight|departure|arrival|gate|boarding|schedule|operate|route|aircraft|plane|outbound|inbound|leg|sector|flying)\\b",
|
||||
"allow_word_numbers": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# Airline Brand Protection
|
||||
# Prevents AI from generating fake incident reports, unauthorized statements, or negative PR
|
||||
# NOTE: The identifier_words use {{brand_name}} placeholder - this file is used as a reference.
|
||||
# The actual brand-specific keywords are injected at template instantiation time via blocked_words.
|
||||
category_name: "airline_brand_protection"
|
||||
description: "Prevents AI from generating fake incident reports, unauthorized airline statements, or reputation-damaging content"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
# Generic airline context (brand-specific terms injected at runtime)
|
||||
- "airline"
|
||||
- "carrier"
|
||||
- "airways"
|
||||
- "flights"
|
||||
|
||||
additional_block_words:
|
||||
# Fake incident reports
|
||||
- "crashed"
|
||||
- "crash landing"
|
||||
- "emergency landing"
|
||||
- "engine failure"
|
||||
- "engine fire"
|
||||
- "depressurization"
|
||||
- "hull breach"
|
||||
- "mayday"
|
||||
- "mid-air collision"
|
||||
- "near miss"
|
||||
- "fatality"
|
||||
- "fatalities"
|
||||
- "dead passengers"
|
||||
- "casualties"
|
||||
# Unauthorized statements
|
||||
- "official statement"
|
||||
- "spokesperson said"
|
||||
- "press release"
|
||||
- "confirms"
|
||||
- "announces"
|
||||
- "announced today"
|
||||
- "breaking news"
|
||||
- "just in"
|
||||
- "BREAKING"
|
||||
- "statement from"
|
||||
# PR / reputation attacks
|
||||
- "scandal"
|
||||
- "cover up"
|
||||
- "coverup"
|
||||
- "lawsuit"
|
||||
- "class action"
|
||||
- "mistreatment"
|
||||
- "safety violation"
|
||||
- "whistleblower"
|
||||
- "banned"
|
||||
- "grounded fleet"
|
||||
- "recall"
|
||||
- "boycott"
|
||||
- "worst airline"
|
||||
- "unsafe"
|
||||
- "dangerous"
|
||||
- "negligence"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "plane crash"
|
||||
severity: "high"
|
||||
- keyword: "flight crashed"
|
||||
severity: "high"
|
||||
- keyword: "mid-air collision"
|
||||
severity: "high"
|
||||
- keyword: "passengers dead"
|
||||
severity: "high"
|
||||
- keyword: "confirms fatalities"
|
||||
severity: "high"
|
||||
- keyword: "safety scandal"
|
||||
severity: "high"
|
||||
- keyword: "fleet grounded"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "how safe is"
|
||||
- "safety record"
|
||||
- "safety rating"
|
||||
- "what is"
|
||||
- "explain"
|
||||
- "historical"
|
||||
- "aviation history"
|
||||
- "customer review"
|
||||
- "customer service"
|
||||
- "feedback"
|
||||
- "improve"
|
||||
- "complaint resolution"
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# Aviation Safety & Security Topic Restrictions
|
||||
# Prevents AI from generating content about aircraft vulnerabilities, security bypass, cockpit access
|
||||
category_name: "aviation_safety_topics"
|
||||
description: "Blocks AI from generating content about aviation security vulnerabilities, bypass procedures, and restricted operations data"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
- "aircraft"
|
||||
- "airplane"
|
||||
- "plane"
|
||||
- "aviation"
|
||||
- "airline"
|
||||
- "airport"
|
||||
- "cockpit"
|
||||
- "flight deck"
|
||||
- "airside"
|
||||
- "tarmac"
|
||||
- "runway"
|
||||
- "cargo hold"
|
||||
- "fuselage"
|
||||
- "avionics"
|
||||
- "transponder"
|
||||
- "ATC"
|
||||
- "air traffic control"
|
||||
- "ACARS"
|
||||
- "flight management system"
|
||||
- "FMS"
|
||||
- "autopilot"
|
||||
- "A380"
|
||||
- "B777"
|
||||
- "777"
|
||||
- "787"
|
||||
|
||||
additional_block_words:
|
||||
# Security bypass
|
||||
- "bypass security"
|
||||
- "bypass screening"
|
||||
- "defeat security"
|
||||
- "circumvent security"
|
||||
- "evade detection"
|
||||
- "avoid screening"
|
||||
- "smuggle"
|
||||
- "smuggling"
|
||||
- "conceal weapon"
|
||||
- "hidden weapon"
|
||||
# Cockpit access
|
||||
- "cockpit door code"
|
||||
- "cockpit access code"
|
||||
- "override cockpit"
|
||||
- "force cockpit door"
|
||||
- "break into cockpit"
|
||||
- "cockpit intrusion"
|
||||
- "cockpit breach"
|
||||
# Aircraft vulnerabilities
|
||||
- "vulnerability"
|
||||
- "exploit"
|
||||
- "disable"
|
||||
- "jamming"
|
||||
- "spoofing"
|
||||
- "hijack"
|
||||
- "hijacking"
|
||||
- "take over"
|
||||
- "commandeer"
|
||||
- "crash intentionally"
|
||||
- "bring down"
|
||||
- "shoot down"
|
||||
- "sabotage"
|
||||
- "tamper"
|
||||
- "interfere with"
|
||||
# System exploitation
|
||||
- "hack"
|
||||
- "hack into"
|
||||
- "inject malware"
|
||||
- "remote access"
|
||||
- "unauthorized access"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "hijack a plane"
|
||||
severity: "high"
|
||||
- keyword: "hijack an aircraft"
|
||||
severity: "high"
|
||||
- keyword: "cockpit door override code"
|
||||
severity: "high"
|
||||
- keyword: "bypass airport security"
|
||||
severity: "high"
|
||||
- keyword: "smuggle weapons onto aircraft"
|
||||
severity: "high"
|
||||
- keyword: "disable aircraft transponder"
|
||||
severity: "high"
|
||||
- keyword: "bring down an aircraft"
|
||||
severity: "high"
|
||||
- keyword: "crash a plane into"
|
||||
severity: "high"
|
||||
- keyword: "aircraft bomb"
|
||||
severity: "high"
|
||||
- keyword: "ACARS vulnerability"
|
||||
severity: "high"
|
||||
- keyword: "flight management system hack"
|
||||
severity: "high"
|
||||
- keyword: "spoof ADS-B"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "aviation safety"
|
||||
- "safety report"
|
||||
- "safety investigation"
|
||||
- "safety improvement"
|
||||
- "NTSB"
|
||||
- "accident investigation"
|
||||
- "safety training"
|
||||
- "security awareness"
|
||||
- "safety management system"
|
||||
- "IATA safety"
|
||||
- "ICAO"
|
||||
- "how to report"
|
||||
- "incident report"
|
||||
- "safety culture"
|
||||
- "what is"
|
||||
- "explain"
|
||||
- "research on"
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# UAE Anti-Discrimination Compliance
|
||||
# UAE Federal Decree-Law No. 2/2015 on combating discrimination and hatred
|
||||
category_name: "uae_anti_discrimination"
|
||||
description: "Blocks content violating UAE Federal anti-discrimination laws (Decree-Law No. 2/2015)"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
- "Emirati"
|
||||
- "Emiratis"
|
||||
- "expatriate"
|
||||
- "expat"
|
||||
- "migrant worker"
|
||||
- "laborer"
|
||||
- "domestic worker"
|
||||
- "nationality"
|
||||
- "religion"
|
||||
- "race"
|
||||
- "ethnicity"
|
||||
- "caste"
|
||||
- "sect"
|
||||
|
||||
additional_block_words:
|
||||
- "inferior"
|
||||
- "superior"
|
||||
- "subhuman"
|
||||
- "uncivilized"
|
||||
- "backward"
|
||||
- "primitive"
|
||||
- "servant class"
|
||||
- "slave"
|
||||
- "slavery"
|
||||
- "second class"
|
||||
- "second-class citizen"
|
||||
- "don't belong"
|
||||
- "go back to"
|
||||
- "deport"
|
||||
- "hate"
|
||||
- "hatred"
|
||||
- "incite"
|
||||
- "incitement"
|
||||
- "sedition"
|
||||
- "sectarian"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "racial superiority"
|
||||
severity: "high"
|
||||
- keyword: "ethnic cleansing"
|
||||
severity: "high"
|
||||
- keyword: "sectarian violence"
|
||||
severity: "high"
|
||||
- keyword: "incite hatred"
|
||||
severity: "high"
|
||||
- keyword: "incite discrimination"
|
||||
severity: "high"
|
||||
- keyword: "religious hatred"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "anti-discrimination"
|
||||
- "equal rights"
|
||||
- "diversity and inclusion"
|
||||
- "tolerance"
|
||||
- "coexistence"
|
||||
- "UAE Year of Tolerance"
|
||||
- "human rights"
|
||||
- "what is"
|
||||
- "explain"
|
||||
- "history of"
|
||||
- "education"
|
||||
- "awareness"
|
||||
- "compliance training"
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# UAE Cultural Sensitivity
|
||||
# Respects UAE cultural norms, royal family references, and religious content policies
|
||||
category_name: "uae_cultural_sensitivity"
|
||||
description: "Ensures AI output respects UAE cultural norms, royal family, and religious content policies"
|
||||
default_action: "BLOCK"
|
||||
|
||||
identifier_words:
|
||||
- "UAE"
|
||||
- "United Arab Emirates"
|
||||
- "Dubai"
|
||||
- "Abu Dhabi"
|
||||
- "Sharjah"
|
||||
- "Ajman"
|
||||
- "Fujairah"
|
||||
- "Ras Al Khaimah"
|
||||
- "Umm Al Quwain"
|
||||
- "Emirati"
|
||||
- "Emiratis"
|
||||
|
||||
additional_block_words:
|
||||
# Royal family disrespect
|
||||
- "mock"
|
||||
- "ridicule"
|
||||
- "insult"
|
||||
- "satirize"
|
||||
- "caricature"
|
||||
- "parody"
|
||||
- "defame"
|
||||
- "slander"
|
||||
- "disrespect"
|
||||
# Cultural sensitivity
|
||||
- "alcohol promotion"
|
||||
- "gambling promotion"
|
||||
- "drug use"
|
||||
- "immodest"
|
||||
- "indecent"
|
||||
|
||||
always_block_keywords:
|
||||
- keyword: "insult the ruler"
|
||||
severity: "high"
|
||||
- keyword: "mock the sheikh"
|
||||
severity: "high"
|
||||
- keyword: "ridicule the royal family"
|
||||
severity: "high"
|
||||
- keyword: "defame Mohammed bin"
|
||||
severity: "high"
|
||||
- keyword: "blasphemy against Islam"
|
||||
severity: "high"
|
||||
- keyword: "insult Islam"
|
||||
severity: "high"
|
||||
- keyword: "mock the Prophet"
|
||||
severity: "high"
|
||||
- keyword: "anti-Islamic"
|
||||
severity: "high"
|
||||
- keyword: "desecrate the Quran"
|
||||
severity: "high"
|
||||
|
||||
exceptions:
|
||||
- "what is"
|
||||
- "explain"
|
||||
- "cultural guide"
|
||||
- "travel guide"
|
||||
- "etiquette"
|
||||
- "customs"
|
||||
- "tradition"
|
||||
- "how to respect"
|
||||
- "cultural awareness"
|
||||
- "history of"
|
||||
- "learn about"
|
||||
- "understand"
|
||||
- "policy on"
|
||||
- "regulation"
|
||||
|
|
@ -14,7 +14,7 @@ import os
|
|||
from typing import TYPE_CHECKING, List, Literal, Optional, TypedDict, cast
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
|
|
@ -507,3 +507,137 @@ async def get_policy_templates(
|
|||
)
|
||||
|
||||
return _load_policy_templates_from_local_backup()
|
||||
|
||||
|
||||
class EnrichTemplateRequest(BaseModel):
|
||||
template_id: str
|
||||
parameters: dict
|
||||
|
||||
|
||||
@router.post(
|
||||
"/policy/templates/enrich",
|
||||
tags=["policy management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def enrich_policy_template(
|
||||
data: EnrichTemplateRequest,
|
||||
request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> dict:
|
||||
"""
|
||||
Enrich a policy template with LLM-discovered data (e.g. competitor names).
|
||||
|
||||
Calls an onboarded LLM to discover competitors for the given brand name,
|
||||
then returns enriched guardrailDefinitions with the discovered data populated.
|
||||
"""
|
||||
templates = _load_policy_templates_from_local_backup()
|
||||
template = next((t for t in templates if t.get("id") == data.template_id), None)
|
||||
if template is None:
|
||||
raise HTTPException(status_code=404, detail=f"Template '{data.template_id}' not found")
|
||||
|
||||
llm_enrichment = template.get("llm_enrichment")
|
||||
if llm_enrichment is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Template does not support LLM enrichment",
|
||||
)
|
||||
|
||||
brand_name = data.parameters.get(llm_enrichment["parameter"], "")
|
||||
if not brand_name:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Parameter '{llm_enrichment['parameter']}' is required",
|
||||
)
|
||||
|
||||
prompt = llm_enrichment["prompt"].replace(
|
||||
"{{" + llm_enrichment["parameter"] + "}}", brand_name
|
||||
)
|
||||
|
||||
competitors = await _discover_competitors_via_llm(prompt)
|
||||
|
||||
enriched_definitions = _build_competitor_guardrail_definitions(
|
||||
template.get("guardrailDefinitions", []),
|
||||
competitors,
|
||||
brand_name,
|
||||
)
|
||||
|
||||
return {"guardrailDefinitions": enriched_definitions, "competitors": competitors}
|
||||
|
||||
|
||||
async def _discover_competitors_via_llm(prompt: str) -> list:
|
||||
"""Call an onboarded LLM to discover competitor names."""
|
||||
import litellm
|
||||
|
||||
try:
|
||||
response = await litellm.acompletion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.3,
|
||||
)
|
||||
raw = response.choices[0].message.content or "" # type: ignore
|
||||
competitors = [
|
||||
line.strip().strip(".-) ").strip()
|
||||
for line in raw.strip().split("\n")
|
||||
if line.strip() and len(line.strip()) > 1
|
||||
]
|
||||
return competitors[:15]
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error("LLM competitor discovery failed: %s", e)
|
||||
return []
|
||||
|
||||
|
||||
def _build_competitor_guardrail_definitions(
|
||||
definitions: list,
|
||||
competitors: list,
|
||||
brand_name: str,
|
||||
) -> list:
|
||||
"""Build enriched guardrailDefinitions with competitor names populated."""
|
||||
import copy
|
||||
|
||||
enriched = copy.deepcopy(definitions)
|
||||
|
||||
output_blocked = [
|
||||
{"keyword": comp, "action": "BLOCK", "description": f"Competitor: {comp}"}
|
||||
for comp in competitors
|
||||
]
|
||||
|
||||
recommendation_blocked = []
|
||||
for comp in competitors:
|
||||
recommendation_blocked.append(
|
||||
{"keyword": f"try {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
|
||||
)
|
||||
recommendation_blocked.append(
|
||||
{"keyword": f"use {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
|
||||
)
|
||||
recommendation_blocked.append(
|
||||
{"keyword": f"switch to {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
|
||||
)
|
||||
recommendation_blocked.append(
|
||||
{"keyword": f"consider {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
|
||||
)
|
||||
|
||||
comparison_blocked = []
|
||||
for comp in competitors:
|
||||
comparison_blocked.append(
|
||||
{"keyword": f"{comp} is better", "action": "BLOCK", "description": "Unfavorable comparison"}
|
||||
)
|
||||
comparison_blocked.append(
|
||||
{"keyword": f"better than {brand_name}", "action": "BLOCK", "description": "Unfavorable comparison"}
|
||||
)
|
||||
comparison_blocked.append(
|
||||
{"keyword": f"{brand_name} is worse", "action": "BLOCK", "description": "Unfavorable comparison"}
|
||||
)
|
||||
|
||||
blocked_words_map = {
|
||||
"competitor-output-blocker": output_blocked,
|
||||
"competitor-recommendation-filter": recommendation_blocked,
|
||||
"competitor-comparison-filter": comparison_blocked,
|
||||
}
|
||||
|
||||
for defn in enriched:
|
||||
guardrail_name = defn.get("guardrail_name", "")
|
||||
if guardrail_name in blocked_words_map:
|
||||
defn["litellm_params"]["blocked_words"] = blocked_words_map[guardrail_name]
|
||||
|
||||
return enriched
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5552,6 +5552,39 @@ export const getPolicyTemplates = async (accessToken: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const enrichPolicyTemplate = async (
|
||||
accessToken: string,
|
||||
templateId: string,
|
||||
parameters: Record<string, string>
|
||||
) => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/policy/templates/enrich`
|
||||
: `/policy/templates/enrich`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ template_id: templateId, parameters }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to enrich policy template:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createPolicyCall = async (accessToken: string, policyData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import AddAttachmentForm from "./add_attachment_form";
|
|||
import PolicyTestPanel from "./policy_test_panel";
|
||||
import PolicyTemplates from "./policy_templates";
|
||||
import GuardrailSelectionModal from "./guardrail_selection_modal";
|
||||
import TemplateParameterModal from "./template_parameter_modal";
|
||||
import {
|
||||
getPoliciesList,
|
||||
deletePolicyCall,
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
updatePolicyCall,
|
||||
createPolicyAttachmentCall,
|
||||
createGuardrailCall,
|
||||
enrichPolicyTemplate,
|
||||
} from "../networking";
|
||||
import {
|
||||
Policy,
|
||||
|
|
@ -58,6 +60,9 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
|||
const [existingGuardrailNames, setExistingGuardrailNames] = useState<Set<string>>(new Set());
|
||||
const [isCreatingGuardrails, setIsCreatingGuardrails] = useState(false);
|
||||
const [showFlowBuilder, setShowFlowBuilder] = useState(false);
|
||||
const [isParameterModalOpen, setIsParameterModalOpen] = useState(false);
|
||||
const [isEnrichingTemplate, setIsEnrichingTemplate] = useState(false);
|
||||
const [pendingTemplate, setPendingTemplate] = useState<any>(null);
|
||||
|
||||
const isAdmin = userRole ? isAdminRole(userRole) : false;
|
||||
|
||||
|
|
@ -187,8 +192,20 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// If template has parameters, show parameter modal first
|
||||
if (template.parameters && template.parameters.length > 0) {
|
||||
setPendingTemplate(template);
|
||||
setIsParameterModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await proceedWithTemplate(template);
|
||||
};
|
||||
|
||||
const proceedWithTemplate = async (template: any) => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
// Fetch existing guardrails to show in the modal
|
||||
const existingGuardrailsResponse = await getGuardrailsList(accessToken);
|
||||
const existingNames = new Set<string>(
|
||||
existingGuardrailsResponse.guardrails?.map((g: any) => g.guardrail_name as string) || []
|
||||
|
|
@ -203,6 +220,53 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const substituteParameters = (template: any, parameters: Record<string, string>): any => {
|
||||
let templateStr = JSON.stringify(template);
|
||||
for (const [key, value] of Object.entries(parameters)) {
|
||||
templateStr = templateStr.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
||||
}
|
||||
return JSON.parse(templateStr);
|
||||
};
|
||||
|
||||
const handleParameterConfirm = async (parameters: Record<string, string>) => {
|
||||
if (!accessToken || !pendingTemplate) return;
|
||||
|
||||
setIsEnrichingTemplate(true);
|
||||
|
||||
try {
|
||||
let enrichedTemplate = pendingTemplate;
|
||||
|
||||
if (pendingTemplate.llm_enrichment) {
|
||||
// Call backend to enrich template with LLM-discovered data
|
||||
const enrichResult = await enrichPolicyTemplate(
|
||||
accessToken,
|
||||
pendingTemplate.id,
|
||||
parameters
|
||||
);
|
||||
// The backend returns the enriched guardrailDefinitions
|
||||
enrichedTemplate = { ...pendingTemplate, guardrailDefinitions: enrichResult.guardrailDefinitions };
|
||||
}
|
||||
|
||||
// Substitute parameters in template
|
||||
enrichedTemplate = substituteParameters(enrichedTemplate, parameters);
|
||||
|
||||
setIsParameterModalOpen(false);
|
||||
setIsEnrichingTemplate(false);
|
||||
setPendingTemplate(null);
|
||||
|
||||
await proceedWithTemplate(enrichedTemplate);
|
||||
} catch (error) {
|
||||
console.error("Error enriching template:", error);
|
||||
message.error("Failed to configure template. Please try again.");
|
||||
setIsEnrichingTemplate(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleParameterCancel = () => {
|
||||
setIsParameterModalOpen(false);
|
||||
setPendingTemplate(null);
|
||||
};
|
||||
|
||||
const handleGuardrailSelectionConfirm = async (selectedGuardrailDefinitions: any[]) => {
|
||||
if (!accessToken || !selectedTemplate) return;
|
||||
|
||||
|
|
@ -420,6 +484,14 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
|||
onCancel={handleGuardrailSelectionCancel}
|
||||
isLoading={isCreatingGuardrails}
|
||||
/>
|
||||
|
||||
<TemplateParameterModal
|
||||
visible={isParameterModalOpen}
|
||||
template={pendingTemplate}
|
||||
onConfirm={handleParameterConfirm}
|
||||
onCancel={handleParameterCancel}
|
||||
isLoading={isEnrichingTemplate}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Card, Button, Spin, message, Radio } from "antd";
|
||||
import { Card, Button, Spin, message, Checkbox, Badge } from "antd";
|
||||
import {
|
||||
ShieldCheckIcon,
|
||||
ShieldExclamationIcon,
|
||||
|
|
@ -16,6 +16,7 @@ interface PolicyTemplateCardProps {
|
|||
iconColor: string;
|
||||
iconBg: string;
|
||||
guardrails: string[];
|
||||
tags: string[];
|
||||
inherits?: string;
|
||||
complexity: "Low" | "Medium" | "High";
|
||||
onUseTemplate: () => void;
|
||||
|
|
@ -28,6 +29,7 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
|
|||
iconColor,
|
||||
iconBg,
|
||||
guardrails,
|
||||
tags,
|
||||
inherits,
|
||||
complexity,
|
||||
onUseTemplate,
|
||||
|
|
@ -60,7 +62,20 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
|
|||
</div>
|
||||
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-2">{title}</h3>
|
||||
<p className="text-sm text-gray-500 mb-6 flex-grow">{description}</p>
|
||||
<p className="text-sm text-gray-500 mb-4 flex-grow">{description}</p>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-4">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inherits && (
|
||||
<div className="mb-4 text-xs">
|
||||
|
|
@ -116,26 +131,45 @@ const iconMap: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>
|
|||
const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, accessToken }) => {
|
||||
const [templates, setTemplates] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>("All");
|
||||
const [selectedType, setSelectedType] = useState<string>("All");
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
|
||||
const availableRegions = useMemo(() => {
|
||||
const regions = new Set(templates.map(t => t.region || "Global"));
|
||||
return ["All", ...Array.from(regions).sort()];
|
||||
}, [templates]);
|
||||
|
||||
const availableTypes = useMemo(() => {
|
||||
const types = new Set(templates.map(t => t.type || "General"));
|
||||
return ["All", ...Array.from(types).sort()];
|
||||
}, [templates]);
|
||||
|
||||
const filteredTemplates = useMemo(() => {
|
||||
return templates.filter(t => {
|
||||
const regionMatch = selectedRegion === "All" || (t.region || "Global") === selectedRegion;
|
||||
const typeMatch = selectedType === "All" || (t.type || "General") === selectedType;
|
||||
return regionMatch && typeMatch;
|
||||
// Compute all unique tags with counts
|
||||
const tagCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
templates.forEach((t) => {
|
||||
const tags: string[] = t.tags || [];
|
||||
tags.forEach((tag: string) => {
|
||||
counts[tag] = (counts[tag] || 0) + 1;
|
||||
});
|
||||
});
|
||||
}, [templates, selectedRegion, selectedType]);
|
||||
// Sort alphabetically
|
||||
return Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
|
||||
}, [templates]);
|
||||
|
||||
// Filter templates: show templates that have ALL selected tags (AND logic)
|
||||
const filteredTemplates = useMemo(() => {
|
||||
if (selectedTags.size === 0) return templates;
|
||||
return templates.filter((t) => {
|
||||
const tags: string[] = t.tags || [];
|
||||
return Array.from(selectedTags).every((selectedTag) => tags.includes(selectedTag));
|
||||
});
|
||||
}, [templates, selectedTags]);
|
||||
|
||||
const handleTagToggle = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(tag)) {
|
||||
next.delete(tag);
|
||||
} else {
|
||||
next.add(tag);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAll = () => {
|
||||
setSelectedTags(new Set());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTemplates = async () => {
|
||||
|
|
@ -178,54 +212,88 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, access
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Region:</span>
|
||||
<Radio.Group
|
||||
value={selectedRegion}
|
||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||
buttonStyle="solid"
|
||||
>
|
||||
{availableRegions.map(region => (
|
||||
<Radio.Button key={region} value={region}>
|
||||
{region}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{availableTypes.length > 2 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Type:</span>
|
||||
<Radio.Group
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
buttonStyle="solid"
|
||||
>
|
||||
{availableTypes.map(type => (
|
||||
<Radio.Button key={type} value={type}>
|
||||
{type}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
<div className="flex gap-6">
|
||||
{/* Left sidebar - tag filters */}
|
||||
{tagCounts.length > 0 && (
|
||||
<div className="w-52 flex-shrink-0">
|
||||
<div className="sticky top-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
Categories
|
||||
</span>
|
||||
{selectedTags.size > 0 && (
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{tagCounts.map(([tag, count]) => (
|
||||
<label
|
||||
key={tag}
|
||||
className={`flex items-center justify-between px-2 py-1.5 rounded-md cursor-pointer transition-colors ${
|
||||
selectedTags.has(tag)
|
||||
? "bg-blue-50"
|
||||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedTags.has(tag)}
|
||||
onChange={() => handleTagToggle(tag)}
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{tag}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 font-medium">
|
||||
{count}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredTemplates.map((template, index) => (
|
||||
<PolicyTemplateCard
|
||||
key={template.id || index}
|
||||
title={template.title}
|
||||
description={template.description}
|
||||
icon={iconMap[template.icon] || ShieldCheckIcon}
|
||||
iconColor={template.iconColor}
|
||||
iconBg={template.iconBg}
|
||||
guardrails={template.guardrails}
|
||||
inherits={template.inherits}
|
||||
complexity={template.complexity}
|
||||
onUseTemplate={() => onUseTemplate(template)}
|
||||
/>
|
||||
))}
|
||||
{/* Right content - template cards */}
|
||||
<div className="flex-1">
|
||||
{selectedTags.size > 0 && (
|
||||
<div className="mb-4 text-sm text-gray-500">
|
||||
Showing {filteredTemplates.length} of {templates.length} templates
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredTemplates.map((template, index) => (
|
||||
<PolicyTemplateCard
|
||||
key={template.id || index}
|
||||
title={template.title}
|
||||
description={template.description}
|
||||
icon={iconMap[template.icon] || ShieldCheckIcon}
|
||||
iconColor={template.iconColor}
|
||||
iconBg={template.iconBg}
|
||||
guardrails={template.guardrails}
|
||||
tags={template.tags || []}
|
||||
inherits={template.inherits}
|
||||
complexity={template.complexity}
|
||||
onUseTemplate={() => onUseTemplate(template)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredTemplates.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<p>No templates match the selected filters.</p>
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className="text-blue-600 hover:text-blue-800 mt-2 text-sm"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Spin } from "antd";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
|
||||
interface TemplateParameter {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface TemplateParameterModalProps {
|
||||
visible: boolean;
|
||||
template: any;
|
||||
onConfirm: (parameters: Record<string, string>) => void;
|
||||
onCancel: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
|
||||
visible,
|
||||
template,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const [parameterValues, setParameterValues] = useState<Record<string, string>>({});
|
||||
|
||||
const parameters: TemplateParameter[] = template?.parameters || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && template) {
|
||||
const initial: Record<string, string> = {};
|
||||
parameters.forEach((p) => {
|
||||
initial[p.name] = "";
|
||||
});
|
||||
setParameterValues(initial);
|
||||
}
|
||||
}, [visible, template]);
|
||||
|
||||
const allRequiredFilled = parameters
|
||||
.filter((p) => p.required)
|
||||
.every((p) => (parameterValues[p.name] || "").trim().length > 0);
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm(parameterValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-1">{template?.title}</h3>
|
||||
<p className="text-sm text-gray-500 font-normal">
|
||||
{template?.llm_enrichment
|
||||
? "Enter your brand name to auto-discover competitors and configure guardrails"
|
||||
: "Configure template parameters"}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
width={500}
|
||||
footer={[
|
||||
<Button key="cancel" variant="secondary" onClick={onCancel} disabled={isLoading}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="confirm"
|
||||
onClick={handleConfirm}
|
||||
loading={isLoading}
|
||||
disabled={!allRequiredFilled || isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? template?.llm_enrichment
|
||||
? "Discovering competitors..."
|
||||
: "Processing..."
|
||||
: "Continue"}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="py-4 space-y-4">
|
||||
{parameters.map((param) => (
|
||||
<div key={param.name}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{param.label}
|
||||
{param.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder={param.placeholder || ""}
|
||||
value={parameterValues[param.name] || ""}
|
||||
onChange={(e) =>
|
||||
setParameterValues((prev) => ({
|
||||
...prev,
|
||||
[param.name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{template?.llm_enrichment && (
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<p className="text-sm text-blue-800">
|
||||
This template uses AI to automatically discover your competitors and configure
|
||||
guardrails. An onboarded LLM will be called to identify competitor names.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-3 mt-4 p-3 bg-gray-50 rounded-lg">
|
||||
<Spin size="small" />
|
||||
<span className="text-sm text-gray-600">
|
||||
{template?.llm_enrichment
|
||||
? "Using AI to discover competitors..."
|
||||
: "Processing template..."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateParameterModal;
|
||||
Loading…
Add table
Reference in a new issue