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()

Reply via email to