refactor: enhance recipe validation flows with error collection, seed-specific updates, and improved UX in execution dialogs
This commit is contained in:
parent
d4655eb8bf
commit
59a15cb5bc
6 changed files with 88 additions and 8 deletions
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue