From e66217f07adf1ec33f0b3fffad55ddf761e6e40f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 30 Sep 2026 00:40:12 -0700 Subject: [PATCH] feat(ui): add agent graph --- .../view_logs/TraceView/AgentGraph.tsx | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/view_logs/TraceView/AgentGraph.tsx diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentGraph.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentGraph.tsx new file mode 100644 index 00000000000..5b8704259fe --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentGraph.tsx @@ -0,0 +1,208 @@ +"use client"; + +import { useMemo, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/cva.config"; + +import type { AgentNode, Span } from "./traceTypes"; +import { + agentsWithErrors, + fmtCost, + fmtMs, + GRAPH_NODE_HEIGHT, + invocationsOf, + layoutAgentGraph, + type GraphEdge, + type GraphNode, +} from "./traceUtils"; + +interface AgentGraphProps { + agents: AgentNode[]; + spans: Span[]; + /** Jump to the Tree view focused on this invocation span. */ + onOpenInvocation: (spanId: string) => void; +} + +const INVOCATIONS_PAGE_SIZE = 10; + +function Edge({ edge }: { edge: GraphEdge }) { + const x1 = edge.from.x + edge.from.width; + const y1 = edge.from.y + GRAPH_NODE_HEIGHT / 2; + const x2 = edge.to.x; + const y2 = edge.to.y + GRAPH_NODE_HEIGHT / 2; + const mid = (x1 + x2) / 2; + return ( + + + + {edge.label} + + + ); +} + +interface NodeProps { + node: GraphNode; + hasError: boolean; + selected: boolean; + onClick: (name: string) => void; +} + +function Node({ node, hasError, selected, onClick }: NodeProps) { + const { agent } = node; + return ( + onClick(agent.name)} + onKeyDown={(event) => event.key === "Enter" && onClick(agent.name)} + > + + + {agent.name} + + + {`${fmtCost(agent.spend)} · ${agent.llm_calls} LLM · ${agent.tool_calls} tool`} + + + ); +} + +function InvocationList({ + agentName, + spans, + onOpenInvocation, +}: { + agentName: string; + spans: Span[]; + onOpenInvocation: (spanId: string) => void; +}) { + const [page, setPage] = useState(0); + const invocations = useMemo(() => invocationsOf(spans, agentName), [spans, agentName]); + const pages = Math.max(1, Math.ceil(invocations.length / INVOCATIONS_PAGE_SIZE)); + const visible = invocations.slice(page * INVOCATIONS_PAGE_SIZE, (page + 1) * INVOCATIONS_PAGE_SIZE); + return ( +
+
+ + {agentName} · {invocations.length} invocation{invocations.length === 1 ? "" : "s"} + + {pages > 1 && ( + + + {page + 1} / {pages} + + + )} +
+ +
+ ); +} + +/** One node per distinct agent, edges parent → child labelled with invocation count. */ +export function AgentGraph({ agents, spans, onOpenInvocation }: AgentGraphProps) { + const [selected, setSelected] = useState(null); + const layout = useMemo(() => layoutAgentGraph(agents), [agents]); + const failing = useMemo(() => agentsWithErrors(spans), [spans]); + + if (agents.length === 0) { + return
No agent spans in this trace.
; + } + + return ( +
+ + + + + + + {layout.edges.map((edge) => ( + ${edge.to.agent.name}`} edge={edge} /> + ))} + {layout.nodes.map((node) => ( + setSelected(selected === name ? null : name)} + /> + ))} + +

+ Node width scales with spend. Click an agent to list its invocations. +

+ {selected && ( + + )} +
+ ); +}