mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-01 16:21:12 +00:00
Custom Code Guardrails UI Playground (#20377)
* feat(guardrails/): allow custom code execution for guardrails first step in allowing teams to submit custom code for guardrails * feat: custom_code_guardrail.md support passing custom code for guardrails * feat: initial commit adding ui for custom code guardrails allows users to write guardrails based on custom code * feat: expose new test custom code guardrail endpoint allows ui testing playground to sanity check if guardrail is working as expected * fix: fix linting errors * fix: fix max recursion check * fix: fix linting error
This commit is contained in:
@@ -101,12 +101,11 @@ model_list:
|
||||
- model_name: gpt-4
|
||||
litellm_params:
|
||||
model: gpt-4
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
litellm_settings:
|
||||
guardrails:
|
||||
guardrails:
|
||||
- guardrail_name: my_guardrail
|
||||
litellm_params:
|
||||
litellm_params:
|
||||
guardrail: my_guardrail
|
||||
mode: during_call
|
||||
api_key: os.environ/MY_GUARDRAIL_API_KEY
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Custom Code Guardrail
|
||||
|
||||
Write custom guardrail logic using Python-like code that runs in a sandboxed environment.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Define the guardrail in config
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: gpt-4
|
||||
litellm_params:
|
||||
model: gpt-4
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
guardrails:
|
||||
- guardrail_name: block-ssn
|
||||
litellm_params:
|
||||
guardrail: custom_code
|
||||
mode: pre_call
|
||||
custom_code: |
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"\d{3}-\d{2}-\d{4}"):
|
||||
return block("SSN detected")
|
||||
return allow()
|
||||
```
|
||||
|
||||
### 2. Start proxy
|
||||
|
||||
```bash
|
||||
litellm --config config.yaml
|
||||
```
|
||||
|
||||
### 3. Test
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/chat/completions \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "My SSN is 123-45-6789"}],
|
||||
"guardrails": ["block-ssn"]
|
||||
}'
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `guardrail` | string | ✅ | Must be `custom_code` |
|
||||
| `mode` | string | ✅ | When to run: `pre_call`, `post_call`, `during_call` |
|
||||
| `custom_code` | string | ✅ | Python-like code with `apply_guardrail` function |
|
||||
| `default_on` | bool | ❌ | Run on all requests (default: `false`) |
|
||||
|
||||
## Writing Custom Code
|
||||
|
||||
### Function Signature
|
||||
|
||||
Your code must define an `apply_guardrail` function:
|
||||
|
||||
```python
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
# inputs: see table below
|
||||
# request_data: {"model": "...", "user_id": "...", "team_id": "...", "metadata": {...}}
|
||||
# input_type: "request" or "response"
|
||||
|
||||
return allow() # or block() or modify()
|
||||
```
|
||||
|
||||
### `inputs` Parameter
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `texts` | `List[str]` | Extracted text from the request/response |
|
||||
| `images` | `List[str]` | Extracted images (for image guardrails) |
|
||||
| `tools` | `List[dict]` | Tools sent to the LLM |
|
||||
| `tool_calls` | `List[dict]` | Tool calls returned from the LLM |
|
||||
| `structured_messages` | `List[dict]` | Full messages with role info (system/user/assistant) |
|
||||
| `model` | `str` | The model being used |
|
||||
|
||||
### `request_data` Parameter
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `model` | `str` | Model name |
|
||||
| `user_id` | `str` | User ID from API key |
|
||||
| `team_id` | `str` | Team ID from API key |
|
||||
| `end_user_id` | `str` | End user ID |
|
||||
| `metadata` | `dict` | Request metadata |
|
||||
|
||||
### Return Values
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `allow()` | Let request/response through |
|
||||
| `block(reason)` | Reject with message |
|
||||
| `modify(texts=[], images=[], tool_calls=[])` | Transform content |
|
||||
|
||||
## Built-in Primitives
|
||||
|
||||
### Regex
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `regex_match(text, pattern)` | Returns `True` if pattern found |
|
||||
| `regex_replace(text, pattern, replacement)` | Replace all matches |
|
||||
| `regex_find_all(text, pattern)` | Return list of matches |
|
||||
|
||||
### JSON
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `json_parse(text)` | Parse JSON string, returns `None` on error |
|
||||
| `json_stringify(obj)` | Convert to JSON string |
|
||||
| `json_schema_valid(obj, schema)` | Validate against JSON schema |
|
||||
|
||||
### URL
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `extract_urls(text)` | Extract all URLs from text |
|
||||
| `is_valid_url(url)` | Check if URL is valid |
|
||||
| `all_urls_valid(text)` | Check all URLs in text are valid |
|
||||
|
||||
### Code Detection
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `detect_code(text)` | Returns `True` if code detected |
|
||||
| `detect_code_languages(text)` | Returns list of detected languages |
|
||||
| `contains_code_language(text, ["sql", "python"])` | Check for specific languages |
|
||||
|
||||
### Text Utilities
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `contains(text, substring)` | Check if substring exists |
|
||||
| `contains_any(text, [substr1, substr2])` | Check if any substring exists |
|
||||
| `word_count(text)` | Count words |
|
||||
| `char_count(text)` | Count characters |
|
||||
| `lower(text)` / `upper(text)` / `trim(text)` | String transforms |
|
||||
|
||||
## Examples
|
||||
|
||||
### Block PII (SSN)
|
||||
|
||||
```python
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"\d{3}-\d{2}-\d{4}"):
|
||||
return block("SSN detected")
|
||||
return allow()
|
||||
```
|
||||
|
||||
### Redact Email Addresses
|
||||
|
||||
```python
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
|
||||
modified = []
|
||||
for text in inputs["texts"]:
|
||||
modified.append(regex_replace(text, pattern, "[EMAIL REDACTED]"))
|
||||
return modify(texts=modified)
|
||||
```
|
||||
|
||||
### Block SQL Injection
|
||||
|
||||
```python
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "request":
|
||||
return allow()
|
||||
for text in inputs["texts"]:
|
||||
if contains_code_language(text, ["sql"]):
|
||||
return block("SQL code not allowed")
|
||||
return allow()
|
||||
```
|
||||
|
||||
### Validate JSON Response
|
||||
|
||||
```python
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "response":
|
||||
return allow()
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"required": ["name", "value"]
|
||||
}
|
||||
|
||||
for text in inputs["texts"]:
|
||||
obj = json_parse(text)
|
||||
if obj is None:
|
||||
return block("Invalid JSON response")
|
||||
if not json_schema_valid(obj, schema):
|
||||
return block("Response missing required fields")
|
||||
return allow()
|
||||
```
|
||||
|
||||
### Check URLs in Response
|
||||
|
||||
```python
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "response":
|
||||
return allow()
|
||||
for text in inputs["texts"]:
|
||||
if not all_urls_valid(text):
|
||||
return block("Response contains invalid URLs")
|
||||
return allow()
|
||||
```
|
||||
|
||||
### Combine Multiple Checks
|
||||
|
||||
```python
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
modified = []
|
||||
|
||||
for text in inputs["texts"]:
|
||||
# Redact SSN
|
||||
text = regex_replace(text, r"\d{3}-\d{2}-\d{4}", "[SSN]")
|
||||
# Redact credit cards
|
||||
text = regex_replace(text, r"\d{16}", "[CARD]")
|
||||
modified.append(text)
|
||||
|
||||
# Block SQL in requests
|
||||
if input_type == "request":
|
||||
for text in inputs["texts"]:
|
||||
if contains_code_language(text, ["sql"]):
|
||||
return block("SQL injection blocked")
|
||||
|
||||
return modify(texts=modified)
|
||||
```
|
||||
|
||||
## Sandbox Restrictions
|
||||
|
||||
Custom code runs in a restricted environment:
|
||||
|
||||
- ❌ No `import` statements
|
||||
- ❌ No file I/O
|
||||
- ❌ No network access
|
||||
- ❌ No `exec()` or `eval()`
|
||||
- ✅ Only LiteLLM-provided primitives available
|
||||
|
||||
## Per-Request Usage
|
||||
|
||||
Enable guardrail per request:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/chat/completions \
|
||||
-H "Authorization: Bearer sk-1234" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"guardrails": ["block-ssn"]
|
||||
}'
|
||||
```
|
||||
|
||||
## Default On
|
||||
|
||||
Run guardrail on all requests:
|
||||
|
||||
```yaml
|
||||
litellm_settings:
|
||||
guardrails:
|
||||
- guardrail_name: block-ssn
|
||||
litellm_params:
|
||||
guardrail: custom_code
|
||||
mode: pre_call
|
||||
default_on: true
|
||||
custom_code: |
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
...
|
||||
```
|
||||
@@ -79,6 +79,7 @@ const sidebars = {
|
||||
"proxy/guardrails/panw_prisma_airs",
|
||||
"proxy/guardrails/secret_detection",
|
||||
"proxy/guardrails/custom_guardrail",
|
||||
"proxy/guardrails/custom_code_guardrail",
|
||||
"proxy/guardrails/prompt_injection",
|
||||
"proxy/guardrails/tool_permission",
|
||||
"proxy/guardrails/zscaler_ai_guard",
|
||||
|
||||
@@ -14,3 +14,14 @@ model_list:
|
||||
litellm_params:
|
||||
model: openai/gpt-4.1-mini
|
||||
|
||||
guardrails:
|
||||
- guardrail_name: redact-ssn
|
||||
litellm_params:
|
||||
guardrail: custom_code
|
||||
mode: pre_call
|
||||
custom_code: |
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"\d{3}-\d{2}-\d{4}"):
|
||||
return block("SSN detected in message")
|
||||
return allow()
|
||||
@@ -1344,6 +1344,275 @@ async def get_provider_specific_params():
|
||||
return provider_params
|
||||
|
||||
|
||||
class TestCustomCodeGuardrailRequest(BaseModel):
|
||||
"""Request model for testing custom code guardrails."""
|
||||
|
||||
custom_code: str
|
||||
"""The Python-like code containing the apply_guardrail function."""
|
||||
|
||||
test_input: Dict[str, Any]
|
||||
"""The test input to pass to the guardrail. Should contain 'texts', optionally 'images', 'tools', etc."""
|
||||
|
||||
input_type: str = "request"
|
||||
"""Whether this is a 'request' or 'response' input type."""
|
||||
|
||||
request_data: Optional[Dict[str, Any]] = None
|
||||
"""Optional mock request_data (model, user_id, team_id, metadata, etc.)."""
|
||||
|
||||
|
||||
class TestCustomCodeGuardrailResponse(BaseModel):
|
||||
"""Response model for testing custom code guardrails."""
|
||||
|
||||
success: bool
|
||||
"""Whether the test executed successfully (no errors)."""
|
||||
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
"""The guardrail result: action (allow/block/modify), reason, modified_texts, etc."""
|
||||
|
||||
error: Optional[str] = None
|
||||
"""Error message if execution failed."""
|
||||
|
||||
error_type: Optional[str] = None
|
||||
"""Type of error: 'compilation' or 'execution'."""
|
||||
|
||||
|
||||
@router.post(
|
||||
"/guardrails/test_custom_code",
|
||||
tags=["Guardrails"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=TestCustomCodeGuardrailResponse,
|
||||
)
|
||||
async def test_custom_code_guardrail(request: TestCustomCodeGuardrailRequest):
|
||||
"""
|
||||
Test custom code guardrail logic without creating a guardrail.
|
||||
|
||||
This endpoint allows admins to experiment with custom code guardrails by:
|
||||
1. Compiling the provided code in a sandbox
|
||||
2. Executing the apply_guardrail function with test input
|
||||
3. Returning the result (allow/block/modify)
|
||||
|
||||
👉 [Custom Code Guardrail docs](https://docs.litellm.ai/docs/proxy/guardrails/custom_code_guardrail)
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/guardrails/test_custom_code" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"custom_code": "def apply_guardrail(inputs, request_data, input_type):\\n for text in inputs[\\"texts\\"]:\\n if regex_match(text, r\\"\\\\d{3}-\\\\d{2}-\\\\d{4}\\"):\\n return block(\\"SSN detected\\")\\n return allow()",
|
||||
"test_input": {
|
||||
"texts": ["My SSN is 123-45-6789"]
|
||||
},
|
||||
"input_type": "request"
|
||||
}'
|
||||
```
|
||||
|
||||
Example Success Response (blocked):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {
|
||||
"action": "block",
|
||||
"reason": "SSN detected"
|
||||
},
|
||||
"error": null,
|
||||
"error_type": null
|
||||
}
|
||||
```
|
||||
|
||||
Example Success Response (allowed):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {
|
||||
"action": "allow"
|
||||
},
|
||||
"error": null,
|
||||
"error_type": null
|
||||
}
|
||||
```
|
||||
|
||||
Example Success Response (modified):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {
|
||||
"action": "modify",
|
||||
"texts": ["My SSN is [REDACTED]"]
|
||||
},
|
||||
"error": null,
|
||||
"error_type": null
|
||||
}
|
||||
```
|
||||
|
||||
Example Error Response (compilation error):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"result": null,
|
||||
"error": "Syntax error in custom code: invalid syntax (<guardrail>, line 1)",
|
||||
"error_type": "compilation"
|
||||
}
|
||||
```
|
||||
"""
|
||||
import concurrent.futures
|
||||
import re
|
||||
|
||||
from litellm.proxy.guardrails.guardrail_hooks.custom_code.primitives import (
|
||||
get_custom_code_primitives,
|
||||
)
|
||||
|
||||
# Security validation patterns
|
||||
FORBIDDEN_PATTERNS = [
|
||||
# Import statements
|
||||
(r"\bimport\s+", "import statements are not allowed"),
|
||||
(r"\bfrom\s+\w+\s+import\b", "from...import statements are not allowed"),
|
||||
(r"__import__\s*\(", "__import__() is not allowed"),
|
||||
# Dangerous builtins
|
||||
(r"\bexec\s*\(", "exec() is not allowed"),
|
||||
(r"\beval\s*\(", "eval() is not allowed"),
|
||||
(r"\bcompile\s*\(", "compile() is not allowed"),
|
||||
(r"\bopen\s*\(", "open() is not allowed"),
|
||||
(r"\bgetattr\s*\(", "getattr() is not allowed"),
|
||||
(r"\bsetattr\s*\(", "setattr() is not allowed"),
|
||||
(r"\bdelattr\s*\(", "delattr() is not allowed"),
|
||||
(r"\bglobals\s*\(", "globals() is not allowed"),
|
||||
(r"\blocals\s*\(", "locals() is not allowed"),
|
||||
(r"\bvars\s*\(", "vars() is not allowed"),
|
||||
(r"\bdir\s*\(", "dir() is not allowed"),
|
||||
(r"\bbreakpoint\s*\(", "breakpoint() is not allowed"),
|
||||
(r"\binput\s*\(", "input() is not allowed"),
|
||||
# Dangerous dunder access
|
||||
(r"__builtins__", "__builtins__ access is not allowed"),
|
||||
(r"__globals__", "__globals__ access is not allowed"),
|
||||
(r"__code__", "__code__ access is not allowed"),
|
||||
(r"__subclasses__", "__subclasses__ access is not allowed"),
|
||||
(r"__bases__", "__bases__ access is not allowed"),
|
||||
(r"__mro__", "__mro__ access is not allowed"),
|
||||
(r"__class__", "__class__ access is not allowed"),
|
||||
(r"__dict__", "__dict__ access is not allowed"),
|
||||
(r"__getattribute__", "__getattribute__ access is not allowed"),
|
||||
(r"__reduce__", "__reduce__ access is not allowed"),
|
||||
(r"__reduce_ex__", "__reduce_ex__ access is not allowed"),
|
||||
# OS/system access
|
||||
(r"\bos\.", "os module access is not allowed"),
|
||||
(r"\bsys\.", "sys module access is not allowed"),
|
||||
(r"\bsubprocess\.", "subprocess module access is not allowed"),
|
||||
]
|
||||
|
||||
EXECUTION_TIMEOUT_SECONDS = 5
|
||||
|
||||
try:
|
||||
# Step 0: Security validation - check for forbidden patterns
|
||||
code = request.custom_code
|
||||
for pattern, error_msg in FORBIDDEN_PATTERNS:
|
||||
if re.search(pattern, code):
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error=f"Security violation: {error_msg}",
|
||||
error_type="compilation",
|
||||
)
|
||||
|
||||
# Step 1: Compile the custom code with restricted environment
|
||||
exec_globals = get_custom_code_primitives().copy()
|
||||
|
||||
# Remove access to builtins to prevent escape
|
||||
exec_globals["__builtins__"] = {}
|
||||
|
||||
try:
|
||||
exec(compile(request.custom_code, "<guardrail>", "exec"), exec_globals)
|
||||
except SyntaxError as e:
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error=f"Syntax error in custom code: {e}",
|
||||
error_type="compilation",
|
||||
)
|
||||
except Exception as e:
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error=f"Failed to compile custom code: {e}",
|
||||
error_type="compilation",
|
||||
)
|
||||
|
||||
# Step 2: Verify apply_guardrail function exists
|
||||
if "apply_guardrail" not in exec_globals:
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error="Custom code must define an 'apply_guardrail' function. "
|
||||
"Expected signature: apply_guardrail(inputs, request_data, input_type)",
|
||||
error_type="compilation",
|
||||
)
|
||||
|
||||
apply_fn = exec_globals["apply_guardrail"]
|
||||
if not callable(apply_fn):
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error="'apply_guardrail' must be a callable function",
|
||||
error_type="compilation",
|
||||
)
|
||||
|
||||
# Step 3: Prepare test inputs
|
||||
test_inputs = request.test_input
|
||||
if "texts" not in test_inputs:
|
||||
test_inputs["texts"] = []
|
||||
|
||||
# Prepare mock request_data
|
||||
mock_request_data = request.request_data or {}
|
||||
safe_request_data = {
|
||||
"model": mock_request_data.get("model", "test-model"),
|
||||
"user_id": mock_request_data.get("user_id"),
|
||||
"team_id": mock_request_data.get("team_id"),
|
||||
"end_user_id": mock_request_data.get("end_user_id"),
|
||||
"metadata": mock_request_data.get("metadata", {}),
|
||||
}
|
||||
|
||||
# Step 4: Execute the function with timeout protection
|
||||
|
||||
def execute_guardrail():
|
||||
return apply_fn(test_inputs, safe_request_data, request.input_type)
|
||||
|
||||
try:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
future = executor.submit(execute_guardrail)
|
||||
try:
|
||||
result = future.result(timeout=EXECUTION_TIMEOUT_SECONDS)
|
||||
except concurrent.futures.TimeoutError:
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error=f"Execution timeout: code took longer than {EXECUTION_TIMEOUT_SECONDS} seconds",
|
||||
error_type="execution",
|
||||
)
|
||||
except Exception as e:
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error=f"Execution error: {e}",
|
||||
error_type="execution",
|
||||
)
|
||||
|
||||
# Step 5: Validate and return result
|
||||
if not isinstance(result, dict):
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=True,
|
||||
result={
|
||||
"action": "allow",
|
||||
"warning": f"Expected dict result, got {type(result).__name__}. Treating as allow.",
|
||||
},
|
||||
)
|
||||
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=True,
|
||||
result=result,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error testing custom code guardrail: {e}")
|
||||
return TestCustomCodeGuardrailResponse(
|
||||
success=False,
|
||||
error=f"Unexpected error: {e}",
|
||||
error_type="execution",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/guardrails/apply_guardrail", response_model=ApplyGuardrailResponse)
|
||||
@router.post("/apply_guardrail", response_model=ApplyGuardrailResponse)
|
||||
async def apply_guardrail(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Custom code guardrail integration for LiteLLM.
|
||||
|
||||
This module allows users to write custom guardrail logic using Python-like code
|
||||
that runs in a sandboxed environment with access to LiteLLM-provided primitives.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .custom_code_guardrail import CustomCodeGuardrail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def initialize_guardrail(
|
||||
litellm_params: "LitellmParams", guardrail: "Guardrail"
|
||||
) -> CustomCodeGuardrail:
|
||||
"""
|
||||
Initialize a custom code guardrail.
|
||||
|
||||
Args:
|
||||
litellm_params: Configuration parameters including the custom code
|
||||
guardrail: The guardrail configuration dict
|
||||
|
||||
Returns:
|
||||
CustomCodeGuardrail instance
|
||||
"""
|
||||
import litellm
|
||||
|
||||
guardrail_name = guardrail.get("guardrail_name")
|
||||
if not guardrail_name:
|
||||
raise ValueError("Custom code guardrail requires a guardrail_name")
|
||||
|
||||
# Get the custom code from litellm_params
|
||||
custom_code = getattr(litellm_params, "custom_code", None)
|
||||
if not custom_code:
|
||||
raise ValueError(
|
||||
"Custom code guardrail requires 'custom_code' in litellm_params"
|
||||
)
|
||||
|
||||
custom_code_guardrail = CustomCodeGuardrail(
|
||||
guardrail_name=guardrail_name,
|
||||
custom_code=custom_code,
|
||||
event_hook=litellm_params.mode,
|
||||
default_on=litellm_params.default_on,
|
||||
)
|
||||
|
||||
litellm.logging_callback_manager.add_litellm_callback(custom_code_guardrail)
|
||||
return custom_code_guardrail
|
||||
|
||||
|
||||
guardrail_initializer_registry = {
|
||||
SupportedGuardrailIntegrations.CUSTOM_CODE.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
guardrail_class_registry = {
|
||||
SupportedGuardrailIntegrations.CUSTOM_CODE.value: CustomCodeGuardrail,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"CustomCodeGuardrail",
|
||||
"initialize_guardrail",
|
||||
]
|
||||
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
Custom code guardrail for LiteLLM.
|
||||
|
||||
This module provides a guardrail that executes user-defined Python-like code
|
||||
to implement custom guardrail logic. The code runs in a sandboxed environment
|
||||
with access to LiteLLM-provided primitives for common guardrail operations.
|
||||
|
||||
Example custom code:
|
||||
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
'''Block messages containing SSNs'''
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
|
||||
return block("Social Security Number detected")
|
||||
return allow()
|
||||
"""
|
||||
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Type, cast
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
|
||||
from litellm.types.utils import GenericGuardrailAPIInputs
|
||||
|
||||
from .primitives import get_custom_code_primitives
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
||||
|
||||
class CustomCodeGuardrailError(Exception):
|
||||
"""Raised when custom code guardrail execution fails."""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None) -> None:
|
||||
super().__init__(message)
|
||||
self.details = details or {}
|
||||
|
||||
|
||||
class CustomCodeCompilationError(CustomCodeGuardrailError):
|
||||
"""Raised when custom code fails to compile."""
|
||||
|
||||
|
||||
class CustomCodeExecutionError(CustomCodeGuardrailError):
|
||||
"""Raised when custom code fails during execution."""
|
||||
|
||||
|
||||
class CustomCodeGuardrailConfigModel(GuardrailConfigModel):
|
||||
"""Configuration parameters for the custom code guardrail."""
|
||||
|
||||
custom_code: str
|
||||
"""The Python-like code containing the apply_guardrail function."""
|
||||
|
||||
|
||||
class CustomCodeGuardrail(CustomGuardrail):
|
||||
"""
|
||||
Guardrail that executes user-defined Python-like code.
|
||||
|
||||
The code runs in a sandboxed environment that provides:
|
||||
- Access to LiteLLM primitives (regex_match, json_parse, etc.)
|
||||
- No file I/O or network access
|
||||
- No imports allowed
|
||||
|
||||
Users write an `apply_guardrail(inputs, request_data, input_type)` function
|
||||
that returns one of:
|
||||
- allow() - let the request/response through
|
||||
- block(reason) - reject with a message
|
||||
- modify(texts=...) - transform the content
|
||||
|
||||
Example:
|
||||
def apply_guardrail(inputs, request_data, input_type):
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"password"):
|
||||
return block("Sensitive content detected")
|
||||
return allow()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
custom_code: str,
|
||||
guardrail_name: Optional[str] = "custom_code",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the custom code guardrail.
|
||||
|
||||
Args:
|
||||
custom_code: The source code containing apply_guardrail function
|
||||
guardrail_name: Name of this guardrail instance
|
||||
**kwargs: Additional arguments passed to CustomGuardrail
|
||||
"""
|
||||
self.custom_code = custom_code
|
||||
self._compiled_function: Optional[Any] = None
|
||||
self._compile_lock = threading.Lock()
|
||||
self._compile_error: Optional[str] = None
|
||||
|
||||
supported_event_hooks = [
|
||||
GuardrailEventHooks.pre_call,
|
||||
GuardrailEventHooks.during_call,
|
||||
GuardrailEventHooks.post_call,
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
guardrail_name=guardrail_name,
|
||||
supported_event_hooks=supported_event_hooks,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Compile the code on initialization
|
||||
self._compile_custom_code()
|
||||
|
||||
@staticmethod
|
||||
def get_config_model() -> Optional[Type[GuardrailConfigModel]]:
|
||||
"""Returns the config model for the UI."""
|
||||
return CustomCodeGuardrailConfigModel
|
||||
|
||||
def _compile_custom_code(self) -> None:
|
||||
"""
|
||||
Compile the custom code and extract the apply_guardrail function.
|
||||
|
||||
The code runs in a sandboxed environment with only the allowed primitives.
|
||||
"""
|
||||
with self._compile_lock:
|
||||
if self._compiled_function is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
# Create a restricted execution environment
|
||||
# Only include our safe primitives
|
||||
exec_globals = get_custom_code_primitives().copy()
|
||||
|
||||
# Execute the user code in the restricted environment
|
||||
exec(compile(self.custom_code, "<guardrail>", "exec"), exec_globals)
|
||||
|
||||
# Extract the apply_guardrail function
|
||||
if "apply_guardrail" not in exec_globals:
|
||||
raise CustomCodeCompilationError(
|
||||
"Custom code must define an 'apply_guardrail' function. "
|
||||
"Expected signature: apply_guardrail(inputs, request_data, input_type)"
|
||||
)
|
||||
|
||||
apply_fn = exec_globals["apply_guardrail"]
|
||||
if not callable(apply_fn):
|
||||
raise CustomCodeCompilationError(
|
||||
"'apply_guardrail' must be a callable function"
|
||||
)
|
||||
|
||||
self._compiled_function = apply_fn
|
||||
verbose_proxy_logger.debug(
|
||||
f"Custom code guardrail '{self.guardrail_name}' compiled successfully"
|
||||
)
|
||||
|
||||
except SyntaxError as e:
|
||||
self._compile_error = f"Syntax error in custom code: {e}"
|
||||
raise CustomCodeCompilationError(self._compile_error) from e
|
||||
except CustomCodeCompilationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
self._compile_error = f"Failed to compile custom code: {e}"
|
||||
raise CustomCodeCompilationError(self._compile_error) from e
|
||||
|
||||
async def apply_guardrail(
|
||||
self,
|
||||
inputs: GenericGuardrailAPIInputs,
|
||||
request_data: dict,
|
||||
input_type: Literal["request", "response"],
|
||||
logging_obj: Optional["LiteLLMLoggingObj"] = None,
|
||||
) -> GenericGuardrailAPIInputs:
|
||||
"""
|
||||
Apply the custom code guardrail to the inputs.
|
||||
|
||||
This method calls the user-defined apply_guardrail function and
|
||||
processes its result to determine the appropriate action.
|
||||
|
||||
Args:
|
||||
inputs: Dictionary containing texts, images, tool_calls
|
||||
request_data: The original request data with metadata
|
||||
input_type: "request" for pre-call, "response" for post-call
|
||||
logging_obj: Optional logging object
|
||||
|
||||
Returns:
|
||||
GenericGuardrailAPIInputs - possibly modified
|
||||
|
||||
Raises:
|
||||
HTTPException: If content is blocked
|
||||
CustomCodeExecutionError: If execution fails
|
||||
"""
|
||||
if self._compiled_function is None:
|
||||
if self._compile_error:
|
||||
raise CustomCodeExecutionError(
|
||||
f"Custom code guardrail not compiled: {self._compile_error}"
|
||||
)
|
||||
raise CustomCodeExecutionError("Custom code guardrail not compiled")
|
||||
|
||||
try:
|
||||
# Prepare inputs dict for the function
|
||||
|
||||
# Prepare request_data with safe subset of information
|
||||
safe_request_data = self._prepare_safe_request_data(request_data)
|
||||
|
||||
# Execute the custom function
|
||||
result = self._compiled_function(inputs, safe_request_data, input_type)
|
||||
|
||||
# Process the result
|
||||
return self._process_result(
|
||||
result=result,
|
||||
inputs=inputs,
|
||||
request_data=request_data,
|
||||
input_type=input_type,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions (from block action)
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(
|
||||
f"Custom code guardrail '{self.guardrail_name}' execution error: {e}"
|
||||
)
|
||||
raise CustomCodeExecutionError(
|
||||
f"Custom code guardrail execution failed: {e}",
|
||||
details={
|
||||
"guardrail_name": self.guardrail_name,
|
||||
"input_type": input_type,
|
||||
},
|
||||
) from e
|
||||
|
||||
def _prepare_safe_request_data(self, request_data: dict) -> Dict[str, Any]:
|
||||
"""
|
||||
Prepare a safe subset of request_data for code execution.
|
||||
|
||||
This filters out sensitive information and provides only what's
|
||||
needed for guardrail logic.
|
||||
|
||||
Args:
|
||||
request_data: The full request data
|
||||
|
||||
Returns:
|
||||
Safe subset of request data
|
||||
"""
|
||||
return {
|
||||
"model": request_data.get("model"),
|
||||
"user_id": request_data.get("user_api_key_user_id"),
|
||||
"team_id": request_data.get("user_api_key_team_id"),
|
||||
"end_user_id": request_data.get("user_api_key_end_user_id"),
|
||||
"metadata": request_data.get("metadata", {}),
|
||||
}
|
||||
|
||||
def _process_result(
|
||||
self,
|
||||
result: Any,
|
||||
inputs: GenericGuardrailAPIInputs,
|
||||
request_data: dict,
|
||||
input_type: Literal["request", "response"],
|
||||
) -> GenericGuardrailAPIInputs:
|
||||
"""
|
||||
Process the result from the custom code function.
|
||||
|
||||
Args:
|
||||
result: The return value from apply_guardrail
|
||||
inputs: The original inputs
|
||||
request_data: The request data
|
||||
input_type: "request" or "response"
|
||||
|
||||
Returns:
|
||||
GenericGuardrailAPIInputs - possibly modified
|
||||
|
||||
Raises:
|
||||
HTTPException: If action is "block"
|
||||
"""
|
||||
if not isinstance(result, dict):
|
||||
verbose_proxy_logger.warning(
|
||||
f"Custom code guardrail '{self.guardrail_name}': "
|
||||
f"Expected dict result, got {type(result).__name__}. Treating as allow."
|
||||
)
|
||||
return inputs
|
||||
|
||||
action = result.get("action", "allow")
|
||||
|
||||
if action == "allow":
|
||||
verbose_proxy_logger.debug(
|
||||
f"Custom code guardrail '{self.guardrail_name}': Allowing {input_type}"
|
||||
)
|
||||
return inputs
|
||||
|
||||
elif action == "block":
|
||||
reason = result.get("reason", "Blocked by custom code guardrail")
|
||||
detection_info = result.get("detection_info", {})
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
f"Custom code guardrail '{self.guardrail_name}': Blocking {input_type} - {reason}"
|
||||
)
|
||||
|
||||
is_output = input_type == "response"
|
||||
|
||||
# For pre-call, raise passthrough exception to return synthetic response
|
||||
if not is_output:
|
||||
self.raise_passthrough_exception(
|
||||
violation_message=reason,
|
||||
request_data=request_data,
|
||||
detection_info=detection_info,
|
||||
)
|
||||
|
||||
# For post-call, raise HTTP exception
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": reason,
|
||||
"guardrail": self.guardrail_name,
|
||||
"detection_info": detection_info,
|
||||
},
|
||||
)
|
||||
|
||||
elif action == "modify":
|
||||
verbose_proxy_logger.debug(
|
||||
f"Custom code guardrail '{self.guardrail_name}': Modifying {input_type}"
|
||||
)
|
||||
|
||||
# Apply modifications
|
||||
modified_inputs = dict(inputs)
|
||||
|
||||
if "texts" in result and result["texts"] is not None:
|
||||
modified_inputs["texts"] = result["texts"]
|
||||
|
||||
if "images" in result and result["images"] is not None:
|
||||
modified_inputs["images"] = result["images"]
|
||||
|
||||
if "tool_calls" in result and result["tool_calls"] is not None:
|
||||
modified_inputs["tool_calls"] = result["tool_calls"]
|
||||
|
||||
return cast(GenericGuardrailAPIInputs, modified_inputs)
|
||||
|
||||
else:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Custom code guardrail '{self.guardrail_name}': "
|
||||
f"Unknown action '{action}'. Treating as allow."
|
||||
)
|
||||
return inputs
|
||||
|
||||
def update_custom_code(self, new_code: str) -> None:
|
||||
"""
|
||||
Update the custom code and recompile.
|
||||
|
||||
This method allows hot-reloading of guardrail logic without
|
||||
restarting the server.
|
||||
|
||||
Args:
|
||||
new_code: The new source code
|
||||
|
||||
Raises:
|
||||
CustomCodeCompilationError: If the new code fails to compile
|
||||
"""
|
||||
with self._compile_lock:
|
||||
# Reset state
|
||||
old_function = self._compiled_function
|
||||
old_code = self.custom_code
|
||||
self._compiled_function = None
|
||||
self._compile_error = None
|
||||
|
||||
try:
|
||||
self.custom_code = new_code
|
||||
self._compile_custom_code()
|
||||
verbose_proxy_logger.info(
|
||||
f"Custom code guardrail '{self.guardrail_name}': Code updated successfully"
|
||||
)
|
||||
except CustomCodeCompilationError:
|
||||
# Rollback on failure
|
||||
self.custom_code = old_code
|
||||
self._compiled_function = old_function
|
||||
raise
|
||||
@@ -0,0 +1,602 @@
|
||||
"""
|
||||
Built-in primitives provided to custom code guardrails.
|
||||
|
||||
These functions are injected into the custom code execution environment
|
||||
and provide safe, sandboxed functionality for common guardrail operations.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
|
||||
# =============================================================================
|
||||
# Result Types - Used by Starlark code to return guardrail decisions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def allow() -> Dict[str, Any]:
|
||||
"""
|
||||
Allow the request/response to proceed unchanged.
|
||||
|
||||
Returns:
|
||||
Dict indicating the request should be allowed
|
||||
"""
|
||||
return {"action": "allow"}
|
||||
|
||||
|
||||
def block(
|
||||
reason: str, detection_info: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Block the request/response with a reason.
|
||||
|
||||
Args:
|
||||
reason: Human-readable reason for blocking
|
||||
detection_info: Optional additional detection metadata
|
||||
|
||||
Returns:
|
||||
Dict indicating the request should be blocked
|
||||
"""
|
||||
result: Dict[str, Any] = {"action": "block", "reason": reason}
|
||||
if detection_info:
|
||||
result["detection_info"] = detection_info
|
||||
return result
|
||||
|
||||
|
||||
def modify(
|
||||
texts: Optional[List[str]] = None,
|
||||
images: Optional[List[Any]] = None,
|
||||
tool_calls: Optional[List[Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Modify the request/response content.
|
||||
|
||||
Args:
|
||||
texts: Modified text content (if None, keeps original)
|
||||
images: Modified image content (if None, keeps original)
|
||||
tool_calls: Modified tool calls (if None, keeps original)
|
||||
|
||||
Returns:
|
||||
Dict indicating the content should be modified
|
||||
"""
|
||||
result: Dict[str, Any] = {"action": "modify"}
|
||||
if texts is not None:
|
||||
result["texts"] = texts
|
||||
if images is not None:
|
||||
result["images"] = images
|
||||
if tool_calls is not None:
|
||||
result["tool_calls"] = tool_calls
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Regex Primitives
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def regex_match(text: str, pattern: str, flags: int = 0) -> bool:
|
||||
"""
|
||||
Check if a regex pattern matches anywhere in the text.
|
||||
|
||||
Args:
|
||||
text: The text to search in
|
||||
pattern: The regex pattern to match
|
||||
flags: Optional regex flags (default: 0)
|
||||
|
||||
Returns:
|
||||
True if pattern matches, False otherwise
|
||||
"""
|
||||
try:
|
||||
return bool(re.search(pattern, text, flags))
|
||||
except re.error as e:
|
||||
verbose_proxy_logger.warning(f"Starlark regex_match error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def regex_match_all(text: str, pattern: str, flags: int = 0) -> bool:
|
||||
"""
|
||||
Check if a regex pattern matches the entire text.
|
||||
|
||||
Args:
|
||||
text: The text to match
|
||||
pattern: The regex pattern
|
||||
flags: Optional regex flags
|
||||
|
||||
Returns:
|
||||
True if pattern matches entire text, False otherwise
|
||||
"""
|
||||
try:
|
||||
return bool(re.fullmatch(pattern, text, flags))
|
||||
except re.error as e:
|
||||
verbose_proxy_logger.warning(f"Starlark regex_match_all error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def regex_replace(text: str, pattern: str, replacement: str, flags: int = 0) -> str:
|
||||
"""
|
||||
Replace all occurrences of a pattern in text.
|
||||
|
||||
Args:
|
||||
text: The text to modify
|
||||
pattern: The regex pattern to find
|
||||
replacement: The replacement string
|
||||
flags: Optional regex flags
|
||||
|
||||
Returns:
|
||||
The text with replacements applied
|
||||
"""
|
||||
try:
|
||||
return re.sub(pattern, replacement, text, flags=flags)
|
||||
except re.error as e:
|
||||
verbose_proxy_logger.warning(f"Starlark regex_replace error: {e}")
|
||||
return text
|
||||
|
||||
|
||||
def regex_find_all(text: str, pattern: str, flags: int = 0) -> List[str]:
|
||||
"""
|
||||
Find all occurrences of a pattern in text.
|
||||
|
||||
Args:
|
||||
text: The text to search
|
||||
pattern: The regex pattern to find
|
||||
flags: Optional regex flags
|
||||
|
||||
Returns:
|
||||
List of all matches
|
||||
"""
|
||||
try:
|
||||
return re.findall(pattern, text, flags)
|
||||
except re.error as e:
|
||||
verbose_proxy_logger.warning(f"Starlark regex_find_all error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# JSON Primitives
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def json_parse(text: str) -> Optional[Any]:
|
||||
"""
|
||||
Parse a JSON string into a Python object.
|
||||
|
||||
Args:
|
||||
text: The JSON string to parse
|
||||
|
||||
Returns:
|
||||
Parsed Python object, or None if parsing fails
|
||||
"""
|
||||
try:
|
||||
return json.loads(text)
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
verbose_proxy_logger.debug(f"Starlark json_parse error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def json_stringify(obj: Any) -> str:
|
||||
"""
|
||||
Convert a Python object to a JSON string.
|
||||
|
||||
Args:
|
||||
obj: The object to serialize
|
||||
|
||||
Returns:
|
||||
JSON string representation
|
||||
"""
|
||||
try:
|
||||
return json.dumps(obj)
|
||||
except (TypeError, ValueError) as e:
|
||||
verbose_proxy_logger.warning(f"Starlark json_stringify error: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def json_schema_valid(obj: Any, schema: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Validate an object against a JSON schema.
|
||||
|
||||
Args:
|
||||
obj: The object to validate
|
||||
schema: The JSON schema to validate against
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Try to import jsonschema, fall back to basic validation if not available
|
||||
try:
|
||||
import jsonschema
|
||||
|
||||
jsonschema.validate(instance=obj, schema=schema)
|
||||
return True
|
||||
except ImportError:
|
||||
# Basic validation without jsonschema library
|
||||
return _basic_json_schema_validate(obj, schema)
|
||||
except Exception as validation_error:
|
||||
# Catch jsonschema.ValidationError and other validation errors
|
||||
if "ValidationError" in type(validation_error).__name__:
|
||||
return False
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(f"Custom code json_schema_valid error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _basic_json_schema_validate(
|
||||
obj: Any, schema: Dict[str, Any], max_depth: int = 50
|
||||
) -> bool:
|
||||
"""
|
||||
Basic JSON schema validation without external library.
|
||||
Handles: type, required, properties
|
||||
|
||||
Uses an iterative approach with a stack to avoid recursion limits.
|
||||
max_depth limits nesting to prevent infinite loops from circular schemas.
|
||||
"""
|
||||
type_map: Dict[str, Union[Type, Tuple[Type, ...]]] = {
|
||||
"object": dict,
|
||||
"array": list,
|
||||
"string": str,
|
||||
"number": (int, float),
|
||||
"integer": int,
|
||||
"boolean": bool,
|
||||
"null": type(None),
|
||||
}
|
||||
|
||||
# Stack of (obj, schema, depth) tuples to process
|
||||
stack: List[Tuple[Any, Dict[str, Any], int]] = [(obj, schema, 0)]
|
||||
|
||||
while stack:
|
||||
current_obj, current_schema, depth = stack.pop()
|
||||
|
||||
# Circuit breaker: stop if we've gone too deep
|
||||
if depth > max_depth:
|
||||
return False
|
||||
|
||||
# Check type
|
||||
schema_type = current_schema.get("type")
|
||||
if schema_type:
|
||||
expected_type = type_map.get(schema_type)
|
||||
if expected_type is not None and not isinstance(current_obj, expected_type):
|
||||
return False
|
||||
|
||||
# Check required fields and properties for dicts
|
||||
if isinstance(current_obj, dict):
|
||||
required = current_schema.get("required", [])
|
||||
for field in required:
|
||||
if field not in current_obj:
|
||||
return False
|
||||
|
||||
# Queue property validations
|
||||
properties = current_schema.get("properties", {})
|
||||
for prop_name, prop_schema in properties.items():
|
||||
if prop_name in current_obj:
|
||||
stack.append((current_obj[prop_name], prop_schema, depth + 1))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# URL Primitives
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# Common URL pattern for extraction
|
||||
_URL_PATTERN = re.compile(
|
||||
r"https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+[^\s]*", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def extract_urls(text: str) -> List[str]:
|
||||
"""
|
||||
Extract all URLs from text.
|
||||
|
||||
Args:
|
||||
text: The text to search for URLs
|
||||
|
||||
Returns:
|
||||
List of URLs found in the text
|
||||
"""
|
||||
return _URL_PATTERN.findall(text)
|
||||
|
||||
|
||||
def is_valid_url(url: str) -> bool:
|
||||
"""
|
||||
Check if a URL is syntactically valid.
|
||||
|
||||
Args:
|
||||
url: The URL to validate
|
||||
|
||||
Returns:
|
||||
True if the URL is valid, False otherwise
|
||||
"""
|
||||
try:
|
||||
result = urlparse(url)
|
||||
return all([result.scheme, result.netloc])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def all_urls_valid(text: str) -> bool:
|
||||
"""
|
||||
Check if all URLs in text are valid.
|
||||
|
||||
Args:
|
||||
text: The text containing URLs
|
||||
|
||||
Returns:
|
||||
True if all URLs are valid (or no URLs), False otherwise
|
||||
"""
|
||||
urls = extract_urls(text)
|
||||
return all(is_valid_url(url) for url in urls)
|
||||
|
||||
|
||||
def get_url_domain(url: str) -> Optional[str]:
|
||||
"""
|
||||
Extract the domain from a URL.
|
||||
|
||||
Args:
|
||||
url: The URL to parse
|
||||
|
||||
Returns:
|
||||
The domain, or None if invalid
|
||||
"""
|
||||
try:
|
||||
result = urlparse(url)
|
||||
return result.netloc if result.netloc else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Code Detection Primitives
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# Common code patterns for detection
|
||||
_CODE_PATTERNS = {
|
||||
"sql": [
|
||||
r"\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE)\b.*\b(FROM|INTO|TABLE|SET|WHERE)\b",
|
||||
r"\b(SELECT)\s+[\w\*,\s]+\s+FROM\s+\w+",
|
||||
r"\b(INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b",
|
||||
],
|
||||
"python": [
|
||||
r"^\s*(def|class|import|from|if|for|while|try|except|with)\s+",
|
||||
r"^\s*@\w+", # decorators
|
||||
r"\b(print|len|range|str|int|float|list|dict|set)\s*\(",
|
||||
],
|
||||
"javascript": [
|
||||
r"\b(function|const|let|var|class|import|export)\s+",
|
||||
r"=>", # arrow functions
|
||||
r"\b(console\.(log|error|warn))\s*\(",
|
||||
],
|
||||
"typescript": [
|
||||
r":\s*(string|number|boolean|any|void|never)\b",
|
||||
r"\b(interface|type|enum)\s+\w+",
|
||||
r"<[A-Z]\w*>", # generics
|
||||
],
|
||||
"java": [
|
||||
r"\b(public|private|protected)\s+(static\s+)?(class|void|int|String)\b",
|
||||
r"\bSystem\.(out|err)\.print",
|
||||
],
|
||||
"go": [
|
||||
r"\bfunc\s+\w+\s*\(",
|
||||
r"\b(package|import)\s+",
|
||||
r":=", # short variable declaration
|
||||
],
|
||||
"rust": [
|
||||
r"\b(fn|let|mut|impl|struct|enum|pub|mod)\s+",
|
||||
r"->", # return type
|
||||
r"\b(println!|format!)\s*\(",
|
||||
],
|
||||
"shell": [
|
||||
r"^#!.*\b(bash|sh|zsh)\b",
|
||||
r"\b(echo|grep|sed|awk|cat|ls|cd|mkdir|rm)\s+",
|
||||
r"\$\{?\w+\}?", # variable expansion
|
||||
],
|
||||
"html": [
|
||||
r"<\s*(html|head|body|div|span|p|a|img|script|style)\b[^>]*>",
|
||||
r"</\s*(html|head|body|div|span|p|a|script|style)\s*>",
|
||||
],
|
||||
"css": [
|
||||
r"\{[^}]*:\s*[^}]+;[^}]*\}",
|
||||
r"@(media|keyframes|import|font-face)\b",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def detect_code(text: str) -> bool:
|
||||
"""
|
||||
Check if text contains code of any language.
|
||||
|
||||
Args:
|
||||
text: The text to check
|
||||
|
||||
Returns:
|
||||
True if code is detected, False otherwise
|
||||
"""
|
||||
return len(detect_code_languages(text)) > 0
|
||||
|
||||
|
||||
def detect_code_languages(text: str) -> List[str]:
|
||||
"""
|
||||
Detect which programming languages are present in text.
|
||||
|
||||
Args:
|
||||
text: The text to analyze
|
||||
|
||||
Returns:
|
||||
List of detected language names
|
||||
"""
|
||||
detected = []
|
||||
for lang, patterns in _CODE_PATTERNS.items():
|
||||
for pattern in patterns:
|
||||
try:
|
||||
if re.search(pattern, text, re.IGNORECASE | re.MULTILINE):
|
||||
detected.append(lang)
|
||||
break # Only add each language once
|
||||
except re.error:
|
||||
continue
|
||||
return detected
|
||||
|
||||
|
||||
def contains_code_language(text: str, languages: List[str]) -> bool:
|
||||
"""
|
||||
Check if text contains code from specific languages.
|
||||
|
||||
Args:
|
||||
text: The text to check
|
||||
languages: List of language names to check for
|
||||
|
||||
Returns:
|
||||
True if any of the specified languages are detected
|
||||
"""
|
||||
detected = detect_code_languages(text)
|
||||
return any(lang.lower() in [d.lower() for d in detected] for lang in languages)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Text Utility Primitives
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def contains(text: str, substring: str) -> bool:
|
||||
"""
|
||||
Check if text contains a substring.
|
||||
|
||||
Args:
|
||||
text: The text to search in
|
||||
substring: The substring to find
|
||||
|
||||
Returns:
|
||||
True if substring is found, False otherwise
|
||||
"""
|
||||
return substring in text
|
||||
|
||||
|
||||
def contains_any(text: str, substrings: List[str]) -> bool:
|
||||
"""
|
||||
Check if text contains any of the given substrings.
|
||||
|
||||
Args:
|
||||
text: The text to search in
|
||||
substrings: List of substrings to find
|
||||
|
||||
Returns:
|
||||
True if any substring is found, False otherwise
|
||||
"""
|
||||
return any(s in text for s in substrings)
|
||||
|
||||
|
||||
def contains_all(text: str, substrings: List[str]) -> bool:
|
||||
"""
|
||||
Check if text contains all of the given substrings.
|
||||
|
||||
Args:
|
||||
text: The text to search in
|
||||
substrings: List of substrings to find
|
||||
|
||||
Returns:
|
||||
True if all substrings are found, False otherwise
|
||||
"""
|
||||
return all(s in text for s in substrings)
|
||||
|
||||
|
||||
def word_count(text: str) -> int:
|
||||
"""
|
||||
Count the number of words in text.
|
||||
|
||||
Args:
|
||||
text: The text to count words in
|
||||
|
||||
Returns:
|
||||
Number of words
|
||||
"""
|
||||
return len(text.split())
|
||||
|
||||
|
||||
def char_count(text: str) -> int:
|
||||
"""
|
||||
Count the number of characters in text.
|
||||
|
||||
Args:
|
||||
text: The text to count characters in
|
||||
|
||||
Returns:
|
||||
Number of characters
|
||||
"""
|
||||
return len(text)
|
||||
|
||||
|
||||
def lower(text: str) -> str:
|
||||
"""Convert text to lowercase."""
|
||||
return text.lower()
|
||||
|
||||
|
||||
def upper(text: str) -> str:
|
||||
"""Convert text to uppercase."""
|
||||
return text.upper()
|
||||
|
||||
|
||||
def trim(text: str) -> str:
|
||||
"""Remove leading and trailing whitespace."""
|
||||
return text.strip()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Primitives Registry
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_custom_code_primitives() -> Dict[str, Any]:
|
||||
"""
|
||||
Get all primitives to inject into the custom code environment.
|
||||
|
||||
Returns:
|
||||
Dict of function name to function
|
||||
"""
|
||||
return {
|
||||
# Result types
|
||||
"allow": allow,
|
||||
"block": block,
|
||||
"modify": modify,
|
||||
# Regex
|
||||
"regex_match": regex_match,
|
||||
"regex_match_all": regex_match_all,
|
||||
"regex_replace": regex_replace,
|
||||
"regex_find_all": regex_find_all,
|
||||
# JSON
|
||||
"json_parse": json_parse,
|
||||
"json_stringify": json_stringify,
|
||||
"json_schema_valid": json_schema_valid,
|
||||
# URL
|
||||
"extract_urls": extract_urls,
|
||||
"is_valid_url": is_valid_url,
|
||||
"all_urls_valid": all_urls_valid,
|
||||
"get_url_domain": get_url_domain,
|
||||
# Code detection
|
||||
"detect_code": detect_code,
|
||||
"detect_code_languages": detect_code_languages,
|
||||
"contains_code_language": contains_code_language,
|
||||
# Text utilities
|
||||
"contains": contains,
|
||||
"contains_any": contains_any,
|
||||
"contains_all": contains_all,
|
||||
"word_count": word_count,
|
||||
"char_count": char_count,
|
||||
"lower": lower,
|
||||
"upper": upper,
|
||||
"trim": trim,
|
||||
# Python builtins (safe subset)
|
||||
"len": len,
|
||||
"str": str,
|
||||
"int": int,
|
||||
"float": float,
|
||||
"bool": bool,
|
||||
"list": list,
|
||||
"dict": dict,
|
||||
"True": True,
|
||||
"False": False,
|
||||
"None": None,
|
||||
}
|
||||
+12
-11
@@ -14,14 +14,14 @@ from litellm.types.proxy.guardrails.guardrail_hooks.grayswan import (
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.ibm import (
|
||||
IBMGuardrailsBaseConfigModel,
|
||||
)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.tool_permission import (
|
||||
ToolPermissionGuardrailConfigModel,
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
|
||||
ContentFilterCategoryConfig,
|
||||
)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.qualifire import (
|
||||
QualifireGuardrailConfigModel,
|
||||
)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
|
||||
ContentFilterCategoryConfig,
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.tool_permission import (
|
||||
ToolPermissionGuardrailConfigModel,
|
||||
)
|
||||
|
||||
"""
|
||||
@@ -68,6 +68,7 @@ class SupportedGuardrailIntegrations(Enum):
|
||||
PROMPT_SECURITY = "prompt_security"
|
||||
GENERIC_GUARDRAIL_API = "generic_guardrail_api"
|
||||
QUALIFIRE = "qualifire"
|
||||
CUSTOM_CODE = "custom_code"
|
||||
|
||||
|
||||
class Role(Enum):
|
||||
@@ -296,13 +297,7 @@ class PresidioConfigModel(PresidioPresidioConfigModelUserInterface):
|
||||
pii_entities_config: Optional[Dict[Union[PiiEntityType, str], PiiAction]] = Field(
|
||||
default=None, description="Configuration for PII entity types and actions"
|
||||
)
|
||||
presidio_filter_scope: Literal["input", "output", "both"] = Field(
|
||||
default="both",
|
||||
description=(
|
||||
"Where to apply Presidio checks: 'input' runs on user → model traffic, "
|
||||
"'output' runs on model → user traffic, and 'both' applies to both."
|
||||
),
|
||||
)
|
||||
|
||||
presidio_score_thresholds: Optional[Dict[Union[PiiEntityType, str], float]] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
@@ -656,6 +651,12 @@ class BaseLitellmParams(
|
||||
description="Additional provider-specific parameters for generic guardrail APIs",
|
||||
)
|
||||
|
||||
# Custom code guardrail params
|
||||
custom_code: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Python-like code containing the apply_guardrail function for custom guardrail logic",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="allow", protected_namespaces=())
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ IGNORE_FUNCTIONS = [
|
||||
"filter_exceptions_from_params", # max depth set (default 20) to prevent infinite recursion.
|
||||
"__getattr__", # lazy loading pattern in litellm/__init__.py with proper caching to prevent infinite recursion.
|
||||
"_validate_inheritance_chain", # max depth set (default 100) to prevent infinite recursion in policy inheritance validation.
|
||||
"_basic_json_schema_validate", # max depth set.
|
||||
"extract_text_from_a2a_message", # max depth set (default 10) to prevent infinite recursion in A2A message parsing.
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import { Dropdown } from "antd";
|
||||
import { DownOutlined, PlusOutlined, CodeOutlined } from "@ant-design/icons";
|
||||
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
|
||||
import AddGuardrailForm from "./guardrails/add_guardrail_form";
|
||||
import GuardrailTable from "./guardrails/guardrail_table";
|
||||
@@ -10,6 +12,7 @@ import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { Guardrail, GuardrailDefinitionLocation } from "./guardrails/types";
|
||||
import DeleteResourceModal from "./common_components/DeleteResourceModal";
|
||||
import { getGuardrailLogoAndName } from "./guardrails/guardrail_info_helpers";
|
||||
import { CustomCodeModal } from "./guardrails/custom_code";
|
||||
|
||||
interface GuardrailsPanelProps {
|
||||
accessToken: string | null;
|
||||
@@ -37,6 +40,7 @@ interface GuardrailsResponse {
|
||||
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole }) => {
|
||||
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
const [isCustomCodeModalVisible, setIsCustomCodeModalVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [guardrailToDelete, setGuardrailToDelete] = useState<Guardrail | null>(null);
|
||||
@@ -74,10 +78,21 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
||||
setIsAddModalVisible(true);
|
||||
};
|
||||
|
||||
const handleAddCustomCodeGuardrail = () => {
|
||||
if (selectedGuardrailId) {
|
||||
setSelectedGuardrailId(null);
|
||||
}
|
||||
setIsCustomCodeModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsAddModalVisible(false);
|
||||
};
|
||||
|
||||
const handleCloseCustomCodeModal = () => {
|
||||
setIsCustomCodeModalVisible(false);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
fetchGuardrails();
|
||||
};
|
||||
@@ -128,9 +143,30 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Button onClick={handleAddGuardrail} disabled={!accessToken}>
|
||||
+ Add New Guardrail
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "provider",
|
||||
icon: <PlusOutlined />,
|
||||
label: "Add Provider Guardrail",
|
||||
onClick: handleAddGuardrail,
|
||||
},
|
||||
{
|
||||
key: "custom_code",
|
||||
icon: <CodeOutlined />,
|
||||
label: "Create Custom Code Guardrail",
|
||||
onClick: handleAddCustomCodeGuardrail,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
disabled={!accessToken}
|
||||
>
|
||||
<Button disabled={!accessToken}>
|
||||
+ Add New Guardrail <DownOutlined className="ml-2" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{selectedGuardrailId ? (
|
||||
@@ -159,6 +195,13 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
<CustomCodeModal
|
||||
visible={isCustomCodeModalVisible}
|
||||
onClose={handleCloseCustomCodeModal}
|
||||
accessToken={accessToken}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Guardrail"
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Input, Tabs, Typography } from "antd";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { CodeOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface CustomCodeEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
height?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CustomCodeEditor: React.FC<CustomCodeEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
height = "350px",
|
||||
placeholder = `def apply_guardrail(inputs, request_data, input_type):
|
||||
# inputs: contains texts, images, tools, tool_calls, structured_messages, model
|
||||
# request_data: contains model, user_id, team_id, end_user_id, metadata
|
||||
# input_type: "request" or "response"
|
||||
|
||||
for text in inputs["texts"]:
|
||||
# Example: Block if SSN pattern is detected
|
||||
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
|
||||
return block("SSN detected in message")
|
||||
|
||||
return allow()`,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>("edit");
|
||||
const [cursorPosition, setCursorPosition] = useState({ line: 1, column: 1 });
|
||||
|
||||
// Calculate cursor position
|
||||
const updateCursorPosition = () => {
|
||||
if (textareaRef.current) {
|
||||
const textarea = textareaRef.current;
|
||||
const textBeforeCursor = value.substring(0, textarea.selectionStart);
|
||||
const lines = textBeforeCursor.split("\n");
|
||||
const line = lines.length;
|
||||
const column = lines[lines.length - 1].length + 1;
|
||||
setCursorPosition({ line, column });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle tab key for indentation
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
const textarea = e.currentTarget;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
|
||||
// Insert 4 spaces at cursor position
|
||||
const newValue = value.substring(0, start) + " " + value.substring(end);
|
||||
onChange(newValue);
|
||||
|
||||
// Move cursor after the inserted spaces
|
||||
setTimeout(() => {
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 4;
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const lineCount = value.split("\n").length;
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<CodeOutlined />
|
||||
Edit
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="relative" style={{ height }}>
|
||||
{/* Line numbers */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-12 bg-[#1e1e1e] border-r border-[#3c3c3c] text-right pr-2 pt-3 overflow-hidden select-none"
|
||||
style={{ fontFamily: "monospace", fontSize: "13px", lineHeight: "1.5" }}
|
||||
>
|
||||
{Array.from({ length: Math.max(lineCount, 15) }, (_, i) => (
|
||||
<div key={i + 1} className="text-gray-500 h-[19.5px]">
|
||||
{i + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Code editor */}
|
||||
<textarea
|
||||
ref={textareaRef as any}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
updateCursorPosition();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={updateCursorPosition}
|
||||
onKeyUp={updateCursorPosition}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
spellCheck={false}
|
||||
className="w-full h-full pl-14 pr-4 pt-3 pb-3 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
style={{
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
border: "1px solid #3c3c3c",
|
||||
borderRadius: "8px",
|
||||
lineHeight: "1.5",
|
||||
tabSize: 4,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Status bar */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-6 bg-[#252526] border-t border-[#3c3c3c] flex items-center justify-between px-3 text-xs text-gray-400 rounded-b-lg">
|
||||
<span>Python-like (Sandboxed)</span>
|
||||
<span>Ln {cursorPosition.line}, Col {cursorPosition.column}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "preview",
|
||||
label: (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<EyeOutlined />
|
||||
Preview
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div style={{ height }} className="overflow-auto rounded-lg border border-gray-200">
|
||||
<SyntaxHighlighter
|
||||
language="python"
|
||||
style={vscDarkPlus}
|
||||
showLineNumbers
|
||||
wrapLines
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
minHeight: height,
|
||||
}}
|
||||
lineNumberStyle={{
|
||||
minWidth: "3em",
|
||||
paddingRight: "1em",
|
||||
color: "#6e7681",
|
||||
borderRight: "1px solid #3c3c3c",
|
||||
marginRight: "1em",
|
||||
}}
|
||||
>
|
||||
{value || placeholder}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="custom-code-editor">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
className="custom-code-tabs"
|
||||
size="small"
|
||||
/>
|
||||
<style>{`
|
||||
.custom-code-tabs .ant-tabs-nav {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.custom-code-tabs .ant-tabs-tab {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
.custom-code-editor textarea::placeholder {
|
||||
color: #6e7681;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomCodeEditor;
|
||||
@@ -0,0 +1,540 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Modal, Select, Switch, Collapse, Input, Spin } from "antd";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
import {
|
||||
CodeOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
CaretRightOutlined,
|
||||
SaveOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { createGuardrailCall, testCustomCodeGuardrail } from "../../networking";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
|
||||
const { Panel } = Collapse;
|
||||
const { TextArea } = Input;
|
||||
|
||||
// Code templates
|
||||
const CODE_TEMPLATES = {
|
||||
empty: {
|
||||
name: "Empty Template",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
# inputs: {texts, images, tools, tool_calls, structured_messages, model}
|
||||
# request_data: {model, user_id, team_id, end_user_id, metadata}
|
||||
# input_type: "request" or "response"
|
||||
return allow()`,
|
||||
},
|
||||
blockSSN: {
|
||||
name: "Block SSN",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
|
||||
return block("SSN detected")
|
||||
return allow()`,
|
||||
},
|
||||
redactEmail: {
|
||||
name: "Redact Emails",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
|
||||
modified = []
|
||||
for text in inputs["texts"]:
|
||||
modified.append(regex_replace(text, pattern, "[EMAIL REDACTED]"))
|
||||
return modify(texts=modified)`,
|
||||
},
|
||||
blockSQL: {
|
||||
name: "Block SQL Injection",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "request":
|
||||
return allow()
|
||||
for text in inputs["texts"]:
|
||||
if contains_code_language(text, ["sql"]):
|
||||
return block("SQL code not allowed")
|
||||
return allow()`,
|
||||
},
|
||||
validateJSON: {
|
||||
name: "Validate JSON",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "response":
|
||||
return allow()
|
||||
|
||||
schema = {"type": "object", "required": ["name", "value"]}
|
||||
|
||||
for text in inputs["texts"]:
|
||||
obj = json_parse(text)
|
||||
if obj is None:
|
||||
return block("Invalid JSON response")
|
||||
if not json_schema_valid(obj, schema):
|
||||
return block("Response missing required fields")
|
||||
return allow()`,
|
||||
},
|
||||
};
|
||||
|
||||
// Available primitives organized by category
|
||||
const PRIMITIVES = {
|
||||
"Return Values": [
|
||||
{ name: "allow()", desc: "Let request/response through" },
|
||||
{ name: "block(reason)", desc: "Reject with message" },
|
||||
{ name: "modify(texts=[], images=[], tool_calls=[])", desc: "Transform content" },
|
||||
],
|
||||
"Regex Functions": [
|
||||
{ name: "regex_match(text, pattern)", desc: "Returns True if pattern found" },
|
||||
{ name: "regex_replace(text, pattern, replacement)", desc: "Replace all matches" },
|
||||
{ name: "regex_find_all(text, pattern)", desc: "Return list of matches" },
|
||||
],
|
||||
"JSON Functions": [
|
||||
{ name: "json_parse(text)", desc: "Parse JSON string, returns None on error" },
|
||||
{ name: "json_stringify(obj)", desc: "Convert to JSON string" },
|
||||
{ name: "json_schema_valid(obj, schema)", desc: "Validate against JSON schema" },
|
||||
],
|
||||
"URL Functions": [
|
||||
{ name: "extract_urls(text)", desc: "Extract all URLs from text" },
|
||||
{ name: "is_valid_url(url)", desc: "Check if URL is valid" },
|
||||
{ name: "all_urls_valid(text)", desc: "Check all URLs in text are valid" },
|
||||
],
|
||||
"Code Detection": [
|
||||
{ name: "detect_code(text)", desc: "Returns True if code detected" },
|
||||
{ name: "detect_code_languages(text)", desc: "Returns list of detected languages" },
|
||||
{ name: 'contains_code_language(text, ["sql"])', desc: "Check for specific languages" },
|
||||
],
|
||||
"Text Utilities": [
|
||||
{ name: "contains(text, substring)", desc: "Check if substring exists" },
|
||||
{ name: "contains_any(text, [substr1, substr2])", desc: "Check if any substring exists" },
|
||||
{ name: "word_count(text)", desc: "Count words" },
|
||||
{ name: "char_count(text)", desc: "Count characters" },
|
||||
{ name: "lower(text) / upper(text) / trim(text)", desc: "String transforms" },
|
||||
],
|
||||
};
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ value: "pre_call", label: "pre_call (Request)" },
|
||||
{ value: "post_call", label: "post_call (Response)" },
|
||||
{ value: "during_call", label: "during_call (Parallel)" },
|
||||
{ value: "logging_only", label: "logging_only" },
|
||||
];
|
||||
|
||||
interface CustomCodeModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
}
|
||||
|
||||
const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
accessToken,
|
||||
}) => {
|
||||
const [guardrailName, setGuardrailName] = useState("");
|
||||
const [mode, setMode] = useState<string>("pre_call");
|
||||
const [defaultOn, setDefaultOn] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>("empty");
|
||||
const [code, setCode] = useState(CODE_TEMPLATES.empty.code);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testExpanded, setTestExpanded] = useState(false);
|
||||
const [testInput, setTestInput] = useState('{"texts": ["Hello, my SSN is 123-45-6789"], "images": [], "tools": [], "tool_calls": [], "structured_messages": [], "model": "gpt-4"}');
|
||||
const [testResult, setTestResult] = useState<any>(null);
|
||||
const [copiedPrimitive, setCopiedPrimitive] = useState<string | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Handle template change
|
||||
const handleTemplateChange = (templateKey: string) => {
|
||||
setSelectedTemplate(templateKey);
|
||||
setCode(CODE_TEMPLATES[templateKey as keyof typeof CODE_TEMPLATES].code);
|
||||
};
|
||||
|
||||
// Reset form when modal opens
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setGuardrailName("");
|
||||
setMode("pre_call");
|
||||
setDefaultOn(false);
|
||||
setSelectedTemplate("empty");
|
||||
setCode(CODE_TEMPLATES.empty.code);
|
||||
setTestResult(null);
|
||||
setTestExpanded(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// Copy primitive to clipboard
|
||||
const copyPrimitive = async (primitive: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(primitive);
|
||||
setCopiedPrimitive(primitive);
|
||||
setTimeout(() => setCopiedPrimitive(null), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle tab key in textarea
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
const textarea = e.currentTarget;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const newValue = code.substring(0, start) + " " + code.substring(end);
|
||||
setCode(newValue);
|
||||
setTimeout(() => {
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 4;
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Save guardrail
|
||||
const handleSave = async () => {
|
||||
if (!guardrailName.trim()) {
|
||||
NotificationsManager.fromBackend("Please enter a guardrail name");
|
||||
return;
|
||||
}
|
||||
if (!code.trim()) {
|
||||
NotificationsManager.fromBackend("Please enter custom code");
|
||||
return;
|
||||
}
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const guardrailData = {
|
||||
guardrail_name: guardrailName,
|
||||
litellm_params: {
|
||||
guardrail: "custom_code",
|
||||
mode: mode,
|
||||
default_on: defaultOn,
|
||||
custom_code: code,
|
||||
},
|
||||
guardrail_info: {},
|
||||
};
|
||||
|
||||
await createGuardrailCall(accessToken, guardrailData);
|
||||
NotificationsManager.success("Custom code guardrail created successfully");
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to create guardrail:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to create guardrail: " + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Test guardrail using backend endpoint
|
||||
const handleTest = async () => {
|
||||
if (!accessToken) {
|
||||
setTestResult({ error: "No access token available" });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTesting(true);
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
// Parse test input JSON
|
||||
let parsedInput;
|
||||
try {
|
||||
parsedInput = JSON.parse(testInput);
|
||||
} catch (e) {
|
||||
setTestResult({ error: "Invalid test input JSON" });
|
||||
setIsTesting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure texts array exists
|
||||
if (!parsedInput.texts) {
|
||||
parsedInput.texts = [];
|
||||
}
|
||||
|
||||
const response = await testCustomCodeGuardrail(accessToken, {
|
||||
custom_code: code,
|
||||
test_input: parsedInput,
|
||||
input_type: mode as "request" | "response",
|
||||
request_data: {
|
||||
model: "test-model",
|
||||
metadata: {},
|
||||
},
|
||||
});
|
||||
|
||||
if (response.success && response.result) {
|
||||
setTestResult(response.result);
|
||||
} else if (response.error) {
|
||||
setTestResult({
|
||||
error: response.error,
|
||||
error_type: response.error_type,
|
||||
});
|
||||
} else {
|
||||
setTestResult({ error: "Unknown error occurred" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to test custom code:", error);
|
||||
setTestResult({
|
||||
error: error instanceof Error ? error.message : "Failed to test custom code",
|
||||
});
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const lineCount = code.split("\n").length;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={1200}
|
||||
className="custom-code-modal"
|
||||
closable={true}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="flex flex-col h-[80vh]">
|
||||
{/* Header */}
|
||||
<div className="pb-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Create Custom Guardrail</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Define custom logic using Python-like syntax</p>
|
||||
</div>
|
||||
|
||||
{/* Top Controls */}
|
||||
<div className="flex items-center gap-4 py-4 border-b border-gray-100">
|
||||
<div className="flex-1 max-w-[200px]">
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Guardrail Name</label>
|
||||
<TextInput
|
||||
value={guardrailName}
|
||||
onValueChange={setGuardrailName}
|
||||
placeholder="e.g., block-pii-custom"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[180px]">
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Mode</label>
|
||||
<Select
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
options={MODE_OPTIONS}
|
||||
className="w-full"
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[180px]">
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Template</label>
|
||||
<Select
|
||||
value={selectedTemplate}
|
||||
onChange={handleTemplateChange}
|
||||
className="w-full"
|
||||
size="middle"
|
||||
>
|
||||
{Object.entries(CODE_TEMPLATES).map(([key, template]) => (
|
||||
<Select.Option key={key} value={key}>
|
||||
{template.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-5">
|
||||
<span className="text-sm text-gray-600">Default On</span>
|
||||
<Switch checked={defaultOn} onChange={setDefaultOn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 overflow-hidden mt-4 gap-4">
|
||||
{/* Code Editor */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Python Logic</span>
|
||||
<span className="text-xs text-gray-400">Restricted environment (no imports)</span>
|
||||
</div>
|
||||
<div className="flex-1 relative rounded-lg overflow-hidden border border-gray-700 bg-[#1e1e1e]">
|
||||
{/* Line numbers */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-10 bg-[#1e1e1e] border-r border-gray-700 text-right pr-2 pt-3 select-none overflow-hidden"
|
||||
style={{ fontFamily: "monospace", fontSize: "13px", lineHeight: "1.5" }}
|
||||
>
|
||||
{Array.from({ length: Math.max(lineCount, 20) }, (_, i) => (
|
||||
<div key={i + 1} className="text-gray-500 h-[19.5px]">{i + 1}</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Code textarea */}
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
spellCheck={false}
|
||||
className="w-full h-full pl-12 pr-4 pt-3 pb-3 font-mono text-sm resize-none focus:outline-none bg-transparent text-gray-200"
|
||||
style={{ lineHeight: "1.5", tabSize: 4 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Test Section */}
|
||||
<Collapse
|
||||
activeKey={testExpanded ? ["test"] : []}
|
||||
onChange={(keys) => setTestExpanded(keys.includes("test"))}
|
||||
className="mt-3 bg-white border border-gray-200 rounded-lg"
|
||||
expandIcon={({ isActive }) => <CaretRightOutlined rotate={isActive ? 90 : 0} />}
|
||||
>
|
||||
<Panel
|
||||
header={
|
||||
<span className="flex items-center gap-2 text-sm font-medium">
|
||||
<PlayCircleOutlined className="text-blue-500" />
|
||||
Test Your Guardrail
|
||||
</span>
|
||||
}
|
||||
key="test"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Test Input (JSON)</label>
|
||||
<TextArea
|
||||
value={testInput}
|
||||
onChange={(e) => setTestInput(e.target.value)}
|
||||
rows={4}
|
||||
className="font-mono text-xs"
|
||||
placeholder='{"texts": ["test message"], ...}'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={handleTest}
|
||||
disabled={isTesting}
|
||||
icon={PlayCircleOutlined}
|
||||
>
|
||||
{isTesting ? "Running..." : "Run Test"}
|
||||
</Button>
|
||||
{testResult && (
|
||||
<div className={`flex items-center gap-2 text-sm ${
|
||||
testResult.error ? "text-red-600" :
|
||||
testResult.action === "allow" ? "text-green-600" :
|
||||
testResult.action === "block" ? "text-orange-600" :
|
||||
"text-blue-600"
|
||||
}`}>
|
||||
{testResult.error ? (
|
||||
<>
|
||||
<CloseCircleOutlined />
|
||||
<span>
|
||||
{testResult.error_type && <span className="font-medium">[{testResult.error_type}] </span>}
|
||||
{testResult.error}
|
||||
</span>
|
||||
</>
|
||||
) : testResult.action === "allow" ? (
|
||||
<><CheckCircleOutlined /> Allowed</>
|
||||
) : testResult.action === "block" ? (
|
||||
<><CloseCircleOutlined /> Blocked: {testResult.reason}</>
|
||||
) : testResult.action === "modify" ? (
|
||||
<>
|
||||
<CheckCircleOutlined /> Modified
|
||||
{testResult.texts && testResult.texts.length > 0 && (
|
||||
<span className="text-xs text-gray-500 ml-1">
|
||||
→ {testResult.texts[0].substring(0, 50)}{testResult.texts[0].length > 50 ? "..." : ""}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<><CheckCircleOutlined /> {testResult.action || "Unknown"}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
{/* Primitives Panel */}
|
||||
<div className="w-[280px] flex-shrink-0 overflow-auto">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<CodeOutlined className="text-blue-500" />
|
||||
<span className="font-semibold text-gray-700">Available Primitives</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mb-3">Click to copy functions to clipboard</p>
|
||||
|
||||
<Collapse
|
||||
defaultActiveKey={["Return Values"]}
|
||||
className="primitives-collapse bg-transparent border-0"
|
||||
expandIconPosition="end"
|
||||
>
|
||||
{Object.entries(PRIMITIVES).map(([category, primitives]) => (
|
||||
<Panel
|
||||
header={<span className="text-sm font-medium text-gray-700">{category}</span>}
|
||||
key={category}
|
||||
className="bg-white mb-2 rounded-lg border border-gray-200"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{primitives.map((p) => (
|
||||
<button
|
||||
key={p.name}
|
||||
onClick={() => copyPrimitive(p.name)}
|
||||
className={`w-full text-left px-2 py-2 rounded transition-colors ${
|
||||
copiedPrimitive === p.name
|
||||
? "bg-green-100"
|
||||
: "bg-gray-50 hover:bg-blue-50"
|
||||
}`}
|
||||
>
|
||||
{copiedPrimitive === p.name ? (
|
||||
<span className="flex items-center gap-1 text-xs font-mono text-green-700">
|
||||
<CheckCircleOutlined /> Copied!
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xs font-mono text-gray-800">{p.name}</div>
|
||||
<div className="text-[10px] text-gray-500 mt-0.5">{p.desc}</div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-200">
|
||||
<span className="text-xs text-gray-400">Changes are auto-saved to local draft</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
disabled={isSaving || !guardrailName.trim()}
|
||||
icon={SaveOutlined}
|
||||
>
|
||||
Save Guardrail
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.custom-code-modal .ant-modal-content {
|
||||
padding: 24px;
|
||||
}
|
||||
.custom-code-modal .ant-modal-close {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
.primitives-collapse .ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
.primitives-collapse .ant-collapse-header {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
.primitives-collapse .ant-collapse-content-box {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
`}</style>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomCodeModal;
|
||||
@@ -0,0 +1,588 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { Card, Title, Text, Button } from "@tremor/react";
|
||||
import { Collapse, Typography, Tooltip, Spin, Alert, Tabs } from "antd";
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
CodeOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
EditOutlined,
|
||||
BookOutlined,
|
||||
ExperimentOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
import { testCustomCodeGuardrail } from "../../networking";
|
||||
import CustomCodeEditor from "./CustomCodeEditor";
|
||||
import { CUSTOM_CODE_PRIMITIVES, CUSTOM_CODE_EXAMPLES, DEFAULT_CUSTOM_CODE } from "./custom_code_constants";
|
||||
|
||||
const { Panel } = Collapse;
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface CustomCodePlaygroundProps {
|
||||
accessToken: string | null;
|
||||
initialCode?: string;
|
||||
onCodeChange?: (code: string) => void;
|
||||
showTestingPanel?: boolean;
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
action: "allow" | "block" | "modify";
|
||||
reason?: string;
|
||||
modified_texts?: string[];
|
||||
modified_images?: string[];
|
||||
modified_tool_calls?: any[];
|
||||
execution_time_ms?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const CustomCodePlayground: React.FC<CustomCodePlaygroundProps> = ({
|
||||
accessToken,
|
||||
initialCode = DEFAULT_CUSTOM_CODE,
|
||||
onCodeChange,
|
||||
showTestingPanel = true,
|
||||
}) => {
|
||||
const [customCode, setCustomCode] = useState(initialCode);
|
||||
const [testInput, setTestInput] = useState(
|
||||
JSON.stringify(
|
||||
{
|
||||
texts: ["Hello, my SSN is 123-45-6789"],
|
||||
images: [],
|
||||
tools: [],
|
||||
tool_calls: [],
|
||||
structured_messages: [
|
||||
{ role: "user", content: "Hello, my SSN is 123-45-6789" },
|
||||
],
|
||||
model: "gpt-4",
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
const [requestData, setRequestData] = useState(
|
||||
JSON.stringify(
|
||||
{
|
||||
model: "gpt-4",
|
||||
user_id: "test-user",
|
||||
team_id: "test-team",
|
||||
end_user_id: "end-user-123",
|
||||
metadata: {},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
const [inputType, setInputType] = useState<"request" | "response">("request");
|
||||
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<string>("editor");
|
||||
|
||||
const handleCodeChange = useCallback(
|
||||
(code: string) => {
|
||||
setCustomCode(code);
|
||||
onCodeChange?.(code);
|
||||
},
|
||||
[onCodeChange]
|
||||
);
|
||||
|
||||
const handleRunTest = async () => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTesting(true);
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
let parsedInputs: any;
|
||||
let parsedRequestData: any;
|
||||
|
||||
try {
|
||||
parsedInputs = JSON.parse(testInput);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid JSON in test input");
|
||||
}
|
||||
|
||||
try {
|
||||
parsedRequestData = JSON.parse(requestData);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid JSON in request data");
|
||||
}
|
||||
|
||||
const response = await testCustomCodeGuardrail(accessToken, {
|
||||
custom_code: customCode,
|
||||
test_input: parsedInputs,
|
||||
input_type: inputType,
|
||||
request_data: parsedRequestData,
|
||||
});
|
||||
|
||||
if (response.success && response.result) {
|
||||
setTestResult(response.result);
|
||||
|
||||
if (response.result.action === "allow") {
|
||||
NotificationsManager.success("Guardrail allowed the request");
|
||||
} else if (response.result.action === "block") {
|
||||
NotificationsManager.fromBackend(`Guardrail blocked: ${response.result.reason || "No reason provided"}`);
|
||||
} else if (response.result.action === "modify") {
|
||||
NotificationsManager.success("Guardrail modified the content");
|
||||
}
|
||||
} else if (response.error) {
|
||||
setTestResult({
|
||||
action: "block",
|
||||
error: response.error,
|
||||
});
|
||||
NotificationsManager.fromBackend(`Test failed: ${response.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
setTestResult({
|
||||
action: "block",
|
||||
error: errorMessage,
|
||||
});
|
||||
NotificationsManager.fromBackend(`Test failed: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadExample = (exampleKey: keyof typeof CUSTOM_CODE_EXAMPLES) => {
|
||||
setCustomCode(CUSTOM_CODE_EXAMPLES[exampleKey]);
|
||||
onCodeChange?.(CUSTOM_CODE_EXAMPLES[exampleKey]);
|
||||
setActiveTab("editor");
|
||||
};
|
||||
|
||||
const renderTestResult = () => {
|
||||
if (!testResult) return null;
|
||||
|
||||
const isError = !!testResult.error;
|
||||
const isAllow = testResult.action === "allow";
|
||||
const isBlock = testResult.action === "block";
|
||||
const isModify = testResult.action === "modify";
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Text className="font-medium text-gray-700 block mb-2">Test Result</Text>
|
||||
<div
|
||||
className={`rounded-lg p-4 border ${
|
||||
isError
|
||||
? "bg-red-50 border-red-200"
|
||||
: isAllow
|
||||
? "bg-green-50 border-green-200"
|
||||
: isBlock
|
||||
? "bg-orange-50 border-orange-200"
|
||||
: "bg-blue-50 border-blue-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{isError ? (
|
||||
<CloseCircleOutlined className="text-red-500 text-lg" />
|
||||
) : isAllow ? (
|
||||
<CheckCircleOutlined className="text-green-500 text-lg" />
|
||||
) : isBlock ? (
|
||||
<CloseCircleOutlined className="text-orange-500 text-lg" />
|
||||
) : (
|
||||
<EditOutlined className="text-blue-500 text-lg" />
|
||||
)}
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
isError
|
||||
? "text-red-700"
|
||||
: isAllow
|
||||
? "text-green-700"
|
||||
: isBlock
|
||||
? "text-orange-700"
|
||||
: "text-blue-700"
|
||||
}`}
|
||||
>
|
||||
{isError ? "Error" : testResult.action.toUpperCase()}
|
||||
</span>
|
||||
{testResult.execution_time_ms && (
|
||||
<span className="text-xs text-gray-500 ml-auto">
|
||||
{testResult.execution_time_ms.toFixed(2)}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isError && (
|
||||
<Alert type="error" message={testResult.error} className="mt-2" />
|
||||
)}
|
||||
|
||||
{isBlock && testResult.reason && (
|
||||
<Paragraph className="text-orange-700 mb-0 mt-2">
|
||||
<strong>Reason:</strong> {testResult.reason}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{isModify && testResult.modified_texts && testResult.modified_texts.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<Text className="font-medium text-blue-700 block mb-1">Modified Texts:</Text>
|
||||
<pre className="bg-white rounded p-2 text-xs overflow-auto max-h-32 border border-blue-100">
|
||||
{JSON.stringify(testResult.modified_texts, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPrimitivesReference = () => (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(CUSTOM_CODE_PRIMITIVES).map(([category, primitives]) => (
|
||||
<div key={category}>
|
||||
<Text className="font-semibold text-gray-700 block mb-2">{category}</Text>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-1 pr-4 font-medium text-gray-600">Function</th>
|
||||
<th className="text-left py-1 font-medium text-gray-600">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{primitives.map((primitive) => (
|
||||
<tr key={primitive.name} className="border-b border-gray-100 last:border-0">
|
||||
<td className="py-1.5 pr-4">
|
||||
<code className="text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded font-mono">
|
||||
{primitive.signature}
|
||||
</code>
|
||||
</td>
|
||||
<td className="py-1.5 text-gray-600">{primitive.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<Text className="font-semibold text-gray-700 block mb-2">Return Values</Text>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-1 pr-4 font-medium text-gray-600">Function</th>
|
||||
<th className="text-left py-1 font-medium text-gray-600">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded font-mono">allow()</code></td>
|
||||
<td className="py-1.5 text-gray-600">Let request/response through</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-red-100 text-red-700 px-1.5 py-0.5 rounded font-mono">block(reason)</code></td>
|
||||
<td className="py-1.5 text-gray-600">Reject with message</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-yellow-100 text-yellow-700 px-1.5 py-0.5 rounded font-mono">modify(texts=[], images=[], tool_calls=[])</code></td>
|
||||
<td className="py-1.5 text-gray-600">Transform content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderInputParamsReference = () => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-semibold text-gray-700 block mb-2">`inputs` Parameter</Text>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-1 pr-4 font-medium text-gray-600">Field</th>
|
||||
<th className="text-left py-1 pr-4 font-medium text-gray-600">Type</th>
|
||||
<th className="text-left py-1 font-medium text-gray-600">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">texts</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">List[str]</td>
|
||||
<td className="py-1.5 text-gray-600">Extracted text from the request/response</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">images</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">List[str]</td>
|
||||
<td className="py-1.5 text-gray-600">Extracted images (for image guardrails)</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">tools</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">List[dict]</td>
|
||||
<td className="py-1.5 text-gray-600">Tools sent to the LLM</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">tool_calls</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">List[dict]</td>
|
||||
<td className="py-1.5 text-gray-600">Tool calls returned from the LLM</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">structured_messages</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">List[dict]</td>
|
||||
<td className="py-1.5 text-gray-600">Full messages with role info</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">model</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">str</td>
|
||||
<td className="py-1.5 text-gray-600">The model being used</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-semibold text-gray-700 block mb-2">`request_data` Parameter</Text>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-1 pr-4 font-medium text-gray-600">Field</th>
|
||||
<th className="text-left py-1 pr-4 font-medium text-gray-600">Type</th>
|
||||
<th className="text-left py-1 font-medium text-gray-600">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">model</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">str</td>
|
||||
<td className="py-1.5 text-gray-600">Model name</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">user_id</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">str</td>
|
||||
<td className="py-1.5 text-gray-600">User ID from API key</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">team_id</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">str</td>
|
||||
<td className="py-1.5 text-gray-600">Team ID from API key</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">end_user_id</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">str</td>
|
||||
<td className="py-1.5 text-gray-600">End user ID</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">metadata</code></td>
|
||||
<td className="py-1.5 pr-4 text-gray-500">dict</td>
|
||||
<td className="py-1.5 text-gray-600">Request metadata</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderExamplesTab = () => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => loadExample("blockSSN")}
|
||||
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<Text className="font-medium text-gray-800 block mb-1">🔒 Block PII (SSN)</Text>
|
||||
<Text className="text-xs text-gray-500">Detect and block Social Security Numbers</Text>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadExample("redactEmail")}
|
||||
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<Text className="font-medium text-gray-800 block mb-1">📧 Redact Emails</Text>
|
||||
<Text className="text-xs text-gray-500">Replace email addresses with [EMAIL REDACTED]</Text>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadExample("blockSQL")}
|
||||
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<Text className="font-medium text-gray-800 block mb-1">🛡️ Block SQL Injection</Text>
|
||||
<Text className="text-xs text-gray-500">Prevent SQL code in requests</Text>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadExample("validateJSON")}
|
||||
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<Text className="font-medium text-gray-800 block mb-1">✅ Validate JSON Response</Text>
|
||||
<Text className="text-xs text-gray-500">Ensure responses have required fields</Text>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadExample("checkURLs")}
|
||||
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<Text className="font-medium text-gray-800 block mb-1">🔗 Check URLs</Text>
|
||||
<Text className="text-xs text-gray-500">Validate all URLs in responses</Text>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadExample("combined")}
|
||||
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<Text className="font-medium text-gray-800 block mb-1">🔄 Combined Checks</Text>
|
||||
<Text className="text-xs text-gray-500">Multiple checks with redaction and blocking</Text>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: "editor",
|
||||
label: (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<CodeOutlined />
|
||||
Code Editor
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-4">
|
||||
<CustomCodeEditor value={customCode} onChange={handleCodeChange} height="350px" />
|
||||
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
|
||||
<strong>⚠️ Sandbox Restrictions:</strong> No imports, no file I/O, no network access, no exec() or eval().
|
||||
Only LiteLLM-provided primitives are available.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "examples",
|
||||
label: (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<BookOutlined />
|
||||
Examples
|
||||
</span>
|
||||
),
|
||||
children: renderExamplesTab(),
|
||||
},
|
||||
{
|
||||
key: "primitives",
|
||||
label: (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<InfoCircleOutlined />
|
||||
Primitives Reference
|
||||
</span>
|
||||
),
|
||||
children: renderPrimitivesReference(),
|
||||
},
|
||||
{
|
||||
key: "params",
|
||||
label: (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<InfoCircleOutlined />
|
||||
Input Parameters
|
||||
</span>
|
||||
),
|
||||
children: renderInputParamsReference(),
|
||||
},
|
||||
];
|
||||
|
||||
if (showTestingPanel) {
|
||||
tabItems.push({
|
||||
key: "test",
|
||||
label: (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<ExperimentOutlined />
|
||||
Test
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="font-medium text-gray-700">Test Input (inputs parameter)</Text>
|
||||
<Tooltip title="This represents the 'inputs' parameter passed to your apply_guardrail function">
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<textarea
|
||||
value={testInput}
|
||||
onChange={(e) => setTestInput(e.target.value)}
|
||||
className="w-full h-48 p-3 font-mono text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Enter test input JSON..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="font-medium text-gray-700">Request Data (request_data parameter)</Text>
|
||||
<Tooltip title="This represents the 'request_data' parameter passed to your apply_guardrail function">
|
||||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<textarea
|
||||
value={requestData}
|
||||
onChange={(e) => setRequestData(e.target.value)}
|
||||
className="w-full h-48 p-3 font-mono text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Enter request data JSON..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text className="text-sm text-gray-600">Input Type:</Text>
|
||||
<select
|
||||
value={inputType}
|
||||
onChange={(e) => setInputType(e.target.value as "request" | "response")}
|
||||
className="border border-gray-200 rounded px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="request">request</option>
|
||||
<option value="response">response</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleRunTest}
|
||||
disabled={!accessToken || isTesting}
|
||||
icon={isTesting ? undefined : PlayCircleOutlined}
|
||||
className="ml-auto"
|
||||
>
|
||||
{isTesting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Spin size="small" /> Running Test...
|
||||
</span>
|
||||
) : (
|
||||
"Run Test"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{renderTestResult()}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-0 overflow-hidden">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
className="custom-code-playground-tabs"
|
||||
tabBarStyle={{ padding: "0 16px", marginBottom: 0 }}
|
||||
/>
|
||||
<div className="p-4">
|
||||
{tabItems.find(tab => tab.key === activeTab)?.children}
|
||||
</div>
|
||||
<style>{`
|
||||
.custom-code-playground-tabs .ant-tabs-nav {
|
||||
background: #f9fafb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.custom-code-playground-tabs .ant-tabs-tab {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.custom-code-playground-tabs .ant-tabs-tab-active {
|
||||
background: white;
|
||||
}
|
||||
`}</style>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomCodePlayground;
|
||||
@@ -0,0 +1,194 @@
|
||||
// Custom Code Guardrail Constants
|
||||
|
||||
export const DEFAULT_CUSTOM_CODE = `def apply_guardrail(inputs, request_data, input_type):
|
||||
# inputs: contains texts, images, tools, tool_calls, structured_messages, model
|
||||
# request_data: contains model, user_id, team_id, end_user_id, metadata
|
||||
# input_type: "request" or "response"
|
||||
|
||||
for text in inputs["texts"]:
|
||||
# Example: Block if SSN pattern is detected
|
||||
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
|
||||
return block("SSN detected in message")
|
||||
|
||||
return allow()
|
||||
`;
|
||||
|
||||
export const CUSTOM_CODE_PRIMITIVES = {
|
||||
"Regex Functions": [
|
||||
{
|
||||
name: "regex_match",
|
||||
signature: "regex_match(text, pattern)",
|
||||
description: "Returns True if pattern found in text",
|
||||
},
|
||||
{
|
||||
name: "regex_replace",
|
||||
signature: "regex_replace(text, pattern, replacement)",
|
||||
description: "Replace all matches of pattern with replacement",
|
||||
},
|
||||
{
|
||||
name: "regex_find_all",
|
||||
signature: "regex_find_all(text, pattern)",
|
||||
description: "Return list of all matches",
|
||||
},
|
||||
],
|
||||
"JSON Functions": [
|
||||
{
|
||||
name: "json_parse",
|
||||
signature: "json_parse(text)",
|
||||
description: "Parse JSON string, returns None on error",
|
||||
},
|
||||
{
|
||||
name: "json_stringify",
|
||||
signature: "json_stringify(obj)",
|
||||
description: "Convert object to JSON string",
|
||||
},
|
||||
{
|
||||
name: "json_schema_valid",
|
||||
signature: "json_schema_valid(obj, schema)",
|
||||
description: "Validate object against JSON schema",
|
||||
},
|
||||
],
|
||||
"URL Functions": [
|
||||
{
|
||||
name: "extract_urls",
|
||||
signature: "extract_urls(text)",
|
||||
description: "Extract all URLs from text",
|
||||
},
|
||||
{
|
||||
name: "is_valid_url",
|
||||
signature: "is_valid_url(url)",
|
||||
description: "Check if URL is valid",
|
||||
},
|
||||
{
|
||||
name: "all_urls_valid",
|
||||
signature: "all_urls_valid(text)",
|
||||
description: "Check all URLs in text are valid",
|
||||
},
|
||||
],
|
||||
"Code Detection": [
|
||||
{
|
||||
name: "detect_code",
|
||||
signature: "detect_code(text)",
|
||||
description: "Returns True if code detected",
|
||||
},
|
||||
{
|
||||
name: "detect_code_languages",
|
||||
signature: "detect_code_languages(text)",
|
||||
description: "Returns list of detected languages",
|
||||
},
|
||||
{
|
||||
name: "contains_code_language",
|
||||
signature: 'contains_code_language(text, ["sql", "python"])',
|
||||
description: "Check for specific languages",
|
||||
},
|
||||
],
|
||||
"Text Utilities": [
|
||||
{
|
||||
name: "contains",
|
||||
signature: "contains(text, substring)",
|
||||
description: "Check if substring exists in text",
|
||||
},
|
||||
{
|
||||
name: "contains_any",
|
||||
signature: "contains_any(text, [substr1, substr2])",
|
||||
description: "Check if any substring exists",
|
||||
},
|
||||
{
|
||||
name: "word_count",
|
||||
signature: "word_count(text)",
|
||||
description: "Count words in text",
|
||||
},
|
||||
{
|
||||
name: "char_count",
|
||||
signature: "char_count(text)",
|
||||
description: "Count characters in text",
|
||||
},
|
||||
{
|
||||
name: "lower",
|
||||
signature: "lower(text)",
|
||||
description: "Convert text to lowercase",
|
||||
},
|
||||
{
|
||||
name: "upper",
|
||||
signature: "upper(text)",
|
||||
description: "Convert text to uppercase",
|
||||
},
|
||||
{
|
||||
name: "trim",
|
||||
signature: "trim(text)",
|
||||
description: "Remove leading/trailing whitespace",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const CUSTOM_CODE_EXAMPLES = {
|
||||
blockSSN: `def apply_guardrail(inputs, request_data, input_type):
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
|
||||
return block("SSN detected")
|
||||
return allow()
|
||||
`,
|
||||
|
||||
redactEmail: `def apply_guardrail(inputs, request_data, input_type):
|
||||
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
|
||||
modified = []
|
||||
for text in inputs["texts"]:
|
||||
modified.append(regex_replace(text, pattern, "[EMAIL REDACTED]"))
|
||||
return modify(texts=modified)
|
||||
`,
|
||||
|
||||
blockSQL: `def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "request":
|
||||
return allow()
|
||||
for text in inputs["texts"]:
|
||||
if contains_code_language(text, ["sql"]):
|
||||
return block("SQL code not allowed")
|
||||
return allow()
|
||||
`,
|
||||
|
||||
validateJSON: `def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "response":
|
||||
return allow()
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"required": ["name", "value"]
|
||||
}
|
||||
|
||||
for text in inputs["texts"]:
|
||||
obj = json_parse(text)
|
||||
if obj is None:
|
||||
return block("Invalid JSON response")
|
||||
if not json_schema_valid(obj, schema):
|
||||
return block("Response missing required fields")
|
||||
return allow()
|
||||
`,
|
||||
|
||||
checkURLs: `def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "response":
|
||||
return allow()
|
||||
for text in inputs["texts"]:
|
||||
if not all_urls_valid(text):
|
||||
return block("Response contains invalid URLs")
|
||||
return allow()
|
||||
`,
|
||||
|
||||
combined: `def apply_guardrail(inputs, request_data, input_type):
|
||||
modified = []
|
||||
|
||||
for text in inputs["texts"]:
|
||||
# Redact SSN
|
||||
text = regex_replace(text, r"\\d{3}-\\d{2}-\\d{4}", "[SSN]")
|
||||
# Redact credit cards
|
||||
text = regex_replace(text, r"\\d{16}", "[CARD]")
|
||||
modified.append(text)
|
||||
|
||||
# Block SQL in requests
|
||||
if input_type == "request":
|
||||
for text in inputs["texts"]:
|
||||
if contains_code_language(text, ["sql"]):
|
||||
return block("SQL injection blocked")
|
||||
|
||||
return modify(texts=modified)
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CustomCodeModal } from "./CustomCodeModal";
|
||||
@@ -7607,6 +7607,89 @@ export const applyGuardrail = async (
|
||||
}
|
||||
};
|
||||
|
||||
export interface TestCustomCodeGuardrailRequest {
|
||||
custom_code: string;
|
||||
test_input: {
|
||||
texts: string[];
|
||||
images?: string[];
|
||||
tools?: Record<string, any>[];
|
||||
tool_calls?: Record<string, any>[];
|
||||
structured_messages?: Record<string, any>[];
|
||||
model?: string;
|
||||
};
|
||||
input_type?: "request" | "response";
|
||||
request_data?: {
|
||||
model?: string;
|
||||
user_id?: string;
|
||||
team_id?: string;
|
||||
end_user_id?: string;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TestCustomCodeGuardrailResponse {
|
||||
success: boolean;
|
||||
result?: {
|
||||
action: "allow" | "block" | "modify";
|
||||
reason?: string;
|
||||
texts?: string[];
|
||||
images?: string[];
|
||||
tool_calls?: Record<string, any>[];
|
||||
detection_info?: Record<string, any>;
|
||||
warning?: string;
|
||||
};
|
||||
error?: string;
|
||||
error_type?: "compilation" | "execution";
|
||||
}
|
||||
|
||||
export const testCustomCodeGuardrail = async (
|
||||
accessToken: string,
|
||||
request: TestCustomCodeGuardrailRequest
|
||||
): Promise<TestCustomCodeGuardrailResponse> => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/guardrails/test_custom_code`
|
||||
: `/guardrails/test_custom_code`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
let errorMessage = "Failed to test custom code guardrail";
|
||||
|
||||
try {
|
||||
const errorJson = JSON.parse(errorData);
|
||||
if (errorJson.error?.message) {
|
||||
errorMessage = errorJson.error.message;
|
||||
} else if (errorJson.detail) {
|
||||
errorMessage = errorJson.detail;
|
||||
} else if (errorJson.message) {
|
||||
errorMessage = errorJson.message;
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage = errorData || errorMessage;
|
||||
}
|
||||
|
||||
handleError(errorData);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Test custom code guardrail response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to test custom code guardrail:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const validateBlockedWordsFile = async (accessToken: string, fileContent: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
|
||||
Reference in New Issue
Block a user