mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
feat: ui/ - new agent builder ui
This commit is contained in:
parent
a271cf28ad
commit
cc1e196167
3 changed files with 514 additions and 1 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import AgentBuilderView from "@/components/playground/chat_ui/AgentBuilderView";
|
||||
import ChatUI from "@/components/playground/chat_ui/ChatUI";
|
||||
import CompareUI from "@/components/playground/compareUI/CompareUI";
|
||||
import ComplianceUI from "@/components/playground/complianceUI/ComplianceUI";
|
||||
|
|
@ -39,6 +40,7 @@ export default function PlaygroundPage() {
|
|||
<Tab>Chat</Tab>
|
||||
<Tab>Compare</Tab>
|
||||
<Tab>Compliance</Tab>
|
||||
<Tab>Agent Builder</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="h-full">
|
||||
<TabPanel className="h-full">
|
||||
|
|
@ -57,6 +59,14 @@ export default function PlaygroundPage() {
|
|||
<TabPanel className="h-full">
|
||||
<ComplianceUI accessToken={accessToken} disabledPersonalKeyCreation={disabledPersonalKeyCreation} />
|
||||
</TabPanel>
|
||||
<TabPanel className="h-full">
|
||||
<AgentBuilderView
|
||||
accessToken={accessToken}
|
||||
userID={userId}
|
||||
userRole={userRole}
|
||||
customProxyBaseUrl={proxySettings?.LITELLM_UI_API_DOC_BASE_URL ?? proxySettings?.PROXY_BASE_URL}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,452 @@
|
|||
"use client";
|
||||
|
||||
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 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";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export interface AgentBuilderViewProps {
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
apiKey?: string;
|
||||
customProxyBaseUrl?: string;
|
||||
}
|
||||
|
||||
const NEW_AGENT_ID = "__new__";
|
||||
|
||||
export default function AgentBuilderView({
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
apiKey,
|
||||
customProxyBaseUrl,
|
||||
}: AgentBuilderViewProps) {
|
||||
const [agentModels, setAgentModels] = useState<AgentModel[]>([]);
|
||||
const [modelGroups, setModelGroups] = useState<ModelGroup[]>([]);
|
||||
const [loadingAgents, setLoadingAgents] = useState(true);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<"configure" | "chat" | "test">("configure");
|
||||
|
||||
// Draft for new agent
|
||||
const [draftName, setDraftName] = useState("");
|
||||
const [draftSystemPrompt, setDraftSystemPrompt] = useState("");
|
||||
const [draftUnderlyingModel, setDraftUnderlyingModel] = useState<string | undefined>(undefined);
|
||||
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;
|
||||
const isNewAgent = selectedId === NEW_AGENT_ID;
|
||||
|
||||
const loadAgents = useCallback(async () => {
|
||||
if (!accessToken || !userID || !userRole) return;
|
||||
setLoadingAgents(true);
|
||||
try {
|
||||
const list = await fetchAvailableAgentModels(accessToken, userID, userRole);
|
||||
setAgentModels(list);
|
||||
if (!selectedId || (selectedId !== NEW_AGENT_ID && !list.some((a) => a.model_name === selectedId))) {
|
||||
setSelectedId(list.length > 0 ? list[0].model_name : null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
NotificationsManager.fromBackend("Failed to load agents");
|
||||
} finally {
|
||||
setLoadingAgents(false);
|
||||
}
|
||||
}, [accessToken, userID, userRole]);
|
||||
|
||||
const loadModels = useCallback(async () => {
|
||||
if (!effectiveApiKey) return;
|
||||
try {
|
||||
const models = await fetchAvailableModels(effectiveApiKey);
|
||||
setModelGroups(models);
|
||||
if (!draftUnderlyingModel && models.length > 0) {
|
||||
setDraftUnderlyingModel(models[0].model_group);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, [effectiveApiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents();
|
||||
}, [loadAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
loadModels();
|
||||
}, [loadModels]);
|
||||
|
||||
const handleAddAgent = () => {
|
||||
setSelectedId(NEW_AGENT_ID);
|
||||
setDraftName("");
|
||||
setDraftSystemPrompt("You are a helpful assistant.");
|
||||
setDraftUnderlyingModel(modelGroups[0]?.model_group);
|
||||
setDraftTemperature(0.7);
|
||||
setDraftMaxTokens(4096);
|
||||
setActiveTab("configure");
|
||||
};
|
||||
|
||||
const handleSaveAgent = async () => {
|
||||
if (!accessToken || !draftName?.trim() || !draftUnderlyingModel) {
|
||||
NotificationsManager.fromBackend("Name and underlying model are required");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await modelCreateCall(accessToken, {
|
||||
model_name: draftName.trim(),
|
||||
litellm_params: {
|
||||
model: `litellm_agent/${draftUnderlyingModel}`,
|
||||
litellm_system_prompt: draftSystemPrompt.trim() || undefined,
|
||||
temperature: draftTemperature,
|
||||
max_tokens: draftMaxTokens,
|
||||
},
|
||||
model_info: {},
|
||||
});
|
||||
const newName = draftName.trim();
|
||||
await loadAgents();
|
||||
setSelectedId(newName);
|
||||
setActiveTab("chat");
|
||||
} catch (e) {
|
||||
NotificationsManager.fromBackend("Failed to save agent");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
Sign in to use Agent Builder.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
{isNewAgent ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveAgent}
|
||||
loading={saving}
|
||||
disabled={!draftName?.trim() || !draftUnderlyingModel}
|
||||
>
|
||||
Save Agent
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-xs text-gray-500">Select an agent or add new</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Roster */}
|
||||
<div className="w-60 flex-shrink-0 border-r border-gray-200 bg-white flex flex-col">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-gray-500">Agents</span>
|
||||
<Button type="text" size="small" icon={<PlusOutlined />} onClick={handleAddAgent} aria-label="Add agent" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{loadingAgents ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{agentModels.map((agent) => (
|
||||
<button
|
||||
key={agent.model_name}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedId(agent.model_name);
|
||||
setChatHistory([]);
|
||||
}}
|
||||
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"
|
||||
: "border-transparent hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium truncate">{agent.model_name}</div>
|
||||
<div className="text-[10px] text-gray-500 truncate">litellm_agent</div>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddAgent}
|
||||
className="mb-1 w-full rounded-md border border-dashed border-gray-300 px-3 py-2 text-left text-sm text-gray-500 hover:border-blue-400 hover:bg-blue-50/50 hover:text-gray-700"
|
||||
>
|
||||
<PlusOutlined className="mr-1" /> New agent
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{selectedId === null && !isNewAgent && agentModels.length === 0 && !loadingAgents && (
|
||||
<div className="flex flex-1 items-center justify-center p-8 text-gray-500">
|
||||
No agents yet. Add an agent to get started.
|
||||
</div>
|
||||
)}
|
||||
{(selectedId !== null || isNewAgent) && (
|
||||
<>
|
||||
<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"
|
||||
items={[
|
||||
{
|
||||
key: "configure",
|
||||
label: (
|
||||
<span>
|
||||
<RobotOutlined className="mr-1" /> Configure
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
{isNewAgent ? (
|
||||
<div className="mx-auto max-w-xl space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Agent name</label>
|
||||
<Input
|
||||
value={draftName}
|
||||
onChange={(e) => setDraftName(e.target.value)}
|
||||
placeholder="My Agent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">System prompt</label>
|
||||
<TextArea
|
||||
value={draftSystemPrompt}
|
||||
onChange={(e) => setDraftSystemPrompt(e.target.value)}
|
||||
placeholder="You are a helpful assistant..."
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Underlying LLM</label>
|
||||
<Select
|
||||
value={draftUnderlyingModel}
|
||||
onChange={setDraftUnderlyingModel}
|
||||
className="w-full"
|
||||
options={modelGroups.map((m) => ({ value: m.model_group, label: m.model_group }))}
|
||||
placeholder="Select model"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Temperature</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
value={draftTemperature}
|
||||
onChange={(e) => setDraftTemperature(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Max tokens</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={draftMaxTokens}
|
||||
onChange={(e) => setDraftMaxTokens(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : selectedAgent ? (
|
||||
<div className="mx-auto max-w-xl space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Name</label>
|
||||
<div className="rounded border border-gray-200 bg-gray-50 px-3 py-2 text-sm">
|
||||
{selectedAgent.model_name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">System prompt</label>
|
||||
<div className="rounded border border-gray-200 bg-gray-50 px-3 py-2 text-sm whitespace-pre-wrap">
|
||||
{selectedAgent.litellm_params?.litellm_system_prompt || "(none)"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Underlying model</label>
|
||||
<div className="rounded border border-gray-200 bg-gray-50 px-3 py-2 text-sm font-mono">
|
||||
{selectedAgent.litellm_params?.model ?? ""}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" icon={<CommentOutlined />} onClick={() => setActiveTab("chat")}>
|
||||
Test in Chat
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "chat",
|
||||
label: (
|
||||
<span>
|
||||
<CommentOutlined className="mr-1" /> Chat
|
||||
</span>
|
||||
),
|
||||
disabled: isNewAgent,
|
||||
children: (
|
||||
<div className="flex h-full flex-col">
|
||||
{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>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Save an agent first to test in Chat.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "test",
|
||||
label: (
|
||||
<span>
|
||||
<ExperimentOutlined className="mr-1" /> Batch Test
|
||||
</span>
|
||||
),
|
||||
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>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// fetch_agents.tsx
|
||||
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "../../networking";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName, modelInfoCall } from "../../networking";
|
||||
|
||||
export interface Agent {
|
||||
agent_id: string;
|
||||
|
|
@ -13,6 +13,17 @@ export interface Agent {
|
|||
};
|
||||
}
|
||||
|
||||
/** Agent model from /model/info where litellm_params.model starts with "litellm_agent/" */
|
||||
export interface AgentModel {
|
||||
model_name: string;
|
||||
litellm_params: {
|
||||
model: string;
|
||||
litellm_system_prompt?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
model_info?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches available A2A agents from /v1/agents endpoint.
|
||||
*/
|
||||
|
|
@ -53,3 +64,43 @@ export const fetchAvailableAgents = async (
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches available litellm_agent models from /v2/model/info.
|
||||
* Filters for models where litellm_params.model starts with "litellm_agent/".
|
||||
*/
|
||||
export const fetchAvailableAgentModels = async (
|
||||
accessToken: string,
|
||||
userID: string,
|
||||
userRole: string,
|
||||
customBaseUrl?: string,
|
||||
): Promise<AgentModel[]> => {
|
||||
try {
|
||||
const size = 200;
|
||||
const response = await modelInfoCall(accessToken, userID, userRole, 1, size);
|
||||
const data = response?.data ?? [];
|
||||
const list = Array.isArray(data) ? data : [];
|
||||
|
||||
const agentModels: AgentModel[] = list
|
||||
.filter(
|
||||
(m: { litellm_params?: { model?: string } }) =>
|
||||
typeof m?.litellm_params?.model === "string" &&
|
||||
m.litellm_params.model.startsWith("litellm_agent/"),
|
||||
)
|
||||
.map((m: any) => ({
|
||||
model_name: m.model_name ?? m.model_group ?? "",
|
||||
litellm_params: {
|
||||
...m.litellm_params,
|
||||
model: m.litellm_params.model,
|
||||
litellm_system_prompt: m.litellm_params?.litellm_system_prompt,
|
||||
},
|
||||
model_info: m.model_info ?? null,
|
||||
}));
|
||||
|
||||
agentModels.sort((a, b) => a.model_name.localeCompare(b.model_name));
|
||||
return agentModels;
|
||||
} catch (error) {
|
||||
console.error("Error fetching agent models:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue