diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx index 375529f5175..88570130a6e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx @@ -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() { Chat Compare Compliance + Agent Builder @@ -57,6 +59,14 @@ export default function PlaygroundPage() { + + + ); diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx new file mode 100644 index 00000000000..f8e3c00cdc0 --- /dev/null +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx @@ -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([]); + const [modelGroups, setModelGroups] = useState([]); + const [loadingAgents, setLoadingAgents] = useState(true); + const [selectedId, setSelectedId] = useState(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(undefined); + const [draftTemperature, setDraftTemperature] = useState(0.7); + const [draftMaxTokens, setDraftMaxTokens] = useState(4096); + + // Chat state (for Chat tab) + const [chatHistory, setChatHistory] = useState([]); + const [chatInput, setChatInput] = useState(""); + const [chatLoading, setChatLoading] = useState(false); + const [saving, setSaving] = useState(false); + const abortControllerRef = useRef(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 ( +
+ Sign in to use Agent Builder. +
+ ); + } + + return ( +
+
+ Agent Builder + {isNewAgent ? ( + + ) : ( + Select an agent or add new + )} +
+ +
+ {/* Roster */} +
+
+ Agents +
+
+ {loadingAgents ? ( +
+ +
+ ) : ( + <> + {agentModels.map((agent) => ( + + ))} + + + )} +
+
+ + {/* Main content */} +
+ {selectedId === null && !isNewAgent && agentModels.length === 0 && !loadingAgents && ( +
+ No agents yet. Add an agent to get started. +
+ )} + {(selectedId !== null || isNewAgent) && ( + <> + 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: ( + + Configure + + ), + children: ( +
+ {isNewAgent ? ( +
+
+ + setDraftName(e.target.value)} + placeholder="My Agent" + /> +
+
+ +