This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-7585-324278eba8ad915a62c0736936ca368aee59b020 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 8e80187a614fd8b82622c44594c6076eb69b80fe Author: Prateek Ganigi <[email protected]> AuthorDate: Fri Aug 14 08:02:06 2026 +0000 fix(workflow-operator): validate zero-shot-image-classification labels before running (#7585) ### What changes were proposed in this PR? When the `zero-shot-image-classification` task ran without at least 2 candidate labels, the operator raised the error from *inside* the per-row loop, so it crashed mid-run instead of failing cleanly. This moves the check up front, alongside the operator's other config validations, so it fails fast with a clear message before any rows are processed. Labels now come from the **Candidate Labels** field only (matching the text `zero-shot-classification` task); the old fallback that read labels from the prompt column is removed. ### Any related issues? Closes #7199 (the Part B follow-up to #7297, which addressed the same issue). ### How was this PR tested? Unit tests + the generated-Python compile check, plus a quick headless run of the generated operator: with 0 labels it now raises a clear "requires at least 2 Candidate Labels" error before the loop; with 2+ labels it proceeds normally. ### Was this PR authored or co-authored using generative AI tooling? Yes, this PR was co-authored with Claude in compliance with ASF policy. --- .../codegen/HuggingFaceCodegenBase.scala | 7 ++++++ .../huggingFace/codegen/ImageTaskCodegen.scala | 15 ++++--------- .../HuggingFaceInferenceOpDescSpec.scala | 25 +++++++++------------- .../huggingFace/codegen/ImageTaskCodegenSpec.scala | 8 ++++--- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala index e37d4f30e4..043e713363 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala @@ -518,6 +518,13 @@ object HuggingFaceCodegenBase { | "Candidate Labels are required for zero-shot-classification. " | "Provide a comma-separated list of labels." | ) + | if task == "zero-shot-image-classification": + | labels = [l.strip() for l in str(self.CANDIDATE_LABELS).split(",") if l.strip()] + | if len(labels) < 2: + | raise ValueError( + | "zero-shot-image-classification requires at least 2 Candidate Labels. " + | "Provide a comma-separated list of labels." + | ) | if task == "question-answering": | ctx_col = self.CONTEXT_COLUMN | if not (ctx_col and ctx_col in table.columns): diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala index 26abcf3215..673227ab9e 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala @@ -90,17 +90,10 @@ object ImageTaskCodegen extends TaskCodegen { | use_raw_binary_body = True | raw_binary_headers = image_headers | elif task == "zero-shot-image-classification": - | # Prefer the dedicated candidateLabels property; fall back to - | # the prompt column for backward compatibility. - | label_source = (self.CANDIDATE_LABELS or "").strip() if self.CANDIDATE_LABELS else "" - | if not label_source and prompt_value: - | label_source = prompt_value - | labels = [s.strip() for s in label_source.split(",") if s.strip()] - | if len(labels) < 2: - | raise ValueError( - | "zero-shot-image-classification requires at least 2 candidate " - | "labels: provide a comma-separated list in the Candidate Labels field." - | ) + | # Labels come from the Candidate Labels property; the >= 2 + | # check runs pre-loop in HuggingFaceCodegenBase (fail-fast), + | # so no per-row validation is needed here. + | labels = [s.strip() for s in str(self.CANDIDATE_LABELS).split(",") if s.strip()] | payload = { | "inputs": self._image_input_as_base64(current_image_bytes), | "parameters": {"candidate_labels": labels}, diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala index b006ed4b0c..e23a9141fd 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala @@ -411,24 +411,19 @@ class HuggingFaceInferenceOpDescSpec extends AnyFlatSpec with Matchers { } it should - "fail fast at runtime when zero-shot-image-classification has fewer than 2 candidate labels" in { - // Without a dedicated candidateLabels field (lands in PR 5), zero-shot - // reuses prompt_value as a comma- - // separated list. Two failure modes the bare list comprehension hides - // are both caught by the >= 2 check: - // 1. Empty prompt column → labels = [] → HF API rejects - // candidate_labels: [] with an opaque 400. - // 2. Missing prompt column → upstream falls back to "What is shown in - // this image?" (no comma) → labels = ["What is shown in this image?"], - // a single nonsense label that returns a useless 1.0 score. - // Zero-shot classification needs >= 2 candidate labels to be meaningful, - // so the fix raises ValueError before the request goes out and the user - // sees a clear configuration error instead of a generic HTTP failure or - // misleading 100%-confidence garbage. + "validate zero-shot-image-classification candidate labels before the row loop" in { + // #7199 Part B: the >= 2 candidate-labels check is a config validation, so it + // runs in the pre-loop validation block (fail-fast with a clear ValueError), + // consistent with the other config checks — instead of being raised inside the + // per-row payload build, where an uncaught ValueError crashed the operator. + // Labels come from the Candidate Labels property; the old prompt-column + // fallback is dropped. val code = makeDesc(task = "zero-shot-image-classification").generatePythonCode() code should include("if len(labels) < 2:") code should include("raise ValueError(") - code should include("at least 2 candidate") + code should include("requires at least 2 Candidate Labels") + // The per-row prompt-column fallback is gone. + code should not include ("label_source") } it should diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala index b6e6d25aa5..f1806d3b94 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala @@ -88,12 +88,14 @@ class ImageTaskCodegenSpec extends AnyFlatSpec with Matchers { out should include(""""question": prompt_value""") } - it should "validate that zero-shot classification supplies at least two candidate labels" in { + it should "build the candidate-labels payload for zero-shot image classification (validation is pre-loop)" in { val out = ImageTaskCodegen.payloadPython(makeCtx()) out should include("""elif task == "zero-shot-image-classification":""") - out should include("if len(labels) < 2:") - out should include("raise ValueError") out should include("candidate_labels") + // #7199 Part B: the >= 2 labels check moved to the pre-loop validation in + // HuggingFaceCodegenBase; the per-task payload build no longer raises. + out should not include ("raise ValueError") + out should not include ("if len(labels) < 2:") } "ImageTaskCodegen.parsePython" should "extract chat-style content for image-text-to-text" in {
