mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
feat(agentbuilderview.tsx): run compliance datasets against litellm agent
This commit is contained in:
parent
a019f434a7
commit
74673fa959
4 changed files with 297 additions and 222 deletions
|
|
@ -40,7 +40,7 @@ export default function PlaygroundPage() {
|
|||
<Tab>Chat</Tab>
|
||||
<Tab>Compare</Tab>
|
||||
<Tab>Compliance</Tab>
|
||||
<Tab>Agent Builder</Tab>
|
||||
<Tab>Agent Builder (Experimental)</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="h-full">
|
||||
<TabPanel className="h-full">
|
||||
|
|
@ -62,8 +62,11 @@ export default function PlaygroundPage() {
|
|||
<TabPanel className="h-full">
|
||||
<AgentBuilderView
|
||||
accessToken={accessToken}
|
||||
token={token}
|
||||
userID={userId}
|
||||
userRole={userRole}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
proxySettings={proxySettings}
|
||||
customProxyBaseUrl={proxySettings?.LITELLM_UI_API_DOC_BASE_URL ?? proxySettings?.PROXY_BASE_URL}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
|
|
|||
|
|
@ -2,21 +2,26 @@
|
|||
|
||||
import { CommentOutlined, ExperimentOutlined, PlusOutlined, RobotOutlined, SaveOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Select, Spin, Tabs } from "antd";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
import { modelCreateCall } from "../../networking";
|
||||
import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion";
|
||||
import { AgentModel, fetchAvailableAgentModels } from "../llm_calls/fetch_agents";
|
||||
import { fetchAvailableModels, ModelGroup } from "../llm_calls/fetch_models";
|
||||
import { createDisplayMessage } from "./ResponsesImageUtils";
|
||||
import { MessageType } from "./types";
|
||||
import ComplianceUI from "../complianceUI/ComplianceUI";
|
||||
import ChatUI from "./ChatUI";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export interface AgentBuilderViewProps {
|
||||
accessToken: string | null;
|
||||
token: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
disabledPersonalKeyCreation?: boolean;
|
||||
proxySettings?: {
|
||||
PROXY_BASE_URL?: string;
|
||||
LITELLM_UI_API_DOC_BASE_URL?: string | null;
|
||||
};
|
||||
apiKey?: string;
|
||||
customProxyBaseUrl?: string;
|
||||
}
|
||||
|
|
@ -25,8 +30,11 @@ const NEW_AGENT_ID = "__new__";
|
|||
|
||||
export default function AgentBuilderView({
|
||||
accessToken,
|
||||
token,
|
||||
userID,
|
||||
userRole,
|
||||
disabledPersonalKeyCreation = false,
|
||||
proxySettings,
|
||||
apiKey,
|
||||
customProxyBaseUrl,
|
||||
}: AgentBuilderViewProps) {
|
||||
|
|
@ -43,12 +51,7 @@ export default function AgentBuilderView({
|
|||
const [draftTemperature, setDraftTemperature] = useState(0.7);
|
||||
const [draftMaxTokens, setDraftMaxTokens] = useState(4096);
|
||||
|
||||
// Chat state (for Chat tab)
|
||||
const [chatHistory, setChatHistory] = useState<MessageType[]>([]);
|
||||
const [chatInput, setChatInput] = useState("");
|
||||
const [chatLoading, setChatLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const effectiveApiKey = apiKey || accessToken || "";
|
||||
const selectedAgent = selectedId === NEW_AGENT_ID ? null : agentModels.find((a) => a.model_name === selectedId) ?? null;
|
||||
|
|
@ -130,65 +133,6 @@ export default function AgentBuilderView({
|
|||
}
|
||||
};
|
||||
|
||||
const updateTextUI = useCallback((role: string, chunk: string, model?: string) => {
|
||||
setChatHistory((prev) => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (last && last.role === role && !last.isImage && !last.isAudio) {
|
||||
return [
|
||||
...prev.slice(0, -1),
|
||||
{ ...last, content: (last.content as string) + chunk, model: last.model ?? model },
|
||||
];
|
||||
}
|
||||
return [...prev, { role, content: chunk, model } as MessageType];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
const text = chatInput.trim();
|
||||
if (!text || !selectedAgent || !effectiveApiKey) return;
|
||||
const displayMessage = createDisplayMessage(text, false);
|
||||
setChatHistory((prev) => [...prev, displayMessage]);
|
||||
setChatInput("");
|
||||
setChatLoading(true);
|
||||
abortControllerRef.current = new AbortController();
|
||||
const apiHistory = [
|
||||
...chatHistory
|
||||
.filter((m) => !m.isImage && !m.isAudio)
|
||||
.map((m) => ({ role: m.role, content: typeof m.content === "string" ? m.content : "" })),
|
||||
{ role: "user" as const, content: text },
|
||||
];
|
||||
try {
|
||||
await makeOpenAIChatCompletionRequest(
|
||||
apiHistory,
|
||||
(chunk, model) => updateTextUI("assistant", chunk, model),
|
||||
selectedAgent.model_name,
|
||||
effectiveApiKey,
|
||||
undefined,
|
||||
abortControllerRef.current.signal,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0.7,
|
||||
4096,
|
||||
undefined,
|
||||
customProxyBaseUrl,
|
||||
);
|
||||
} catch (e) {
|
||||
NotificationsManager.fromBackend("Chat request failed");
|
||||
updateTextUI("assistant", "Error: request failed.");
|
||||
} finally {
|
||||
setChatLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!accessToken || !userID || !userRole) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-8 text-gray-500">
|
||||
|
|
@ -199,8 +143,9 @@ export default function AgentBuilderView({
|
|||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-white text-gray-900">
|
||||
<div className="flex h-12 flex-shrink-0 items-center justify-between border-b border-gray-200 px-4">
|
||||
<span className="text-sm font-medium text-gray-900">Agent Builder</span>
|
||||
<div className="flex flex-shrink-0 flex-col border-b border-gray-200">
|
||||
<div className="flex h-12 items-center justify-between px-4">
|
||||
<span className="text-sm font-medium text-gray-900">Agent Builder</span>
|
||||
{isNewAgent ? (
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -212,8 +157,15 @@ export default function AgentBuilderView({
|
|||
Save Agent
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-xs text-gray-500">Select an agent or add new</span>
|
||||
<span className="text-xs text-gray-500">Build Agents that pass your compliance requirements.</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-t border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800">
|
||||
<ExperimentOutlined className="flex-shrink-0 text-amber-600" />
|
||||
<span>
|
||||
Agent Builder is experimental and may change or be removed without notice.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
|
|
@ -234,10 +186,7 @@ export default function AgentBuilderView({
|
|||
<button
|
||||
key={agent.model_name}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedId(agent.model_name);
|
||||
setChatHistory([]);
|
||||
}}
|
||||
onClick={() => setSelectedId(agent.model_name)}
|
||||
className={`mb-1 w-full rounded-md border-l-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
selectedId === agent.model_name
|
||||
? "border-blue-500 bg-blue-50 text-blue-800"
|
||||
|
|
@ -272,7 +221,7 @@ export default function AgentBuilderView({
|
|||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(k) => setActiveTab(k as "configure" | "chat" | "test")}
|
||||
className="flex-1 overflow-hidden [&_.ant-tabs-content]:h-full [&_.ant-tabs-tabpane]:h-full"
|
||||
className="flex-1 overflow-hidden [&_.ant-tabs-content]:h-full [&_.ant-tabs-tabpane]:h-full [&_.ant-tabs-nav]:pl-4"
|
||||
items={[
|
||||
{
|
||||
key: "configure",
|
||||
|
|
@ -372,54 +321,19 @@ export default function AgentBuilderView({
|
|||
),
|
||||
disabled: isNewAgent,
|
||||
children: (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex h-full flex-col min-h-0">
|
||||
{selectedAgent ? (
|
||||
<>
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{chatHistory.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm ${
|
||||
msg.role === "user"
|
||||
? "bg-blue-600 text-white rounded-br-none"
|
||||
: "bg-gray-100 text-gray-800 rounded-bl-none"
|
||||
}`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap">{String(msg.content)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{chatLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-2xl rounded-bl-none bg-gray-100 px-4 py-2 text-sm text-gray-500">
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 border-t border-gray-200 p-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={chatInput}
|
||||
onChange={(e) => setChatInput(e.target.value)}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
placeholder="Type a message..."
|
||||
disabled={chatLoading}
|
||||
/>
|
||||
<Button type="primary" onClick={handleSendMessage} loading={chatLoading} disabled={!chatInput.trim()}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<ChatUI
|
||||
key={selectedAgent.model_name}
|
||||
simplified
|
||||
fixedModel={selectedAgent.model_name}
|
||||
accessToken={accessToken}
|
||||
token={token}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Save an agent first to test in Chat.
|
||||
|
|
@ -435,9 +349,22 @@ export default function AgentBuilderView({
|
|||
<ExperimentOutlined className="mr-1" /> Batch Test
|
||||
</span>
|
||||
),
|
||||
disabled: isNewAgent,
|
||||
children: (
|
||||
<div className="flex flex-1 items-center justify-center p-8 text-gray-500">
|
||||
Batch Test placeholder. Select an agent and use Chat to test.
|
||||
<div className="flex h-full flex-col min-h-0">
|
||||
{selectedAgent ? (
|
||||
<ComplianceUI
|
||||
accessToken={accessToken}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
backendMode="chat_completions"
|
||||
fixedModel={selectedAgent.model_name}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Select an agent to run batch tests.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ interface ChatUIProps {
|
|||
PROXY_BASE_URL?: string;
|
||||
LITELLM_UI_API_DOC_BASE_URL?: string | null;
|
||||
};
|
||||
/** When true, hide configuration sidebar and use fixedModel only (e.g. embedded in Agent Builder). */
|
||||
simplified?: boolean;
|
||||
/** When simplified is true, use this as the model and do not show model selector. */
|
||||
fixedModel?: string;
|
||||
}
|
||||
|
||||
const MCP_SUPPORTED_ENDPOINTS = new Set<EndpointType>([
|
||||
|
|
@ -99,6 +103,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
userID,
|
||||
disabledPersonalKeyCreation,
|
||||
proxySettings,
|
||||
simplified = false,
|
||||
fixedModel,
|
||||
}) => {
|
||||
const [mcpServers, setMCPServers] = useState<MCPServer[]>([]);
|
||||
const [selectedMCPServers, setSelectedMCPServers] = useState<string[]>(() => {
|
||||
|
|
@ -140,6 +146,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
);
|
||||
const [inputMessage, setInputMessage] = useState("");
|
||||
const [chatHistory, setChatHistory] = useState<MessageType[]>(() => {
|
||||
if (simplified) return [];
|
||||
try {
|
||||
const saved = sessionStorage.getItem("chatHistory");
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
|
|
@ -148,7 +155,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
return [];
|
||||
}
|
||||
});
|
||||
const [selectedModel, setSelectedModel] = useState<string | undefined>(undefined);
|
||||
const [selectedModel, setSelectedModel] = useState<string | undefined>(simplified ? fixedModel : undefined);
|
||||
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
|
||||
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
||||
const [agentInfo, setAgentInfo] = useState<Agent[]>([]);
|
||||
|
|
@ -252,6 +259,14 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// When simplified, keep selectedModel and endpointType in sync with fixedModel / chat-only
|
||||
useEffect(() => {
|
||||
if (simplified && fixedModel) {
|
||||
setSelectedModel(fixedModel);
|
||||
setEndpointType(EndpointType.CHAT);
|
||||
}
|
||||
}, [simplified, fixedModel]);
|
||||
|
||||
// Fetch tools for a specific server
|
||||
const loadServerTools = async (serverId: string) => {
|
||||
const userApiKey = apiKeySource === "session" ? accessToken : apiKey;
|
||||
|
|
@ -312,6 +327,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (simplified) return; // Do not persist chat history in simplified (embedded) mode
|
||||
const handler = setTimeout(() => {
|
||||
sessionStorage.setItem("chatHistory", JSON.stringify(chatHistory));
|
||||
}, 500); // Debounce by 500ms
|
||||
|
|
@ -319,7 +335,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [chatHistory]);
|
||||
}, [chatHistory, simplified]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem("apiKeySource", JSON.stringify(apiKeySource));
|
||||
|
|
@ -334,10 +350,12 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
sessionStorage.setItem("selectedVoice", selectedVoice);
|
||||
sessionStorage.removeItem("selectedMCPTools"); // Clean up old key
|
||||
|
||||
if (selectedModel) {
|
||||
sessionStorage.setItem("selectedModel", selectedModel);
|
||||
} else {
|
||||
sessionStorage.removeItem("selectedModel");
|
||||
if (!simplified) {
|
||||
if (selectedModel) {
|
||||
sessionStorage.setItem("selectedModel", selectedModel);
|
||||
} else {
|
||||
sessionStorage.removeItem("selectedModel");
|
||||
}
|
||||
}
|
||||
if (messageTraceId) {
|
||||
sessionStorage.setItem("messageTraceId", messageTraceId);
|
||||
|
|
@ -352,6 +370,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
sessionStorage.setItem("useApiSessionManagement", JSON.stringify(useApiSessionManagement));
|
||||
// Note: codeInterpreterEnabled and selectedContainerId are persisted by useCodeInterpreter hook
|
||||
}, [
|
||||
simplified,
|
||||
apiKeySource,
|
||||
apiKey,
|
||||
selectedModel,
|
||||
|
|
@ -375,7 +394,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// Fetch model info and set the default selected model
|
||||
// Fetch model info and set the default selected model (skip in simplified mode; we use fixedModel)
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
if (!userApiKey) {
|
||||
|
|
@ -400,9 +419,11 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
loadModels();
|
||||
if (!simplified) {
|
||||
loadModels();
|
||||
}
|
||||
loadMCPServers();
|
||||
}, [accessToken, userID, userRole, apiKeySource, apiKey, token]);
|
||||
}, [accessToken, userID, userRole, apiKeySource, apiKey, token, simplified]);
|
||||
|
||||
// Load tools when MCP direct mode has a server selected
|
||||
useEffect(() => {
|
||||
|
|
@ -868,7 +889,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const effectiveApiKey = apiKeySource === "session" ? accessToken : apiKey;
|
||||
const effectiveApiKey = simplified ? accessToken : apiKeySource === "session" ? accessToken : apiKey;
|
||||
|
||||
if (!effectiveApiKey) {
|
||||
NotificationsManager.fromBackend("Please provide a Virtual Key or select Current UI Session");
|
||||
|
|
@ -959,6 +980,10 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
newUserMessage,
|
||||
];
|
||||
|
||||
const requestProxyBaseUrl =
|
||||
simplified && proxySettings
|
||||
? (proxySettings.LITELLM_UI_API_DOC_BASE_URL ?? proxySettings.PROXY_BASE_URL ?? undefined)
|
||||
: (customProxyBaseUrl || undefined);
|
||||
await makeOpenAIChatCompletionRequest(
|
||||
apiChatHistory,
|
||||
(chunk, model) => updateTextUI("assistant", chunk, model),
|
||||
|
|
@ -979,7 +1004,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
useAdvancedParams ? temperature : undefined,
|
||||
useAdvancedParams ? maxTokens : undefined,
|
||||
updateTotalLatency,
|
||||
customProxyBaseUrl || undefined,
|
||||
requestProxyBaseUrl,
|
||||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
handleMCPEvent,
|
||||
|
|
@ -1207,9 +1232,11 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
handleRemoveResponsesImage(); // Clear any uploaded images for responses
|
||||
handleRemoveChatImage(); // Clear any uploaded images for chat completions
|
||||
handleRemoveAudio(); // Clear any uploaded audio for transcription
|
||||
sessionStorage.removeItem("chatHistory");
|
||||
sessionStorage.removeItem("messageTraceId");
|
||||
sessionStorage.removeItem("responsesSessionId");
|
||||
if (!simplified) {
|
||||
sessionStorage.removeItem("chatHistory");
|
||||
sessionStorage.removeItem("messageTraceId");
|
||||
sessionStorage.removeItem("responsesSessionId");
|
||||
}
|
||||
NotificationsManager.success("Chat history cleared.");
|
||||
};
|
||||
|
||||
|
|
@ -1246,10 +1273,11 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
|
||||
|
||||
return (
|
||||
<div className="w-full p-4 pb-0 bg-white">
|
||||
<Card className="w-full rounded-xl shadow-md overflow-hidden">
|
||||
<div className="flex h-[80vh] w-full gap-4">
|
||||
{/* Left Sidebar with Controls */}
|
||||
<div className={`w-full bg-white ${simplified ? "h-full flex flex-col" : "p-4 pb-0"}`}>
|
||||
<Card className={`w-full rounded-xl shadow-md overflow-hidden ${simplified ? "h-full flex flex-col" : ""}`}>
|
||||
<div className={`flex w-full gap-4 ${simplified ? "h-full" : "h-[80vh]"}`}>
|
||||
{/* Left Sidebar with Controls - hidden in simplified mode */}
|
||||
{!simplified && (
|
||||
<div className="w-1/4 p-4 bg-gray-50 overflow-y-auto">
|
||||
<Title className="text-xl font-semibold mb-6 mt-2">Configurations</Title>
|
||||
<div className="space-y-4">
|
||||
|
|
@ -1794,11 +1822,12 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Chat Area */}
|
||||
<div className="w-3/4 flex flex-col bg-white">
|
||||
<div className={`flex flex-col bg-white ${simplified ? "flex-1 w-full" : "w-3/4"}`}>
|
||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<Title className="text-xl font-semibold mb-0">Test Key</Title>
|
||||
<Title className="text-xl font-semibold mb-0">{simplified ? "Chat" : "Test Key"}</Title>
|
||||
<div className="flex gap-2">
|
||||
<TremorButton
|
||||
onClick={clearChatHistory}
|
||||
|
|
@ -1807,6 +1836,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
>
|
||||
Clear Chat
|
||||
</TremorButton>
|
||||
{!simplified && (
|
||||
<TremorButton
|
||||
onClick={() => setIsGetCodeModalVisible(true)}
|
||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-300"
|
||||
|
|
@ -1814,6 +1844,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
>
|
||||
Get Code
|
||||
</TremorButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4 pb-0">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
getPoliciesList,
|
||||
testPoliciesAndGuardrails,
|
||||
} from "@/components/networking";
|
||||
import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion";
|
||||
import {
|
||||
AlertTriangle,
|
||||
BarChart3,
|
||||
|
|
@ -110,11 +111,23 @@ interface GuardrailOption {
|
|||
interface ComplianceUIProps {
|
||||
accessToken: string | null;
|
||||
disabledPersonalKeyCreation?: boolean;
|
||||
/** When "chat_completions", use /chat/completions with fixedModel instead of test_policies_and_guardrails. */
|
||||
backendMode?: "policies" | "chat_completions";
|
||||
/** Required when backendMode is "chat_completions"; model name for chat completions (e.g. selected agent). */
|
||||
fixedModel?: string;
|
||||
/** Used when backendMode is "chat_completions" for the request base URL. */
|
||||
proxySettings?: {
|
||||
PROXY_BASE_URL?: string;
|
||||
LITELLM_UI_API_DOC_BASE_URL?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export default function ComplianceUI({
|
||||
accessToken,
|
||||
disabledPersonalKeyCreation,
|
||||
backendMode = "policies",
|
||||
fixedModel,
|
||||
proxySettings,
|
||||
}: ComplianceUIProps) {
|
||||
const frameworks = getFrameworks();
|
||||
|
||||
|
|
@ -432,6 +445,9 @@ export default function ComplianceUI({
|
|||
if (csvInputRef.current) csvInputRef.current.value = "";
|
||||
};
|
||||
|
||||
const requestProxyBaseUrl =
|
||||
proxySettings?.LITELLM_UI_API_DOC_BASE_URL ?? proxySettings?.PROXY_BASE_URL ?? undefined;
|
||||
|
||||
const runQuickTest = useCallback(async () => {
|
||||
if (!quickTestInput.trim() || !accessToken) return;
|
||||
const text = quickTestInput.trim();
|
||||
|
|
@ -445,44 +461,82 @@ export default function ComplianceUI({
|
|||
setQuickTestInput("");
|
||||
setIsQuickTesting(true);
|
||||
try {
|
||||
const { inputs, guardrail_errors = [] } = await testPoliciesAndGuardrails(
|
||||
accessToken,
|
||||
{
|
||||
policy_names:
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
guardrail_names:
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
inputs: { texts: [text] },
|
||||
request_data: {},
|
||||
input_type: "request",
|
||||
}
|
||||
);
|
||||
const result: "blocked" | "allowed" =
|
||||
guardrail_errors.length > 0 ? "blocked" : "allowed";
|
||||
const triggeredBy =
|
||||
guardrail_errors.length > 0
|
||||
? guardrail_errors
|
||||
.map((e) => `${e.guardrail_name}: ${e.message}`)
|
||||
.join("; ")
|
||||
: undefined;
|
||||
const returnedText =
|
||||
Array.isArray(inputs?.texts) && inputs.texts.length > 0
|
||||
? inputs.texts[0]
|
||||
: undefined;
|
||||
const displayText =
|
||||
result === "blocked"
|
||||
? `Blocked — ${triggeredBy ?? "content filter"}`
|
||||
: "Allowed — no policy or guardrail violations detected.";
|
||||
const sysMsg: QuickTestMessage = {
|
||||
id: `msg-${Date.now()}-sys`,
|
||||
type: "system",
|
||||
text: displayText,
|
||||
result,
|
||||
triggeredBy,
|
||||
returnedText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setQuickTestMessages((prev) => [...prev, sysMsg]);
|
||||
if (backendMode === "chat_completions" && fixedModel) {
|
||||
let fullResponse = "";
|
||||
await makeOpenAIChatCompletionRequest(
|
||||
[{ role: "user", content: text }],
|
||||
(chunk: string) => {
|
||||
fullResponse += chunk;
|
||||
},
|
||||
fixedModel,
|
||||
accessToken,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined, // vector_store_ids (param 11)
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
requestProxyBaseUrl,
|
||||
undefined
|
||||
);
|
||||
const sysMsg: QuickTestMessage = {
|
||||
id: `msg-${Date.now()}-sys`,
|
||||
type: "system",
|
||||
text: "Allowed — model response received.",
|
||||
result: "allowed",
|
||||
returnedText: fullResponse,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setQuickTestMessages((prev) => [...prev, sysMsg]);
|
||||
} else {
|
||||
const { inputs, guardrail_errors = [] } = await testPoliciesAndGuardrails(
|
||||
accessToken,
|
||||
{
|
||||
policy_names:
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
guardrail_names:
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
inputs: { texts: [text] },
|
||||
request_data: {},
|
||||
input_type: "request",
|
||||
}
|
||||
);
|
||||
const result: "blocked" | "allowed" =
|
||||
guardrail_errors.length > 0 ? "blocked" : "allowed";
|
||||
const triggeredBy =
|
||||
guardrail_errors.length > 0
|
||||
? guardrail_errors
|
||||
.map((e) => `${e.guardrail_name}: ${e.message}`)
|
||||
.join("; ")
|
||||
: undefined;
|
||||
const returnedText =
|
||||
Array.isArray(inputs?.texts) && inputs.texts.length > 0
|
||||
? inputs.texts[0]
|
||||
: undefined;
|
||||
const displayText =
|
||||
result === "blocked"
|
||||
? `Blocked — ${triggeredBy ?? "content filter"}`
|
||||
: "Allowed — no policy or guardrail violations detected.";
|
||||
const sysMsg: QuickTestMessage = {
|
||||
id: `msg-${Date.now()}-sys`,
|
||||
type: "system",
|
||||
text: displayText,
|
||||
result,
|
||||
triggeredBy,
|
||||
returnedText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setQuickTestMessages((prev) => [...prev, sysMsg]);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
const sysMsg: QuickTestMessage = {
|
||||
|
|
@ -502,6 +556,9 @@ export default function ComplianceUI({
|
|||
quickTestInput,
|
||||
selectedPolicies,
|
||||
selectedGuardrails,
|
||||
backendMode,
|
||||
fixedModel,
|
||||
requestProxyBaseUrl,
|
||||
]);
|
||||
|
||||
const handleQuickTestKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
|
@ -533,45 +590,99 @@ export default function ComplianceUI({
|
|||
}));
|
||||
setTestResults(pendingResults);
|
||||
try {
|
||||
const inputsList = allTexts.map((text) => ({ texts: [text] }));
|
||||
const response = await testPoliciesAndGuardrails(accessToken, {
|
||||
policy_names:
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
guardrail_names:
|
||||
if (backendMode === "chat_completions" && fixedModel) {
|
||||
const newResults = [...pendingResults];
|
||||
for (let index = 0; index < selected.length; index++) {
|
||||
const row = pendingResults[index];
|
||||
const prompt = allTexts[index];
|
||||
let responseText = "";
|
||||
try {
|
||||
await makeOpenAIChatCompletionRequest(
|
||||
[{ role: "user", content: prompt }],
|
||||
(chunk: string) => {
|
||||
responseText += chunk;
|
||||
},
|
||||
fixedModel,
|
||||
accessToken,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
inputs_list: inputsList,
|
||||
request_data: {},
|
||||
input_type: "request",
|
||||
});
|
||||
const results = response.results ?? [];
|
||||
setTestResults(
|
||||
pendingResults.map((row, index) => {
|
||||
const item = results[index];
|
||||
const guardrail_errors = item?.guardrail_errors ?? [];
|
||||
const actualResult: "blocked" | "allowed" =
|
||||
guardrail_errors.length > 0 ? "blocked" : "allowed";
|
||||
const triggeredBy =
|
||||
guardrail_errors.length > 0
|
||||
? guardrail_errors
|
||||
.map((e) => `${e.guardrail_name}: ${e.message}`)
|
||||
.join("; ")
|
||||
: undefined;
|
||||
const returnedText =
|
||||
Array.isArray(item?.inputs?.texts) && item.inputs.texts.length > 0
|
||||
? item.inputs.texts[0]
|
||||
: undefined;
|
||||
return {
|
||||
...row,
|
||||
actualResult,
|
||||
isMatch:
|
||||
(row.expectedResult === "fail" && actualResult === "blocked") ||
|
||||
(row.expectedResult === "pass" && actualResult === "allowed"),
|
||||
triggeredBy,
|
||||
returnedText,
|
||||
status: "complete" as const,
|
||||
};
|
||||
})
|
||||
);
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
requestProxyBaseUrl,
|
||||
undefined
|
||||
);
|
||||
const actualResult: "blocked" | "allowed" = "allowed";
|
||||
newResults[index] = {
|
||||
...row,
|
||||
actualResult,
|
||||
returnedText: responseText,
|
||||
isMatch: row.expectedResult === "pass",
|
||||
status: "complete" as const,
|
||||
};
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
newResults[index] = {
|
||||
...row,
|
||||
actualResult: "blocked" as const,
|
||||
isMatch: false,
|
||||
triggeredBy: errorMessage,
|
||||
status: "complete" as const,
|
||||
};
|
||||
}
|
||||
setTestResults([...newResults]);
|
||||
}
|
||||
} else {
|
||||
const inputsList = allTexts.map((text) => ({ texts: [text] }));
|
||||
const response = await testPoliciesAndGuardrails(accessToken, {
|
||||
policy_names:
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
guardrail_names:
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
inputs_list: inputsList,
|
||||
request_data: {},
|
||||
input_type: "request",
|
||||
});
|
||||
const results = response.results ?? [];
|
||||
setTestResults(
|
||||
pendingResults.map((row, index) => {
|
||||
const item = results[index];
|
||||
const guardrail_errors = item?.guardrail_errors ?? [];
|
||||
const actualResult: "blocked" | "allowed" =
|
||||
guardrail_errors.length > 0 ? "blocked" : "allowed";
|
||||
const triggeredBy =
|
||||
guardrail_errors.length > 0
|
||||
? guardrail_errors
|
||||
.map((e) => `${e.guardrail_name}: ${e.message}`)
|
||||
.join("; ")
|
||||
: undefined;
|
||||
const returnedText =
|
||||
Array.isArray(item?.inputs?.texts) && item.inputs.texts.length > 0
|
||||
? item.inputs.texts[0]
|
||||
: undefined;
|
||||
return {
|
||||
...row,
|
||||
actualResult,
|
||||
isMatch:
|
||||
(row.expectedResult === "fail" && actualResult === "blocked") ||
|
||||
(row.expectedResult === "pass" && actualResult === "allowed"),
|
||||
triggeredBy,
|
||||
returnedText,
|
||||
status: "complete" as const,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
setTestResults(
|
||||
|
|
@ -591,6 +702,9 @@ export default function ComplianceUI({
|
|||
selectedPolicies,
|
||||
selectedGuardrails,
|
||||
allFrameworks,
|
||||
backendMode,
|
||||
fixedModel,
|
||||
requestProxyBaseUrl,
|
||||
]);
|
||||
|
||||
const completedResults = testResults.filter((r) => r.status === "complete");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue