mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
working v0 for policy mgmt
This commit is contained in:
parent
6a5b73f52b
commit
971c2f7577
10 changed files with 433 additions and 14 deletions
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue