diff --git a/README.md b/README.md index eac803b..131e5d6 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ The task builder also reads `GET /api/system/gpus` and lets an operator choose CPU or one or more GPUs before launch. Selected GPUs are passed to the backend as `gpus`, exported as `CUDA_VISIBLE_DEVICES`, and reflected into YOLO/visual `device` parameters and MMSeg config-generation `gpu_count/gpu_ids`. +The same job launcher reads `GET /api/system/envs` and provides an Auto/manual +conda environment selector. Auto keeps the backend defaults (`seg_smp` for +general SegModel/YOLO/dataset tasks and `seg_mmcv` for MMSeg); manual mode +sends `conda_env` with the job request for custom algorithm environments. The coverage panel calls `GET /api/coverage` and verifies that the user-facing scripts from the existing `Seg/` workspace are mapped to web jobs. MMSeg diff --git a/backend/app/agents/evaluation_agent.py b/backend/app/agents/evaluation_agent.py index 15c12e0..cf3a459 100644 --- a/backend/app/agents/evaluation_agent.py +++ b/backend/app/agents/evaluation_agent.py @@ -68,6 +68,11 @@ def evaluate_project() -> dict: and "gpu_count" in frontend_text and "gpus" in frontend_text and "CUDA_VISIBLE_DEVICES" in jobs_text, + "conda_env_selection_ui": "/api/system/envs" in frontend_text + and "selectedCondaEnv" in frontend_text + and "runtimeSelector" in frontend_text + and "conda_env" in frontend_text + and "request.conda_env" in jobs_text, "live_log_stream_ui": "EventSource" in frontend_text and "eventSourceRef" in frontend_text and "log_size" in frontend_text diff --git a/backend/tests/test_jobs.py b/backend/tests/test_jobs.py index 55fd921..07303e6 100644 --- a/backend/tests/test_jobs.py +++ b/backend/tests/test_jobs.py @@ -22,6 +22,19 @@ def test_build_task_sets_cuda_visible_devices(tmp_path, monkeypatch): assert spec.env["CUDA_VISIBLE_DEVICES"] == "2,0" +def test_build_task_uses_requested_conda_env(tmp_path, monkeypatch): + captured = {} + + def fake_build_module_task(job_type, params, conda_env): + captured["conda_env"] = conda_env + return CommandSpec(["python", "-c", "print('ok')"], tmp_path, "fake task") + + monkeypatch.setattr(jobs, "build_module_task", fake_build_module_task) + jobs._build_task(JobCreate(type="segmodel.train", params={}, conda_env="seg_custom")) + + assert captured["conda_env"] == "seg_custom" + + def test_job_progress_reports_log_size(tmp_path): log_path = tmp_path / "job.log" log_path.write_text("line one\nline two\n", encoding="utf-8") diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d02f56c..5ee4dc8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -167,6 +167,13 @@ type GpuPayload = { }>; }; +type CondaEnvPayload = { + available: boolean; + envs: Array<{ name: string; path: string; active?: boolean }>; + task_default?: string; + mmseg_default?: string; +}; + type RuntimeCheck = { module: string; package?: string; @@ -327,6 +334,7 @@ function formatBytes(value?: number) { function useData() { const [catalog, setCatalog] = useState(null); const [gpus, setGpus] = useState(null); + const [condaEnvs, setCondaEnvs] = useState(null); const [jobs, setJobs] = useState([]); const [results, setResults] = useState([]); const [curves, setCurves] = useState([]); @@ -342,9 +350,10 @@ function useData() { async function refresh() { try { - const [catalogNext, gpusNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([ + const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([ api("/api/catalog"), api("/api/system/gpus"), + api("/api/system/envs"), api("/api/system/readiness"), api("/api/capabilities"), api("/api/jobs"), @@ -358,6 +367,7 @@ function useData() { ]); setCatalog(catalogNext); setGpus(gpusNext); + setCondaEnvs(envsNext); setRuntimeReadiness(readinessNext); setCapabilities(capabilitiesNext); setJobs(jobsNext); @@ -392,7 +402,7 @@ function useData() { return () => window.clearInterval(timer); }, []); - return { catalog, gpus, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh }; + return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh }; } function StatusPill({ status }: { status: string }) { @@ -415,7 +425,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) { } function App() { - const { catalog, gpus, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData(); + const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData(); const [taskType, setTaskType] = useState("mock.echo"); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); const [selectedJob, setSelectedJob] = useState(null); @@ -428,6 +438,7 @@ function App() { const [resultFamilyFilter, setResultFamilyFilter] = useState("all"); const [resultRoleFilter, setResultRoleFilter] = useState("all"); const [selectedGpuIds, setSelectedGpuIds] = useState([]); + const [selectedCondaEnv, setSelectedCondaEnv] = useState("auto"); const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images"); const [uploadFiles, setUploadFiles] = useState(null); const [agentValidation, setAgentValidation] = useState(null); @@ -482,6 +493,7 @@ function App() { }, [curves, results, selectedDataset]); const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight); const availableGpus = gpus?.gpus ?? []; + const condaEnvOptions = useMemo(() => ["auto", ...Array.from(new Set((condaEnvs?.envs ?? []).map((item) => item.name))).sort()], [condaEnvs]); const selectedGpuDevice = selectedGpuIds.length ? selectedGpuIds.map((_, index) => index).join(",") : "cpu"; const resultFamilyOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.family ?? "artifact"))).sort()], [results]); const resultRoleOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.role ?? "artifact"))).sort()], [results]); @@ -522,11 +534,12 @@ function App() { } function jobPayload(type: string, rawParams: Record) { - const payload: { type: string; params: Record; gpus?: number[] } = { + const payload: { type: string; params: Record; gpus?: number[]; conda_env?: string } = { type, params: paramsWithGpuSelection(type, rawParams) }; if (selectedGpuIds.length) payload.gpus = selectedGpuIds; + if (selectedCondaEnv !== "auto") payload.conda_env = selectedCondaEnv; return payload; } @@ -889,6 +902,18 @@ function App() { ))} +
+ +