This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 6ae7be58a13 Reject non-string prompts in LLMFileAnalysisOperator
before reading files (#71734)
6ae7be58a13 is described below
commit 6ae7be58a13ae478a11f8b833ae20505c29cbd10
Author: Jyun-An Chen <[email protected]>
AuthorDate: Wed Sep 23 07:08:56 2026 +0800
Reject non-string prompts in LLMFileAnalysisOperator before reading files
(#71734)
* Add require_approval preflight check to LLMFileAnalysisOperator
LLMFileAnalysisOperator was the only require_approval-capable
operator in common.ai that didn't fail fast on a non-string prompt
before doing any work. Its four siblings (LLMOperator,
LLMBranchOperator, LLMSqlOperator, LLMSchemaCompareOperator) all
call validate_approval_prompt() as the first line of execute(), so
a bad prompt is rejected before any LLM call. LLMFileAnalysisOperator
only discovered the same error inside defer_for_approval(), after
already reading the target file(s) from storage and paying for a
real (possibly multimodal) LLM call.
* Reject non-string prompts in LLMFileAnalysisOperator regardless of
require_approval
The prompt is embedded in the text preamble with the file content, so a
non-string value fails whether or not approval is required; checking it
up front avoids reading files from storage only to hit an opaque join error.
---
.../common/ai/operators/llm_file_analysis.py | 5 ++++
.../common/ai/operators/test_llm_file_analysis.py | 34 ++++++++++++++++++++++
2 files changed, 39 insertions(+)
diff --git
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
index a9686dc8242..4005d516c4e 100644
---
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
+++
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
@@ -134,6 +134,11 @@ class LLMFileAnalysisOperator(LLMOperator):
def execute(self, context: Context) -> Any:
# Coerced first so a bad rendered value fails before the expensive
setup below.
usage_limits = coerce_usage_limits(self.usage_limits)
+ if not isinstance(self.prompt, str):
+ raise TypeError(
+ f"{type(self).__name__} requires a string prompt (got
{type(self.prompt).__name__}). "
+ "Supply images or PDFs via file_path with multi_modal=True
instead."
+ )
request = build_file_analysis_request(
file_path=self.file_path,
diff --git
a/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py
index d5ff67fd881..a55fbd095f1 100644
---
a/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py
+++
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py
@@ -384,3 +384,37 @@ class TestLLMFileAnalysisOperatorApproval:
assert exc_info.value.timeout == timeout
else:
assert mock_trigger_cls.call_args[1]["timeout_datetime"] is not
None
+
+
+class TestLLMFileAnalysisOperatorPromptTypeGuard:
+ @pytest.mark.parametrize(
+ "require_approval",
+ [
+ pytest.param(
+ True,
+ marks=pytest.mark.skipif(
+ not AIRFLOW_V_3_1_PLUS, reason="require_approval=True
needs Airflow 3.1+"
+ ),
+ ),
+ False,
+ ],
+ )
+ @patch(
+
"airflow.providers.common.ai.operators.llm_file_analysis.build_file_analysis_request",
autospec=True
+ )
+ def test_execute_rejects_non_string_prompt_before_reading_files(
+ self, mock_build_request, require_approval
+ ):
+ op = LLMFileAnalysisOperator(
+ task_id="t",
+ prompt="placeholder",
+ llm_conn_id="c",
+ file_path="/tmp/app.log",
+ require_approval=require_approval,
+ )
+ op.prompt = ["x", object()] # simulate a native-templating render to
a Sequence
+
+ with pytest.raises(TypeError, match="requires a string prompt"):
+ op.execute(context=_make_context())
+
+ mock_build_request.assert_not_called()