feat(recipe-studio): normalize and slugify run_name, update job naming logic

This commit is contained in:
Shine1i 2026-03-04 20:22:33 +01:00
commit a66b1678e8
6 changed files with 56 additions and 6 deletions

View file

@ -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 {}

View file

@ -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:

View file

@ -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,
},
};
}

View file

@ -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(

View file

@ -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<{

View file

@ -362,7 +362,7 @@ export function DatasetSection() {
}}
className="w-full"
>
<TabsList className="mb-2 w-full">
<TabsList className=" w-full">
<TabsTrigger value="huggingface">Hugging Face</TabsTrigger>
<TabsTrigger value="local">Local</TabsTrigger>
</TabsList>