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 {

Reply via email to