mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(ui/): cleanup
This commit is contained in:
parent
1dc2df14f2
commit
36ca05739c
8 changed files with 1440 additions and 11 deletions
|
|
@ -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<string | null>(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 (
|
||||
<Drawer
|
||||
title={session ? session.rootAgentName : "Agent Trace"}
|
||||
placement="right"
|
||||
width={Math.min(DRAWER_WIDTH, typeof window !== "undefined" ? window.innerWidth * 0.9 : 960)}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
destroyOnClose
|
||||
styles={{ body: { padding: 0, display: "flex", flexDirection: "column", height: "100%" } }}
|
||||
>
|
||||
{session == null ? (
|
||||
<div className="flex flex-col items-center justify-center flex-1 text-slate-500 p-6 text-center">
|
||||
<p className="text-sm">No session selected</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<div className="flex-1 min-w-0 border-r border-slate-200 overflow-hidden flex flex-col p-4">
|
||||
<TraceView
|
||||
session={session}
|
||||
selectedSpanId={selectedSpanId}
|
||||
onSelectSpan={setSelectedSpanId}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[380px] flex-shrink-0 overflow-hidden flex flex-col p-4 bg-slate-50/50">
|
||||
<SpanDetail span={selectedSpan} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="flex flex-col items-center justify-center h-full min-h-[200px] text-slate-500 p-6 text-center">
|
||||
<p className="font-medium">Select a span</p>
|
||||
<p className="text-sm mt-1">Click a span in the trace view to see details.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 border-b border-slate-200 pb-3 mb-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Icon className={`flex-shrink-0 w-4 h-4 ${config.color}`} />
|
||||
<span className="font-medium text-slate-900 truncate">{span.name}</span>
|
||||
<Tag className={`flex-shrink-0 ${config.badgeClass} border`}>
|
||||
{config.label}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyToClipboard(span.id)}
|
||||
title="Copy span ID"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-2 flex-wrap">
|
||||
<span className={`flex items-center gap-1.5 text-sm ${statusConfig.color}`}>
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full flex-shrink-0 ${statusConfig.dotClass}`}
|
||||
/>
|
||||
{span.status}
|
||||
</span>
|
||||
<Text type="secondary" className="text-sm">
|
||||
{span.durationMs} ms
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collapsible: Metrics */}
|
||||
<div className="border border-slate-200 rounded-lg mb-3 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-slate-50 hover:bg-slate-100 text-left text-sm font-medium text-slate-700 transition-colors"
|
||||
onClick={() => setMetricsOpen(!metricsOpen)}
|
||||
>
|
||||
<span>Metrics</span>
|
||||
{metricsOpen ? (
|
||||
<DownOutlined className="text-slate-500" />
|
||||
) : (
|
||||
<RightOutlined className="text-slate-500" />
|
||||
)}
|
||||
</button>
|
||||
{metricsOpen && (
|
||||
<div className="p-3 bg-white text-sm space-y-1.5">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Duration</span>
|
||||
<span className="font-mono">{span.durationMs} ms</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Start (ms)</span>
|
||||
<span className="font-mono">{span.startMs}</span>
|
||||
</div>
|
||||
{span.tokens != null && (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Prompt tokens</span>
|
||||
<span className="font-mono">{span.tokens.prompt}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Completion tokens</span>
|
||||
<span className="font-mono">{span.tokens.completion}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{span.cost != null && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Cost</span>
|
||||
<span className="font-mono">${span.cost.toFixed(6)}</span>
|
||||
</div>
|
||||
)}
|
||||
{span.mcpServer != null && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">MCP Server</span>
|
||||
<span className="font-mono truncate max-w-[180px]" title={span.mcpServer}>
|
||||
{span.mcpServer}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{span.mcpTool != null && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">MCP Tool</span>
|
||||
<span className="font-mono">{span.mcpTool}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Collapsible: Input / Output */}
|
||||
<div className="border border-slate-200 rounded-lg mb-3 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-slate-50 hover:bg-slate-100 text-left text-sm font-medium text-slate-700 transition-colors"
|
||||
onClick={() => setIoOpen(!ioOpen)}
|
||||
>
|
||||
<span>Input / Output</span>
|
||||
{ioOpen ? (
|
||||
<DownOutlined className="text-slate-500" />
|
||||
) : (
|
||||
<RightOutlined className="text-slate-500" />
|
||||
)}
|
||||
</button>
|
||||
{ioOpen && (
|
||||
<div className="p-2 bg-white">
|
||||
<Tabs
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: "input",
|
||||
label: "Input",
|
||||
children: (
|
||||
<pre className="text-xs bg-slate-50 p-3 rounded border border-slate-200 overflow-auto max-h-64 font-mono whitespace-pre-wrap break-words">
|
||||
{tryFormatJson(span.input) || "—"}
|
||||
</pre>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "output",
|
||||
label: "Output",
|
||||
children: (
|
||||
<pre className="text-xs bg-slate-50 p-3 rounded border border-slate-200 overflow-auto max-h-64 font-mono whitespace-pre-wrap break-words">
|
||||
{tryFormatJson(span.output) || "—"}
|
||||
</pre>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cost breakdown (LLM spans only) */}
|
||||
{span.type === "llm" && (span.cost != null || span.tokens != null) && (
|
||||
<div className="border border-slate-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-slate-50 hover:bg-slate-100 text-left text-sm font-medium text-slate-700 transition-colors"
|
||||
onClick={() => setCostOpen(!costOpen)}
|
||||
>
|
||||
<span>Cost breakdown</span>
|
||||
{costOpen ? (
|
||||
<DownOutlined className="text-slate-500" />
|
||||
) : (
|
||||
<RightOutlined className="text-slate-500" />
|
||||
)}
|
||||
</button>
|
||||
{costOpen && (
|
||||
<div className="p-3 bg-white text-sm space-y-1.5">
|
||||
{span.model != null && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Model</span>
|
||||
<span className="font-mono">{span.model}</span>
|
||||
</div>
|
||||
)}
|
||||
{span.tokens != null && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Total tokens</span>
|
||||
<span className="font-mono">
|
||||
{span.tokens.prompt + span.tokens.completion}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{span.cost != null && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Cost</span>
|
||||
<span className="font-mono">${span.cost.toFixed(6)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onSelect}
|
||||
onKeyDown={(e) => {
|
||||
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` }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
className="w-4 h-4 flex items-center justify-center text-slate-400 hover:text-slate-600 flex-shrink-0"
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{hasChildren ? (
|
||||
isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
)
|
||||
) : (
|
||||
<span className="w-3 h-3 block" />
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`w-5 h-5 rounded flex items-center justify-center flex-shrink-0 ${config.bg} border ${config.border}`}
|
||||
>
|
||||
<Icon className={`w-2.5 h-2.5 ${config.text}`} />
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs font-mono flex-shrink-0 truncate max-w-[140px] ${isSelected ? "text-slate-900" : "text-slate-700"}`}
|
||||
>
|
||||
{span.name}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] px-1 py-0.5 rounded border flex-shrink-0 ${config.bg} ${config.text} ${config.border}`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0" />
|
||||
{span.tokens != null && (
|
||||
<div className="flex items-center gap-1 text-[10px] text-slate-400 font-mono flex-shrink-0">
|
||||
<Cpu className="w-2.5 h-2.5" />
|
||||
<span>{span.tokens.prompt + span.tokens.completion} tok</span>
|
||||
</div>
|
||||
)}
|
||||
{span.cost != null && (
|
||||
<span className="text-[10px] text-slate-400 font-mono flex-shrink-0">
|
||||
${span.cost.toFixed(5)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-slate-500 font-mono flex-shrink-0 w-14 text-right">
|
||||
{formatDuration(span.durationMs)}
|
||||
</span>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${span.status === "success" ? "bg-green-500" : span.status === "error" ? "bg-red-500" : "bg-amber-500"}`}
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeView({
|
||||
session,
|
||||
selectedSpanId,
|
||||
onSelectSpan,
|
||||
}: TraceViewProps) {
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => {
|
||||
const ids = new Set<string>();
|
||||
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(
|
||||
<TreeRow
|
||||
key={span.id}
|
||||
span={span}
|
||||
depth={depth}
|
||||
isSelected={selectedSpanId === span.id}
|
||||
isExpanded={isExpanded}
|
||||
hasChildren={hasChildren}
|
||||
onSelect={() => 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 (
|
||||
<div className="flex-1 flex items-center justify-center text-slate-500 text-sm">
|
||||
No trace data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto flex flex-col min-h-0">
|
||||
<div className="flex items-center px-2 py-1.5 border-b border-slate-200 bg-slate-50 sticky top-0 z-10 shrink-0">
|
||||
<div className="w-4 shrink-0" />
|
||||
<div className="w-5 shrink-0 ml-1.5" />
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider ml-1.5">
|
||||
Span
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider w-20 text-right">
|
||||
Tokens
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider w-16 text-right">
|
||||
Cost
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider w-14 text-right">
|
||||
Duration
|
||||
</span>
|
||||
<div className="w-4 shrink-0 ml-1.5" />
|
||||
</div>
|
||||
<div className="shrink-0">{renderSpans(session.spans, 0)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WaterfallView({
|
||||
session,
|
||||
selectedSpanId,
|
||||
onSelectSpan,
|
||||
}: TraceViewProps) {
|
||||
const totalMs = session.totalDurationMs;
|
||||
const flatSpans = useMemo(() => flattenSpans(session.spans), [session.spans]);
|
||||
|
||||
if (!flatSpans.length) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-slate-500 text-sm">
|
||||
No trace data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LABEL_WIDTH = 200;
|
||||
const tickCount = 6;
|
||||
const ticks = Array.from(
|
||||
{ length: tickCount + 1 },
|
||||
(_, i) => Math.round((totalMs / tickCount) * i)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-auto flex flex-col min-h-0">
|
||||
<div className="flex border-b border-slate-200 bg-slate-50 sticky top-0 z-10 shrink-0">
|
||||
<div
|
||||
style={{ width: LABEL_WIDTH }}
|
||||
className="shrink-0 px-3 py-1.5"
|
||||
>
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider">
|
||||
Span
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 relative py-1.5 pr-4">
|
||||
<div className="relative h-4">
|
||||
{ticks.map((tick) => (
|
||||
<div
|
||||
key={tick}
|
||||
className="absolute top-0 flex flex-col items-center"
|
||||
style={{ left: `${(tick / totalMs) * 100}%` }}
|
||||
>
|
||||
<span className="text-[9px] text-slate-400 font-mono -translate-x-1/2">
|
||||
{tick >= 1000 ? `${(tick / 1000).toFixed(1)}s` : `${tick}ms`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{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 (
|
||||
<div
|
||||
key={span.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => 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"}`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: LABEL_WIDTH,
|
||||
paddingLeft: `${12 + depth * 16}px`,
|
||||
}}
|
||||
className="shrink-0 flex items-center gap-1.5 py-1.5 pr-3"
|
||||
>
|
||||
<div
|
||||
className={`w-4 h-4 rounded flex items-center justify-center shrink-0 ${config.bg} border ${config.border}`}
|
||||
>
|
||||
<Icon className={`w-2 h-2 ${config.text}`} />
|
||||
</div>
|
||||
<span
|
||||
className={`text-[11px] font-mono truncate ${isSelected ? "text-slate-900" : "text-slate-600"}`}
|
||||
>
|
||||
{span.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 relative py-2 pr-4 min-h-[24px]">
|
||||
{ticks.slice(1, -1).map((tick) => (
|
||||
<div
|
||||
key={tick}
|
||||
className="absolute top-0 bottom-0 w-px bg-slate-200"
|
||||
style={{ left: `${(tick / totalMs) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className={`absolute top-1.5 bottom-1.5 rounded-sm ${config.bar} ${isSelected ? "opacity-90" : "opacity-70"} transition-opacity`}
|
||||
style={{
|
||||
left: `${leftPct}%`,
|
||||
width: `${widthPct}%`,
|
||||
minWidth: "3px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="shrink-0 px-2 py-2 border-b border-slate-200 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-semibold text-slate-900 truncate">
|
||||
{session.rootAgentName}
|
||||
</h3>
|
||||
<span
|
||||
className={`text-[10px] px-2 py-0.5 rounded-full font-medium shrink-0 ${session.status === "success" ? "bg-green-50 text-green-700 border border-green-200" : "bg-red-50 text-red-700 border border-red-200"}`}
|
||||
>
|
||||
{session.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-[11px] text-slate-500 truncate">
|
||||
{session.shortId}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-wrap text-xs text-slate-600">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3 text-slate-400" />
|
||||
{formatDuration(session.totalDurationMs)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Cpu className="w-3 h-3 text-slate-400" />
|
||||
{totalTokens > 0
|
||||
? `${totalTokens} tokens`
|
||||
: `${session.totalSpans} spans`}
|
||||
</span>
|
||||
<span className="font-mono">${session.totalCost.toFixed(4)}</span>
|
||||
<span className="text-slate-400">{session.relativeTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-0 border-b border-slate-200 shrink-0">
|
||||
{(["tree", "waterfall"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`relative px-3 py-2 text-xs font-medium capitalize transition-colors ${activeTab === tab ? "text-blue-600" : "text-slate-500 hover:text-slate-700"}`}
|
||||
>
|
||||
{tab === "tree" ? "Tree View" : "Waterfall"}
|
||||
{activeTab === tab && (
|
||||
<span
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
{activeTab === "tree" ? (
|
||||
<TreeView
|
||||
session={session}
|
||||
selectedSpanId={selectedSpanId}
|
||||
onSelectSpan={onSelectSpan}
|
||||
/>
|
||||
) : (
|
||||
<WaterfallView
|
||||
session={session}
|
||||
selectedSpanId={selectedSpanId}
|
||||
onSelectSpan={onSelectSpan}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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 ? (
|
||||
<div data-testid="log-details-drawer" role="dialog">
|
||||
Log details drawer
|
||||
<button type="button" onClick={onClose}>
|
||||
Close log drawer
|
||||
</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock("./AgentTrace", () => ({
|
||||
AgentTraceDrawer: ({
|
||||
open,
|
||||
onClose,
|
||||
session,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
session: { rootAgentName: string; shortId: string } | null;
|
||||
}) =>
|
||||
open && session ? (
|
||||
<div data-testid="agent-trace-drawer" role="dialog">
|
||||
Agent trace: {session.rootAgentName}
|
||||
<p>Session: {session.shortId}</p>
|
||||
<button type="button" onClick={onClose}>
|
||||
Close trace drawer
|
||||
</button>
|
||||
</div>
|
||||
) : 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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe("LogViewer", () => {
|
||||
it("should render agent-trace demo rows first then guardrail log entries", () => {
|
||||
render(
|
||||
<LogViewer
|
||||
guardrailName="Test Guardrail"
|
||||
logs={mockLogs}
|
||||
logsLoading={false}
|
||||
/>,
|
||||
{ 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(
|
||||
<LogViewer
|
||||
guardrailName="Test Guardrail"
|
||||
logs={mockLogs}
|
||||
logsLoading={false}
|
||||
/>,
|
||||
{ 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<string>(filterAction);
|
||||
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [selectedTraceSession, setSelectedTraceSession] =
|
||||
useState<AgentTraceSession | null>(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({
|
|||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{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."}
|
||||
</p>
|
||||
</div>
|
||||
{logs.length > 0 && (
|
||||
|
|
@ -170,13 +186,64 @@ export function LogViewer({
|
|||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
{!logsLoading && displayLogs.length === 0 && (
|
||||
{!logsLoading && demoTraces.length === 0 && displayLogs.length === 0 && (
|
||||
<div className="py-12 text-center text-sm text-gray-500">
|
||||
No logs to display. Adjust filters or date range.
|
||||
</div>
|
||||
)}
|
||||
{!logsLoading && displayLogs.length > 0 && (
|
||||
{!logsLoading && (demoTraces.length > 0 || displayLogs.length > 0) && (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{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 (
|
||||
<button
|
||||
key={session.id}
|
||||
type="button"
|
||||
onClick={() => handleTraceSessionClick(session)}
|
||||
className="w-full text-left px-4 py-3 hover:bg-gray-50 transition-colors flex items-start gap-3"
|
||||
>
|
||||
<Activity className="w-4 h-4 mt-0.5 flex-shrink-0 text-indigo-500" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded border bg-indigo-50 text-indigo-600 border-indigo-200">
|
||||
Trace
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{session.relativeTime}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">·</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{session.totalSpans} spans
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">·</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{session.totalDurationMs}ms
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">·</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
${session.totalCost.toFixed(4)}
|
||||
</span>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${statusDotColor}`}
|
||||
title={session.status}
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-800 truncate font-medium">
|
||||
{session.rootAgentName}
|
||||
</p>
|
||||
</div>
|
||||
<DownOutlined className="w-4 h-4 text-gray-400 flex-shrink-0 mt-1" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{displayLogs.map((log) => {
|
||||
const config = actionConfig[log.action];
|
||||
const ActionIcon = config.icon;
|
||||
|
|
@ -222,6 +289,11 @@ export function LogViewer({
|
|||
allLogs={selectedLog ? [selectedLog] : []}
|
||||
startTime={startTime}
|
||||
/>
|
||||
<AgentTraceDrawer
|
||||
open={selectedTraceSession != null}
|
||||
onClose={() => setSelectedTraceSession(null)}
|
||||
session={selectedTraceSession}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -136,12 +136,6 @@ const menuGroups: MenuGroup[] = [
|
|||
label: "Vector Stores",
|
||||
icon: <DatabaseOutlined />,
|
||||
},
|
||||
{
|
||||
key: "tool-policies",
|
||||
page: "tool-policies",
|
||||
label: "Tool Policies",
|
||||
icon: <SafetyOutlined />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -169,6 +163,12 @@ const menuGroups: MenuGroup[] = [
|
|||
icon: <SafetyOutlined />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
},
|
||||
{
|
||||
key: "tool-policies",
|
||||
page: "tool-policies",
|
||||
label: "Tool Monitor",
|
||||
icon: <SafetyOutlined />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue