From a66b1678e8545c07e4cce85547258abdf0da7276 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Wed, 4 Mar 2026 20:22:33 +0100 Subject: [PATCH] feat(recipe-studio): normalize and slugify `run_name`, update job naming logic --- .../backend/core/data_recipe/jobs/worker.py | 31 ++++++++++++++++++- studio/backend/routes/data_recipe/jobs.py | 12 +++++++ .../recipe-studio/executions/run-settings.ts | 4 ++- .../hooks/use-recipe-executions.ts | 11 +++++-- .../recipe-studio/utils/payload/types.ts | 2 ++ .../studio/sections/dataset-section.tsx | 2 +- 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/studio/backend/core/data_recipe/jobs/worker.py b/studio/backend/core/data_recipe/jobs/worker.py index d8f744868c..245484f2b6 100644 --- a/studio/backend/core/data_recipe/jobs/worker.py +++ b/studio/backend/core/data_recipe/jobs/worker.py @@ -1,9 +1,11 @@ from __future__ import annotations import logging +import re import shutil import time import traceback +import unicodedata from pathlib import Path from typing import Any @@ -34,6 +36,27 @@ class _QueueLogHandler(logging.Handler): pass +def _slugify_run_name(value: str) -> str: + normalized = unicodedata.normalize("NFKD", value) + ascii_only = normalized.encode("ascii", "ignore").decode("ascii") + slug = re.sub(r"[^a-zA-Z0-9]+", "-", ascii_only).strip("-").lower() + if not slug: + return "" + return slug[:80].strip("-") + + +def _build_dataset_name(*, run_name: str | None, job_id: str, artifact_root: Path) -> str: + fallback = f"recipe_{job_id}" + slug = _slugify_run_name(run_name or "") + base_name = f"recipe_{slug}" if slug else fallback + candidate = base_name + suffix = 2 + while (artifact_root / candidate).exists(): + candidate = f"{base_name}_{suffix}" + suffix += 1 + return candidate + + def run_job_process( *, event_queue, @@ -53,7 +76,13 @@ def run_job_process( job_id = str(run.get("_job_id") or "").strip() if not job_id: job_id = f"{int(time.time())}" - dataset_name = f"recipe_{job_id}" + run_name_raw = run.get("run_name") + run_name = run_name_raw if isinstance(run_name_raw, str) else None + dataset_name = _build_dataset_name( + run_name=run_name, + job_id=job_id, + artifact_root=_ARTIFACT_ROOT, + ) merge_batches = bool(run.get("merge_batches")) _ARTIFACT_ROOT.mkdir(parents=True, exist_ok=True) run_config_raw = run.get("run_config") or {} diff --git a/studio/backend/routes/data_recipe/jobs.py b/studio/backend/routes/data_recipe/jobs.py index ffbded9474..67a64ec784 100644 --- a/studio/backend/routes/data_recipe/jobs.py +++ b/studio/backend/routes/data_recipe/jobs.py @@ -14,6 +14,17 @@ from models.data_recipe import JobCreateResponse, RecipePayload router = APIRouter() +def _normalize_run_name(value: Any) -> str | None: + if value is None: + return None + if not isinstance(value, str): + raise HTTPException(status_code=400, detail="invalid run_name: must be a string") + trimmed = value.strip() + if not trimmed: + return None + return trimmed[:120] + + @router.post("/jobs", response_class=JSONResponse, response_model=JobCreateResponse) def create_job(payload: RecipePayload): recipe = payload.recipe @@ -27,6 +38,7 @@ def create_job(payload: RecipePayload): if execution_type not in {"preview", "full"}: raise HTTPException(status_code=400, detail="invalid execution_type: must be 'preview' or 'full'") run["execution_type"] = execution_type + run["run_name"] = _normalize_run_name(run.get("run_name")) run_config_raw = run.get("run_config") if run_config_raw is not None: try: diff --git a/studio/frontend/src/features/recipe-studio/executions/run-settings.ts b/studio/frontend/src/features/recipe-studio/executions/run-settings.ts index b19fcb2280..627722b21e 100644 --- a/studio/frontend/src/features/recipe-studio/executions/run-settings.ts +++ b/studio/frontend/src/features/recipe-studio/executions/run-settings.ts @@ -153,13 +153,13 @@ export function buildExecutionPayload(input: { kind: RecipeExecutionKind; rows: number; settings: RecipeRunSettings; + runName?: string | null; }): RecipePayload { const normalizedSettings = normalizeRunSettings(input.settings); const payloadWithParallelism = applyGlobalParallelismOverride( input.payload, normalizedSettings.llmParallelRequests, ); - return { ...payloadWithParallelism, run: { @@ -174,6 +174,8 @@ export function buildExecutionPayload(input: { input.kind === "full" && normalizedSettings.batchEnabled && normalizedSettings.mergeBatches, + // biome-ignore lint/style/useNamingConvention: backend schema + run_name: input.kind === "full" ? (input.runName ?? null) : null, }, }; } diff --git a/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts b/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts index f597177da0..6a06473120 100644 --- a/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts +++ b/studio/frontend/src/features/recipe-studio/hooks/use-recipe-executions.ts @@ -250,7 +250,6 @@ export function useRecipeExecutions({ const { kind, payload, rows, settings, runName } = input; const setLoading = kind === "preview" ? setPreviewLoading : setFullLoading; const label = executionLabel(kind); - const normalizedRunName = kind === "full" ? normalizeRunName(runName) : null; setLoading(true); const baseExecution = createBaseExecutionRecord({ @@ -258,7 +257,7 @@ export function useRecipeExecutions({ kind, rows, currentSignature, - runName: normalizedRunName, + runName, }); upsertAndPersist(baseExecution); @@ -271,6 +270,7 @@ export function useRecipeExecutions({ kind, rows, settings, + runName, }); const createdJob = await createRecipeJob(jobPayload); const executionWithJob = { @@ -330,11 +330,13 @@ export function useRecipeExecutions({ } const normalizedRows = sanitizeExecutionRows(rows, kind); + const normalizedRunName = kind === "full" ? normalizeRunName(runName) : null; const executionPayload = buildExecutionPayload({ payload, kind, rows: normalizedRows, settings: runSettings, + runName: normalizedRunName, }); try { @@ -359,7 +361,7 @@ export function useRecipeExecutions({ payload, rows: normalizedRows, settings: runSettings, - runName, + runName: normalizedRunName, }); }, [readExecutablePayload, runExecution, runSettings, setRunErrors], @@ -403,6 +405,7 @@ export function useRecipeExecutions({ kind: runDialogKind, rows: normalizedRows, settings: runSettings, + runName: runDialogKind === "full" ? normalizeRunName(fullRunName) : null, }); setValidateLoading(true); @@ -427,6 +430,7 @@ export function useRecipeExecutions({ setValidateLoading(false); } }, [ + fullRunName, fullRows, payloadErrorMessage, payloadResult.errors, @@ -434,6 +438,7 @@ export function useRecipeExecutions({ readPayload, runDialogKind, runSettings, + setRunErrors, ]); const openRunDialog = useCallback( diff --git a/studio/frontend/src/features/recipe-studio/utils/payload/types.ts b/studio/frontend/src/features/recipe-studio/utils/payload/types.ts index af048107ce..e8e207a3e7 100644 --- a/studio/frontend/src/features/recipe-studio/utils/payload/types.ts +++ b/studio/frontend/src/features/recipe-studio/utils/payload/types.ts @@ -28,6 +28,8 @@ export type RecipePayload = { artifact_path?: string; // biome-ignore lint/style/useNamingConvention: backend schema merge_batches?: boolean; + // biome-ignore lint/style/useNamingConvention: backend schema + run_name?: string | null; }; ui: { nodes: Array<{ diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index 9fff45cb09..e04ba31fff 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -362,7 +362,7 @@ export function DatasetSection() { }} className="w-full" > - + Hugging Face Local