PG1204 commented on code in PR #5278: URL: https://github.com/apache/texera/pull/5278#discussion_r3384819095
########## common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDesc.scala: ########## @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.amber.operator.huggingFace + +import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle +import org.apache.texera.amber.core.tuple.{AttributeType, Schema} +import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} +import org.apache.texera.amber.operator.PythonOperatorDescriptor +import org.apache.texera.amber.operator.huggingFace.codegen.{ + CodegenContext, + PythonCodegenBase, + TaskCodegen, + TextGenCodegen +} +import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName +import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} +import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString + +/** + * Generic Hugging Face inference operator. + * + * This is the first slice of a feature that will eventually cover ~20 HF + * pipeline tasks. PR 2 ships text-generation only; image, audio, + * media-generation, and QA task families land in subsequent PRs as new + * `TaskCodegen` implementations registered in `registeredCodegens`. + * + * The Python script that runs at execution time is assembled by + * `PythonCodegenBase.render(ctx, codegen)`, which composes the shared + * provider-fallback / request-loop infrastructure with the per-task + * payload + parse snippets supplied by the selected `TaskCodegen`. + * + * User-provided string fields are typed as [[EncodableString]] so the + * `pyb"..."` macro inside `PythonCodegenBase` emits them as + * base64-decoded expressions at runtime instead of raw Python literals — + * this is what allows the operator to satisfy + * `PythonCodeRawInvalidTextSpec`'s contract that arbitrary `@JsonProperty` + * values must not leak into generated source. + */ +class HuggingFaceInferenceOpDesc extends PythonOperatorDescriptor { + + @JsonProperty(value = "hfApiToken", required = true) + @JsonSchemaTitle("HF API Token") + @JsonPropertyDescription( + "Your Hugging Face API token (from https://huggingface.co/settings/tokens)" + ) + var hfApiToken: EncodableString = "" + + @JsonProperty(value = "task", required = true, defaultValue = "text-generation") + @JsonSchemaTitle("Task") + @JsonPropertyDescription("The Hugging Face pipeline task type") + var task: EncodableString = "text-generation" + + @JsonProperty( + value = "modelId", + required = true, + defaultValue = "Qwen/Qwen2.5-72B-Instruct" + ) + @JsonSchemaTitle("Model") + @JsonPropertyDescription("Select a Hugging Face model") + var modelId: EncodableString = "Qwen/Qwen2.5-72B-Instruct" + + @JsonProperty(value = "promptColumn", required = true) + @JsonSchemaTitle("Prompt Column") + @JsonPropertyDescription("Column in the input table to use as the user prompt") + @AutofillAttributeName + var promptColumn: EncodableString = "" + + @JsonProperty( + value = "systemPrompt", + required = false, + defaultValue = "You are a helpful assistant." + ) + @JsonSchemaTitle("System Prompt") + @JsonPropertyDescription("Optional system message to set model behavior") + var systemPrompt: EncodableString = "You are a helpful assistant." + + @JsonProperty(value = "maxNewTokens", required = false, defaultValue = "256") + @JsonSchemaTitle("Max New Tokens") + @JsonPropertyDescription("Maximum number of tokens to generate (1-4096)") + var maxNewTokens: java.lang.Integer = 256 + + @JsonProperty(value = "temperature", required = false) + @JsonSchemaTitle("Temperature") + @JsonPropertyDescription("Sampling temperature (0.0 = deterministic, up to 2.0)") + var temperature: java.lang.Double = 0.7 + + @JsonProperty( + value = "resultColumn", + required = false, + defaultValue = "hf_response" + ) + @JsonSchemaTitle("Result Column Name") + @JsonPropertyDescription("Name of the new column added to the output table") + var resultColumn: EncodableString = "hf_response" + + /** + * Per-task code generators. New entries are added as task families land + * in subsequent PRs (e.g. ImageTaskCodegen, AudioTaskCodegen, etc.). + * + * An unrecognized task string falls back to [[TextGenCodegen]]; the + * generated Python's `else` branch then produces a generic `{"inputs": + * prompt_value}` payload and the HF endpoint surfaces the real error at + * runtime. This matches the original monolithic operator's behavior and + * keeps `generatePythonCode` total (it never throws on arbitrary input, + * which is required by `PythonCodeRawInvalidTextSpec`). + */ + private val registeredCodegens: Map[String, TaskCodegen] = + Map(TextGenCodegen.task -> TextGenCodegen) + + private def codegenForTask(t: String): TaskCodegen = + registeredCodegens.getOrElse(t, TextGenCodegen) + + override def generatePythonCode(): String = { + val safeTask: EncodableString = + if (task == null || task.trim.isEmpty) "text-generation" else task + val safeModelId: EncodableString = + if (modelId == null) "" else modelId.trim + val safePromptCol: EncodableString = + if (promptColumn == null) "" else promptColumn + val safeResultCol: EncodableString = + if (resultColumn == null || resultColumn.trim.isEmpty) "hf_response" else resultColumn + val safeSystemPrompt: EncodableString = + if (systemPrompt == null) "" else systemPrompt + val safeToken: EncodableString = + if (hfApiToken == null) "" else hfApiToken + + val safeMaxTokens = + math.max(1, math.min(if (maxNewTokens != null) maxNewTokens.intValue else 256, 4096)) + val safeTemp = + math.max(0.0, math.min(if (temperature != null) temperature.doubleValue else 0.7, 2.0)) + + val ctx = CodegenContext( + hfApiToken = safeToken, + modelId = safeModelId, + promptColumn = safePromptCol, + resultColumn = safeResultCol, + task = safeTask, + systemPrompt = safeSystemPrompt, + safeMaxTokens = safeMaxTokens, + safeTemp = safeTemp + ) + + PythonCodegenBase.render(ctx, codegenForTask(safeTask)) + } + + override def operatorInfo: OperatorInfo = + OperatorInfo( + "Hugging Face", + "Call a Hugging Face model via the Inference API", + OperatorGroupConstants.HUGGINGFACE_GROUP, + inputPorts = List(InputPort()), + outputPorts = List(OutputPort()) + ) + + override def getOutputSchemas( + inputSchemas: Map[PortIdentity, Schema] + ): Map[PortIdentity, Schema] = { + val resCol = + if (resultColumn == null || resultColumn.trim.isEmpty) "hf_response" + else resultColumn Review Comment: The requested refactor has been applied. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
