Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.py: 81%
69 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2Prebuilt regex patterns for content filtering.
4This module loads predefined regex patterns from patterns.json for detecting
5sensitive information like SSNs, credit cards, API keys, etc.
6"""
8import json
9import os
10import re
11from enum import Enum
12from re import Pattern
13from typing import Any, Final
16def _load_patterns_from_json() -> dict:
17 """Load pattern definitions from patterns.json file"""
18 json_path: Final = os.path.join(os.path.dirname(__file__), "patterns.json")
19 with open(json_path, "r") as f:
20 return json.load(f)
23# Load patterns from JSON
24_PATTERNS_DATA: Final = _load_patterns_from_json()
27class PrebuiltPatternName(str, Enum):
28 """Enum for prebuilt pattern names - dynamically generated from JSON"""
31# Dynamically create enum values from JSON
32for pattern_data in _PATTERNS_DATA["patterns"]:
33 setattr(PrebuiltPatternName, pattern_data["name"].upper(), pattern_data["name"])
36# Build lookup dictionaries from JSON
37PREBUILT_PATTERNS: Final[dict[str, str]] = {
38 pattern_data["name"]: pattern_data["pattern"] for pattern_data in _PATTERNS_DATA["patterns"]
39}
42# Capture any extra configuration declared per pattern (e.g., contextual keywords)
43KNOWN_PATTERN_KEYS: Final = {
44 "name",
45 "display_name",
46 "pattern",
47 "category",
48 "action",
49 "description",
50}
52PATTERN_EXTRA_CONFIG: Final[dict[str, dict[str, Any]]] = {}
53for pattern_data in _PATTERNS_DATA["patterns"]:
54 extra_config = {key: value for key, value in pattern_data.items() if key not in KNOWN_PATTERN_KEYS}
55 PATTERN_EXTRA_CONFIG[pattern_data["name"]] = extra_config
58def get_compiled_pattern(pattern_name: str) -> Pattern[str]:
59 """
60 Get a compiled regex pattern by name.
62 Args:
63 pattern_name: Name of the prebuilt pattern
65 Returns:
66 Compiled regex pattern
68 Raises:
69 ValueError: If pattern_name is not found in PREBUILT_PATTERNS
70 """
71 if pattern_name not in PREBUILT_PATTERNS:
72 available_patterns: Final = ", ".join(PREBUILT_PATTERNS.keys())
73 raise ValueError(f"Unknown pattern name: '{pattern_name}'. Available patterns: {available_patterns}")
75 return re.compile(PREBUILT_PATTERNS[pattern_name], re.IGNORECASE)
78def get_all_pattern_names() -> list[str]:
79 """
80 Get a list of all available prebuilt pattern names.
82 Returns:
83 List of pattern names
84 """
85 return list(PREBUILT_PATTERNS.keys())
88# Build category mapping from JSON
89PATTERN_CATEGORIES: Final[dict[str, list[str]]] = {}
90for pattern_data in _PATTERNS_DATA["patterns"]:
91 category = pattern_data["category"]
92 if category not in PATTERN_CATEGORIES:
93 PATTERN_CATEGORIES[category] = []
94 PATTERN_CATEGORIES[category].append(pattern_data["name"])
97# Build display names mapping from JSON
98PATTERN_DISPLAY_NAMES: Final[dict[str, str]] = {
99 pattern_data["name"]: pattern_data["display_name"] for pattern_data in _PATTERNS_DATA["patterns"]
100}
103# Build descriptions mapping from JSON
104PATTERN_DESCRIPTIONS: Final[dict[str, str]] = {
105 pattern_data["name"]: pattern_data["description"] for pattern_data in _PATTERNS_DATA["patterns"]
106}
109def get_pattern_metadata() -> list[dict[str, str]]:
110 """
111 Return pattern metadata for UI display.
113 Returns:
114 List of dictionaries containing pattern name, display_name, category, and description
115 """
116 return [
117 {
118 "name": pattern_data["name"],
119 "display_name": pattern_data["display_name"],
120 "category": pattern_data["category"],
121 "description": pattern_data["description"],
122 }
123 for pattern_data in _PATTERNS_DATA["patterns"]
124 ]
127def get_available_content_categories() -> list[dict[str, str]]:
128 """
129 Return available content categories for UI display.
131 Includes categories defined in .yaml/.yml files and in .json files
132 (e.g. harm_toxic_abuse.json).
134 Returns:
135 List of dictionaries containing category name, display_name, and description
136 """
137 import yaml
139 categories_dir: Final = os.path.join(os.path.dirname(__file__), "categories")
140 available_categories: Final = []
142 if not os.path.exists(categories_dir): 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true
143 return []
145 # Scan the categories directory for YAML files
146 for filename in os.listdir(categories_dir):
147 if filename.endswith(".yaml") or filename.endswith(".yml"):
148 category_file_path = os.path.join(categories_dir, filename)
149 try:
150 with open(category_file_path, "r") as f:
151 category_data = yaml.safe_load(f)
153 if category_data and "category_name" in category_data: 153 ↛ 146line 153 didn't jump to line 146 because the condition on line 153 was always true
154 # Use explicit display_name if provided, otherwise auto-generate from category_name
155 display_name = category_data.get("display_name") or (
156 category_data["category_name"].replace("_", " ").title()
157 )
159 available_categories.append(
160 {
161 "name": category_data["category_name"],
162 "display_name": display_name,
163 "description": category_data.get("description", ""),
164 "default_action": category_data.get("default_action", "BLOCK"),
165 }
166 )
167 except Exception as e:
168 # Skip files that can't be loaded but log the error for debugging
169 from litellm._logging import verbose_proxy_logger
171 verbose_proxy_logger.warning("Failed to load category file %s: %s", filename, e)
172 continue
173 elif filename.endswith(".json"): 173 ↛ 146line 173 didn't jump to line 146 because the condition on line 173 was always true
174 # JSON category files (e.g. harm_toxic_abuse.json) - no YAML header, use filename
175 category_name = os.path.splitext(filename)[0]
176 try:
177 if category_name == "harm_toxic_abuse":
178 display_name = "Harmful Toxic Abuse"
179 description = "Detects harmful, toxic, or abusive language and content"
180 else:
181 display_name = category_name.replace("_", " ").title()
182 description = f"Content category: {display_name}"
183 available_categories.append(
184 {
185 "name": category_name,
186 "display_name": display_name,
187 "description": description,
188 "default_action": "BLOCK",
189 }
190 )
191 except Exception:
192 continue
194 # Sort by name for consistent ordering
195 available_categories.sort(key=lambda x: x["name"])
197 return available_categories