feat: add new agent monitor

This commit is contained in:
Krrish Dholakia 2026-03-14 10:36:08 -07:00
parent e7f17a873f
commit 5dbd3dc7b8
11 changed files with 894 additions and 10 deletions

View file

@ -11,15 +11,13 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import *
from litellm.proxy._types import ProviderBudgetResponse, ProviderBudgetResponseObject
from litellm.proxy._types import (ProviderBudgetResponse,
ProviderBudgetResponseObject)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_utils import (
_is_user_team_admin,
_user_has_admin_view,
)
from litellm.proxy.spend_tracking.spend_tracking_utils import (
get_spend_by_team_and_customer,
)
_is_user_team_admin, _user_has_admin_view)
from litellm.proxy.spend_tracking.spend_tracking_utils import \
get_spend_by_team_and_customer
from litellm.proxy.utils import handle_exception_on_proxy
from litellm.router_strategy.budget_limiter import RouterBudgetLimiting
@ -1696,6 +1694,9 @@ async def ui_view_spend_logs( # noqa: PLR0915
error_message: Optional[str] = fastapi.Query(
default=None, description="Filter logs by error message (partial string match)"
),
agent_id: Optional[str] = fastapi.Query(
default=None, description="Filter logs by agent_id"
),
sort_by: str = fastapi.Query(
default="startTime",
description="Sort logs by field: spend, total_tokens, startTime, or endTime",
@ -1833,6 +1834,9 @@ async def ui_view_spend_logs( # noqa: PLR0915
if end_user is not None:
where_conditions["end_user"] = end_user
if agent_id is not None:
where_conditions["agent_id"] = agent_id
if min_spend is not None or max_spend is not None:
where_conditions["spend"] = {}
if min_spend is not None:
@ -1897,6 +1901,7 @@ async def ui_view_spend_logs( # noqa: PLR0915
("model", "model"),
("model_id", "model_id"),
("end_user", "end_user"),
("agent_id", "agent_id"),
]:
val = where_conditions.get(wc_key)
if val is not None and isinstance(val, str):
@ -2488,9 +2493,7 @@ async def global_spend_logs(
import traceback
from litellm.integrations.prometheus_helpers.prometheus_api import (
get_daily_spend_from_prometheus,
is_prometheus_connected,
)
get_daily_spend_from_prometheus, is_prometheus_connected)
from litellm.proxy.proxy_server import prisma_client
try:

View file

@ -13,6 +13,7 @@ import { fetchTeams } from "@/components/common_components/fetch_teams";
import LoadingScreen from "@/components/common_components/LoadingScreen";
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
import GeneralSettings from "@/components/general_settings";
import AgentMonitorView from "@/components/AgentMonitor/AgentMonitorView";
import GuardrailsMonitorView from "@/components/GuardrailsMonitor/GuardrailsMonitorView";
import GuardrailsPanel from "@/components/guardrails";
import PoliciesPanel from "@/components/policies";
@ -617,6 +618,8 @@ function CreateKeyPageContent() {
<ToolPoliciesView accessToken={accessToken} userRole={userRole} />
) : page == "guardrails-monitor" ? (
<GuardrailsMonitorView accessToken={accessToken} />
) : page == "agent-monitor" ? (
<AgentMonitorView accessToken={accessToken} />
) : page == "new_usage" ? (
<NewUsagePage
teams={(teams as Team[]) ?? []}

View file

@ -0,0 +1,98 @@
import { Card, Title } from "@tremor/react";
import React from "react";
type Severity = "CRITICAL" | "WARNING" | "INFO";
interface Alert {
id: string;
severity: Severity;
timestamp: string;
agent: string;
description: string;
}
const alerts: Alert[] = [
{
id: "1",
severity: "CRITICAL",
timestamp: "2 mins ago",
agent: "order-processor-v3",
description: "Hit max iterations 12 times in last hour",
},
{
id: "2",
severity: "WARNING",
timestamp: "15 mins ago",
agent: "support-bot-v2",
description: "Showing 34% purpose drift from baseline",
},
{
id: "3",
severity: "CRITICAL",
timestamp: "1 hour ago",
agent: "data-analyst",
description: "Abnormal data volume: uploaded 2.3GB in 10 min",
},
{
id: "4",
severity: "WARNING",
timestamp: "2 hours ago",
agent: "code-reviewer",
description: "Response payload exceeded 50MB threshold",
},
{
id: "5",
severity: "INFO",
timestamp: "3 hours ago",
agent: "research-bot-v1",
description: "Kill switch activated by admin@company.com",
},
];
const severityConfig: Record<Severity, { border: string; bg: string; text: string }> = {
CRITICAL: { border: "border-red-500", bg: "bg-red-500", text: "text-white" },
WARNING: { border: "border-amber-500", bg: "bg-amber-500", text: "text-white" },
INFO: { border: "border-blue-500", bg: "bg-blue-500", text: "text-white" },
};
export const ActiveAlerts: React.FC = () => {
const criticalCount = alerts.filter((a) => a.severity === "CRITICAL").length;
return (
<Card className="bg-white border border-gray-200 h-full flex flex-col">
<div className="flex items-center mb-6">
<Title className="text-base font-semibold text-gray-900 mr-3">Active Alerts</Title>
<span className="bg-red-500 text-white text-xs font-bold px-2 py-0.5 rounded-full">
{criticalCount}
</span>
</div>
<div className="flex-1 overflow-y-auto pr-2 -mr-2 space-y-3">
{alerts.map((alert) => {
const config = severityConfig[alert.severity];
return (
<div
key={alert.id}
className={`border-l-4 ${config.border} bg-white border-y border-r border-gray-100 rounded-r-md p-3 shadow-sm`}
>
<div className="flex justify-between items-start mb-1.5">
<span
className={`text-[10px] font-bold px-1.5 py-0.5 rounded uppercase tracking-wider ${config.bg} ${config.text}`}
>
{alert.severity}
</span>
<span className="text-xs text-gray-400">{alert.timestamp}</span>
</div>
<div className="mb-1">
<span className="font-mono text-xs bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded border border-gray-200">
{alert.agent}
</span>
</div>
<p className="text-sm text-gray-700 leading-snug">{alert.description}</p>
</div>
);
})}
</div>
</Card>
);
};

View file

@ -0,0 +1,398 @@
import { useQuery } from "@tanstack/react-query";
import { Card, LineChart, Title, Text } from "@tremor/react";
import { Spin } from "antd";
import {
ArrowLeft,
ShieldCheck,
} from "lucide-react";
import moment from "moment";
import React, { useState } from "react";
import { uiSpendLogsCall } from "@/components/networking";
import { getProviderLogoAndName } from "@/components/provider_info_helpers";
import { LogDetailsDrawer } from "@/components/view_logs/LogDetailsDrawer";
import type { LogEntry as SpendLogEntry } from "@/components/view_logs/columns";
import type { AgentData } from "./AgentTable";
interface AgentDetailProps {
agent: AgentData;
onClose: () => void;
accessToken?: string | null;
}
function generateDriftData(baseScore: number, trend: "up" | "down" | "flat") {
const data: Array<{ date: string; purpose: number; tone: number; hallucination: number }> = [];
let currentPurpose = baseScore;
let currentTone = baseScore * 0.8;
let currentHallucination = baseScore * 0.5;
for (let i = 14; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
data.push({
date: date.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
purpose: Math.max(0, currentPurpose + (Math.random() * 0.05 - 0.025)),
tone: Math.max(0, currentTone + (Math.random() * 0.04 - 0.02)),
hallucination: Math.max(0, currentHallucination + (Math.random() * 0.03 - 0.015)),
});
if (trend === "up") {
currentPurpose += 0.02;
currentTone += 0.015;
currentHallucination += 0.01;
} else if (trend === "down") {
currentPurpose -= 0.01;
currentTone -= 0.01;
currentHallucination -= 0.005;
}
}
return data;
}
function getAgentDetails(agent: AgentData) {
const isCritical = agent.status === "Critical" || agent.status === "Killed";
const isWarning = agent.status === "Warning";
const score = Math.round(agent.driftScore * 100);
const trend: "up" | "down" | "flat" = isCritical ? "up" : isWarning ? "up" : "flat";
return {
score,
driftData: generateDriftData(agent.driftScore, trend),
factors: {
purpose: isCritical ? 45 : isWarning ? 72 : 98,
stability: isCritical ? 30 : isWarning ? 65 : 95,
compliance: isCritical ? 20 : isWarning ? 85 : 100,
resistance: isCritical ? 60 : isWarning ? 90 : 99,
},
};
}
function FactorBar({ label, value }: { label: string; value: number }) {
const color = value < 50 ? "bg-red-500" : value < 80 ? "bg-amber-500" : "bg-emerald-500";
const textColor = value < 50 ? "text-red-500" : value < 80 ? "text-amber-500" : "text-emerald-500";
return (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex justify-between items-center mb-2">
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">{label}</span>
<span className={`text-sm font-bold ${textColor}`}>{value}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className={`h-1.5 rounded-full ${color}`} style={{ width: `${value}%` }} />
</div>
</div>
);
}
function SpendLogRow({ log, onClick }: { log: SpendLogEntry; onClick: () => void }) {
const statusDot =
log.status === "failure"
? "bg-red-500"
: log.status === "success" || !log.status
? "bg-emerald-500"
: "bg-gray-400";
const providerInfo = log.custom_llm_provider
? getProviderLogoAndName(log.custom_llm_provider)
: null;
const durationMs = log.request_duration_ms ?? (
log.startTime && log.endTime
? Date.parse(log.endTime) - Date.parse(log.startTime)
: null
);
return (
<button
type="button"
onClick={onClick}
className="w-full text-left px-4 py-3 hover:bg-gray-50 transition-colors flex items-center gap-3"
>
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${statusDot}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
<span className="text-xs text-gray-400">
{moment(log.startTime).format("MMM D, HH:mm:ss")}
</span>
{providerInfo && (
<>
<span className="text-xs text-gray-300">&middot;</span>
<span className="text-xs text-gray-500">{providerInfo.name}</span>
</>
)}
<span className="text-xs text-gray-300">&middot;</span>
<span className="text-xs font-medium text-gray-700">{log.model}</span>
{log.call_type && (
<>
<span className="text-xs text-gray-300">&middot;</span>
<span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium rounded bg-gray-100 text-gray-600 border border-gray-200">
{log.call_type}
</span>
</>
)}
</div>
<div className="flex items-center gap-3 text-xs text-gray-500">
{log.spend != null && (
<span>${log.spend.toFixed(6)}</span>
)}
{log.total_tokens != null && log.total_tokens > 0 && (
<span>{log.total_tokens} tokens</span>
)}
{durationMs != null && (
<span>{(durationMs / 1000).toFixed(2)}s</span>
)}
{log.status === "failure" && (
<span className="text-red-500 font-medium">Failed</span>
)}
</div>
</div>
<svg className="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
);
}
export const AgentDetail: React.FC<AgentDetailProps> = ({ agent, onClose, accessToken = null }) => {
const details = getAgentDetails(agent);
const [activeTab, setActiveTab] = useState<"overview" | "logs">("overview");
const [selectedLog, setSelectedLog] = useState<SpendLogEntry | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 50;
const startTime = moment().subtract(7, "days").utc().format("YYYY-MM-DD HH:mm:ss");
const endTime = moment().utc().format("YYYY-MM-DD HH:mm:ss");
const { data: logsResponse, isLoading: logsLoading } = useQuery({
queryKey: ["agent-spend-logs", agent.id, currentPage, pageSize, startTime, endTime],
queryFn: async () => {
if (!accessToken) return { data: [], total: 0, page: 1, page_size: pageSize, total_pages: 0 };
return await uiSpendLogsCall({
accessToken,
start_date: startTime,
end_date: endTime,
page: currentPage,
page_size: pageSize,
params: {
agent_id: agent.id,
sort_by: "startTime",
sort_order: "desc",
},
});
},
enabled: !!accessToken && activeTab === "logs",
});
const logs: SpendLogEntry[] = logsResponse?.data ?? [];
const totalLogs = logsResponse?.total ?? 0;
const totalPages = logsResponse?.total_pages ?? 0;
const handleLogClick = (log: SpendLogEntry) => {
setSelectedLog(log);
setDrawerOpen(true);
};
const handleCloseDrawer = () => {
setDrawerOpen(false);
setSelectedLog(null);
};
const avgLatency = agent.status === "Critical" ? "450ms" : agent.status === "Warning" ? "280ms" : "120ms";
const statusStyle = agent.status === "Healthy"
? "bg-emerald-50 text-emerald-700 border-emerald-200"
: agent.status === "Warning"
? "bg-amber-50 text-amber-700 border-amber-200"
: agent.status === "Critical"
? "bg-red-50 text-red-700 border-red-200"
: "bg-gray-50 text-gray-700 border-gray-200";
const dotStyle = agent.status === "Healthy"
? "bg-emerald-500"
: agent.status === "Warning"
? "bg-amber-500"
: agent.status === "Critical"
? "bg-red-500 animate-pulse"
: "bg-gray-400";
return (
<div className="pb-20">
{/* Header */}
<div className="max-w-[1200px] mx-auto pt-8 pb-6">
<button
onClick={onClose}
className="flex items-center text-sm font-medium text-blue-600 hover:text-blue-700 transition-colors mb-6"
>
<ArrowLeft className="h-4 w-4 mr-1.5" />
Back to Overview
</button>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center space-x-3 mb-2">
<ShieldCheck className="h-6 w-6 text-gray-400" />
<h1 className="text-2xl font-bold text-gray-900">{agent.name}</h1>
<div className={`flex items-center px-2.5 py-1 rounded-full text-xs font-medium border ${statusStyle}`}>
<div className={`w-1.5 h-1.5 rounded-full mr-1.5 ${dotStyle}`} />
{agent.status}
</div>
</div>
<p className="text-sm text-gray-500 ml-9">
{agent.type} &bull; Evaluates prompts and responses for behavioral drift and policy violations
</p>
</div>
<div className="flex items-center space-x-3">
<span className="inline-flex items-center px-3 py-1 rounded-md text-xs font-mono font-medium bg-blue-50 text-blue-700 border border-blue-200">
{agent.type.toLowerCase().replace(" ", "_")}
</span>
</div>
</div>
</div>
{/* Tabs */}
<div className="max-w-[1200px] mx-auto border-b border-gray-200">
<div className="flex space-x-8">
{(["overview", "logs"] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`pb-4 text-sm font-medium transition-colors relative capitalize ${activeTab === tab ? "text-gray-900" : "text-gray-500 hover:text-gray-700"}`}
>
{tab}
{activeTab === tab && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600" />}
</button>
))}
</div>
</div>
{/* Tab Content */}
<div className="max-w-[1200px] mx-auto pt-8">
{activeTab === "overview" ? (
<div className="space-y-10">
{/* Stat Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="bg-white border border-gray-200">
<Text className="text-sm font-medium text-gray-600 mb-2">Drift Score</Text>
<Title className={`text-4xl font-bold ${details.score > 40 ? "text-red-600" : details.score > 20 ? "text-amber-500" : "text-gray-900"}`}>
{details.score}
</Title>
<Text className="text-sm text-gray-500 mt-1">out of 100</Text>
</Card>
<Card className="bg-white border border-gray-200">
<Text className="text-sm font-medium text-gray-600 mb-2">Total Requests</Text>
<Title className="text-4xl font-bold text-gray-900">
{totalLogs.toLocaleString()}
</Title>
<Text className="text-sm text-gray-500 mt-1">last 7 days</Text>
</Card>
<Card className="bg-white border border-gray-200">
<Text className="text-sm font-medium text-gray-600 mb-2">Avg. Latency Added</Text>
<Title className="text-4xl font-bold text-gray-900">{avgLatency}</Title>
<Text className="text-sm text-gray-500 mt-1">last 24h</Text>
</Card>
</div>
{/* Drift Chart */}
<div>
<Title className="text-lg font-semibold text-gray-900 mb-6">Behavioral Drift</Title>
<div className="h-[300px] mb-8">
<LineChart
data={details.driftData}
index="date"
categories={["purpose", "tone", "hallucination"]}
colors={["blue", "amber", "red"]}
valueFormatter={(v) => v.toFixed(2)}
yAxisWidth={48}
showLegend={true}
curveType="natural"
connectNulls={true}
/>
</div>
{/* Factor Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<FactorBar label="Purpose Alignment" value={details.factors.purpose} />
<FactorBar label="Behavioral Stability" value={details.factors.stability} />
<FactorBar label="Data Compliance" value={details.factors.compliance} />
<FactorBar label="Attack Resistance" value={details.factors.resistance} />
</div>
</div>
</div>
) : (
<div className="bg-white border border-gray-200 rounded-lg">
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h3 className="text-base font-semibold text-gray-900">
Logs &mdash; {agent.name}
</h3>
<p className="text-xs text-gray-500 mt-0.5">
{logsLoading
? "Loading\u2026"
: logs.length > 0
? `Showing ${logs.length} of ${totalLogs} entries`
: "No logs for this period."}
</p>
</div>
{totalPages > 1 && (
<div className="flex items-center gap-2">
<button
className="px-3 py-1 text-xs font-medium rounded border border-gray-200 text-gray-600 hover:bg-gray-50 disabled:opacity-40"
disabled={currentPage <= 1}
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
Prev
</button>
<span className="text-xs text-gray-500">
Page {currentPage} of {totalPages}
</span>
<button
className="px-3 py-1 text-xs font-medium rounded border border-gray-200 text-gray-600 hover:bg-gray-50 disabled:opacity-40"
disabled={currentPage >= totalPages}
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
>
Next
</button>
</div>
)}
</div>
</div>
{logsLoading && (
<div className="flex items-center justify-center py-12">
<Spin />
</div>
)}
{!logsLoading && logs.length === 0 && (
<div className="py-12 text-center text-sm text-gray-500">
No logs to display. Adjust filters or date range.
</div>
)}
{!logsLoading && logs.length > 0 && (
<div className="divide-y divide-gray-100">
{logs.map((log) => (
<SpendLogRow
key={log.request_id}
log={log}
onClick={() => handleLogClick(log)}
/>
))}
</div>
)}
</div>
)}
</div>
<LogDetailsDrawer
open={drawerOpen}
onClose={handleCloseDrawer}
logEntry={selectedLog}
accessToken={accessToken}
allLogs={logs}
onSelectLog={setSelectedLog}
startTime={startTime}
/>
</div>
);
};

View file

@ -0,0 +1,49 @@
import { BarChart, Card, Title } from "@tremor/react";
import React from "react";
const data = [
{ date: "2026-03-05", healthy: 120, drifting: 10, rogue: 2 },
{ date: "2026-03-06", healthy: 132, drifting: 15, rogue: 1 },
{ date: "2026-03-07", healthy: 145, drifting: 12, rogue: 3 },
{ date: "2026-03-08", healthy: 150, drifting: 8, rogue: 0 },
{ date: "2026-03-09", healthy: 148, drifting: 20, rogue: 5 },
{ date: "2026-03-10", healthy: 160, drifting: 18, rogue: 2 },
{ date: "2026-03-11", healthy: 165, drifting: 14, rogue: 4 },
];
export const AgentHealthChart: React.FC = () => (
<Card className="bg-white border border-gray-200 h-full flex flex-col">
<div className="flex justify-between items-center mb-4">
<Title className="text-base font-semibold text-gray-900">
Agent Outcomes Over Time
</Title>
<div className="flex items-center space-x-4 text-sm">
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2" />
<span className="text-gray-600">healthy</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-amber-500 mr-2" />
<span className="text-gray-600">drifting</span>
</div>
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-red-500 mr-2" />
<span className="text-gray-600">rogue</span>
</div>
</div>
</div>
<div className="flex-1 min-h-[250px]">
<BarChart
data={data}
index="date"
categories={["healthy", "drifting", "rogue"]}
colors={["emerald", "amber", "red"]}
valueFormatter={(v) => v.toLocaleString()}
yAxisWidth={48}
showLegend={false}
stack={true}
/>
</div>
</Card>
);

View file

@ -0,0 +1,68 @@
import { ShieldCheck, Clock, Download, ChevronDown } from "lucide-react";
import React, { useState } from "react";
import { ActiveAlerts } from "./ActiveAlerts";
import { AgentDetail } from "./AgentDetail";
import { AgentHealthChart } from "./AgentHealthChart";
import { AgentTable } from "./AgentTable";
import type { AgentData } from "./AgentTable";
import { StatsCards } from "./StatsCards";
interface AgentMonitorViewProps {
accessToken?: string | null;
}
export default function AgentMonitorView({ accessToken = null }: AgentMonitorViewProps) {
const [selectedAgent, setSelectedAgent] = useState<AgentData | null>(null);
if (selectedAgent) {
return (
<div className="p-6 w-full min-w-0 flex-1">
<AgentDetail agent={selectedAgent} onClose={() => setSelectedAgent(null)} accessToken={accessToken} />
</div>
);
}
return (
<div className="p-6 w-full min-w-0 flex-1">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<div className="bg-blue-50 p-2 rounded-lg border border-blue-100">
<ShieldCheck className="h-6 w-6 text-blue-600" />
</div>
<div>
<h1 className="text-xl font-semibold text-gray-900">Agent Monitor</h1>
<p className="text-sm text-gray-500 mt-0.5">
Monitor agent performance across all requests
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center px-3 py-2 bg-white border border-gray-200 rounded-md shadow-sm text-sm text-gray-700 hover:bg-gray-50 cursor-pointer transition-colors">
<Clock className="h-4 w-4 text-gray-400 mr-2" />
<span>4 Mar, 20:31 - 11 Mar, 20:31</span>
<ChevronDown className="h-4 w-4 text-gray-400 ml-2" />
</div>
<button className="flex items-center px-3 py-2 bg-white border border-gray-200 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
<Download className="h-4 w-4 text-gray-400 mr-2" />
Export Data
</button>
</div>
</div>
<StatsCards />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div className="lg:col-span-2 h-[350px]">
<AgentHealthChart />
</div>
<div className="h-[350px]">
<ActiveAlerts />
</div>
</div>
<AgentTable onSelectAgent={setSelectedAgent} />
</div>
);
}

View file

@ -0,0 +1,208 @@
import {
Card,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Title,
Text,
} from "@tremor/react";
import { AlertTriangle, Settings } from "lucide-react";
import React, { useState } from "react";
import { Modal } from "antd";
export interface AgentData {
id: string;
name: string;
type: string;
driftScore: number;
iterationsAvg: number;
iterationsMax: number;
status: "Healthy" | "Warning" | "Critical" | "Killed";
killSwitchActive: boolean;
}
const mockData: AgentData[] = [
{ id: "1", name: "order-processor-v3", type: "Tool Agent", driftScore: 0.12, iterationsAvg: 4, iterationsMax: 8, status: "Healthy", killSwitchActive: false },
{ id: "2", name: "support-bot-v2", type: "Chat Agent", driftScore: 0.34, iterationsAvg: 6, iterationsMax: 15, status: "Warning", killSwitchActive: false },
{ id: "3", name: "data-analyst", type: "RAG Agent", driftScore: 0.05, iterationsAvg: 12, iterationsMax: 45, status: "Critical", killSwitchActive: false },
{ id: "4", name: "code-reviewer", type: "Code Agent", driftScore: 0.08, iterationsAvg: 3, iterationsMax: 5, status: "Healthy", killSwitchActive: false },
{ id: "5", name: "research-bot-v1", type: "Search Agent", driftScore: 0.67, iterationsAvg: 8, iterationsMax: 22, status: "Killed", killSwitchActive: true },
{ id: "6", name: "invoice-handler", type: "Workflow Agent", driftScore: 0.15, iterationsAvg: 2, iterationsMax: 4, status: "Warning", killSwitchActive: false },
{ id: "7", name: "content-writer", type: "Creative Agent", driftScore: 0.22, iterationsAvg: 5, iterationsMax: 7, status: "Healthy", killSwitchActive: false },
{ id: "8", name: "security-scanner", type: "Tool Agent", driftScore: 0.03, iterationsAvg: 1, iterationsMax: 2, status: "Healthy", killSwitchActive: false },
];
function getDriftColor(score: number): string {
if (score < 0.2) return "text-emerald-600";
if (score < 0.5) return "text-amber-500";
return "text-red-500 font-semibold";
}
function StatusIndicator({ status }: { status: AgentData["status"] }) {
const config = {
Healthy: { dot: "bg-emerald-500", text: "text-gray-700", label: "Healthy", extra: "" },
Warning: { dot: "bg-amber-500", text: "text-gray-700", label: "Warning", extra: "" },
Critical: { dot: "bg-red-500 animate-pulse", text: "text-gray-700 font-medium", label: "Critical", extra: "" },
Killed: { dot: "bg-gray-400", text: "text-gray-500 line-through", label: "Killed", extra: "" },
}[status];
return (
<div className="flex items-center">
<div className={`w-2 h-2 rounded-full ${config.dot} mr-2`} />
<span className={`text-sm ${config.text}`}>{config.label}</span>
</div>
);
}
interface AgentTableProps {
onSelectAgent?: (agent: AgentData) => void;
}
export const AgentTable: React.FC<AgentTableProps> = ({ onSelectAgent }) => {
const [data, setData] = useState<AgentData[]>(mockData);
const [confirmAgent, setConfirmAgent] = useState<AgentData | null>(null);
const executeKillSwitch = (id: string) => {
setData((prev) =>
prev.map((agent) => {
if (agent.id !== id) return agent;
const newActive = !agent.killSwitchActive;
return {
...agent,
killSwitchActive: newActive,
status: newActive
? "Killed"
: agent.driftScore > 0.5
? "Critical"
: agent.driftScore > 0.2
? "Warning"
: "Healthy",
};
}),
);
};
const handleKillSwitchClick = (agent: AgentData) => {
if (agent.killSwitchActive) {
executeKillSwitch(agent.id);
} else {
setConfirmAgent(agent);
}
};
return (
<Card className="bg-white border border-gray-200">
<Modal
title={
<div className="flex items-center space-x-2">
<AlertTriangle className="h-5 w-5 text-red-500" />
<span>Activate Kill Switch</span>
</div>
}
open={confirmAgent !== null}
onCancel={() => setConfirmAgent(null)}
onOk={() => {
if (confirmAgent) executeKillSwitch(confirmAgent.id);
setConfirmAgent(null);
}}
okText="Activate Kill Switch"
okButtonProps={{ danger: true }}
cancelText="Cancel"
>
<div className="py-2 space-y-3">
<p className="text-sm text-gray-700">
You are about to activate the kill switch for{" "}
<span className="font-semibold font-mono">{confirmAgent?.name}</span>.
</p>
<div className="bg-red-50 border border-red-200 rounded-md p-3">
<p className="text-sm text-red-800 font-medium mb-1">This will immediately:</p>
<ul className="text-sm text-red-700 list-disc list-inside space-y-1">
<li>Stop the agent from accepting any incoming requests</li>
<li>Terminate all in-progress agent runs</li>
<li>Mark the agent status as &ldquo;Killed&rdquo;</li>
</ul>
</div>
<p className="text-xs text-gray-500">
You can re-enable the agent later by toggling the kill switch off.
</p>
</div>
</Modal>
<div className="flex justify-between items-center mb-4">
<div>
<Title className="text-base font-semibold text-gray-900">Agent Status</Title>
<Text className="text-sm text-gray-500 mt-0.5">
Click an agent to view details and configuration
</Text>
</div>
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-md transition-colors border border-gray-200">
<Settings className="h-4 w-4" />
</button>
</div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Agent</TableHeaderCell>
<TableHeaderCell>Provider / Type</TableHeaderCell>
<TableHeaderCell>Drift Score</TableHeaderCell>
<TableHeaderCell>Iterations (Avg/Max)</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell className="text-right">Kill Switch</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((agent) => (
<TableRow
key={agent.id}
className="hover:bg-gray-50 cursor-pointer"
onClick={() => onSelectAgent?.(agent)}
>
<TableCell>
<span className={`text-sm font-medium ${agent.status === "Killed" ? "text-gray-400 line-through" : "text-gray-900"}`}>
{agent.name}
</span>
</TableCell>
<TableCell>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium bg-gray-100 text-gray-800 border border-gray-200">
{agent.type}
</span>
</TableCell>
<TableCell>
<span className={`text-sm ${getDriftColor(agent.driftScore)}`}>
{agent.driftScore.toFixed(2)}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-gray-900">
{agent.iterationsAvg} <span className="text-gray-400">/</span> {agent.iterationsMax}
</span>
</TableCell>
<TableCell>
<StatusIndicator status={agent.status} />
</TableCell>
<TableCell className="text-right">
<button
onClick={(e) => {
e.stopPropagation();
handleKillSwitchClick(agent);
}}
className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${agent.killSwitchActive ? "bg-red-500" : "bg-gray-200"}`}
role="switch"
aria-checked={agent.killSwitchActive}
>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${agent.killSwitchActive ? "translate-x-4" : "translate-x-0"}`}
/>
</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
);
};

View file

@ -0,0 +1,48 @@
import { Card, Text, Title } from "@tremor/react";
import { AlertTriangle, CheckCircle2, Power } from "lucide-react";
import React from "react";
interface StatCardProps {
label: string;
value: string | number;
valueColor?: string;
icon?: React.ReactNode;
}
const StatCard: React.FC<StatCardProps> = ({
label,
value,
valueColor = "text-gray-900",
icon,
}) => (
<Card className="bg-white border border-gray-200 p-5">
<div className="flex justify-between items-start mb-2">
<Text className="text-sm font-medium text-gray-500">{label}</Text>
{icon && <div>{icon}</div>}
</div>
<Title className={`text-3xl font-bold ${valueColor}`}>{value}</Title>
</Card>
);
export const StatsCards: React.FC = () => (
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
<StatCard label="Monitored Agents" value={24} />
<StatCard
label="Drift Alerts"
value={3}
icon={<AlertTriangle className="h-4 w-4 text-amber-500" />}
/>
<StatCard
label="Pass Rate"
value="96%"
valueColor="text-emerald-500"
icon={<CheckCircle2 className="h-4 w-4 text-emerald-500" />}
/>
<StatCard label="Avg. Detection Latency" value="12ms" valueColor="text-emerald-500" />
<StatCard
label="Active Kill Switches"
value={2}
icon={<Power className="h-4 w-4 text-gray-400" />}
/>
</div>
);

View file

@ -175,6 +175,13 @@ const menuGroups: MenuGroup[] = [
icon: <SafetyOutlined />,
roles: [...all_admin_roles, ...internalUserRoles],
},
{
key: "agent-monitor",
page: "agent-monitor",
label: "Agent Monitor",
icon: <RobotOutlined />,
roles: [...all_admin_roles, ...internalUserRoles],
},
],
},
{

View file

@ -2547,6 +2547,7 @@ interface UiSpendLogsParams {
key_alias?: string;
error_code?: string;
error_message?: string;
agent_id?: string;
sort_by?: string;
sort_order?: "asc" | "desc";
min_spend?: number;

View file

@ -18,6 +18,7 @@ export const pageDescriptions: Record<string, string> = {
new_usage: "View usage analytics and metrics",
logs: "Access request and response logs",
"guardrails-monitor": "Monitor guardrail performance and view logs",
"agent-monitor": "Monitor agent performance, drift, and behavioral health",
users: "Manage internal user accounts and permissions",
teams: "Create and manage teams for access control",
organizations: "Manage organizations and their members",