diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/AgentTraceDrawer.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/AgentTraceDrawer.tsx new file mode 100644 index 00000000000..0aad7cfe4ca --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/AgentTraceDrawer.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Drawer } from "antd"; +import type { AgentTraceSession, Span } from "../agentTraceTypes"; +import { TraceView } from "./TraceView"; +import { SpanDetail } from "./SpanDetail"; + +function findSpan(spans: Span[], id: string): Span | null { + for (const s of spans) { + if (s.id === id) return s; + if (s.children?.length) { + const found = findSpan(s.children, id); + if (found) return found; + } + } + return null; +} + +export interface AgentTraceDrawerProps { + open: boolean; + onClose: () => void; + session: AgentTraceSession | null; +} + +const DRAWER_WIDTH = 960; + +export function AgentTraceDrawer({ + open, + onClose, + session, +}: AgentTraceDrawerProps) { + const [selectedSpanId, setSelectedSpanId] = useState(null); + + const selectedSpan = useMemo(() => { + if (!session?.spans || selectedSpanId == null) return null; + return findSpan(session.spans, selectedSpanId); + }, [session?.spans, selectedSpanId]); + + const handleClose = () => { + setSelectedSpanId(null); + onClose(); + }; + + return ( + + {session == null ? ( +
+

No session selected

+
+ ) : ( +
+
+ +
+
+ +
+
+ )} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/SpanDetail.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/SpanDetail.tsx new file mode 100644 index 00000000000..3ee9b718af4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/SpanDetail.tsx @@ -0,0 +1,271 @@ +"use client"; + +import { useState } from "react"; +import { Button, Tabs, Tag, Typography } from "antd"; +import { CopyOutlined, DownOutlined, RightOutlined } from "@ant-design/icons"; +import { Bot, MessageSquare, Wrench, Zap } from "lucide-react"; +import type { Span, SpanType } from "../agentTraceTypes"; + +const { Text } = Typography; + +const SPAN_CONFIG: Record< + SpanType, + { icon: React.ElementType; label: string; color: string; badgeClass: string } +> = { + orchestrator: { + icon: Bot, + label: "Orchestrator", + color: "text-blue-600", + badgeClass: "bg-blue-100 text-blue-800 border-blue-200", + }, + agent: { + icon: Bot, + label: "Agent", + color: "text-indigo-600", + badgeClass: "bg-indigo-100 text-indigo-800 border-indigo-200", + }, + llm: { + icon: MessageSquare, + label: "LLM", + color: "text-purple-600", + badgeClass: "bg-purple-100 text-purple-800 border-purple-200", + }, + mcp: { + icon: Wrench, + label: "MCP", + color: "text-amber-600", + badgeClass: "bg-amber-100 text-amber-800 border-amber-200", + }, + function: { + icon: Zap, + label: "Function", + color: "text-green-600", + badgeClass: "bg-green-100 text-green-800 border-green-200", + }, +}; + +const STATUS_CONFIG: Record< + string, + { color: string; dotClass: string } +> = { + success: { color: "text-green-600", dotClass: "bg-green-500" }, + error: { color: "text-red-600", dotClass: "bg-red-500" }, + running: { color: "text-blue-600", dotClass: "bg-blue-500" }, + retry: { color: "text-amber-600", dotClass: "bg-amber-500" }, +}; + +function copyToClipboard(text: string) { + void navigator.clipboard.writeText(text); +} + +export interface SpanDetailProps { + span: Span | null; +} + +export function SpanDetail({ span }: SpanDetailProps) { + const [metricsOpen, setMetricsOpen] = useState(true); + const [ioOpen, setIoOpen] = useState(true); + const [costOpen, setCostOpen] = useState(true); + + if (span === null) { + return ( +
+

Select a span

+

Click a span in the trace view to see details.

+
+ ); + } + + const config = SPAN_CONFIG[span.type]; + const statusConfig = STATUS_CONFIG[span.status] ?? STATUS_CONFIG.success; + const Icon = config.icon; + + const tryFormatJson = (raw: string | undefined): string => { + if (raw == null || raw === "") return ""; + try { + return JSON.stringify(JSON.parse(raw), null, 2); + } catch { + return raw; + } + }; + + return ( +
+ {/* Header */} +
+
+
+ + {span.name} + + {config.label} + +
+
+
+ + + {span.status} + + + {span.durationMs} ms + +
+
+ + {/* Collapsible: Metrics */} +
+ + {metricsOpen && ( +
+
+ Duration + {span.durationMs} ms +
+
+ Start (ms) + {span.startMs} +
+ {span.tokens != null && ( + <> +
+ Prompt tokens + {span.tokens.prompt} +
+
+ Completion tokens + {span.tokens.completion} +
+ + )} + {span.cost != null && ( +
+ Cost + ${span.cost.toFixed(6)} +
+ )} + {span.mcpServer != null && ( +
+ MCP Server + + {span.mcpServer} + +
+ )} + {span.mcpTool != null && ( +
+ MCP Tool + {span.mcpTool} +
+ )} +
+ )} +
+ + {/* Collapsible: Input / Output */} +
+ + {ioOpen && ( +
+ + {tryFormatJson(span.input) || "—"} + + ), + }, + { + key: "output", + label: "Output", + children: ( +
+                      {tryFormatJson(span.output) || "—"}
+                    
+ ), + }, + ]} + /> +
+ )} +
+ + {/* Cost breakdown (LLM spans only) */} + {span.type === "llm" && (span.cost != null || span.tokens != null) && ( +
+ + {costOpen && ( +
+ {span.model != null && ( +
+ Model + {span.model} +
+ )} + {span.tokens != null && ( +
+ Total tokens + + {span.tokens.prompt + span.tokens.completion} + +
+ )} + {span.cost != null && ( +
+ Cost + ${span.cost.toFixed(6)} +
+ )} +
+ )} +
+ )} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/TraceView.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/TraceView.tsx new file mode 100644 index 00000000000..411dcd71f5f --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/TraceView.tsx @@ -0,0 +1,451 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { ChevronDown, ChevronRight, Clock, Cpu } from "lucide-react"; +import { Bot, MessageSquare, Wrench, Zap } from "lucide-react"; +import type { AgentTraceSession, Span, SpanType } from "../agentTraceTypes"; + +export interface TraceViewProps { + session: AgentTraceSession; + selectedSpanId: string | null; + onSelectSpan: (id: string) => void; +} + +const SPAN_CONFIG: Record< + SpanType, + { icon: React.ElementType; label: string; bg: string; text: string; border: string; bar: string } +> = { + orchestrator: { + icon: Bot, + label: "Orchestrator", + bg: "bg-blue-50", + text: "text-blue-700", + border: "border-blue-200", + bar: "bg-blue-500", + }, + agent: { + icon: Bot, + label: "Agent", + bg: "bg-indigo-50", + text: "text-indigo-700", + border: "border-indigo-200", + bar: "bg-indigo-500", + }, + llm: { + icon: MessageSquare, + label: "LLM", + bg: "bg-purple-50", + text: "text-purple-700", + border: "border-purple-200", + bar: "bg-purple-500", + }, + mcp: { + icon: Wrench, + label: "MCP", + bg: "bg-amber-50", + text: "text-amber-700", + border: "border-amber-200", + bar: "bg-amber-500", + }, + function: { + icon: Zap, + label: "Function", + bg: "bg-green-50", + text: "text-green-700", + border: "border-green-200", + bar: "bg-green-500", + }, +}; + +function formatDuration(ms: number): string { + if (ms >= 1000) return `${(ms / 1000).toFixed(3)}s`; + return `${ms}ms`; +} + +function flattenSpans( + spans: Span[], + depth = 0 +): Array<{ span: Span; depth: number }> { + const result: Array<{ span: Span; depth: number }> = []; + for (const span of spans) { + result.push({ span, depth }); + if (span.children?.length) { + result.push(...flattenSpans(span.children, depth + 1)); + } + } + return result; +} + +function TreeRow({ + span, + depth, + isSelected, + isExpanded, + hasChildren, + onSelect, + onToggle, +}: { + span: Span; + depth: number; + isSelected: boolean; + isExpanded: boolean; + hasChildren: boolean; + onSelect: () => void; + onToggle: () => void; +}) { + const config = SPAN_CONFIG[span.type]; + const Icon = config.icon; + return ( +
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelect(); + } + }} + className={`flex items-center gap-1.5 px-2 py-1.5 cursor-pointer border-b border-slate-100 transition-colors ${isSelected ? "bg-blue-50" : "hover:bg-slate-50"}`} + style={{ paddingLeft: `${8 + depth * 20}px` }} + > + +
+ +
+ + {span.name} + + + {config.label} + +
+ {span.tokens != null && ( +
+ + {span.tokens.prompt + span.tokens.completion} tok +
+ )} + {span.cost != null && ( + + ${span.cost.toFixed(5)} + + )} + + {formatDuration(span.durationMs)} + + +
+ ); +} + +function TreeView({ + session, + selectedSpanId, + onSelectSpan, +}: TraceViewProps) { + const [expandedIds, setExpandedIds] = useState>(() => { + const ids = new Set(); + function collect(spans: Span[]) { + for (const s of spans) { + ids.add(s.id); + if (s.children?.length) collect(s.children); + } + } + collect(session.spans); + return ids; + }); + + const toggleExpand = (id: string) => { + setExpandedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const renderSpans = (spans: Span[], depth: number): React.ReactNode[] => { + const rows: React.ReactNode[] = []; + for (const span of spans) { + const isExpanded = expandedIds.has(span.id); + const hasChildren = !!(span.children && span.children.length > 0); + rows.push( + onSelectSpan(span.id)} + onToggle={() => toggleExpand(span.id)} + /> + ); + if (hasChildren && isExpanded && span.children) { + rows.push(...renderSpans(span.children, depth + 1)); + } + } + return rows; + }; + + if (!session.spans.length) { + return ( +
+ No trace data available +
+ ); + } + + return ( +
+
+
+
+ + Span + +
+ + Tokens + + + Cost + + + Duration + +
+
+
{renderSpans(session.spans, 0)}
+
+ ); +} + +function WaterfallView({ + session, + selectedSpanId, + onSelectSpan, +}: TraceViewProps) { + const totalMs = session.totalDurationMs; + const flatSpans = useMemo(() => flattenSpans(session.spans), [session.spans]); + + if (!flatSpans.length) { + return ( +
+ No trace data available +
+ ); + } + + const LABEL_WIDTH = 200; + const tickCount = 6; + const ticks = Array.from( + { length: tickCount + 1 }, + (_, i) => Math.round((totalMs / tickCount) * i) + ); + + return ( +
+
+
+ + Span + +
+
+
+ {ticks.map((tick) => ( +
+ + {tick >= 1000 ? `${(tick / 1000).toFixed(1)}s` : `${tick}ms`} + +
+ ))} +
+
+
+ {flatSpans.map(({ span, depth }) => { + const config = SPAN_CONFIG[span.type]; + const Icon = config.icon; + const leftPct = (span.startMs / totalMs) * 100; + const widthPct = Math.max((span.durationMs / totalMs) * 100, 0.5); + const isSelected = selectedSpanId === span.id; + return ( +
onSelectSpan(span.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelectSpan(span.id); + } + }} + className={`flex items-center border-b border-slate-100 cursor-pointer transition-colors ${isSelected ? "bg-blue-50" : "hover:bg-slate-50"}`} + > +
+
+ +
+ + {span.name} + +
+
+ {ticks.slice(1, -1).map((tick) => ( +
+ ))} +
+
+
+ ); + })} +
+ ); +} + +export function TraceView({ + session, + selectedSpanId, + onSelectSpan, +}: TraceViewProps) { + const [activeTab, setActiveTab] = useState<"tree" | "waterfall">("tree"); + + const totalTokens = useMemo(() => { + let t = 0; + function sum(spans: Span[]) { + for (const s of spans) { + if (s.tokens) t += s.tokens.prompt + s.tokens.completion; + if (s.children?.length) sum(s.children); + } + } + sum(session.spans); + return t; + }, [session.spans]); + + return ( +
+
+
+

+ {session.rootAgentName} +

+ + {session.status} + +
+
+ {session.shortId} +
+
+ + + {formatDuration(session.totalDurationMs)} + + + + {totalTokens > 0 + ? `${totalTokens} tokens` + : `${session.totalSpans} spans`} + + ${session.totalCost.toFixed(4)} + {session.relativeTime} +
+
+ +
+ {(["tree", "waterfall"] as const).map((tab) => ( + + ))} +
+ +
+ {activeTab === "tree" ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/index.ts b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/index.ts new file mode 100644 index 00000000000..316cb274caa --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/AgentTrace/index.ts @@ -0,0 +1,8 @@ +export { AgentTraceDrawer } from "./AgentTraceDrawer"; +export type { AgentTraceDrawerProps } from "./AgentTraceDrawer"; +export { TraceView } from "./TraceView"; +export type { TraceViewProps } from "./TraceView"; +export { SpanDetail } from "./SpanDetail"; +export type { SpanDetailProps } from "./SpanDetail"; +export type { Span, AgentTraceSession, SpanType, SpanStatus } from "../agentTraceTypes"; +export { MOCK_AGENT_TRACE_SESSIONS } from "../agentTraceTypes"; diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.test.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.test.tsx new file mode 100644 index 00000000000..3fa8c6e233f --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.test.tsx @@ -0,0 +1,127 @@ +import { act, render, screen } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, expect, it, vi } from "vitest"; +import { LogViewer } from "./LogViewer"; +import type { LogEntry } from "./mockData"; + +vi.mock("@/components/view_logs/LogDetailsDrawer", () => ({ + LogDetailsDrawer: ({ + open, + onClose, + }: { + open: boolean; + onClose: () => void; + }) => + open ? ( +
+ Log details drawer + +
+ ) : null, +})); + +vi.mock("./AgentTrace", () => ({ + AgentTraceDrawer: ({ + open, + onClose, + session, + }: { + open: boolean; + onClose: () => void; + session: { rootAgentName: string; shortId: string } | null; + }) => + open && session ? ( +
+ Agent trace: {session.rootAgentName} +

Session: {session.shortId}

+ +
+ ) : null, +})); + +const mockLogs: LogEntry[] = [ + { + id: "log-1", + timestamp: "2026-03-02 21:00:00", + action: "passed", + model: "gpt-4o", + input_snippet: "First guardrail log request", + }, + { + id: "log-2", + timestamp: "2026-03-02 21:01:00", + action: "blocked", + model: "claude-3", + input_snippet: "Second guardrail log request", + }, + { + id: "log-3", + timestamp: "2026-03-02 21:02:00", + action: "flagged", + input_snippet: "Third guardrail log request", + }, +]; + +function wrapper({ children }: { children: React.ReactNode }) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); + return ( + + {children} + + ); +} + +describe("LogViewer", () => { + it("should render agent-trace demo rows first then guardrail log entries", () => { + render( + , + { wrapper } + ); + + // First 2 rows are the fixed agent-trace demo rows + expect(screen.getByText("Currency Research Agent")).toBeDefined(); + expect(screen.getByText("Travel Booking Agent")).toBeDefined(); + // Trace badge appears for demo rows + const traceBadges = screen.getAllByText("Trace"); + expect(traceBadges.length).toBeGreaterThanOrEqual(2); + + // Following rows are guardrail log entries (input snippets are unique to log rows) + expect(screen.getByText("First guardrail log request")).toBeDefined(); + expect(screen.getByText("Second guardrail log request")).toBeDefined(); + expect(screen.getByText("Third guardrail log request")).toBeDefined(); + }); + + it("should open AgentTraceDrawer when first demo trace row is clicked", async () => { + render( + , + { wrapper } + ); + + const currencyRow = screen.getByText("Currency Research Agent"); + expect(screen.queryByTestId("agent-trace-drawer")).toBeNull(); + + await act(async () => { + currencyRow.closest("button")?.click(); + }); + + expect(screen.getByTestId("agent-trace-drawer")).toBeDefined(); + expect(screen.getByText(/Agent trace: Currency Research Agent/)).toBeDefined(); + expect(screen.getByText(/Session: 0c4b4759-83aa/)).toBeDefined(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx index aed19ddfc8a..a4631e2cb9d 100644 --- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx @@ -5,12 +5,18 @@ import { WarningOutlined, } from "@ant-design/icons"; import { useQuery } from "@tanstack/react-query"; +import { Activity } from "lucide-react"; import moment from "moment"; import { Button, Spin } from "antd"; import React, { useState } from "react"; import { uiSpendLogsCall } from "@/components/networking"; import { LogDetailsDrawer } from "@/components/view_logs/LogDetailsDrawer"; import type { LogEntry as ViewLogsLogEntry } from "@/components/view_logs/columns"; +import { AgentTraceDrawer } from "./AgentTrace"; +import { + type AgentTraceSession, + MOCK_AGENT_TRACE_SESSIONS, +} from "./agentTraceTypes"; import type { LogEntry } from "./mockData"; const actionConfig: Record< @@ -65,7 +71,10 @@ export function LogViewer({ const [activeFilter, setActiveFilter] = useState(filterAction); const [selectedRequestId, setSelectedRequestId] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); + const [selectedTraceSession, setSelectedTraceSession] = + useState(null); + const demoTraces = MOCK_AGENT_TRACE_SESSIONS.slice(0, 2); const filteredLogs = logs.filter( (log) => activeFilter === "all" || log.action === activeFilter ); @@ -107,10 +116,15 @@ export function LogViewer({ fullLogResponse?.data?.[0] ?? null; const handleLogClick = (log: LogEntry) => { + setSelectedTraceSession(null); setSelectedRequestId(log.id); setDrawerOpen(true); }; + const handleTraceSessionClick = (session: AgentTraceSession) => { + setSelectedTraceSession(session); + }; + const handleCloseDrawer = () => { setDrawerOpen(false); setSelectedRequestId(null); @@ -127,9 +141,11 @@ export function LogViewer({

{logsLoading ? "Loading…" - : logs.length > 0 - ? `Showing ${displayLogs.length} of ${total} entries` - : "No logs for this period. Select a guardrail and date range."} + : demoTraces.length > 0 + ? `2 demo traces, then showing ${displayLogs.length} of ${total} log entries` + : logs.length > 0 + ? `Showing ${displayLogs.length} of ${total} entries` + : "No logs for this period. Select a guardrail and date range."}

{logs.length > 0 && ( @@ -170,13 +186,64 @@ export function LogViewer({
)} - {!logsLoading && displayLogs.length === 0 && ( + {!logsLoading && demoTraces.length === 0 && displayLogs.length === 0 && (
No logs to display. Adjust filters or date range.
)} - {!logsLoading && displayLogs.length > 0 && ( + {!logsLoading && (demoTraces.length > 0 || displayLogs.length > 0) && (
+ {demoTraces.map((session) => { + const statusDotColor = + session.status === "success" + ? "bg-green-500" + : session.status === "error" + ? "bg-red-500" + : session.status === "running" + ? "bg-blue-500" + : "bg-amber-500"; + return ( + + ); + })} {displayLogs.map((log) => { const config = actionConfig[log.action]; const ActionIcon = config.icon; @@ -222,6 +289,11 @@ export function LogViewer({ allLogs={selectedLog ? [selectedLog] : []} startTime={startTime} /> + setSelectedTraceSession(null)} + session={selectedTraceSession} + />
); } diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/agentTraceTypes.ts b/ui/litellm-dashboard/src/components/GuardrailsMonitor/agentTraceTypes.ts new file mode 100644 index 00000000000..2afd6e85d31 --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/agentTraceTypes.ts @@ -0,0 +1,425 @@ +/** + * Types and mock data for agent trace demo in GuardrailsMonitor LogViewer. + * Supports orchestrator → sub-agents → LLM + MCP hierarchy. + */ + +export type SpanType = + | "orchestrator" + | "agent" + | "llm" + | "mcp" + | "function"; + +export type SpanStatus = "success" | "error" | "running" | "retry"; + +export interface Span { + id: string; + name: string; + type: SpanType; + status: SpanStatus; + startMs: number; + durationMs: number; + model?: string; + tokens?: { prompt: number; completion: number }; + cost?: number; + mcpServer?: string; + mcpTool?: string; + input?: string; + output?: string; + children?: Span[]; +} + +export interface AgentTraceSession { + id: string; + shortId: string; + rootAgentName: string; + timestamp: string; + relativeTime: string; + totalSpans: number; + totalCost: number; + totalDurationMs: number; + status: SpanStatus; + spans: Span[]; +} + +/** First 2 sessions for demo: always shown as first 1–2 rows in LogViewer. */ +export const MOCK_AGENT_TRACE_SESSIONS: AgentTraceSession[] = [ + { + id: "0c4b4759-83aa-4f2e-b91c-d3e7f8a12345", + shortId: "0c4b4759-83aa", + rootAgentName: "Currency Research Agent", + timestamp: "2026-03-02 09:36:12 PM", + relativeTime: "2 min ago", + totalSpans: 10, + totalCost: 0.00366, + totalDurationMs: 2677, + status: "success", + spans: [ + { + id: "span-orch-001", + name: "OrchestratorAgent", + type: "orchestrator", + status: "success", + startMs: 0, + durationMs: 2677, + input: JSON.stringify( + { + task: "Research current currency exchange rates and recent news for USD/EUR, USD/GBP", + context: { user_id: "u_8821", session: "0c4b4759" }, + }, + null, + 2 + ), + output: JSON.stringify( + { + summary: + "Completed currency research. EUR/USD at 1.0842 (+0.23%), GBP/USD at 1.2634 (+0.11%). Key news: ECB holds rates, UK inflation data due Thursday.", + agents_used: ["CurrencyAgent", "NewsAgent"], + total_tokens: 1026, + }, + null, + 2 + ), + children: [ + { + id: "span-currency-001", + name: "CurrencyAgent", + type: "agent", + status: "success", + startMs: 50, + durationMs: 2100, + input: JSON.stringify( + { + task: "Fetch current exchange rates and 7-day history for USD/EUR and USD/GBP", + tools: ["forex-api"], + }, + null, + 2 + ), + output: JSON.stringify( + { + rates: { "USD/EUR": 1.0842, "USD/GBP": 1.2634 }, + trend: "bullish", + confidence: 0.87, + }, + null, + 2 + ), + children: [ + { + id: "span-llm-001", + name: "gpt-4o", + type: "llm", + status: "success", + startMs: 100, + durationMs: 1346, + model: "gpt-4o", + tokens: { prompt: 198, completion: 74 }, + cost: 0.00096, + input: JSON.stringify( + { + messages: [ + { + role: "system", + content: + "You are a currency research agent. Use the available tools to fetch exchange rates.", + }, + { + role: "user", + content: + "Get current USD/EUR and USD/GBP rates and 7-day history.", + }, + ], + tools: [ + { name: "get_rates", description: "Get current exchange rates" }, + { name: "get_history", description: "Get historical rate data" }, + ], + }, + null, + 2 + ), + output: JSON.stringify( + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_abc123", + type: "function", + function: { + name: "get_rates", + arguments: '{"pairs": ["USD/EUR", "USD/GBP"]}', + }, + }, + ], + }, + null, + 2 + ), + children: [ + { + id: "span-mcp-001", + name: "forex-api/get_rates", + type: "mcp", + status: "success", + startMs: 150, + durationMs: 234, + mcpServer: "forex-api", + mcpTool: "get_rates", + input: JSON.stringify( + { pairs: ["USD/EUR", "USD/GBP"], timestamp: "latest" }, + null, + 2 + ), + output: JSON.stringify( + { + USD_EUR: 1.0842, + USD_GBP: 1.2634, + timestamp: "2026-03-02T21:36:12Z", + source: "ECB", + }, + null, + 2 + ), + }, + { + id: "span-mcp-002", + name: "forex-api/get_history", + type: "mcp", + status: "success", + startMs: 400, + durationMs: 189, + mcpServer: "forex-api", + mcpTool: "get_history", + input: JSON.stringify( + { pairs: ["USD/EUR", "USD/GBP"], days: 7 }, + null, + 2 + ), + output: JSON.stringify( + { + data: [ + { date: "2026-02-24", USD_EUR: 1.0791 }, + { date: "2026-02-25", USD_EUR: 1.0803 }, + { date: "2026-02-26", USD_EUR: 1.0821 }, + { date: "2026-02-27", USD_EUR: 1.0834 }, + { date: "2026-02-28", USD_EUR: 1.0842 }, + ], + }, + null, + 2 + ), + }, + ], + }, + { + id: "span-llm-002", + name: "gpt-4o", + type: "llm", + status: "success", + startMs: 1500, + durationMs: 826, + model: "gpt-4o", + tokens: { prompt: 171, completion: 44 }, + cost: 0.00087, + input: JSON.stringify( + { + messages: [ + { + role: "system", + content: + "Analyze the fetched currency data and provide a summary.", + }, + { + role: "user", + content: "Summarize the exchange rate data and trends.", + }, + { + role: "tool", + content: '{"USD_EUR": 1.0842, "USD_GBP": 1.2634}', + }, + ], + }, + null, + 2 + ), + output: JSON.stringify( + { + role: "assistant", + content: + "EUR/USD is trading at 1.0842, up 0.23% over the past week showing a steady bullish trend. GBP/USD at 1.2634 with modest gains of 0.11%. Both pairs showing positive momentum.", + }, + null, + 2 + ), + }, + ], + }, + { + id: "span-news-001", + name: "NewsAgent", + type: "agent", + status: "success", + startMs: 50, + durationMs: 1800, + input: JSON.stringify( + { + task: "Search for recent news about USD/EUR and USD/GBP exchange rates", + tools: ["web-search"], + }, + null, + 2 + ), + output: JSON.stringify( + { + articles: 3, + sentiment: "neutral", + key_events: ["ECB rate hold", "UK CPI data pending"], + }, + null, + 2 + ), + children: [ + { + id: "span-llm-003", + name: "gpt-4o", + type: "llm", + status: "success", + startMs: 100, + durationMs: 923, + model: "gpt-4o", + tokens: { prompt: 245, completion: 96 }, + cost: 0.00112, + input: JSON.stringify( + { + messages: [ + { + role: "system", + content: "You are a financial news research agent.", + }, + { + role: "user", + content: + "Find recent news about EUR and GBP currency movements.", + }, + ], + tools: [ + { name: "query", description: "Search the web" }, + { name: "fetch", description: "Fetch a URL" }, + ], + }, + null, + 2 + ), + output: JSON.stringify( + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_def456", + type: "function", + function: { + name: "query", + arguments: + '{"q": "EUR USD GBP exchange rate news March 2026"}', + }, + }, + ], + }, + null, + 2 + ), + children: [ + { + id: "span-mcp-003", + name: "web-search/query", + type: "mcp", + status: "success", + startMs: 150, + durationMs: 445, + mcpServer: "web-search", + mcpTool: "query", + input: JSON.stringify( + { q: "EUR USD GBP exchange rate news March 2026", num_results: 5 }, + null, + 2 + ), + output: JSON.stringify( + { + results: [ + { + title: "ECB Holds Rates, Euro Steady Against Dollar", + url: "https://reuters.com/...", + snippet: + "The European Central Bank held rates steady...", + }, + { + title: "UK Inflation Data Due Thursday, GBP Cautious", + url: "https://ft.com/...", + snippet: "Markets await UK CPI data...", + }, + ], + }, + null, + 2 + ), + }, + ], + }, + { + id: "span-llm-004", + name: "gpt-4o", + type: "llm", + status: "success", + startMs: 1100, + durationMs: 654, + model: "gpt-4o", + tokens: { prompt: 156, completion: 42 }, + cost: 0.00071, + input: JSON.stringify( + { + messages: [ + { + role: "system", + content: "Summarize the news findings into key insights.", + }, + { + role: "tool", + content: + "ECB holds rates steady. UK CPI data pending Thursday.", + }, + ], + }, + null, + 2 + ), + output: JSON.stringify( + { + role: "assistant", + content: + "Key findings: (1) ECB held rates steady — neutral for EUR short-term. (2) UK CPI data due Thursday — potential GBP volatility. (3) Overall sentiment: cautiously neutral with slight bullish bias.", + }, + null, + 2 + ), + }, + ], + }, + ], + }, + ], + }, + { + id: "6419f0a4-82ef-4b3c-a921-e8f2d3c45678", + shortId: "6419f0a4-82ef", + rootAgentName: "Travel Booking Agent", + timestamp: "2026-03-02 09:34:27 PM", + relativeTime: "4 min ago", + totalSpans: 4, + totalCost: 0.0031, + totalDurationMs: 3200, + status: "success", + spans: [], + }, +]; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index e84b6e86e4a..264fbc8afbd 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -136,12 +136,6 @@ const menuGroups: MenuGroup[] = [ label: "Vector Stores", icon: , }, - { - key: "tool-policies", - page: "tool-policies", - label: "Tool Policies", - icon: , - }, ], }, ], @@ -169,6 +163,12 @@ const menuGroups: MenuGroup[] = [ icon: , roles: [...all_admin_roles, ...internalUserRoles], }, + { + key: "tool-policies", + page: "tool-policies", + label: "Tool Monitor", + icon: , + }, ], }, {