working v0 for policy mgmt

This commit is contained in:
Ishaan Jaffer 2026-01-23 12:18:50 -08:00
parent 6a5b73f52b
commit 971c2f7577
10 changed files with 433 additions and 14 deletions

View file

@ -5564,6 +5564,32 @@ export const deletePolicyAttachmentCall = async (accessToken: string, attachment
}
};
export const getResolvedGuardrails = async (accessToken: string, policyId: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}/resolved-guardrails` : `/policies/${policyId}/resolved-guardrails`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to get resolved guardrails:", error);
throw error;
}
};
export const getPromptsList = async (accessToken: string): Promise<ListPromptsResponse> => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/list` : `/prompts/list`;

View file

@ -30,6 +30,7 @@ import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
import { v4 as uuidv4 } from "uuid";
import { truncateString } from "../../../utils/textUtils";
import GuardrailSelector from "../../guardrails/GuardrailSelector";
import PolicySelector from "../../policies/PolicySelector";
import { MCPServer } from "../../mcp_tools/types";
import NotificationsManager from "../../molecules/notifications_manager";
import { fetchMCPServers, listMCPTools } from "../../networking";
@ -188,6 +189,15 @@ const ChatUI: React.FC<ChatUIProps> = ({
return [];
}
});
const [selectedPolicies, setSelectedPolicies] = useState<string[]>(() => {
const saved = sessionStorage.getItem("selectedPolicies");
try {
return saved ? JSON.parse(saved) : [];
} catch (error) {
console.error("Error parsing selectedPolicies from sessionStorage", error);
return [];
}
});
const [messageTraceId, setMessageTraceId] = useState<string | null>(
() => sessionStorage.getItem("messageTraceId") || null,
);
@ -261,6 +271,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
selectedTags,
selectedVectorStores,
selectedGuardrails,
selectedPolicies,
selectedMCPServers,
mcpServers,
mcpServerToolRestrictions,
@ -283,6 +294,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
selectedTags,
selectedVectorStores,
selectedGuardrails,
selectedPolicies,
selectedMCPServers,
mcpServers,
mcpServerToolRestrictions,
@ -308,6 +320,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
sessionStorage.setItem("selectedTags", JSON.stringify(selectedTags));
sessionStorage.setItem("selectedVectorStores", JSON.stringify(selectedVectorStores));
sessionStorage.setItem("selectedGuardrails", JSON.stringify(selectedGuardrails));
sessionStorage.setItem("selectedPolicies", JSON.stringify(selectedPolicies));
sessionStorage.setItem("selectedMCPServers", JSON.stringify(selectedMCPServers));
sessionStorage.setItem("mcpServerToolRestrictions", JSON.stringify(mcpServerToolRestrictions));
sessionStorage.setItem("selectedVoice", selectedVoice);
@ -897,6 +910,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
traceId,
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
selectedPolicies.length > 0 ? selectedPolicies : undefined,
selectedMCPServers,
updateChatImageUI,
updateSearchResults,
@ -977,6 +991,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
traceId,
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
selectedPolicies.length > 0 ? selectedPolicies : undefined,
selectedMCPServers, // Pass the selected servers array
useApiSessionManagement ? responsesSessionId : null, // Only pass session ID if API mode is enabled
handleResponseId, // Pass callback to capture new response ID
@ -1008,6 +1023,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
traceId,
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
selectedPolicies.length > 0 ? selectedPolicies : undefined,
selectedMCPServers, // Pass the selected tools array
customProxyBaseUrl || undefined,
);
@ -1587,6 +1603,32 @@ const ChatUI: React.FC<ChatUIProps> = ({
/>
</div>
<div>
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
<SafetyOutlined className="mr-2" /> Policies
<Tooltip
className="ml-1"
title={
<span>
Select policy/policies to apply to this LLM API call. Policies define which guardrails are applied based on conditions. You can set up your policies{" "}
<a href="?page=policies" style={{ color: "#1890ff" }}>
here
</a>
.
</span>
}
>
<InfoCircleOutlined />
</Tooltip>
</Text>
<PolicySelector
value={selectedPolicies}
onChange={setSelectedPolicies}
className="mb-4"
accessToken={accessToken || ""}
/>
</div>
{/* Code Interpreter Toggle - Only for Responses endpoint */}
{endpointType === EndpointType.RESPONSES && (
<div>

View file

@ -6,6 +6,7 @@ interface CodeGenMetadata {
tags?: string[];
vector_stores?: string[];
guardrails?: string[];
policies?: string[];
}
interface GenerateCodeParams {
@ -17,6 +18,7 @@ interface GenerateCodeParams {
selectedTags: string[];
selectedVectorStores: string[];
selectedGuardrails: string[];
selectedPolicies: string[];
selectedMCPServers: string[];
mcpServers?: MCPServer[];
mcpServerToolRestrictions?: Record<string, string[]>;
@ -40,6 +42,7 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
selectedTags,
selectedVectorStores,
selectedGuardrails,
selectedPolicies,
selectedMCPServers,
mcpServers,
mcpServerToolRestrictions,
@ -72,6 +75,7 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
if (selectedTags.length > 0) metadata.tags = selectedTags;
if (selectedVectorStores.length > 0) metadata.vector_stores = selectedVectorStores;
if (selectedGuardrails.length > 0) metadata.guardrails = selectedGuardrails;
if (selectedPolicies.length > 0) metadata.policies = selectedPolicies;
const modelNameForCode = selectedModel || "your-model-name";

View file

@ -17,6 +17,7 @@ export async function makeAnthropicMessagesRequest(
traceId?: string,
vector_store_ids?: string[],
guardrails?: string[],
policies?: string[],
selectedMCPTools?: string[],
customBaseUrl?: string,
) {
@ -59,6 +60,7 @@ export async function makeAnthropicMessagesRequest(
if (vector_store_ids) requestBody.vector_store_ids = vector_store_ids;
if (guardrails) requestBody.guardrails = guardrails;
if (policies) requestBody.policies = policies;
// Use the streaming helper method for cleaner async iteration
// @ts-ignore - The SDK types might not include all litellm-specific parameters
const stream = client.messages.stream(requestBody, { signal });

View file

@ -19,6 +19,7 @@ export async function makeOpenAIChatCompletionRequest(
traceId?: string,
vector_store_ids?: string[],
guardrails?: string[],
policies?: string[],
selectedMCPServers?: string[],
onImageGenerated?: (imageUrl: string, model?: string) => void,
onSearchResults?: (searchResults: VectorStoreSearchResponse[]) => void,
@ -110,6 +111,7 @@ export async function makeOpenAIChatCompletionRequest(
messages: chatHistory as ChatCompletionMessageParam[],
...(vector_store_ids ? { vector_store_ids } : {}),
...(guardrails ? { guardrails } : {}),
...(policies ? { policies } : {}),
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
...(temperature !== undefined ? { temperature } : {}),
...(max_tokens !== undefined ? { max_tokens } : {}),

View file

@ -27,6 +27,7 @@ export async function makeOpenAIResponsesRequest(
traceId?: string,
vector_store_ids?: string[],
guardrails?: string[],
policies?: string[],
selectedMCPServers?: string[],
previousResponseId?: string | null,
onResponseId?: (responseId: string) => void,
@ -137,6 +138,7 @@ export async function makeOpenAIResponsesRequest(
...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
...(vector_store_ids ? { vector_store_ids } : {}),
...(guardrails ? { guardrails } : {}),
...(policies ? { policies } : {}),
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
},
{ signal },

View file

@ -0,0 +1,77 @@
import React, { useEffect, useState } from "react";
import { Select } from "antd";
import { Policy } from "./types";
import { getPoliciesList } from "../networking";
interface PolicySelectorProps {
onChange: (selectedPolicies: string[]) => void;
value?: string[];
className?: string;
accessToken: string;
disabled?: boolean;
}
const PolicySelector: React.FC<PolicySelectorProps> = ({
onChange,
value,
className,
accessToken,
disabled
}) => {
const [policies, setPolicies] = useState<Policy[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchPolicies = async () => {
if (!accessToken) return;
setLoading(true);
try {
const response = await getPoliciesList(accessToken);
console.log("Policies response:", response);
if (response.policies) {
console.log("Policies data:", response.policies);
setPolicies(response.policies);
}
} catch (error) {
console.error("Error fetching policies:", error);
} finally {
setLoading(false);
}
};
fetchPolicies();
}, [accessToken]);
const handlePolicyChange = (selectedValues: string[]) => {
console.log("Selected policies:", selectedValues);
onChange(selectedValues);
};
return (
<div>
<Select
mode="multiple"
disabled={disabled}
placeholder={disabled ? "Setting policies is a premium feature." : "Select policies"}
onChange={handlePolicyChange}
value={value}
loading={loading}
className={className}
allowClear
options={policies.map((policy) => {
console.log("Mapping policy:", policy);
return {
label: `${policy.policy_name}${policy.description ? ` - ${policy.description}` : ""}`,
value: policy.policy_name,
};
})}
optionFilterProp="label"
showSearch
style={{ width: "100%" }}
/>
</div>
);
};
export default PolicySelector;

View file

@ -1,8 +1,8 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
import { Button } from "@tremor/react";
import { Policy, PolicyAttachmentCreateRequest } from "./types";
import { createPolicyAttachmentCall } from "../networking";
import { createPolicyAttachmentCall, teamListCall, keyInfoCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
const { Text } = Typography;
@ -25,6 +25,52 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
const [availableTeams, setAvailableTeams] = useState<string[]>([]);
const [availableKeys, setAvailableKeys] = useState<string[]>([]);
const [isLoadingTeams, setIsLoadingTeams] = useState(false);
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
useEffect(() => {
if (visible && accessToken) {
loadTeamsAndKeys();
}
}, [visible, accessToken]);
const loadTeamsAndKeys = async () => {
if (!accessToken) return;
// Load teams
setIsLoadingTeams(true);
try {
const teamsResponse = await teamListCall(accessToken);
if (teamsResponse?.data) {
const teamAliases = teamsResponse.data
.map((t: any) => t.team_alias)
.filter(Boolean);
setAvailableTeams(teamAliases);
}
} catch (error) {
console.error("Failed to load teams:", error);
} finally {
setIsLoadingTeams(false);
}
// Load keys
setIsLoadingKeys(true);
try {
const keysResponse = await keyInfoCall(accessToken, null, null);
if (keysResponse?.data) {
const keyAliases = keysResponse.data
.map((k: any) => k.key_alias)
.filter(Boolean);
setAvailableKeys(keyAliases);
}
} catch (error) {
console.error("Failed to load keys:", error);
} finally {
setIsLoadingKeys(false);
}
};
const resetForm = () => {
form.resetFields();
@ -135,12 +181,21 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<Form.Item
name="teams"
label="Teams"
tooltip="Team aliases this attachment applies to. Supports wildcards (e.g., healthcare-*)"
tooltip="Select team aliases or enter custom patterns. Supports wildcards (e.g., healthcare-*)"
>
<Select
mode="tags"
placeholder="Enter team aliases (e.g., healthcare-team)"
placeholder={isLoadingTeams ? "Loading teams..." : "Select or enter team aliases"}
loading={isLoadingTeams}
options={availableTeams.map((team) => ({
label: team,
value: team,
}))}
tokenSeparators={[","]}
showSearch
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
style={{ width: "100%" }}
/>
</Form.Item>
@ -148,12 +203,21 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<Form.Item
name="keys"
label="Keys"
tooltip="Key aliases this attachment applies to. Supports wildcards (e.g., dev-*)"
tooltip="Select key aliases or enter custom patterns. Supports wildcards (e.g., dev-*)"
>
<Select
mode="tags"
placeholder="Enter key aliases (e.g., dev-key-*)"
placeholder={isLoadingKeys ? "Loading keys..." : "Select or enter key aliases"}
loading={isLoadingKeys}
options={availableKeys.map((key) => ({
label: key,
value: key,
}))}
tokenSeparators={[","]}
showSearch
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
style={{ width: "100%" }}
/>
</Form.Item>

View file

@ -1,9 +1,9 @@
import React, { useState, useEffect } from "react";
import { Form, Select, Modal, Divider, Typography } from "antd";
import { Form, Select, Modal, Divider, Typography, Tag, Alert, Radio } from "antd";
import { Button, TextInput, Textarea } from "@tremor/react";
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
import { Guardrail } from "../guardrails/types";
import { createPolicyCall, updatePolicyCall } from "../networking";
import { createPolicyCall, updatePolicyCall, getResolvedGuardrails, modelAvailableCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
const { Text } = Typography;
@ -30,24 +30,131 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
}) => {
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
const [isLoadingResolved, setIsLoadingResolved] = useState(false);
const [modelConditionType, setModelConditionType] = useState<"model" | "regex">("model");
const [availableModels, setAvailableModels] = useState<string[]>([]);
const isEditing = !!editingPolicy;
useEffect(() => {
if (visible && editingPolicy) {
const modelCondition = editingPolicy.condition?.model;
// Detect if it's a regex pattern (contains *, ., [, ], etc.)
const isRegex = modelCondition && /[.*+?^${}()|[\]\\]/.test(modelCondition);
setModelConditionType(isRegex ? "regex" : "model");
form.setFieldsValue({
policy_name: editingPolicy.policy_name,
description: editingPolicy.description,
inherit: editingPolicy.inherit,
guardrails_add: editingPolicy.guardrails_add || [],
guardrails_remove: editingPolicy.guardrails_remove || [],
model_condition: editingPolicy.condition?.model,
model_condition: modelCondition,
});
// Load resolved guardrails for editing
if (editingPolicy.policy_id && accessToken) {
loadResolvedGuardrails(editingPolicy.policy_id);
}
} else if (visible) {
form.resetFields();
setResolvedGuardrails([]);
setModelConditionType("model");
}
}, [visible, editingPolicy, form]);
useEffect(() => {
if (visible && accessToken) {
loadAvailableModels();
}
}, [visible, accessToken]);
const loadAvailableModels = async () => {
if (!accessToken) return;
try {
const response = await modelAvailableCall(accessToken, null, null, null);
if (response?.data) {
const models = response.data.map((m: any) => m.id || m.model_name).filter(Boolean);
setAvailableModels(models);
}
} catch (error) {
console.error("Failed to load available models:", error);
}
};
const loadResolvedGuardrails = async (policyId: string) => {
if (!accessToken) return;
setIsLoadingResolved(true);
try {
const data = await getResolvedGuardrails(accessToken, policyId);
setResolvedGuardrails(data.resolved_guardrails || []);
} catch (error) {
console.error("Failed to load resolved guardrails:", error);
} finally {
setIsLoadingResolved(false);
}
};
const computeResolvedGuardrails = (): string[] => {
const values = form.getFieldsValue(true);
const inheritFrom = values.inherit;
const guardrailsAdd = values.guardrails_add || [];
const guardrailsRemove = values.guardrails_remove || [];
let resolved = new Set<string>();
// If inheriting, find parent policy and get its guardrails
if (inheritFrom) {
const parentPolicy = existingPolicies.find(p => p.policy_name === inheritFrom);
if (parentPolicy) {
// Recursively resolve parent's guardrails
const parentResolved = resolveParentGuardrails(parentPolicy);
parentResolved.forEach(g => resolved.add(g));
}
}
// Add guardrails
guardrailsAdd.forEach((g: string) => resolved.add(g));
// Remove guardrails
guardrailsRemove.forEach((g: string) => resolved.delete(g));
return Array.from(resolved).sort();
};
const resolveParentGuardrails = (policy: Policy): string[] => {
let resolved = new Set<string>();
// If parent inherits, resolve recursively
if (policy.inherit) {
const grandparent = existingPolicies.find(p => p.policy_name === policy.inherit);
if (grandparent) {
const grandparentResolved = resolveParentGuardrails(grandparent);
grandparentResolved.forEach(g => resolved.add(g));
}
}
// Add parent's guardrails
if (policy.guardrails_add) {
policy.guardrails_add.forEach(g => resolved.add(g));
}
// Remove parent's removed guardrails
if (policy.guardrails_remove) {
policy.guardrails_remove.forEach(g => resolved.delete(g));
}
return Array.from(resolved);
};
// Recompute resolved guardrails when form values change
const handleFormChange = () => {
const resolved = computeResolvedGuardrails();
setResolvedGuardrails(resolved);
};
const resetForm = () => {
form.resetFields();
};
@ -126,6 +233,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
guardrails_add: [],
guardrails_remove: [],
}}
onValuesChange={handleFormChange}
>
<Form.Item
name="policy_name"
@ -201,16 +309,72 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
/>
</Form.Item>
{resolvedGuardrails.length > 0 && (
<Alert
message="Resolved Guardrails"
description={
<div>
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
These are the final guardrails that will be applied (including inheritance):
</Text>
<div className="flex flex-wrap gap-1">
{resolvedGuardrails.map((g) => (
<Tag key={g} color="blue">
{g}
</Tag>
))}
</div>
</div>
}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
)}
<Divider orientation="left">
<Text strong>Conditions (Optional)</Text>
</Divider>
<Form.Item label="Model Condition Type">
<Radio.Group
value={modelConditionType}
onChange={(e) => {
setModelConditionType(e.target.value);
form.setFieldValue("model_condition", undefined);
}}
>
<Radio value="model">Select Model</Radio>
<Radio value="regex">Custom Regex Pattern</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="model_condition"
label="Model Condition"
tooltip="Only apply this policy when the model matches this pattern (supports regex)"
label={modelConditionType === "model" ? "Model" : "Regex Pattern"}
tooltip={
modelConditionType === "model"
? "Select a specific model to apply this policy to"
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*)"
}
>
<TextInput placeholder="e.g., gpt-4.* or bedrock/claude-3" />
{modelConditionType === "model" ? (
<Select
showSearch
allowClear
placeholder="Select a model"
options={availableModels.map((model) => ({
label: model,
value: model,
}))}
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
style={{ width: "100%" }}
/>
) : (
<TextInput placeholder="e.g., gpt-4.* or bedrock/claude-.*" />
)}
</Form.Item>
<div className="flex justify-end space-x-2 mt-4">

View file

@ -1,9 +1,9 @@
import React, { useState, useEffect, useCallback } from "react";
import { Card, Badge, Button } from "@tremor/react";
import { ArrowLeftIcon, PencilIcon } from "@heroicons/react/outline";
import { Descriptions, Tag, Spin, Divider, Typography } from "antd";
import { Descriptions, Tag, Spin, Divider, Typography, Alert } from "antd";
import { Policy } from "./types";
import { getPolicyInfo } from "../networking";
import { getPolicyInfo, getResolvedGuardrails } from "../networking";
const { Title, Text } = Typography;
@ -24,6 +24,8 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
}) => {
const [policy, setPolicy] = useState<Policy | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
const [isLoadingResolved, setIsLoadingResolved] = useState(false);
const fetchPolicy = useCallback(async () => {
if (!accessToken || !policyId) return;
@ -32,6 +34,17 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
try {
const data = await getPolicyInfo(accessToken, policyId);
setPolicy(data);
// Also fetch resolved guardrails
setIsLoadingResolved(true);
try {
const resolvedData = await getResolvedGuardrails(accessToken, policyId);
setResolvedGuardrails(resolvedData.resolved_guardrails || []);
} catch (error) {
console.error("Error fetching resolved guardrails:", error);
} finally {
setIsLoadingResolved(false);
}
} catch (error) {
console.error("Error fetching policy:", error);
} finally {
@ -116,6 +129,29 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
<Text strong>Guardrails Configuration</Text>
</Divider>
{resolvedGuardrails.length > 0 && (
<Alert
message="Resolved Guardrails"
description={
<div>
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
Final guardrails that will be applied (including inheritance):
</Text>
<div className="flex flex-wrap gap-1">
{resolvedGuardrails.map((g) => (
<Tag key={g} color="blue">
{g}
</Tag>
))}
</div>
</div>
}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
)}
<Descriptions bordered column={1}>
<Descriptions.Item label="Guardrails to Add">
<div className="flex flex-wrap gap-1">