From 6116d10e8f402e62275ac51d91542f3cb484b8f7 Mon Sep 17 00:00:00 2001 From: wuyang6 Date: Thu, 23 Jul 2026 19:46:15 +0800 Subject: [PATCH] feat: persist single evaluation history --- backend/api/server.py | 21 +++ frontend/app/app/evaluations/page.tsx | 148 ++++++++++++++++++-- src/evaluation_runtime.py | 194 +++++++++++++++++++++++++- tests/test_evaluation_api.py | 39 ++++++ tests/test_evaluation_runtime.py | 9 ++ 5 files changed, 398 insertions(+), 13 deletions(-) diff --git a/backend/api/server.py b/backend/api/server.py index 4cf1f3b..925f451 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -1554,6 +1554,27 @@ def create_app(state: AgentState) -> FastAPI: except EvaluationError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + @app.get('/api/evaluations/analyses') + async def list_evaluation_analyses( + account_id: str, + ) -> list[dict[str, Any]]: + return state.evaluation_runtime.list_single_analyses( + _safe_account_id(account_id) + ) + + @app.get('/api/evaluations/analyses/{analysis_id}') + async def get_evaluation_analysis( + analysis_id: str, + account_id: str, + ) -> dict[str, Any]: + try: + return state.evaluation_runtime.get_single_analysis( + analysis_id, + _safe_account_id(account_id), + ) + except EvaluationError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + @app.get('/api/evaluations/datasets') async def list_evaluation_datasets(account_id: str) -> list[dict[str, Any]]: return state.evaluation_runtime.list_datasets(_safe_account_id(account_id)) diff --git a/frontend/app/app/evaluations/page.tsx b/frontend/app/app/evaluations/page.tsx index a100b11..cbffefc 100644 --- a/frontend/app/app/evaluations/page.tsx +++ b/frontend/app/app/evaluations/page.tsx @@ -11,6 +11,7 @@ import { LoaderCircleIcon, PauseIcon, PlayIcon, + PlusIcon, RefreshCwIcon, RotateCcwIcon, SearchIcon, @@ -122,6 +123,8 @@ type EvaluationCase = { skill_name?: string; skill_version?: string; model?: string; + created_at?: number; + updated_at?: number; }; type Experiment = { @@ -213,6 +216,8 @@ function EvaluationWorkspace() { const [selectedDatasetId, setSelectedDatasetId] = useState(""); const [selectedExperimentId, setSelectedExperimentId] = useState(""); const [experiment, setExperiment] = useState(null); + const [singleAnalyses, setSingleAnalyses] = useState([]); + const [selectedSingleAnalysisId, setSelectedSingleAnalysisId] = useState(""); const [mapping, setMapping] = useState({ fields: {}, label_map: {}, @@ -233,6 +238,7 @@ function EvaluationWorkspace() { const uploadRef = useRef(null); const selectedDatasetIdRef = useRef(""); const selectedExperimentIdRef = useRef(""); + const selectedSingleAnalysisIdRef = useRef(""); useEffect(() => { selectedDatasetIdRef.current = selectedDatasetId; @@ -242,6 +248,10 @@ function EvaluationWorkspace() { selectedExperimentIdRef.current = selectedExperimentId; }, [selectedExperimentId]); + useEffect(() => { + selectedSingleAnalysisIdRef.current = selectedSingleAnalysisId; + }, [selectedSingleAnalysisId]); + const selectedDataset = useMemo( () => datasets.find((item) => item.id === selectedDatasetId) ?? null, [datasets, selectedDatasetId], @@ -264,12 +274,14 @@ function EvaluationWorkspace() { try { const [ metadataResponse, + analysisResponse, datasetResponse, experimentResponse, modelResponse, stateResponse, ] = await Promise.all([ fetch("/api/claw/evaluations/metadata", { cache: "no-store" }), + fetch("/api/claw/evaluations/analyses", { cache: "no-store" }), fetch("/api/claw/evaluations/datasets", { cache: "no-store" }), fetch("/api/claw/evaluations/experiments", { cache: "no-store" }), fetch("/api/claw/models", { cache: "no-store" }), @@ -277,6 +289,8 @@ function EvaluationWorkspace() { ]); const metadataPayload = await readJson(metadataResponse); + const analysisPayload = + await readJson(analysisResponse); const datasetPayload = await readJson(datasetResponse); const experimentPayload = await readJson(experimentResponse); @@ -287,11 +301,21 @@ function EvaluationWorkspace() { ? ((await stateResponse.json()) as ClawState) : {}; setMetadata(metadataPayload); + setSingleAnalyses(analysisPayload); setDatasets(datasetPayload); setExperiments(experimentPayload); setSkillName( (current) => current || metadataPayload.default_skill || "label-master", ); + const nextSingleAnalysis = + analysisPayload.find( + (item) => item.id === selectedSingleAnalysisIdRef.current, + ) ?? analysisPayload[0]; + if (nextSingleAnalysis) { + setSelectedSingleAnalysisId(nextSingleAnalysis.id); + setSingleResult((current) => current ?? nextSingleAnalysis); + setSingleQuery((current) => current || nextSingleAnalysis.query); + } const nextModels = modelPayload.models ?? []; setModels(nextModels); setModel((current) => { @@ -454,7 +478,13 @@ function EvaluationWorkspace() { model: model || undefined, }), }); - setSingleResult(await readJson(response)); + const payload = await readJson(response); + setSingleResult(payload); + setSelectedSingleAnalysisId(payload.id); + setSingleAnalyses((current) => [ + payload, + ...current.filter((item) => item.id !== payload.id), + ]); } catch (reason) { setError(messageFrom(reason)); } finally { @@ -462,6 +492,35 @@ function EvaluationWorkspace() { } } + async function selectSingleAnalysis(analysisId: string) { + setSelectedSingleAnalysisId(analysisId); + setBusy("single-history"); + setError(""); + try { + const response = await fetch( + `/api/claw/evaluations/analyses/${encodeURIComponent(analysisId)}`, + { cache: "no-store" }, + ); + const payload = await readJson(response); + setSingleResult(payload); + setSingleQuery(payload.query); + setSingleAnalyses((current) => + current.map((item) => (item.id === payload.id ? payload : item)), + ); + } catch (reason) { + setError(messageFrom(reason)); + } finally { + setBusy(""); + } + } + + function newSingleAnalysis() { + setSelectedSingleAnalysisId(""); + setSingleResult(null); + setSingleQuery(""); + setError(""); + } + async function act(action: "start" | "pause" | "resume" | "cancel") { if (!experiment) return; setBusy(action); @@ -617,7 +676,12 @@ function EvaluationWorkspace() { ))} - {workspaceMode === "batch" ? ( + {workspaceMode === "single" ? ( + + ) : ( - ) : null} + )} @@ -640,13 +704,60 @@ function EvaluationWorkspace() { ) : null} {workspaceMode === "single" ? ( - +
+ +
+ +
+
) : (