Expose evaluation agents in the web console

This commit is contained in:
2026-06-30 14:55:46 +08:00
parent b913877929
commit 4d0c26be05
6 changed files with 189 additions and 9 deletions

View File

@@ -155,6 +155,8 @@ Use `GET /api/capabilities` to inspect the grouped full-function readiness
matrix used by the web dashboard and agents. matrix used by the web dashboard and agents.
Use `GET /api/results/curves` to inspect parsed training curves discovered Use `GET /api/results/curves` to inspect parsed training curves discovered
from YOLO, SegModel, MMSeg, visual-tool, and analysis output directories. from YOLO, SegModel, MMSeg, visual-tool, and analysis output directories.
Use `GET /api/agents/evaluate` and `GET /api/agents/validate` to surface the
same evaluation and validation feedback shown in the web dashboard Agent panel.
## Agents ## Agents
@@ -171,3 +173,8 @@ backend/frontend endpoints when the services are running. With live validation
enabled it also runs the lightweight acceptance smoke above. By default it enabled it also runs the lightweight acceptance smoke above. By default it
also runs the deep training acceptance; set `SEG_VALIDATE_DEEP=0` when a quick also runs the deep training acceptance; set `SEG_VALIDATE_DEEP=0` when a quick
non-training validation pass is needed. non-training validation pass is needed.
The web dashboard calls validation in light mode by default:
`/api/agents/validate?run_build=false&run_acceptance=false&run_deep=false`.
Pass `run_acceptance=true` or `run_deep=true` only when you explicitly want the
agent to launch the heavier runtime acceptance checks from the browser/API.

View File

@@ -56,6 +56,8 @@ def evaluate_project() -> dict:
"deep_acceptance_api": "/api/acceptance/deep" in backend_text, "deep_acceptance_api": "/api/acceptance/deep" in backend_text,
"deep_acceptance_ui": "runDeepAcceptance" in frontend_text and "深度训练" in frontend_text, "deep_acceptance_ui": "runDeepAcceptance" in frontend_text and "深度训练" in frontend_text,
"deep_yolo_heatmap_validation": "yolo_tiny_heatmap_generation" in acceptance_text, "deep_yolo_heatmap_validation": "yolo_tiny_heatmap_generation" in acceptance_text,
"agent_api": "/api/agents/evaluate" in backend_text and "/api/agents/validate" in backend_text,
"agent_panel_ui": "runAgentValidation" in frontend_text and "评价建议" in frontend_text and "Validation Agent" in frontend_text,
"coverage_api": "/api/coverage" in backend_text and coverage["task_build_passed"], "coverage_api": "/api/coverage" in backend_text and coverage["task_build_passed"],
"visual_tools": "visual.yolo11_heatmap_v2" in catalog["task_types"] and "visual.fps" in catalog["task_types"], "visual_tools": "visual.yolo11_heatmap_v2" in catalog["task_types"] and "visual.fps" in catalog["task_types"],
"yolo_custom_train": "yolo.train_custom" in catalog["task_types"], "yolo_custom_train": "yolo.train_custom" in catalog["task_types"],

View File

@@ -42,7 +42,7 @@ def _fetch(url: str, timeout: int = 5) -> dict:
return {"url": url, "error": str(exc), "passed": False} return {"url": url, "error": str(exc), "passed": False}
def validate_project(run_build: bool = False) -> dict: def validate_project(run_build: bool = False, run_acceptance: bool | None = None, run_deep: bool | None = None) -> dict:
"""Validate current runtime readiness without launching heavy training.""" """Validate current runtime readiness without launching heavy training."""
checks = [] checks = []
catalog = get_catalog() catalog = get_catalog()
@@ -140,10 +140,12 @@ def validate_project(run_build: bool = False) -> dict:
checks.append({"name": "live_coverage_api", "passed": live_coverage["passed"] and '"task_build_passed":true' in live_coverage.get("body", "").replace(" ", ""), "detail": live_coverage}) checks.append({"name": "live_coverage_api", "passed": live_coverage["passed"] and '"task_build_passed":true' in live_coverage.get("body", "").replace(" ", ""), "detail": live_coverage})
checks.append({"name": "live_training_curves_api", "passed": live_curves["passed"] and live_curves.get("body", "").lstrip().startswith("["), "detail": live_curves}) checks.append({"name": "live_training_curves_api", "passed": live_curves["passed"] and live_curves.get("body", "").lstrip().startswith("["), "detail": live_curves})
checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend}) checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend})
if os.getenv("SEG_VALIDATE_ACCEPTANCE", "1") == "1": acceptance_enabled = run_acceptance if run_acceptance is not None else os.getenv("SEG_VALIDATE_ACCEPTANCE", "1") == "1"
deep_enabled = run_deep if run_deep is not None else os.getenv("SEG_VALIDATE_DEEP", "1") == "1"
if acceptance_enabled:
acceptance = run_live_acceptance(backend_url) acceptance = run_live_acceptance(backend_url)
checks.append({"name": "live_acceptance_smoke", "passed": acceptance["passed"], "detail": acceptance}) checks.append({"name": "live_acceptance_smoke", "passed": acceptance["passed"], "detail": acceptance})
if os.getenv("SEG_VALIDATE_DEEP", "1") == "1": if deep_enabled:
deep_acceptance = run_deep_acceptance() deep_acceptance = run_deep_acceptance()
checks.append({"name": "deep_training_acceptance", "passed": deep_acceptance["passed"], "detail": deep_acceptance}) checks.append({"name": "deep_training_acceptance", "passed": deep_acceptance["passed"], "detail": deep_acceptance})

View File

@@ -269,5 +269,5 @@ def api_agent_evaluate() -> dict:
@app.get("/api/agents/validate") @app.get("/api/agents/validate")
def api_agent_validate(run_build: bool = False) -> dict: def api_agent_validate(run_build: bool = False, run_acceptance: bool = False, run_deep: bool = False) -> dict:
return validate_project(run_build=run_build) return validate_project(run_build=run_build, run_acceptance=run_acceptance, run_deep=run_deep)

View File

@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import { import {
Activity, Activity,
BarChart3, BarChart3,
Bot,
Boxes, Boxes,
ClipboardCheck, ClipboardCheck,
Cpu, Cpu,
@@ -231,6 +232,26 @@ type CapabilityPayload = {
domains: CapabilityDomain[]; domains: CapabilityDomain[];
}; };
type AgentCheck = {
name: string;
passed: boolean;
detail?: unknown;
details?: unknown;
};
type EvaluationAgentPayload = {
agent: string;
score: number;
checks: AgentCheck[];
suggestions: string[];
};
type ValidationAgentPayload = {
agent: string;
passed: boolean;
checks: AgentCheck[];
};
async function api<T>(path: string, init?: RequestInit): Promise<T> { async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, { const res = await fetch(`${API_BASE}${path}`, {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -305,11 +326,12 @@ function useData() {
const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null); const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null);
const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null); const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null);
const [capabilities, setCapabilities] = useState<CapabilityPayload | null>(null); const [capabilities, setCapabilities] = useState<CapabilityPayload | null>(null);
const [agentEvaluation, setAgentEvaluation] = useState<EvaluationAgentPayload | null>(null);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
async function refresh() { async function refresh() {
try { try {
const [catalogNext, gpusNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext] = await Promise.all([ const [catalogNext, gpusNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([
api<Catalog>("/api/catalog"), api<Catalog>("/api/catalog"),
api<GpuPayload>("/api/system/gpus"), api<GpuPayload>("/api/system/gpus"),
api<RuntimeReadinessPayload>("/api/system/readiness"), api<RuntimeReadinessPayload>("/api/system/readiness"),
@@ -320,7 +342,8 @@ function useData() {
api<UploadedDataset[]>("/api/datasets"), api<UploadedDataset[]>("/api/datasets"),
api<CoveragePayload>("/api/coverage"), api<CoveragePayload>("/api/coverage"),
api<AcceptancePayload>("/api/acceptance/latest"), api<AcceptancePayload>("/api/acceptance/latest"),
api<DeepAcceptancePayload>("/api/acceptance/deep/latest") api<DeepAcceptancePayload>("/api/acceptance/deep/latest"),
api<EvaluationAgentPayload>("/api/agents/evaluate")
]); ]);
setCatalog(catalogNext); setCatalog(catalogNext);
setGpus(gpusNext); setGpus(gpusNext);
@@ -345,6 +368,7 @@ function useData() {
setCoverage(coverageNext); setCoverage(coverageNext);
setAcceptance(acceptanceNext); setAcceptance(acceptanceNext);
setDeepAcceptance(deepAcceptanceNext); setDeepAcceptance(deepAcceptanceNext);
setAgentEvaluation(agentEvaluationNext);
setError(""); setError("");
} catch (err) { } catch (err) {
setError(String(err)); setError(String(err));
@@ -357,7 +381,7 @@ function useData() {
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, []); }, []);
return { catalog, gpus, runtimeReadiness, capabilities, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh }; return { catalog, gpus, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
} }
function StatusPill({ status }: { status: string }) { function StatusPill({ status }: { status: string }) {
@@ -380,7 +404,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
} }
function App() { function App() {
const { catalog, gpus, runtimeReadiness, capabilities, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData(); const { catalog, gpus, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
const [taskType, setTaskType] = useState("mock.echo"); const [taskType, setTaskType] = useState("mock.echo");
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
const [selectedJob, setSelectedJob] = useState<Job | null>(null); const [selectedJob, setSelectedJob] = useState<Job | null>(null);
@@ -392,6 +416,8 @@ function App() {
const [selectedCurvePath, setSelectedCurvePath] = useState(""); const [selectedCurvePath, setSelectedCurvePath] = useState("");
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images"); const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null); const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
const [agentBusy, setAgentBusy] = useState(false);
const runningCount = jobs.filter((job) => job.status === "running").length; const runningCount = jobs.filter((job) => job.status === "running").length;
const successCount = jobs.filter((job) => job.status === "success").length; const successCount = jobs.filter((job) => job.status === "success").length;
@@ -491,6 +517,16 @@ function App() {
} }
} }
async function runAgentValidation() {
setAgentBusy(true);
try {
const result = await api<ValidationAgentPayload>("/api/agents/validate?run_build=false&run_acceptance=false&run_deep=false");
setAgentValidation(result);
} finally {
setAgentBusy(false);
}
}
async function createDataset() { async function createDataset() {
setBusy(true); setBusy(true);
try { try {
@@ -575,6 +611,7 @@ function App() {
<a href="#gpu"><Cpu size={18} />GPU</a> <a href="#gpu"><Cpu size={18} />GPU</a>
<a href="#runtime"><ShieldCheck size={18} /></a> <a href="#runtime"><ShieldCheck size={18} /></a>
<a href="#coverage"><ClipboardCheck size={18} /></a> <a href="#coverage"><ClipboardCheck size={18} /></a>
<a href="#agents"><Bot size={18} />Agent</a>
<a href="#weights"><HardDrive size={18} /></a> <a href="#weights"><HardDrive size={18} /></a>
<a href="#results"><BarChart3 size={18} /></a> <a href="#results"><BarChart3 size={18} /></a>
</nav> </nav>
@@ -892,6 +929,45 @@ function App() {
</div> </div>
</section> </section>
<section className="grid two" id="agents">
<div className="panel agentPanel">
<div className="panelHead">
<div>
<p className="eyebrow">Evaluation Agent</p>
<h2></h2>
</div>
<StatusPill status={(agentEvaluation?.score ?? 0) >= 1 ? "success" : "queued"} />
</div>
<div className="agentScore">
<strong>{Math.round((agentEvaluation?.score ?? 0) * 100)}%</strong>
<span>{agentEvaluation?.checks.filter((item) => item.passed).length ?? 0}/{agentEvaluation?.checks.length ?? 0} checks passed</span>
</div>
<div className="suggestionList">
{(agentEvaluation?.suggestions ?? ["等待评价 agent 返回建议。"]).slice(0, 6).map((item, index) => (
<div key={`${index}-${item}`}>{item}</div>
))}
</div>
<AgentCheckList checks={agentEvaluation?.checks ?? []} limit={14} />
</div>
<div className="panel agentPanel">
<div className="panelHead">
<div>
<p className="eyebrow">Validation Agent</p>
<h2></h2>
</div>
<button className="primary" disabled={agentBusy} onClick={runAgentValidation}>
<ClipboardCheck size={17} />
</button>
</div>
<div className="agentScore">
<strong>{agentValidation ? (agentValidation.passed ? "OK" : "Check") : "Ready"}</strong>
<span>{agentValidation ? `${agentValidation.checks.filter((item) => item.passed).length}/${agentValidation.checks.length} checks passed` : "轻量验证不会触发深度训练"}</span>
</div>
<AgentCheckList checks={agentValidation?.checks ?? []} limit={18} />
</div>
</section>
<section className="grid four"> <section className="grid four">
<div className="panel" id="gpu"> <div className="panel" id="gpu">
<div className="panelHead"> <div className="panelHead">
@@ -1047,6 +1123,22 @@ function App() {
); );
} }
function AgentCheckList({ checks, limit }: { checks: AgentCheck[]; limit: number }) {
if (!checks.length) {
return <p className="muted"></p>;
}
return (
<div className="agentCheckList">
{checks.slice(0, limit).map((check) => (
<div key={check.name} className={check.passed ? "ok" : "bad"} title={check.name}>
<span>{check.name}</span>
<small>{check.passed ? "passed" : "needs attention"}</small>
</div>
))}
</div>
);
}
function ResultPreview({ results }: { results: ResultItem[] }) { function ResultPreview({ results }: { results: ResultItem[] }) {
if (!results.length) { if (!results.length) {
return <p className="muted"></p>; return <p className="muted"></p>;

View File

@@ -610,6 +610,82 @@ textarea {
white-space: nowrap; white-space: nowrap;
} }
.agentPanel {
min-height: 360px;
}
.agentScore {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: end;
gap: 12px;
margin-bottom: 12px;
}
.agentScore strong {
font-size: 38px;
line-height: 1;
}
.agentScore span {
min-width: 0;
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.suggestionList,
.agentCheckList {
display: grid;
gap: 8px;
}
.suggestionList {
margin-bottom: 12px;
}
.suggestionList div,
.agentCheckList div {
min-width: 0;
padding: 9px;
border-radius: 6px;
border: 1px solid var(--line);
background: #101310;
}
.suggestionList div {
color: var(--ink);
line-height: 1.45;
}
.agentCheckList {
grid-template-columns: repeat(2, minmax(0, 1fr));
max-height: 310px;
overflow: auto;
}
.agentCheckList div.ok {
border-color: rgba(157, 226, 111, 0.32);
}
.agentCheckList div.bad {
border-color: rgba(240, 113, 103, 0.55);
}
.agentCheckList span,
.agentCheckList small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agentCheckList small {
color: var(--muted);
margin-top: 2px;
}
.datasetCard { .datasetCard {
width: 100%; width: 100%;
display: block; display: block;
@@ -1126,6 +1202,7 @@ meter {
.opGrid, .opGrid,
.sampleStrip, .sampleStrip,
.taskCheckList, .taskCheckList,
.agentCheckList,
.qualityStats, .qualityStats,
.qualityChecks { .qualityChecks {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));