mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
feat: add new agent monitor
This commit is contained in:
parent
e7f17a873f
commit
5dbd3dc7b8
11 changed files with 894 additions and 10 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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[]) ?? []}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
398
ui/litellm-dashboard/src/components/AgentMonitor/AgentDetail.tsx
Normal file
398
ui/litellm-dashboard/src/components/AgentMonitor/AgentDetail.tsx
Normal 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">·</span>
|
||||
<span className="text-xs text-gray-500">{providerInfo.name}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-xs text-gray-300">·</span>
|
||||
<span className="text-xs font-medium text-gray-700">{log.model}</span>
|
||||
{log.call_type && (
|
||||
<>
|
||||
<span className="text-xs text-gray-300">·</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} • 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 — {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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
208
ui/litellm-dashboard/src/components/AgentMonitor/AgentTable.tsx
Normal file
208
ui/litellm-dashboard/src/components/AgentMonitor/AgentTable.tsx
Normal 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 “Killed”</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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue