feat: initial commit adding ui for custom code guardrails

allows users to write guardrails based on custom code
This commit is contained in:
Krrish Dholakia 2026-02-03 10:07:30 -08:00
parent dd00c117c2
commit 6541528ee7
6 changed files with 1503 additions and 3 deletions

View file

@ -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"

View file

@ -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;

View file

@ -0,0 +1,493 @@
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 } 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 (placeholder - would need backend endpoint)
const handleTest = async () => {
setIsTesting(true);
setTestResult(null);
// Simulate test - in real implementation, call backend
setTimeout(() => {
try {
JSON.parse(testInput);
// Mock result based on code content
if (code.includes("block(")) {
setTestResult({ action: "block", reason: "Test blocked" });
} else if (code.includes("modify(")) {
setTestResult({ action: "modify", modified_texts: ["[REDACTED]"] });
} else {
setTestResult({ action: "allow" });
}
} catch (e) {
setTestResult({ error: "Invalid test input JSON" });
}
setIsTesting(false);
}, 500);
};
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 /> {testResult.error}</>
) : testResult.action === "allow" ? (
<><CheckCircleOutlined /> Allowed</>
) : testResult.action === "block" ? (
<><CloseCircleOutlined /> Blocked: {testResult.reason}</>
) : (
<><CheckCircleOutlined /> Modified</>
)}
</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;

View file

@ -0,0 +1,581 @@
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 result = await testCustomCodeGuardrail(
accessToken,
customCode,
parsedInputs,
parsedRequestData,
inputType
);
setTestResult(result);
if (result.action === "allow") {
NotificationsManager.success("Guardrail allowed the request");
} else if (result.action === "block") {
NotificationsManager.fromBackend(`Guardrail blocked: ${result.reason || "No reason provided"}`);
} else if (result.action === "modify") {
NotificationsManager.success("Guardrail modified the content");
}
} 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;

View file

@ -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)
`,
};

View file

@ -0,0 +1 @@
export { default as CustomCodeModal } from "./CustomCodeModal";