refactor: enhance recipe validation flows with error collection, seed-specific updates, and improved UX in execution dialogs

This commit is contained in:
Shine1i 2026-02-23 20:34:53 +01:00
commit 59a15cb5bc
6 changed files with 88 additions and 8 deletions

View file

@ -21,7 +21,11 @@ if str(backend_path) not in sys.path:
sys.path.insert(0, str(backend_path))
from core.data_recipe.jobs import get_job_manager
from core.data_recipe.service import validate_recipe
from core.data_recipe.service import (
build_config_builder,
create_data_designer,
validate_recipe,
)
from models.data_recipe import (
JobCreateResponse,
RecipePayload,
@ -198,6 +202,55 @@ def _read_preview_rows_from_local_file(path: Path, preview_size: int) -> list[di
return _serialize_preview_rows(rows)
def _collect_validation_errors(recipe: dict[str, Any]) -> list[ValidateError]:
try:
from data_designer.engine.compiler import (
_add_internal_row_id_column_if_needed,
_get_allowed_references,
_resolve_and_add_seed_columns,
)
from data_designer.engine.validation import (
ViolationLevel,
validate_data_designer_config,
)
except Exception:
return []
try:
builder = build_config_builder(recipe)
designer = create_data_designer(recipe)
resource_provider = designer._create_resource_provider( # type: ignore[attr-defined]
"validate-configuration",
builder,
)
config = builder.build()
_resolve_and_add_seed_columns(config, resource_provider.seed_reader)
_add_internal_row_id_column_if_needed(config)
violations = validate_data_designer_config(
columns=config.columns,
processor_configs=config.processors or [],
allowed_references=_get_allowed_references(config),
)
except Exception:
return []
errors: list[ValidateError] = []
for violation in violations:
if violation.level != ViolationLevel.ERROR:
continue
code = getattr(violation.type, "value", None)
path = violation.column if violation.column else None
message = str(violation.message).strip() or "Validation failed."
errors.append(
ValidateError(
message=message,
path=path,
code=code,
)
)
return errors
@router.post("/seed/inspect", response_model=SeedInspectResponse)
def inspect_seed_dataset(payload: SeedInspectRequest) -> SeedInspectResponse:
dataset_name = payload.dataset_name.strip()
@ -327,9 +380,10 @@ def validate(payload: RecipePayload) -> ValidateResponse:
raise HTTPException(status_code=503, detail=str(exc)) from exc
except Exception as exc:
detail = str(exc).strip() or "Validation failed."
parsed_errors = _collect_validation_errors(recipe)
return ValidateResponse(
valid=False,
errors=[ValidateError(message=detail)],
errors=parsed_errors or [ValidateError(message=detail)],
raw_detail=detail,
)

View file

@ -107,7 +107,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
{
kind: "seed",
type: "seed_hf",
title: "Seed (Hugging Face)",
title: "Hugginface dataset",
description: "Load real rows from HF and use them as generation context.",
icon: Plant01Icon,
dialogKey: "seed",
@ -116,7 +116,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
{
kind: "seed",
type: "seed_local",
title: "Seed (Local File)",
title: "Local file",
description: "Upload CSV/JSON/JSONL and use rows as seed context.",
icon: DocumentCodeIcon,
dialogKey: "seed",
@ -125,7 +125,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
{
kind: "seed",
type: "seed_unstructured",
title: "Seed (Unstructured)",
title: "Unstructured documents",
description: "Upload PDF/DOCX/TXT, chunk to text rows, then seed.",
icon: DocumentAttachmentIcon,
dialogKey: "seed",

View file

@ -338,7 +338,8 @@ function RecipeGraphNodeBase({
const showDataHandles =
data.kind === "llm" ||
data.kind === "expression" ||
data.kind === "sampler";
data.kind === "sampler" ||
data.kind === "seed";
const showSemanticIn = data.kind === "llm" || data.kind === "model_config";
const showSemanticOut = data.kind === "model_config" || data.kind === "model_provider";
const {

View file

@ -75,6 +75,22 @@ type UseRecipeExecutionsResult = {
loadExecutionDatasetPage: (id: string, page: number) => Promise<void>;
};
function formatValidationMessages(input: {
errors: Array<{ message: string; path?: string | null; code?: string | null }>;
}): string[] {
return input.errors.map((item) => {
const path = item.path?.trim();
const code = item.code?.trim();
const prefix = [
code ? code.toUpperCase() : null,
path ? `column ${path}` : null,
]
.filter(Boolean)
.join(" · ");
return prefix ? `${prefix}: ${item.message}` : item.message;
});
}
export function useRecipeExecutions({
recipeId,
currentSignature,
@ -310,7 +326,7 @@ export function useRecipeExecutions({
try {
const validation = await validateRecipe(executionPayload);
if (!validation.valid) {
const errors = validation.errors.map((item) => item.message);
const errors = formatValidationMessages({ errors: validation.errors });
const fallback = validation.raw_detail ?? "Validation failed.";
const nextErrors = errors.length > 0 ? errors : [fallback];
setRunErrors(nextErrors);
@ -343,6 +359,7 @@ export function useRecipeExecutions({
}, [fullRows, runWithValidation]);
const runFromDialog = useCallback(async (): Promise<boolean> => {
setValidateResult(null);
if (runDialogKind === "preview") {
return runPreview();
}
@ -350,6 +367,7 @@ export function useRecipeExecutions({
}, [runDialogKind, runFull, runPreview]);
const validateFromDialog = useCallback(async (): Promise<boolean> => {
setRunErrors([]);
const payload = readPayload();
if (!payload) {
const nextErrors = payloadResult.errors.length > 0
@ -375,7 +393,7 @@ export function useRecipeExecutions({
setValidateLoading(true);
try {
const validation = await validateRecipe(executionPayload);
const errors = validation.errors.map((item) => item.message);
const errors = formatValidationMessages({ errors: validation.errors });
setValidateResult({
valid: validation.valid,
errors,

View file

@ -193,6 +193,7 @@ export function applyRecipeConnection(
}
if (
isLlmConfig(target) &&
source.kind !== "seed" &&
source.kind !== "model_provider" &&
source.kind !== "model_config"
) {
@ -205,6 +206,7 @@ export function applyRecipeConnection(
}
if (
isExpressionConfig(target) &&
source.kind !== "seed" &&
source.kind !== "model_provider" &&
source.kind !== "model_config"
) {

View file

@ -275,6 +275,11 @@ export function importRecipePayload(input: string): ImportResult {
unstructured_chunk_overlap: uiUnstructuredChunkOverlap,
});
if (seedConfig) {
if (nameToId.has(seedConfig.name)) {
errors.push(`Duplicate column name: ${seedConfig.name}.`);
} else {
nameToId.set(seedConfig.name, seedConfig.id);
}
configs.push(seedConfig);
}
}