Connect a worker
@@ -83,7 +92,7 @@ export function InvestigationsWelcome({
data-state={workerReady ? "active" : "inactive"}
className="grid grid-cols-[28px_minmax(0,1fr)] items-center gap-x-4 gap-y-3 py-5 text-muted-foreground data-[state=active]:text-foreground sm:grid-cols-[28px_minmax(0,1fr)_auto]"
>
-
+
Run an investigation
diff --git a/ui/litellm-dashboard/src/components/lens/setup/LensGettingStarted.tsx b/ui/litellm-dashboard/src/components/lens/setup/LensGettingStarted.tsx
new file mode 100644
index 00000000000..887aea1f20d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/LensGettingStarted.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Check, ShieldCheck } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import type { TraceSummary } from "@/components/view_logs/TraceView/traceTypes";
+import { lensQueries } from "../api/queries";
+import { useSaveLens } from "../api/mutations";
+import { useLensApi } from "../services";
+import { TraceSheet } from "../investigations/TraceSheet";
+import { LensPreviewButton } from "../LensPreviewButton";
+import { useAnalysisKeyInfo } from "./worker/AnalysisKeyDetails";
+import { WorkerDialog } from "./worker/WorkerDialog";
+import { InvestigationSetupDialog } from "./InvestigationSetupDialog";
+import { LensIntroduction } from "./LensIntroduction";
+import type { LensSetupState } from "./useLensSetup";
+import type { Settings } from "../model/types";
+import { initialSetupStep, LensSetupSteps } from "./LensSetupSteps";
+
+export function LensGettingStarted({
+ accessToken,
+ state,
+ readOnly,
+ canInvestigate,
+ canMintTracingKey,
+ onStart,
+ onExit,
+ onCreated,
+ onDemo,
+}: {
+ accessToken: string;
+ state: LensSetupState;
+ readOnly: boolean;
+ canInvestigate: boolean;
+ canMintTracingKey: boolean;
+ onStart: () => void;
+ onExit: () => void;
+ onCreated: (id: string) => void;
+ onDemo?: () => void;
+}) {
+ const setupRef = useRef
(null);
+ const [step, setStep] = useState(() => initialSetupStep(state));
+ const [workerOpen, setWorkerOpen] = useState(false);
+ const [investigationOpen, setInvestigationOpen] = useState(false);
+ const [trace, setTrace] = useState(null);
+ const selectStep = (index: number) => {
+ onStart();
+ setStep(index);
+ setupRef.current?.querySelector(`[aria-controls="lens-setup-step-${index}"]`)?.focus();
+ };
+ const canLeave = state.tracesReady || state.requestsReady || state.hasInvestigations;
+ const start = () => {
+ onStart();
+ setupRef.current?.scrollIntoView({ block: "start" });
+ setupRef.current?.focus({ preventScroll: true });
+ };
+ const connectWorker = () => {
+ selectStep(2);
+ setWorkerOpen(true);
+ };
+ const createInvestigation = () => {
+ selectStep(3);
+ setInvestigationOpen(true);
+ };
+
+ return (
+
+ {onDemo &&
}
+
+
+
+
+
+
+ Get Lens running
+
+
+ We’ll check each connection. Stay here until your first investigation is ready.
+
+
+ {canLeave && (
+
+ {state.tracesReady ? "View traces" : "View investigations"}
+
+ )}
+
+
+ {state.error && (
+
+
Could not check setup. {state.error}
+
+ Retry
+
+
+ )}
+ {(readOnly || !canInvestigate) && (
+
+ A gateway administrator can connect a worker and run investigations.
+
+ )}
+
+
+
+ {workerOpen && (
+
setWorkerOpen(false)}
+ onChanged={state.refresh}
+ onReady={
+ state.ready
+ ? () => {
+ setWorkerOpen(false);
+ createInvestigation();
+ }
+ : undefined
+ }
+ />
+ )}
+ {investigationOpen && (
+ setInvestigationOpen(false)}
+ onCreated={onCreated}
+ />
+ )}
+ {trace && (
+ setTrace(null)}
+ />
+ )}
+
+ );
+}
+
+function FirstInvestigation({
+ accessToken,
+ state,
+ onClose,
+ onCreated,
+}: {
+ accessToken: string;
+ state: LensSetupState;
+ onClose: () => void;
+ onCreated: (id: string) => void;
+}) {
+ const api = useLensApi(accessToken);
+ const saveLens = useSaveLens(accessToken);
+ const models = useQuery({ ...lensQueries.models(api) });
+ const modelDetails = useQuery({ ...lensQueries.modelDetails(api) });
+ const workers = state.workers.filter((worker) => !worker.revoked);
+ const analysisAccess = useAnalysisKeyInfo(
+ accessToken,
+ workers.length === 1 ? workers[0].analysis_key_id ?? undefined : undefined,
+ );
+ const defaultModel = analysisAccess.data?.models.length === 1 ? analysisAccess.data.models[0] : undefined;
+ const save = async (settings: Settings) => {
+ if (!state.ready)
+ throw new Error("Wait for recorded activity and a connected worker before starting an investigation");
+ const saved = await saveLens.mutateAsync({ settings });
+ state.refresh();
+ onCreated(saved.id);
+ };
+
+ return (
+ model.id) ?? []}
+ modelDetails={modelDetails.data?.data ?? []}
+ modelsLoading={models.isLoading}
+ modelsError={models.error?.message}
+ onClose={onClose}
+ onSave={save}
+ />
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.tsx b/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.tsx
new file mode 100644
index 00000000000..36fe67cdcaf
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.tsx
@@ -0,0 +1,71 @@
+import { Aperture, ArrowRight, ArrowUpRight, GitBranch } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { scenarios } from "../demo/scenarios";
+
+export function LensIntroduction({ onStart, onDemo }: { onStart: () => void; onDemo?: () => void }) {
+ const example = scenarios[1];
+ return (
+
+
+ The gateway that helps your agents improve
+
+
+ Lens reviews your agents’ recorded activity and finds behavior worth fixing. Each finding links to the original
+ steps, so you can see what happened and what to change next.
+
+
+ {onDemo && (
+
+ Explore with sample data
+
+ )}
+
+ Set up Lens
+
+
+ Docs
+
+
+
+
+
+
+
+ Agent trace
+
+ Sample · {example.agent}
+
+
+
+
{example.tool}
+ {example.result}
+
+
+
Final answer
+ “{example.answer}”
+
+
+
+
+
+
+ Lens finding
+
+
Performance claim has no supporting benchmark
+
+ The answer claims a 40% improvement, but the retrieved documentation contains no comparative benchmark.
+
+
+ Suggested change: Require a benchmark source for numerical performance
+ claims, or remove the comparison.
+
+
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/LensSetupSteps.tsx b/ui/litellm-dashboard/src/components/lens/setup/LensSetupSteps.tsx
new file mode 100644
index 00000000000..dff5f747541
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/LensSetupSteps.tsx
@@ -0,0 +1,201 @@
+import { ArrowRight, Check, ChevronDown } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { TracingSetupFields } from "@/components/view_logs/TraceView/TracingSetupCard";
+import type { TraceSummary } from "@/components/view_logs/TraceView/traceTypes";
+import type { LensSetupState } from "./useLensSetup";
+
+type StepProps = {
+ accessToken: string;
+ state: LensSetupState;
+ readOnly: boolean;
+ canInvestigate: boolean;
+ canMintTracingKey: boolean;
+ onStep: (step: number) => void;
+ onTrace: (trace: TraceSummary) => void;
+ onConnect: () => void;
+ onCreate: () => void;
+};
+
+export function initialSetupStep(state: LensSetupState) {
+ if (!state.tracingEnabled) return 0;
+ if (!state.tracesReady) return 1;
+ return state.connected ? 3 : 2;
+}
+
+function StorageStep({ state, onStep, ...props }: StepProps) {
+ if (!state.tracingEnabled)
+ return (
+
+ );
+ return (
+
+
+ Trace storage is connected
+
+
onStep(1)}>
+ Continue to your agent
+
+
+ );
+}
+
+function continuationLabel(state: LensSetupState) {
+ if (state.connected) return "Continue to investigation";
+ return state.tracesReady ? "Continue to worker" : "Continue with request logs";
+}
+
+function AgentStep({ state, onConnect, onCreate, ...props }: StepProps) {
+ if (!state.tracingEnabled)
+ return Connect trace storage in step 1 before sending a trace.
;
+ const activityReady = state.tracesReady || state.requestsReady;
+ return (
+ <>
+
+ {
+ state.refresh();
+ props.onTrace(value);
+ }}
+ onCheck={state.refresh}
+ checking={state.checking}
+ readOnly={props.readOnly}
+ canMintTracingKey={props.canMintTracingKey}
+ />
+
+ {state.tracesReady && (
+
+ Your first trace is ready. Continue setup so Lens can investigate your agent’s behavior.
+
+ )}
+ {activityReady && (
+
+
+ {continuationLabel(state)}
+
+
+ {state.requestsReady && !state.tracesReady && (
+
+ Request logs are already available. You can investigate them now and add agent traces later.
+
+ )}
+
+ )}
+ >
+ );
+}
+
+function WorkerStep({ state, onConnect, onCreate, readOnly, canInvestigate }: StepProps) {
+ const activityReady = state.tracesReady || state.requestsReady;
+ return (
+
+
+ {state.connected
+ ? "Worker connected. You’re ready to create an investigation."
+ : "The worker reviews recorded activity using a model on your gateway. You choose its analysis model and spending limit."}
+
+ {!activityReady && (
+
Record activity in step 2 before connecting a worker.
+ )}
+
+ {state.connected ? "Continue to investigation" : "Connect worker"}
+
+
+
+ );
+}
+
+function InvestigationStep({ state, onCreate, readOnly, canInvestigate }: StepProps) {
+ return (
+
+
+ Choose the activity to review and describe how your agent should behave. Lens will show findings with evidence
+ and suggested changes.
+
+ {!state.ready && (
+
+ Recorded activity and a connected worker are required before you can run an investigation.
+
+ )}
+
+ New investigation
+
+
+ );
+}
+
+export function LensSetupSteps({ step, ...props }: StepProps & { step: number }) {
+ const items = [
+ {
+ title: "Enable tracing on the gateway",
+ description: "Connect ClickHouse and restart the gateway.",
+ complete: props.state.tracingEnabled,
+ content: ,
+ },
+ {
+ title: "Send your first trace",
+ description: "Capture your agent’s inputs, outputs, and tool calls.",
+ complete: props.state.tracesReady,
+ content: ,
+ },
+ {
+ title: "Connect a worker",
+ description: "Choose a model and run the worker on your infrastructure.",
+ complete: props.state.connected,
+ content: ,
+ },
+ {
+ title: "Run your first investigation",
+ description: "Describe the expected behavior and review a sample of activity.",
+ complete: props.state.hasInvestigations,
+ content: ,
+ },
+ ];
+ return (
+
+ {items.map((item, index) => (
+
+
+ props.onStep(index)}
+ className="flex w-full items-start gap-3 p-5 text-left hover:bg-muted/30"
+ >
+
+ {item.complete ? : index + 1}
+
+
+ {item.title}
+ {item.description}
+
+
+
+
+
+ {item.content}
+
+
+ ))}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/useLensSetup.ts b/ui/litellm-dashboard/src/components/lens/setup/useLensSetup.ts
new file mode 100644
index 00000000000..3d770b8cadc
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/useLensSetup.ts
@@ -0,0 +1,56 @@
+import { useQuery } from "@tanstack/react-query";
+import { useNow } from "@/hooks/useNow";
+import { isTracingNotEnabled, useTraceAvailability } from "@/components/view_logs/TraceView/useAgentTraces";
+import { lensQueries } from "../api/queries";
+import { workerConnected } from "../model/status";
+import { useLensApi } from "../services";
+
+function traceSetupState(traces: ReturnType, configured: boolean) {
+ const disabled = isTracingNotEnabled(traces.error);
+ return {
+ tracesReady: traces.data === true && !traces.error,
+ tracingEnabled: !disabled && (traces.isSuccess || configured),
+ missingTraces: disabled || traces.data === false,
+ error: disabled ? null : traces.error,
+ };
+}
+
+export function useLensSetup(accessToken: string, enabled: boolean, canInvestigate: boolean, settingUp: boolean) {
+ const api = useLensApi(accessToken);
+ const now = useNow(2000);
+ const traces = useTraceAvailability(accessToken, enabled);
+ const list = useQuery({ ...lensQueries.list(api, false, settingUp), enabled: enabled && canInvestigate });
+ const activity = useQuery(lensQueries.activity(api, enabled && canInvestigate && list.isSuccess, false));
+ const data = list.data ?? { lenses: [], workers: [], tracing_enabled: false };
+ const traceState = traceSetupState(traces, data.tracing_enabled);
+ const requestsReady = activity.data?.requests === true && !activity.error;
+ const connected = data.workers.some((worker) => workerConnected(worker, now));
+ const hasInvestigations = data.lenses.length > 0;
+ const error = traceState.error || list.error || activity.error;
+ const activityReady = traceState.tracesReady || requestsReady;
+ const loadingActivity = list.isSuccess && activity.isPending;
+ const loadingInvestigations = canInvestigate && (list.isPending || loadingActivity);
+ const refresh = () => {
+ void traces.refetch();
+ if (canInvestigate) {
+ void list.refetch();
+ void activity.refetch();
+ }
+ };
+ return {
+ tracingEnabled: traceState.tracingEnabled,
+ tracesReady: traceState.tracesReady,
+ requestsReady,
+ connected,
+ hasInvestigations,
+ missingTraces: traceState.missingTraces,
+ loading: traces.isPending || loadingInvestigations,
+ checking: traces.isFetching || list.isFetching || activity.isFetching,
+ ready: activityReady && connected && !error,
+ workers: data.workers,
+ error: error?.message,
+ refresh,
+ };
+}
+
+export type LensSetupState = ReturnType;
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
index 7c6155446ea..e4ef3591af6 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
@@ -645,17 +645,7 @@ function ConnectAgent({
);
}
-export function TracingSetupCard({
- detail,
- accessToken,
- onOpenTrace,
- connected = false,
- onCheck,
- checking = false,
- readOnly = false,
- canMintTracingKey = false,
- onDemo,
-}: {
+type TracingSetupProps = {
detail: string | null;
accessToken: string;
onOpenTrace: (trace: TraceSummary) => void;
@@ -664,20 +654,47 @@ export function TracingSetupCard({
checking?: boolean;
readOnly?: boolean;
canMintTracingKey?: boolean;
- onDemo?: () => void;
-}) {
+};
+
+export function TracingSetupFields({
+ detail,
+ accessToken,
+ onOpenTrace,
+ connected = false,
+ onCheck,
+ checking = false,
+ readOnly = false,
+ canMintTracingKey = false,
+}: TracingSetupProps) {
const [checked, setChecked] = useState(false);
- const enabled = detail === null;
const check = () => {
setChecked(true);
onCheck?.();
};
+ return detail === null ? (
+
+ ) : (
+
+ );
+}
+
+export function TracingSetupCard({ onDemo, ...props }: TracingSetupProps & { onDemo?: () => void }) {
+ const enabled = props.detail === null;
return (
{onDemo &&
}
-
{setupTitle(enabled, connected)}
+
{setupTitle(enabled, props.connected ?? false)}
{enabled ? (
@@ -700,20 +717,7 @@ export function TracingSetupCard({
? "Send your agent’s runs to LiteLLM to see its inputs, outputs, and tool calls."
: "Tracing needs ClickHouse and a small update to your LiteLLM proxy configuration."}
- {enabled ? (
-
- ) : (
-
- )}
+
);
}