feat: ui/ - new agent builder ui

This commit is contained in:
Krrish Dholakia 2026-02-20 22:16:17 -08:00
parent a271cf28ad
commit cc1e196167
3 changed files with 514 additions and 1 deletions

View file

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

View file

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

View file

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