Skip to content

fix(workflow-operator): degrade instead of raising on malformed chat responses - #8617

Open
PG1204 wants to merge 5 commits into
apache:mainfrom
PG1204:fix/hf-chat-response-degrade
Open

PG1204 wants to merge 5 commits into
apache:mainfrom
PG1204:fix/hf-chat-response-degrade

Conversation

@PG1204

@PG1204 PG1204 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

The text-generation and QA/ranking codegens read a chat provider's reply by indexing straight into it:

return body["choices"][0]["message"]["content"]

That assumes the response is a dict, that choices is non-empty, and that the first choice carries message.content. When a provider returns a 200 with anything else, the indexing raises, and since the parse step runs once per row, a single malformed response aborts the entire run. TextGenCodegen was the most exposed: it had no isinstance check either, so even a plain list body raised.

All four extractions (one in TextGenCodegen, three in QaRankingCodegen) now guard on a truthy choices and use .get chaining, falling back to the raw JSON body. That is the same idiom ImageTaskCodegen adopted in #7920, and it matches how the native hf-inference shapes beside it already degrade via body.get("answer", json.dumps(body)). A bad response now lands in the result column as raw JSON instead of killing the run.

Any related issues?

Closes #8486

How was this PR tested?

145 tests pass in the Hugging Face suites, PythonCodeRawInvalidTextSpec py-compiles the generated Python for all 117 operators, and scalafmtCheck and scalafix --check are clean. Added a degradation test to TextGenCodegenSpec and QaRankingCodegenSpec, and updated the assertions in HuggingFaceInferenceOpDescSpec that pinned the old form.

The emitted Python was also run directly against the failing shapes, comparing this branch with main across 33 task/response combinations: 16 of them raised before (IndexError on an empty choices list, KeyError on a missing message/content, TypeError on a list body) and none raise now. Well-formed chat responses and native hf-inference responses parse identically to before.

Was this PR authored or co-authored using generative AI tooling?

Yes, this PR was co-authored with Claude in compliance with ASF policy.

@Yicong-Huang Yicong-Huang added the release/v1.3 back porting to release/v1.3 label Sep 21, 2026
@github-actions
github-actions Bot requested a review from mengw15 September 21, 2026 00:10
@github-actions

Copy link
Copy Markdown
Contributor

Backport auto-label report

This fix: PR was checked against each actively-supported release branch. A release/* label nominates a backport target; the branch's release manager approving this PR is what sends the fix there. The required Backport Approvals check stays red until every label below is approved, so each manager either approves or removes their own label — which is why the labels left on a merged PR are exactly the branches it reached.

Release branch Analysis
release/v1.3 Change detected on this branch — label added; this fix is queued to backport here. @mengw15 decides: approving sends the fix here, removing this label declines it. The merge waits on one or the other. Review requested.
⚠️ release/v1.2 Not labeled automatically — none of the files this PR modifies exist on this branch (common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/QaRankingCodegen.scala, common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/TextGenCodegen.scala, common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala, common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/QaRankingCodegenSpec.scala, common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/TextGenCodegenSpec.scala). The fix may target code that isn't on this release, or the files were moved/renamed after the branch was cut. Please check and add release/v1.2 by hand if this fix should be backported here — its release manager then has to approve before this PR can merge.

Auto-label run.

@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @EmilySun621
    You can notify them by mentioning @EmilySun621 in a comment.

@PG1204

PG1204 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

/request-review @Ma77Ball

@codecov-commenter

codecov-commenter commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.66%. Comparing base (6defa2c) to head (a685efb).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8617      +/-   ##
============================================
- Coverage     92.75%   92.66%   -0.09%     
- Complexity     4923     4977      +54     
============================================
  Files          1244     1244              
  Lines         52525    52997     +472     
  Branches       6473     6582     +109     
============================================
+ Hits          48718    49110     +392     
- Misses         2203     2261      +58     
- Partials       1604     1626      +22     
Flag Coverage Δ *Carryforward flag
access-control-service 79.63% <ø> (+2.25%) ⬆️
agent-service 99.16% <ø> (ø) Carriedforward from 6defa2c
amber 88.42% <ø> (ø) Carriedforward from 6defa2c
computing-unit-managing-service 57.90% <ø> (+2.66%) ⬆️
config-service 90.51% <ø> (+3.13%) ⬆️
file-service 81.53% <ø> (ø)
frontend 96.57% <ø> (ø) Carriedforward from 6defa2c
notebook-migration-service 85.13% <ø> (+1.40%) ⬆️
pyamber 98.48% <ø> (ø) Carriedforward from 6defa2c
workflow-compiling-service 73.93% <ø> (-0.17%) ⬇️

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

🟢 2 better · 🔴 5 worse · ⚪ 8 noise (<±5%) · 0 without baseline

Compared against main 6defa2c benchmarked on this same runner, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.

Dashboard · Run

config throughput MB/s latency max Δ latest / 7d
🔴 bs=10 sw=10 sl=64 385 0.235 24,950/32,822/32,822 us 🔴 +16.3% / 🔴 +110.8%
🟢 bs=100 sw=10 sl=64 824 0.503 122,045/133,393/133,393 us 🟢 -13.1% / 🔴 +23.2%
bs=1000 sw=10 sl=64 929 0.567 1,081,529/1,125,642/1,125,642 us ⚪ within ±5% / 🔴 -9.2%
Baseline details

Latest main 6defa2c from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 385 tuples/sec 444 tuples/sec 775.4 tuples/sec -13.3% -50.3%
bs=10 sw=10 sl=64 MB/s 0.235 MB/s 0.271 MB/s 0.473 MB/s -13.3% -50.3%
bs=10 sw=10 sl=64 p50 24,950 us 21,858 us 12,716 us +14.1% +96.2%
bs=10 sw=10 sl=64 p95 32,822 us 28,234 us 15,572 us +16.3% +110.8%
bs=10 sw=10 sl=64 p99 32,822 us 28,234 us 19,527 us +16.3% +68.1%
bs=100 sw=10 sl=64 throughput 824 tuples/sec 811 tuples/sec 992.44 tuples/sec +1.6% -17.0%
bs=100 sw=10 sl=64 MB/s 0.503 MB/s 0.495 MB/s 0.606 MB/s +1.6% -17.0%
bs=100 sw=10 sl=64 p50 122,045 us 119,515 us 102,140 us +2.1% +19.5%
bs=100 sw=10 sl=64 p95 133,393 us 153,453 us 108,282 us -13.1% +23.2%
bs=100 sw=10 sl=64 p99 133,393 us 153,453 us 118,936 us -13.1% +12.2%
bs=1000 sw=10 sl=64 throughput 929 tuples/sec 937 tuples/sec 1,023 tuples/sec -0.9% -9.2%
bs=1000 sw=10 sl=64 MB/s 0.567 MB/s 0.572 MB/s 0.624 MB/s -0.9% -9.2%
bs=1000 sw=10 sl=64 p50 1,081,529 us 1,072,273 us 998,263 us +0.9% +8.3%
bs=1000 sw=10 sl=64 p95 1,125,642 us 1,092,063 us 1,036,365 us +3.1% +8.6%
bs=1000 sw=10 sl=64 p99 1,125,642 us 1,092,063 us 1,064,941 us +3.1% +5.7%
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,519.91,200,128000,385,0.235,24950.23,32822.41,32822.41
1,100,10,64,20,2426.75,2000,1280000,824,0.503,122045.20,133393.01,133393.01
2,1000,10,64,20,21535.67,20000,12800000,929,0.567,1081528.80,1125642.04,1125642.04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Nested malformed values can still produce request errors instead of the promised raw-JSON fallback.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 4 Medium severity

Open (4)
What changed in this PR

Improves Hugging Face code-generated parsers so malformed chat responses fall back to raw JSON.

Changes:

  • Guards chat response extraction in text-generation and QA/ranking tasks.
  • Updates generated-code assertions and regression tests.
File Description
common/​workflow-operator/​src/​test/​scala/​org/​apache/​texera/​amber/​operator/​huggingFace/​HuggingFaceInferenceOpDescSpec.scala Updates generated-parser assertions.
common/​workflow-operator/​src/​test/​scala/​org/​apache/​texera/​amber/​operator/​huggingFace/​codegen/​TextGenCodegenSpec.scala Adds text-generation degradation coverage.
common/​workflow-operator/​src/​test/​scala/​org/​apache/​texera/​amber/​operator/​huggingFace/​codegen/​QaRankingCodegenSpec.scala Adds QA/ranking degradation coverage.
common/​workflow-operator/​src/​main/​scala/​org/​apache/​texera/​amber/​operator/​huggingFace/​codegen/​TextGenCodegen.scala Adds guarded text response parsing.
common/​workflow-operator/​src/​main/​scala/​org/​apache/​texera/​amber/​operator/​huggingFace/​codegen/​QaRankingCodegen.scala Adds guarded QA/ranking response parsing.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +82 to +94
| if body.get("choices"):
| return body["choices"][0].get("message", {}).get("content", json.dumps(body))
| return body.get("answer", json.dumps(body))
| return json.dumps(body)
| elif task == "table-question-answering":
| if isinstance(body, dict):
| if "choices" in body:
| return body["choices"][0]["message"]["content"]
| if body.get("choices"):
| return body["choices"][0].get("message", {}).get("content", json.dumps(body))
| return body.get("answer", json.dumps(body))
| return json.dumps(body)
| elif task in ("zero-shot-classification", "sentence-similarity", "text-ranking"):
| if isinstance(body, dict) and "choices" in body:
| return body["choices"][0]["message"]["content"]
| if isinstance(body, dict) and body.get("choices"):
| return body["choices"][0].get("message", {}).get("content", json.dumps(body))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and there were two more: {"choices": {...}} raises KeyError: 0 and {"choices": [42]} raises AttributeError. Two shapes were also silently wrong rather than raising a null content returned Python None into the cell, and a list-valued content returned the raw list.

Worth flagging what your comment uncovered: _parse_response already catches (KeyError, IndexError, TypeError), so on main these shapes were degrading to raw JSON correctly. Moving to .get chaining changed the exception type to AttributeError, which that handler doesn't catch, so this PR was introducing the escape, not just failing to fix it. Good catch.

Rather than repeat type checks at each site, I added a shared _chat_message_content(body) helper to HuggingFaceCodegenBase that validates every level and returns None when the body isn't a readable chat response. All four extractions here and in TextGenCodegen go through it.

Comment on lines +55 to +57
| if isinstance(body, dict) and body.get("choices"):
| return body["choices"][0].get("message", {}).get("content", json.dumps(body))
| return json.dumps(body)""".stripMargin

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by the same shared helper: TextGenCodegen.parsePython now just calls self._chat_message_content(body) and falls back to json.dumps(body).

One deliberate difference from the suggested patch: it returns content whenever it isn't None, so {"content": 42} would put 42 in the result column and a list-valued content would put a raw list there. The helper requires a string, and joins list-of-parts content ([{"type": "text", "text": ...}]) into text, which is a shape some OpenAI-compatible providers return.

Comment on lines +130 to +132
val out = QaRankingCodegen.parsePython(makeCtx())
out should not include ("""["message"]["content"]""")
out.split("""body\.get\("choices"\)""").length - 1 shouldBe 3

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that these assert that the source text changed, not that behavior did. Added HuggingFaceParseBehaviorSpec, which renders the operator, lifts _parse_response and its helpers out of the generated source, and runs them under a real interpreter against 13 response shapes: valid, empty, missing, null and wrong-typed, asserting the actual return value for every task in this PR. The contract it pins: always returns a string, never raises, never None.

It has teeth: run against this PR's previous commit, 2 of its 4 tests fail on exactly the AttributeError cases you identified.

Comment on lines +82 to +86
val out = TextGenCodegen.parsePython(makeCtx())
out should include("""if isinstance(body, dict) and body.get("choices"):""")
out should include("""body["choices"][0].get("message", {}).get("content", json.dumps(body))""")
out should include("return json.dumps(body)")
out should not include ("""["message"]["content"]""")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix, HuggingFaceParseBehaviorSpec now executes the generated parser rather than inspecting its text, covering text-generation and all five QA/ranking tasks across valid, empty, missing, null and wrong-typed nested shapes, and asserting malformed input returns the serialized body.

It follows the existing PythonCodeRawInvalidTextSpec pattern for locating an interpreter, and cancels rather than fails where none is available. ImageTaskCodegen has the same extractions from #7920 and will be routed through the helper and added to this spec in a small follow-up PR, so that change arrives with its own failing-first test.

@mengw15 mengw15 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

common fix release/v1.3 back porting to release/v1.3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Malformed chat responses abort the run in the text-generation and QA codegens

5 participants