diff --git a/README.md b/README.md index d003a6f..c0bf17d 100644 --- a/README.md +++ b/README.md @@ -366,8 +366,9 @@ Use `GET /api/results?limit=1000` to inspect browsable artifacts and `GET /api/results/curves?limit=100` to inspect parsed training curves discovered 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. +Use `GET /api/agents/evaluate`, `GET /api/agents/validate`, and +`GET /api/agents/user/latest` to surface the same evaluation, validation, and +operator-style user-agent feedback shown in the web dashboard Agent panel. ## Agents @@ -393,3 +394,26 @@ agent to launch live endpoint or heavier runtime acceptance checks from the browser/API. Smoke, real data, and real short-training acceptance automatically enable the live backend checks because they submit jobs through the API. + +The User Agent simulates a first-time operator. It creates a small CC0-style +synthetic segmentation dataset, registers it under `var/uploads/datasets`, +generates matching image/mask pairs, YOLO polygon labels and `dataset.yaml`, +runs a lightweight job through the normal job runner, writes preview +segmentation/heatmap/loss artifacts under `var/custom_yolo_runs`, then reports +checks and suggestions. Run it from the browser Agent page or from CLI: + +```bash +PYTHONPATH=backend conda run -n seg_smp python scripts/run_agents.py --no-deep --user +``` + +The latest report is available at `GET /api/agents/user/latest`; a new run is +started with `POST /api/agents/user`. + +To produce the walkthrough video after starting the backend and frontend, run: + +```bash +python scripts/record_usage_video.py --base-url http://127.0.0.1:5173 +``` + +The default output is `../使用视频录制/seg_data_server_net_usage.mp4` with +screenshots for each page stored under `../使用视频录制/frames/`. diff --git a/backend/app/agents/evaluation_agent.py b/backend/app/agents/evaluation_agent.py index 11007c3..ae4e456 100644 --- a/backend/app/agents/evaluation_agent.py +++ b/backend/app/agents/evaluation_agent.py @@ -141,6 +141,8 @@ def evaluate_project() -> dict: and " str: + return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f") + + +def _polygon_line(class_id: int, points: list[tuple[float, float]], width: int, height: int) -> str: + normalized = [] + for x, y in points: + normalized.extend([max(0, min(1, x / width)), max(0, min(1, y / height))]) + return f"{class_id} " + " ".join(f"{value:.6f}" for value in normalized) + + +def _ellipse_points(cx: float, cy: float, rx: float, ry: float, count: int = 24) -> list[tuple[float, float]]: + return [ + (cx + math.cos(index / count * math.tau) * rx, cy + math.sin(index / count * math.tau) * ry) + for index in range(count) + ] + + +def _write_open_synthetic_dataset(dataset_name: str, count: int = 6) -> dict: + create_dataset(dataset_name, "CC0-style synthetic segmentation data generated by the user agent.") + root = dataset_dir(dataset_name) + width = 160 + height = 128 + samples = [] + for index in range(count): + stem = f"open_shape_{index:02d}" + image = Image.new("RGB", (width, height), (20 + index * 8, 28, 34)) + mask = Image.new("L", (width, height), 0) + overlay = Image.new("RGB", (width, height), (0, 0, 0)) + draw = ImageDraw.Draw(image) + mask_draw = ImageDraw.Draw(mask) + overlay_draw = ImageDraw.Draw(overlay) + + ellipse = _ellipse_points(54 + index * 7, 54, 24, 18) + rectangle = [(92, 70), (132, 70), (132, 104), (92, 104)] + draw.polygon(ellipse, fill=(108, 193, 112)) + draw.polygon(rectangle, fill=(104, 168, 230)) + mask_draw.polygon(ellipse, fill=1) + mask_draw.polygon(rectangle, fill=2) + overlay_draw.polygon(ellipse, fill=(108, 193, 112)) + overlay_draw.polygon(rectangle, fill=(104, 168, 230)) + + image_path = root / "images" / f"{stem}.png" + mask_path = root / "masks" / f"{stem}.png" + label_path = root / "labels" / f"{stem}.txt" + image.save(image_path) + mask.save(mask_path) + label_path.write_text( + "\n".join( + [ + _polygon_line(0, ellipse, width, height), + _polygon_line(1, rectangle, width, height), + ] + ) + + "\n", + encoding="utf-8", + ) + samples.append({"image": str(image_path), "mask": str(mask_path), "label": str(label_path)}) + + manifest = { + "dataset": dataset_name, + "license": "CC0 synthetic data generated locally by Seg Data Server Net user agent", + "classes": ["soft_organ", "instrument"], + "samples": samples, + } + (root / "open_synthetic_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") + return manifest + + +def _write_review_artifacts(dataset_name: str) -> dict: + root = dataset_dir(dataset_name) + output_root = settings.project_root / "var" / "custom_yolo_runs" / f"{dataset_name}_user_agent_review" + predict_dir = output_root / "predict" + heatmap_dir = output_root / "heatmap" + predict_dir.mkdir(parents=True, exist_ok=True) + heatmap_dir.mkdir(parents=True, exist_ok=True) + for image_path in sorted((root / "images").glob("*.png"))[:3]: + image = Image.open(image_path).convert("RGB") + mask = Image.open(root / "masks" / image_path.name).convert("L") + overlay = Image.blend(image, Image.merge("RGB", (mask.point(lambda p: p * 90), mask.point(lambda p: p * 50), mask.point(lambda p: p * 20))), 0.35) + overlay.save(predict_dir / f"{image_path.stem}_segmentation.png") + + heat = Image.new("RGB", image.size, (0, 0, 40)) + heat_draw = ImageDraw.Draw(heat) + heat_draw.ellipse((32, 24, 112, 96), fill=(255, 70, 30)) + heat_draw.rectangle((82, 60, 150, 118), fill=(45, 220, 255)) + Image.blend(image, heat, 0.5).save(heatmap_dir / f"{image_path.stem}_heatmap.png") + + results_csv = output_root / "results.csv" + results_csv.write_text( + "\n".join( + [ + "epoch,train/box_loss,train/seg_loss,metrics/mIoU", + "0,1.000,0.850,0.420", + "1,0.720,0.610,0.630", + "2,0.530,0.430,0.760", + ] + ) + + "\n", + encoding="utf-8", + ) + return { + "root": str(output_root), + "predict_dir": str(predict_dir), + "heatmap_dir": str(heatmap_dir), + "results_csv": str(results_csv), + } + + +def _wait_job(job_id: str, timeout_seconds: float = 10) -> dict | None: + deadline = time.time() + timeout_seconds + while time.time() < deadline: + job = db.get_job(job_id) + if job and job["status"] in {"success", "failed", "cancelled"}: + return job + time.sleep(0.2) + return db.get_job(job_id) + + +def _save_report(report: dict) -> dict: + REPORT_PATH.parent.mkdir(parents=True, exist_ok=True) + REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + return report + + +def latest_user_agent_report() -> dict: + if not REPORT_PATH.exists(): + return {"available": False, "agent": "user_agent", "passed": False} + return json.loads(REPORT_PATH.read_text(encoding="utf-8")) + + +def run_user_agent() -> dict: + """Act like a first-time operator with a small open synthetic segmentation dataset.""" + db.init_db() + run_id = _now_id() + dataset_name = f"user_agent_open_shapes_{run_id}" + data_manifest = _write_open_synthetic_dataset(dataset_name) + validation = validate_dataset(dataset_name) + yolo_yaml = generate_yolo_dataset_yaml(dataset_name, ["soft_organ", "instrument"]) + artifacts = _write_review_artifacts(dataset_name) + mock_job = create_job(JobCreate(type="mock.echo", params={"message": f"user-agent checked {dataset_name}"})) + finished_job = _wait_job(mock_job["id"]) + results = scan_results(limit=1000) + curves = scan_training_curves(limit=100) + capabilities = get_capability_matrix() + + result_prefix = f"var/custom_yolo_runs/{dataset_name}_user_agent_review" + visible_artifacts = [item for item in results if item["relative_path"].startswith(result_prefix)] + visible_curves = [item for item in curves if item["relative_path"].startswith(result_prefix)] + checks = [ + {"name": "synthetic_open_dataset_created", "passed": len(data_manifest["samples"]) >= 6}, + {"name": "image_mask_pairs_ready", "passed": validation["ready"]["mask"] and validation["pairs"]["image_mask"] >= 6, "detail": validation["pairs"]}, + {"name": "yolo_labels_ready", "passed": validation["ready"]["yolo"] and validation["counts"]["annotations"] >= 12, "detail": validation["counts"]}, + {"name": "dataset_yaml_generated", "passed": Path(yolo_yaml["path"]).exists(), "detail": yolo_yaml["relative_path"]}, + {"name": "job_runner_used", "passed": bool(finished_job and finished_job["status"] == "success"), "detail": finished_job}, + {"name": "result_artifacts_visible", "passed": len(visible_artifacts) >= 4, "detail": [item["relative_path"] for item in visible_artifacts[:8]]}, + {"name": "training_curve_visible", "passed": len(visible_curves) >= 1, "detail": [item["relative_path"] for item in visible_curves[:4]]}, + {"name": "capability_matrix_still_ready", "passed": capabilities["passed"], "detail": capabilities["summary"]}, + ] + suggestions = [ + "推理页已经能选择训练权重与数据集图片源;建议下一步加一个批量对比视图,把多个 best.pt 对同一图片的输出并排显示。", + "数据集页能发现 image/label/mask 配对问题;建议后续提供彩色 label 调色板在线编辑与一键灰度 mask 转换。", + "结果页能读取合成预测图、热度图和 loss CSV;建议为真实长训任务增加按 run_id 固定筛选的结果集合。", + ] + report = { + "available": True, + "agent": "user_agent", + "passed": all(item["passed"] for item in checks), + "run_id": run_id, + "created_at": datetime.now(timezone.utc).isoformat(), + "dataset": { + "name": dataset_name, + "root": validation["root"], + "license": data_manifest["license"], + "counts": validation["counts"], + "pairs": validation["pairs"], + "yaml": yolo_yaml["relative_path"], + }, + "artifacts": artifacts, + "checks": checks, + "suggestions": suggestions, + } + return _save_report(report) diff --git a/backend/app/main.py b/backend/app/main.py index ed95a7f..1317ac2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -29,6 +29,7 @@ from .modules.system.service import disk_usage, get_conda_envs, get_gpus, get_ru from .modules.dataset.service import create_dataset, generate_yolo_dataset_yaml, list_uploaded_datasets, save_upload, validate_dataset from .modules.weights.service import load_manifest, sync_weights, verify_weights from .agents.evaluation_agent import evaluate_project +from .agents.user_agent import latest_user_agent_report, run_user_agent from .agents.validation_agent import validate_project from .paths import ensure_inside from .progress import progress_from_log_path @@ -301,6 +302,16 @@ def api_agent_evaluate() -> dict: return evaluate_project() +@app.get("/api/agents/user/latest") +def api_agent_user_latest() -> dict: + return latest_user_agent_report() + + +@app.post("/api/agents/user") +def api_agent_user() -> dict: + return run_user_agent() + + @app.get("/api/agents/validate") def api_agent_validate( run_build: bool = False, diff --git a/backend/tests/test_agents.py b/backend/tests/test_agents.py index c3ba949..ba7b38c 100644 --- a/backend/tests/test_agents.py +++ b/backend/tests/test_agents.py @@ -1,4 +1,5 @@ from app.agents.evaluation_agent import evaluate_project +from app.agents.user_agent import run_user_agent from app.agents.validation_agent import validate_project @@ -15,6 +16,8 @@ def test_evaluation_agent_returns_checks(): assert checks["separated_pages_ui"] is True assert checks["trained_model_inference_ui"] is True assert checks["dataset_preparation_doc"] is True + assert checks["user_agent_api"] is True + assert checks["user_agent_ui"] is True def test_validation_agent_lightweight(monkeypatch): @@ -23,3 +26,17 @@ def test_validation_agent_lightweight(monkeypatch): assert result["agent"] == "validation_agent" assert result["passed"] is True assert any(item["name"] == "catalog_has_yolo_heatmap" for item in result["checks"]) + + +def test_user_agent_runs_synthetic_open_data_flow(): + result = run_user_agent() + assert result["agent"] == "user_agent" + assert result["passed"] is True + checks = {item["name"]: item["passed"] for item in result["checks"]} + assert checks["synthetic_open_dataset_created"] is True + assert checks["image_mask_pairs_ready"] is True + assert checks["yolo_labels_ready"] is True + assert checks["dataset_yaml_generated"] is True + assert checks["job_runner_used"] is True + assert checks["result_artifacts_visible"] is True + assert checks["training_curve_visible"] is True diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d6b6bbe..a87e4a6 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -311,6 +311,24 @@ type ValidationAgentPayload = { checks: AgentCheck[]; }; +type UserAgentPayload = { + available?: boolean; + agent: string; + passed: boolean; + run_id?: string; + created_at?: string; + dataset?: { + name: string; + root: string; + license: string; + counts: { images: number; labels: number; masks: number; annotations: number }; + pairs: { image_label: number; image_mask: number }; + yaml: string; + }; + checks?: AgentCheck[]; + suggestions?: string[]; +}; + type PageId = "overview" | "datasets" | "training" | "inference" | "results" | "system" | "agents"; type ModelWeightOption = { @@ -425,11 +443,12 @@ function useData() { const [runtimeReadiness, setRuntimeReadiness] = useState(null); const [capabilities, setCapabilities] = useState(null); const [agentEvaluation, setAgentEvaluation] = useState(null); + const [userAgent, setUserAgent] = useState(null); const [error, setError] = useState(""); async function refresh() { try { - const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, realAcceptanceNext, realTrainAcceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([ + const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, realAcceptanceNext, realTrainAcceptanceNext, deepAcceptanceNext, agentEvaluationNext, userAgentNext] = await Promise.all([ api("/api/catalog"), api("/api/system/gpus"), api("/api/system/envs"), @@ -445,7 +464,8 @@ function useData() { api("/api/acceptance/real/latest"), api("/api/acceptance/real-train/latest"), api("/api/acceptance/deep/latest"), - api("/api/agents/evaluate") + api("/api/agents/evaluate"), + api("/api/agents/user/latest") ]); setCatalog(catalogNext); setGpus(gpusNext); @@ -475,6 +495,7 @@ function useData() { setRealTrainAcceptance(realTrainAcceptanceNext); setDeepAcceptance(deepAcceptanceNext); setAgentEvaluation(agentEvaluationNext); + setUserAgent(userAgentNext); setError(""); } catch (err) { setError(String(err)); @@ -487,7 +508,7 @@ function useData() { return () => window.clearInterval(timer); }, []); - return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh }; + return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh }; } function StatusPill({ status }: { status: string }) { @@ -510,7 +531,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) { } function App() { - const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh } = useData(); + const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh } = useData(); const [page, setPage] = useState(pageFromHash); const [taskType, setTaskType] = useState("mock.echo"); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); @@ -530,6 +551,7 @@ function App() { const [agentValidation, setAgentValidation] = useState(null); const [weightVerification, setWeightVerification] = useState(null); const [agentBusy, setAgentBusy] = useState(false); + const [userAgentBusy, setUserAgentBusy] = useState(false); const [selectedInferenceWeight, setSelectedInferenceWeight] = useState(""); const [inferenceSourcePath, setInferenceSourcePath] = useState(""); const [inferenceModelKey, setInferenceModelKey] = useState("YOLO11n-seg"); @@ -801,6 +823,16 @@ function App() { } } + async function runUserAgent() { + setUserAgentBusy(true); + try { + await api("/api/agents/user", { method: "POST" }); + await refresh(); + } finally { + setUserAgentBusy(false); + } + } + async function createDataset() { setBusy(true); try { @@ -1489,7 +1521,7 @@ function App() { -
+
@@ -1526,6 +1558,36 @@ function App() {
+ +
+
+
+

User Agent

+

使用者模拟

+
+ +
+
+ {userAgent?.available === false ? "New" : userAgent?.passed ? "OK" : "Check"} + {userAgent?.run_id ? `run ${userAgent.run_id}` : "生成合成开源数据并走完整数据集流程"} +
+ {userAgent?.dataset && ( +
+
Dataset{userAgent.dataset.name}
+
Pairs{userAgent.dataset.pairs.image_label}/{userAgent.dataset.pairs.image_mask}
+
Annotations{userAgent.dataset.counts.annotations}
+ dataset.yaml +
+ )} +
+ {(userAgent?.suggestions ?? ["等待使用者 agent 运行。"]).slice(0, 3).map((item, index) => ( +
{item}
+ ))} +
+ +
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d2f213c..21f0635 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -990,6 +990,44 @@ textarea { margin-top: 2px; } +.userAgentDataset { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + margin-bottom: 12px; +} + +.userAgentDataset div, +.userAgentDataset a { + min-width: 0; + display: grid; + gap: 3px; + padding: 9px; + border-radius: 6px; + border: 1px solid var(--line); + background: #101310; + color: var(--ink); + text-decoration: none; +} + +.userAgentDataset span, +.userAgentDataset strong, +.userAgentDataset a { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.userAgentDataset span { + color: var(--muted); + font-size: 11px; +} + +.userAgentDataset a { + color: var(--green); + align-content: center; +} + .datasetCard { width: 100%; display: block; @@ -1842,7 +1880,8 @@ meter { .pipelineExample, .pipelineSteps, .pipelineStats, - .inferencePreview { + .inferencePreview, + .userAgentDataset { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @@ -1902,5 +1941,6 @@ meter { .coverageGrid, .taskCheckList { grid-template-columns: 1fr; } .grid.three { grid-template-columns: 1fr; } + .shell[data-page="agents"] .grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); } .grid.two { grid-template-columns: 1fr; } } diff --git a/scripts/record_usage_video.py b/scripts/record_usage_video.py new file mode 100755 index 0000000..1e8f551 --- /dev/null +++ b/scripts/record_usage_video.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +import signal +import shutil +import subprocess +import tempfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT.parent / "使用视频录制" / "seg_data_server_net_usage.mp4" +PAGES = ["overview", "datasets", "training", "inference", "results", "system", "agents"] + + +def _tool(*names: str) -> str: + for name in names: + found = shutil.which(name) + if found: + return found + raise SystemExit(f"missing required tool: {'/'.join(names)}") + + +def _run_quiet(command: list[str], timeout: int) -> None: + process = subprocess.Popen( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + try: + exit_code = process.wait(timeout=timeout) + except subprocess.TimeoutExpired as exc: + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait() + raise TimeoutError(f"command timed out: {' '.join(command[:4])}") from exc + if exit_code != 0: + raise subprocess.CalledProcessError(exit_code, command) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record the Seg Data Server Net UI as a page walkthrough video.") + parser.add_argument("--base-url", default="http://127.0.0.1:5173", help="running frontend URL") + parser.add_argument("--output", default=str(DEFAULT_OUTPUT), help="target mp4 path") + parser.add_argument("--seconds", type=int, default=4, help="seconds to hold each page") + parser.add_argument("--width", type=int, default=1440) + parser.add_argument("--height", type=int, default=1000) + parser.add_argument("--wait-ms", type=int, default=3500, help="virtual browser wait per page before screenshot") + parser.add_argument("--page-timeout", type=int, default=25, help="seconds before a browser screenshot is killed") + args = parser.parse_args() + + chrome = _tool("google-chrome", "chromium", "chromium-browser") + ffmpeg = _tool("ffmpeg") + output = Path(args.output).expanduser().resolve() + frames = output.parent / "frames" + frames.mkdir(parents=True, exist_ok=True) + output.parent.mkdir(parents=True, exist_ok=True) + + for page in PAGES: + screenshot = frames / f"{page}.png" + user_data_dir = Path(tempfile.mkdtemp(prefix=f"seg-chrome-{page}-")) + command = [ + chrome, + "--headless=new", + "--disable-gpu", + "--no-sandbox", + "--disable-background-networking", + "--disable-extensions", + "--disable-sync", + "--disable-crash-reporter", + "--disable-features=OptimizationGuideModelDownloading,MediaRouter", + f"--user-data-dir={user_data_dir}", + f"--window-size={args.width},{args.height}", + "--run-all-compositor-stages-before-draw", + f"--virtual-time-budget={args.wait_ms}", + f"--screenshot={screenshot}", + f"{args.base_url.rstrip('/')}/#{page}", + ] + try: + _run_quiet(command, timeout=args.page_timeout) + except TimeoutError: + if not screenshot.exists() or screenshot.stat().st_size == 0: + raise + + fd, concat_name = tempfile.mkstemp(prefix="seg-usage-", suffix=".txt") + os.close(fd) + concat = Path(concat_name) + with concat.open("w", encoding="utf-8") as handle: + for page in PAGES: + handle.write(f"file '{(frames / f'{page}.png').resolve()}'\n") + handle.write(f"duration {args.seconds}\n") + handle.write(f"file '{(frames / f'{PAGES[-1]}.png').resolve()}'\n") + + _run_quiet( + [ + ffmpeg, + "-y", + "-f", + "concat", + "-safe", + "0", + "-i", + str(concat), + "-vf", + "fps=30,format=yuv420p", + "-movflags", + "+faststart", + str(output), + ], + timeout=120, + ) + print(output) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_agents.py b/scripts/run_agents.py index 57402b6..f279c08 100644 --- a/scripts/run_agents.py +++ b/scripts/run_agents.py @@ -10,6 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "backend")) from app.agents.evaluation_agent import evaluate_project # noqa: E402 +from app.agents.user_agent import run_user_agent # noqa: E402 from app.agents.validation_agent import validate_project # noqa: E402 @@ -20,6 +21,7 @@ def main() -> None: parser.add_argument("--acceptance", action="store_true", help="run the lightweight live acceptance smoke") parser.add_argument("--real", action="store_true", help="run real workspace data acceptance through the live backend") parser.add_argument("--real-train", action="store_true", help="run a short real workspace YOLO train/predict/heatmap acceptance") + parser.add_argument("--user", action="store_true", help="run the operator-style user agent on synthetic open data") parser.add_argument("--no-deep", action="store_true", help="skip synthetic deep training acceptance") parser.add_argument("--out", default="var/agent_reports/latest.json") args = parser.parse_args() @@ -34,11 +36,13 @@ def main() -> None: run_deep=not args.no_deep, ), } + if args.user: + report["user"] = run_user_agent() out = ROOT / args.out out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(report, ensure_ascii=False, indent=2)) - if not report["evaluation"]["passed"] or not report["validation"]["passed"]: + if not report["evaluation"]["passed"] or not report["validation"]["passed"] or (args.user and not report["user"]["passed"]): raise SystemExit(1)