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-7904-30681cd38406ca273f94714da2d7b118d8100f9f in repository https://gitbox.apache.org/repos/asf/texera.git
commit 797674477846b722cccd926b8928b3514b6a5993 Author: Prateek Ganigi <[email protected]> AuthorDate: Wed Sep 2 18:14:14 2026 +0000 fix(gui): only offer Hugging Face tasks the operator can run (#7904) ### What changes were proposed in this PR? The task dropdown offered six tasks the operator has no codegen for: `text-classification`, `token-classification`, `translation`, `summarization`, `feature-extraction`, and `fill-mask`. Selecting one silently fell back to text-generation and wrote raw JSON into the result column. The dynamic `/api/tasks` fetch could surface further unsupported tags on top of that. Meanwhile four tasks that *are* supported (`image-to-image`, `image-text-to-text`, `text-to-image`, `text-to-video`) were missing from the list. `STATIC_TASK_OPTIONS` is now exactly the 20 tasks with a registered codegen, grouped by the codegen that handles them, and a `SUPPORTED_TASK_TAGS` set derived from it filters the dynamic `/api/tasks` result. If that filter leaves nothing, say HF renames its tags, the static list is used rather than an empty dropdown. A comment ties the list to `HuggingFaceInferenceOpDesc.registeredCodegens` so the two stay in sync. ### Any related issues? Closes #7047 ### How was this PR tested? 95 tests pass in `hugging-face.component.spec.ts`, and Prettier is clean. Five tests were added: the unsupported tasks are absent from the list, the four newly added supported tasks are present, `SUPPORTED_TASK_TAGS` mirrors the static tags, an API response mixing supported and unsupported tasks is filtered down to the supported ones, and an all-unsupported response falls back to the static list. ### Was this PR authored or co-authored using generative AI tooling? Yes, this PR was co-authored with Claude in compliance with ASF policy. --- .../hugging-face/hugging-face.component.spec.ts | 58 ++++++++++++++++++++++ .../hugging-face/hugging-face.component.ts | 44 ++++++++++++---- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts index dd199b8987..da53a0f20e 100644 --- a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts +++ b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts @@ -29,6 +29,7 @@ import { HuggingFaceModelOption, HuggingFaceTaskOption, STATIC_TASK_OPTIONS, + SUPPORTED_TASK_TAGS, invalidateHuggingFaceModelCache, } from "./hugging-face.component"; @@ -145,6 +146,32 @@ describe("HuggingFaceComponent (unit)", () => { const uniqueTags = new Set(tags); expect(uniqueTags.size).toBe(tags.length); }); + + it("should not list tasks that have no operator codegen", () => { + // These tasks were previously offered but have no TaskCodegen, so they + // silently fell back to text-generation and emitted raw JSON. + const unsupported = [ + "text-classification", + "token-classification", + "translation", + "summarization", + "feature-extraction", + "fill-mask", + ]; + const tags = STATIC_TASK_OPTIONS.map(t => t.tag); + unsupported.forEach(tag => expect(tags).not.toContain(tag)); + }); + + it("should list the supported media/image tasks that have a codegen", () => { + const tags = STATIC_TASK_OPTIONS.map(t => t.tag); + ["image-to-image", "image-text-to-text", "text-to-image", "text-to-video"].forEach(tag => + expect(tags).toContain(tag) + ); + }); + + it("SUPPORTED_TASK_TAGS should exactly mirror the static task tags", () => { + expect([...SUPPORTED_TASK_TAGS].sort()).toEqual(STATIC_TASK_OPTIONS.map(t => t.tag).sort()); + }); }); // ── TestBed-based integration tests ── @@ -252,6 +279,37 @@ describe("HuggingFaceComponent (TestBed)", () => { expect(component.tasksLoading).toBe(false); }); + it("should drop unsupported tasks from the dynamic API response", () => { + const { field } = buildFieldWithFormGroup(); + component.field = field; + fixture.detectChanges(); + + // HF returns a mix of supported and unsupported tasks. + http.expectOne(`${API}/huggingface/tasks`).flush([ + { tag: "text-generation", label: "Text Generation" }, + { tag: "summarization", label: "Summarization" }, // no codegen + { tag: "image-classification", label: "Image Classification" }, + { tag: "fill-mask", label: "Fill-Mask" }, // no codegen + ]); + http.expectOne(req => req.url.startsWith(`${API}/huggingface/models`)).flush([]); + + expect(component.taskOptions.map(t => t.tag)).toEqual(["text-generation", "image-classification"]); + }); + + it("should fall back to STATIC_TASK_OPTIONS when the API returns only unsupported tasks", () => { + const { field } = buildFieldWithFormGroup(); + component.field = field; + fixture.detectChanges(); + + http.expectOne(`${API}/huggingface/tasks`).flush([ + { tag: "translation", label: "Translation" }, + { tag: "fill-mask", label: "Fill-Mask" }, + ]); + http.expectOne(req => req.url.startsWith(`${API}/huggingface/models`)).flush([]); + + expect(component.taskOptions).toEqual(STATIC_TASK_OPTIONS); + }); + it("retryTasksLoad should clear error and re-fetch tasks", fakeAsync(() => { initComponent(); diff --git a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts index f634c66a2a..4925656425 100644 --- a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts +++ b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts @@ -44,32 +44,49 @@ export interface HuggingFaceTaskOption { label: string; } -// ── Static fallback task list (used when the dynamic fetch fails) ── +// ── Supported task list ── +// The tasks the operator actually has a codegen for. Anything outside this set +// has no codegen and would silently fall back to text-generation (emitting raw +// JSON), so the UI must not offer it. This doubles as the static fallback list +// shown when the dynamic /api/tasks fetch fails. +// +// Keep in sync with the codegens registered in +// `HuggingFaceInferenceOpDesc.registeredCodegens` (TextGen / ImageTask / +// AudioTask / MediaGen / QaRanking). A newly added codegen surfaces in the UI +// only once its task tag is added here. export const STATIC_TASK_OPTIONS: HuggingFaceTaskOption[] = [ + // text-generation (TextGenCodegen) { tag: "text-generation", label: "Text Generation" }, - { tag: "automatic-speech-recognition", label: "Automatic Speech Recognition" }, - { tag: "audio-classification", label: "Audio Classification" }, - { tag: "text-classification", label: "Text Classification" }, - { tag: "text-to-speech", label: "Text to Speech" }, - { tag: "token-classification", label: "Token Classification" }, + // question-answering family (QaRankingCodegen) { tag: "question-answering", label: "Question Answering" }, { tag: "table-question-answering", label: "Table Question Answering" }, { tag: "zero-shot-classification", label: "Zero-Shot Classification" }, - { tag: "translation", label: "Translation" }, - { tag: "summarization", label: "Summarization" }, - { tag: "feature-extraction", label: "Feature Extraction" }, - { tag: "fill-mask", label: "Fill-Mask" }, { tag: "sentence-similarity", label: "Sentence Similarity" }, { tag: "text-ranking", label: "Text Ranking" }, + // audio (AudioTaskCodegen) + { tag: "automatic-speech-recognition", label: "Automatic Speech Recognition" }, + { tag: "audio-classification", label: "Audio Classification" }, + { tag: "text-to-speech", label: "Text to Speech" }, + // image (ImageTaskCodegen) { tag: "image-classification", label: "Image Classification" }, { tag: "object-detection", label: "Object Detection" }, { tag: "image-segmentation", label: "Image Segmentation" }, { tag: "image-to-text", label: "Image to Text" }, + { tag: "image-to-image", label: "Image to Image" }, + { tag: "image-text-to-text", label: "Image-Text to Text" }, { tag: "visual-question-answering", label: "Visual Question Answering" }, { tag: "document-question-answering", label: "Document Question Answering" }, { tag: "zero-shot-image-classification", label: "Zero-Shot Image Classification" }, + // media generation (MediaGenCodegen) + { tag: "text-to-image", label: "Text to Image" }, + { tag: "text-to-video", label: "Text to Video" }, ]; +// Task tags the operator supports — the single gate for what the UI may offer. +// Derived from STATIC_TASK_OPTIONS so the two never drift; used to filter the +// dynamic /api/tasks result down to tasks that actually have a codegen. +export const SUPPORTED_TASK_TAGS: ReadonlySet<string> = new Set(STATIC_TASK_OPTIONS.map(o => o.tag)); + const PAGE_SIZE = 50; const TRUNCATED_HEADER = "X-Texera-Truncated"; @@ -262,7 +279,12 @@ export class HuggingFaceComponent extends FieldType<FieldTypeConfig> implements .subscribe({ next: tasks => { tasksFetchSubscription = null; - cachedTaskOptions = tasks.length > 0 ? tasks : STATIC_TASK_OPTIONS; + // Only surface tasks the operator has a codegen for — anything else + // silently falls back to text-generation and emits raw JSON. If the + // filter leaves nothing (e.g. HF changed its tags), use the static + // supported list rather than showing an empty dropdown. + const supported = tasks.filter(t => SUPPORTED_TASK_TAGS.has(t.tag)); + cachedTaskOptions = supported.length > 0 ? supported : STATIC_TASK_OPTIONS; this.taskOptions = cachedTaskOptions; this.tasksLoading = false; this.cdr.detectChanges();
