From b05d0eb55cd35ee945faec2c4df3b7691c5589b4 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Wed, 3 Dec 2025 14:55:52 -0300 Subject: [PATCH] feat: Add configurable API key validation source (db/env) (#10783) * add xapikey to env authentication * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * remove-docs-for-1.8-release * add fallback to db * [autofix.ci] apply automated fixes * test(api_key_source.py): enhance tests for check_key function to cover more scenarios and improve reliability - Add tests to verify routing to environment and database based on API_KEY_SOURCE. - Implement fallback logic tests when environment validation fails. - Ensure correct behavior when both environment and database validations fail. - Refactor existing tests to improve clarity and coverage of edge cases. * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * fix tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com> --- .env.example | 15 + .../starter_projects/Document Q&A.json | 3280 +++--- .../Portfolio Website Code Generator.json | 4746 ++++---- .../Text Sentiment Analysis.json | 5465 +++++---- .../starter_projects/Vector Store RAG.json | 9958 ++++++++--------- .../services/database/models/api_key/crud.py | 47 +- src/backend/tests/unit/test_api_key_source.py | 579 + src/backend/tests/unit/test_auth_settings.py | 67 +- .../features/filterEdge-shard-1.spec.ts | 38 +- src/lfx/src/lfx/_assets/component_index.json | 2 +- src/lfx/src/lfx/services/settings/auth.py | 10 + 11 files changed, 12127 insertions(+), 12080 deletions(-) create mode 100644 src/backend/tests/unit/test_api_key_source.py diff --git a/.env.example b/.env.example index 9ea7727c1..54eacd3fa 100644 --- a/.env.example +++ b/.env.example @@ -116,6 +116,21 @@ LANGFLOW_SUPERUSER= # Example: LANGFLOW_SUPERUSER_PASSWORD=123456 LANGFLOW_SUPERUSER_PASSWORD= +# API Key Source +# Controls how API keys are validated for the x-api-key header +# Values: db, env +# - db (default): Validates against API keys stored in the database +# - env: Validates against the LANGFLOW_API_KEY environment variable +# Example: LANGFLOW_API_KEY_SOURCE=db +LANGFLOW_API_KEY_SOURCE= + +# API Key (only used when LANGFLOW_API_KEY_SOURCE=env) +# The API key to use for authentication when API_KEY_SOURCE is set to 'env' +# This allows injecting a pre-defined API key via environment variables +# (useful for Kubernetes Secrets, CI/CD pipelines, etc.) +# Example: LANGFLOW_API_KEY=your-secure-api-key +LANGFLOW_API_KEY= + # Should store environment variables in the database # Values: true, false LANGFLOW_STORE_ENVIRONMENT_VARIABLES= diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json b/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json index 1e921e957..094d4ed58 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json @@ -1,1690 +1,1592 @@ { - "data": { - "edges": [ - { - "animated": false, - "className": "", - "data": { - "sourceHandle": { - "dataType": "ChatInput", - "id": "ChatInput-e74mn", - "name": "message", - "output_types": [ - "Message" - ] - }, - "targetHandle": { - "fieldName": "input_value", - "id": "LanguageModelComponent-htMuI", - "inputTypes": [ - "Message" - ], - "type": "str" - } - }, - "id": "reactflow__edge-ChatInput-e74mn{œdataTypeœ:œChatInputœ,œidœ:œChatInput-e74mnœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-LanguageModelComponent-htMuI{œfieldNameœ:œinput_valueœ,œidœ:œLanguageModelComponent-htMuIœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", - "selected": false, - "source": "ChatInput-e74mn", - "sourceHandle": "{œdataTypeœ: œChatInputœ, œidœ: œChatInput-e74mnœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", - "target": "LanguageModelComponent-htMuI", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œLanguageModelComponent-htMuIœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" - }, - { - "animated": false, - "className": "", - "data": { - "sourceHandle": { - "dataType": "Prompt", - "id": "Prompt-odlqe", - "name": "prompt", - "output_types": [ - "Message" - ] - }, - "targetHandle": { - "fieldName": "system_message", - "id": "LanguageModelComponent-htMuI", - "inputTypes": [ - "Message" - ], - "type": "str" - } - }, - "id": "reactflow__edge-Prompt-odlqe{œdataTypeœ:œPromptœ,œidœ:œPrompt-odlqeœ,œnameœ:œpromptœ,œoutput_typesœ:[œMessageœ]}-LanguageModelComponent-htMuI{œfieldNameœ:œsystem_messageœ,œidœ:œLanguageModelComponent-htMuIœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", - "selected": false, - "source": "Prompt-odlqe", - "sourceHandle": "{œdataTypeœ: œPromptœ, œidœ: œPrompt-odlqeœ, œnameœ: œpromptœ, œoutput_typesœ: [œMessageœ]}", - "target": "LanguageModelComponent-htMuI", - "targetHandle": "{œfieldNameœ: œsystem_messageœ, œidœ: œLanguageModelComponent-htMuIœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" - }, - { - "animated": false, - "className": "", - "data": { - "sourceHandle": { - "dataType": "LanguageModelComponent", - "id": "LanguageModelComponent-htMuI", - "name": "text_output", - "output_types": [ - "Message" - ] - }, - "targetHandle": { - "fieldName": "input_value", - "id": "ChatOutput-bcQIH", - "inputTypes": [ - "Data", - "DataFrame", - "Message" - ], - "type": "str" - } - }, - "id": "reactflow__edge-LanguageModelComponent-htMuI{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-htMuIœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-bcQIH{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-bcQIHœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", - "selected": false, - "source": "LanguageModelComponent-htMuI", - "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-htMuIœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", - "target": "ChatOutput-bcQIH", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-bcQIHœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" - }, - { - "animated": false, - "className": "", - "data": { - "sourceHandle": { - "dataType": "File", - "id": "File-b2gOG", - "name": "message", - "output_types": [ - "Message" - ] - }, - "targetHandle": { - "fieldName": "Document", - "id": "Prompt-odlqe", - "inputTypes": [ - "Message", - "Text" - ], - "type": "str" - } - }, - "id": "reactflow__edge-File-b2gOG{œdataTypeœ:œFileœ,œidœ:œFile-b2gOGœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-Prompt-odlqe{œfieldNameœ:œDocumentœ,œidœ:œPrompt-odlqeœ,œinputTypesœ:[œMessageœ,œTextœ],œtypeœ:œstrœ}", - "selected": false, - "source": "File-b2gOG", - "sourceHandle": "{œdataTypeœ: œFileœ, œidœ: œFile-b2gOGœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", - "target": "Prompt-odlqe", - "targetHandle": "{œfieldNameœ: œDocumentœ, œidœ: œPrompt-odlqeœ, œinputTypesœ: [œMessageœ, œTextœ], œtypeœ: œstrœ}" - } - ], - "nodes": [ - { - "data": { - "description": "Get chat inputs from the Playground.", - "display_name": "Chat Input", - "id": "ChatInput-e74mn", - "node": { - "base_classes": [ - "Message" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Get chat inputs from the Playground.", - "display_name": "Chat Input", - "documentation": "", - "edited": false, - "field_order": [ - "input_value", - "store_message", - "sender", - "sender_name", - "session_id", - "files" - ], - "frozen": false, - "icon": "MessagesSquare", - "legacy": false, - "lf_version": "1.4.3", - "metadata": { - "code_hash": "7a26c54d89ed", - "dependencies": { - "dependencies": [ - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 1 - }, - "module": "lfx.components.input_output.chat.ChatInput" - }, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "Chat Message", - "group_outputs": false, - "method": "message_response", - "name": "message", - "selected": "Message", - "tool_mode": true, - "types": [ - "Message" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from lfx.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import (\n DropdownInput,\n FileInput,\n MessageTextInput,\n MultilineInput,\n Output,\n)\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n minimized = True\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n input_types=[],\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n temp_file=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Chat Message\", name=\"message\", method=\"message_response\"),\n ]\n\n async def message_response(self) -> Message:\n # Ensure files is a list and filter out empty/None values\n files = self.files if self.files else []\n if files and not isinstance(files, list):\n files = [files]\n # Filter out None/empty values\n files = [f for f in files if f is not None and f != \"\"]\n\n session_id = self.session_id or self.graph.session_id or \"\"\n message = await Message.create(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=session_id,\n context_id=self.context_id,\n files=files,\n )\n if session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = await self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n" - }, - "context_id": { - "_input_type": "MessageTextInput", - "advanced": true, - "display_name": "Context ID", - "dynamic": false, - "info": "The context ID of the chat. Adds an extra layer to the local memory.", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "context_id", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - }, - "files": { - "advanced": true, - "display_name": "Files", - "dynamic": false, - "fileTypes": [ - "csv", - "json", - "pdf", - "txt", - "md", - "mdx", - "yaml", - "yml", - "xml", - "html", - "htm", - "docx", - "py", - "sh", - "sql", - "js", - "ts", - "tsx", - "jpg", - "jpeg", - "png", - "bmp", - "image" - ], - "file_path": "", - "info": "Files to be sent with the message.", - "list": true, - "name": "files", - "placeholder": "", - "required": false, - "show": true, - "temp_file": true, - "title_case": false, - "trace_as_metadata": true, - "type": "file", - "value": "" - }, - "input_value": { - "advanced": false, - "display_name": "Input Text", - "dynamic": false, - "info": "Message to be passed as input.", - "input_types": [], - "list": false, - "load_from_db": false, - "multiline": true, - "name": "input_value", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "What is this document is about?" - }, - "sender": { - "advanced": true, - "display_name": "Sender Type", - "dynamic": false, - "info": "Type of sender.", - "name": "sender", - "options": [ - "Machine", - "User" - ], - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_metadata": true, - "type": "str", - "value": "User" - }, - "sender_name": { - "advanced": true, - "display_name": "Sender Name", - "dynamic": false, - "info": "Name of the sender.", - "input_types": [ - "Message" - ], - "list": false, - "load_from_db": false, - "name": "sender_name", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "User" - }, - "session_id": { - "advanced": true, - "display_name": "Session ID", - "dynamic": false, - "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", - "input_types": [ - "Message" - ], - "list": false, - "load_from_db": false, - "name": "session_id", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - }, - "should_store_message": { - "_input_type": "BoolInput", - "advanced": true, - "display_name": "Store Messages", - "dynamic": false, - "info": "Store the message in the history.", - "list": false, - "name": "should_store_message", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_metadata": true, - "type": "bool", - "value": true - } - } - }, - "selected_output": "message", - "type": "ChatInput" - }, - "dragging": false, - "height": 234, - "id": "ChatInput-e74mn", - "measured": { - "height": 234, - "width": 320 - }, - "position": { - "x": 516.7529480335185, - "y": 237.04967879541528 - }, - "positionAbsolute": { - "x": 516.7529480335185, - "y": 237.04967879541528 - }, - "selected": false, - "type": "genericNode", - "width": 320 - }, - { - "data": { - "description": "Display a chat message in the Playground.", - "display_name": "Chat Output", - "id": "ChatOutput-bcQIH", - "node": { - "base_classes": [ - "Message" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Display a chat message in the Playground.", - "display_name": "Chat Output", - "documentation": "", - "edited": false, - "field_order": [ - "input_value", - "should_store_message", - "sender", - "sender_name", - "session_id", - "data_template", - "background_color", - "chat_icon", - "text_color" - ], - "frozen": false, - "icon": "MessagesSquare", - "legacy": false, - "lf_version": "1.4.3", - "metadata": { - "code_hash": "cae45e2d53f6", - "dependencies": { - "dependencies": [ - { - "name": "orjson", - "version": "3.10.15" - }, - { - "name": "fastapi", - "version": "0.123.0" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 3 - }, - "module": "lfx.components.input_output.chat_output.ChatOutput" - }, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "Output Message", - "group_outputs": false, - "method": "message_response", - "name": "message", - "selected": "Message", - "tool_mode": true, - "types": [ - "Message" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "clean_data": { - "_input_type": "BoolInput", - "advanced": true, - "display_name": "Basic Clean Data", - "dynamic": false, - "info": "Whether to clean data before converting to string.", - "list": false, - "list_add_label": "Add More", - "name": "clean_data", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "bool", - "value": true - }, - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from collections.abc import Generator\nfrom typing import Any\n\nimport orjson\nfrom fastapi.encoders import jsonable_encoder\n\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, HandleInput, MessageTextInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.schema.properties import Source\nfrom lfx.template.field.base import Output\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_AI,\n MESSAGE_SENDER_USER,\n)\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatOutput\"\n minimized = True\n\n inputs = [\n HandleInput(\n name=\"input_value\",\n display_name=\"Inputs\",\n info=\"Message to be passed as output.\",\n input_types=[\"Data\", \"DataFrame\", \"Message\"],\n required=True,\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_AI,\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_AI,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n BoolInput(\n name=\"clean_data\",\n display_name=\"Basic Clean Data\",\n value=True,\n advanced=True,\n info=\"Whether to clean data before converting to string.\",\n ),\n ]\n outputs = [\n Output(\n display_name=\"Output Message\",\n name=\"message\",\n method=\"message_response\",\n ),\n ]\n\n def _build_source(self, id_: str | None, display_name: str | None, source: str | None) -> Source:\n source_dict = {}\n if id_:\n source_dict[\"id\"] = id_\n if display_name:\n source_dict[\"display_name\"] = display_name\n if source:\n # Handle case where source is a ChatOpenAI object\n if hasattr(source, \"model_name\"):\n source_dict[\"source\"] = source.model_name\n elif hasattr(source, \"model\"):\n source_dict[\"source\"] = str(source.model)\n else:\n source_dict[\"source\"] = str(source)\n return Source(**source_dict)\n\n async def message_response(self) -> Message:\n # First convert the input to string if needed\n text = self.convert_to_string()\n\n # Get source properties\n source, _, display_name, source_id = self.get_properties_from_source_component()\n\n # Create or use existing Message object\n if isinstance(self.input_value, Message) and not self.is_connected_to_chat_input():\n message = self.input_value\n # Update message properties\n message.text = text\n else:\n message = Message(text=text)\n\n # Set message properties\n message.sender = self.sender\n message.sender_name = self.sender_name\n message.session_id = self.session_id or self.graph.session_id or \"\"\n message.context_id = self.context_id\n message.flow_id = self.graph.flow_id if hasattr(self, \"graph\") else None\n message.properties.source = self._build_source(source_id, display_name, source)\n\n # Store message if needed\n if message.session_id and self.should_store_message:\n stored_message = await self.send_message(message)\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" - }, - "context_id": { - "_input_type": "MessageTextInput", - "advanced": true, - "display_name": "Context ID", - "dynamic": false, - "info": "The context ID of the chat. Adds an extra layer to the local memory.", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "context_id", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - }, - "data_template": { - "_input_type": "MessageTextInput", - "advanced": true, - "display_name": "Data Template", - "dynamic": false, - "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", - "input_types": [ - "Message" - ], - "list": false, - "load_from_db": false, - "name": "data_template", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "{text}" - }, - "input_value": { - "_input_type": "MessageInput", - "advanced": false, - "display_name": "Inputs", - "dynamic": false, - "info": "Message to be passed as output.", - "input_types": [ - "Data", - "DataFrame", - "Message" - ], - "list": false, - "load_from_db": false, - "name": "input_value", - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - }, - "sender": { - "_input_type": "DropdownInput", - "advanced": true, - "combobox": false, - "display_name": "Sender Type", - "dynamic": false, - "info": "Type of sender.", - "name": "sender", - "options": [ - "Machine", - "User" - ], - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "str", - "value": "Machine" - }, - "sender_name": { - "_input_type": "MessageTextInput", - "advanced": true, - "display_name": "Sender Name", - "dynamic": false, - "info": "Name of the sender.", - "input_types": [ - "Message" - ], - "list": false, - "load_from_db": false, - "name": "sender_name", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "AI" - }, - "session_id": { - "_input_type": "MessageTextInput", - "advanced": true, - "display_name": "Session ID", - "dynamic": false, - "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", - "input_types": [ - "Message" - ], - "list": false, - "load_from_db": false, - "name": "session_id", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - }, - "should_store_message": { - "_input_type": "BoolInput", - "advanced": true, - "display_name": "Store Messages", - "dynamic": false, - "info": "Store the message in the history.", - "list": false, - "name": "should_store_message", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_metadata": true, - "type": "bool", - "value": true - } - }, - "tool_mode": false - }, - "type": "ChatOutput" - }, - "dragging": false, - "height": 234, - "id": "ChatOutput-bcQIH", - "measured": { - "height": 234, - "width": 320 - }, - "position": { - "x": 1631.3766926569258, - "y": 136.66509468115308 - }, - "positionAbsolute": { - "x": 1631.3766926569258, - "y": 136.66509468115308 - }, - "selected": false, - "type": "genericNode", - "width": 320 - }, - { - "data": { - "id": "note-gLzpv", - "node": { - "description": "# Document Q&A\n\nThis flow loads a file and uses an LLM to answer questions based on content from the loaded document. \n\n## Prerequisites\n\n* An [OpenAI API key](https://platform.openai.com/)\n\n## Quickstart\n\n1. Paste your OpenAI API key in the **OpenAI** model component.\n2. In the **File** component, select a file you want to load.\n3. Open the **Playground** and chat with your document.", - "display_name": "", - "documentation": "", - "template": {} - }, - "type": "note" - }, - "dragging": false, - "height": 509, - "id": "note-gLzpv", - "measured": { - "height": 509, - "width": 420 - }, - "position": { - "x": 59.87035937663103, - "y": -81.89823824161797 - }, - "positionAbsolute": { - "x": -338.7070086205371, - "y": -177.11912020709357 - }, - "resizing": false, - "selected": false, - "style": { - "height": 452, - "width": 324 - }, - "type": "noteNode", - "width": 420 - }, - { - "data": { - "description": "Create a prompt template with dynamic variables.", - "display_name": "Prompt", - "id": "Prompt-odlqe", - "node": { - "base_classes": [ - "Message" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": { - "template": [ - "Document" - ] - }, - "description": "Create a prompt template with dynamic variables.", - "display_name": "Prompt", - "documentation": "", - "edited": false, - "error": null, - "field_order": [ - "template" - ], - "frozen": false, - "full_path": null, - "icon": "braces", - "is_composition": null, - "is_input": null, - "is_output": null, - "legacy": false, - "lf_version": "1.4.3", - "metadata": { - "code_hash": "3bf0b511e227", - "module": "langflow.components.prompts.prompt.PromptComponent" - }, - "name": "", - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "Prompt", - "group_outputs": false, - "method": "build_prompt", - "name": "prompt", - "selected": "Message", - "tool_mode": true, - "types": [ - "Message" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "Document": { - "advanced": false, - "display_name": "Document", - "dynamic": false, - "field_type": "str", - "fileTypes": [], - "file_path": "", - "info": "", - "input_types": [ - "Message", - "Text" - ], - "list": false, - "load_from_db": false, - "multiline": true, - "name": "Document", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "type": "str", - "value": "" - }, - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from langflow.base.prompts.api_utils import process_prompt_template\nfrom langflow.custom.custom_component.component import Component\nfrom langflow.inputs.inputs import DefaultPromptField\nfrom langflow.io import MessageTextInput, Output, PromptInput\nfrom langflow.schema.message import Message\nfrom langflow.template.utils import update_template_values\n\n\nclass PromptComponent(Component):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"braces\"\n trace_type = \"prompt\"\n name = \"Prompt\"\n\n inputs = [\n PromptInput(name=\"template\", display_name=\"Template\"),\n MessageTextInput(\n name=\"tool_placeholder\",\n display_name=\"Tool Placeholder\",\n tool_mode=True,\n advanced=True,\n info=\"A placeholder input for tool mode.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Prompt\", name=\"prompt\", method=\"build_prompt\"),\n ]\n\n async def build_prompt(self) -> Message:\n prompt = Message.from_template(**self._attributes)\n self.status = prompt.text\n return prompt\n\n def _update_template(self, frontend_node: dict):\n prompt_template = frontend_node[\"template\"][\"template\"][\"value\"]\n custom_fields = frontend_node[\"custom_fields\"]\n frontend_node_template = frontend_node[\"template\"]\n _ = process_prompt_template(\n template=prompt_template,\n name=\"template\",\n custom_fields=custom_fields,\n frontend_node_template=frontend_node_template,\n )\n return frontend_node\n\n async def update_frontend_node(self, new_frontend_node: dict, current_frontend_node: dict):\n \"\"\"This function is called after the code validation is done.\"\"\"\n frontend_node = await super().update_frontend_node(new_frontend_node, current_frontend_node)\n template = frontend_node[\"template\"][\"template\"][\"value\"]\n # Kept it duplicated for backwards compatibility\n _ = process_prompt_template(\n template=template,\n name=\"template\",\n custom_fields=frontend_node[\"custom_fields\"],\n frontend_node_template=frontend_node[\"template\"],\n )\n # Now that template is updated, we need to grab any values that were set in the current_frontend_node\n # and update the frontend_node with those values\n update_template_values(new_template=frontend_node, previous_template=current_frontend_node[\"template\"])\n return frontend_node\n\n def _get_fallback_input(self, **kwargs):\n return DefaultPromptField(**kwargs)\n" - }, - "template": { - "advanced": false, - "display_name": "Template", - "dynamic": false, - "info": "", - "list": false, - "load_from_db": false, - "name": "template", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_input": true, - "type": "prompt", - "value": "Answer user's questions based on the document below:\n\n---\n\n{Document}\n\n---\n\nQuestion:" - }, - "tool_placeholder": { - "_input_type": "MessageTextInput", - "advanced": true, - "display_name": "Tool Placeholder", - "dynamic": false, - "info": "A placeholder input for tool mode.", - "input_types": [ - "Message" - ], - "list": false, - "load_from_db": false, - "name": "tool_placeholder", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": true, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - } - }, - "tool_mode": false - }, - "selected_output": "prompt", - "type": "Prompt" - }, - "dragging": false, - "height": 347, - "id": "Prompt-odlqe", - "measured": { - "height": 347, - "width": 320 - }, - "position": { - "x": 882.4192413332464, - "y": -63.08797684105531 - }, - "positionAbsolute": { - "x": 895.1947781377585, - "y": -59.89409263992732 - }, - "selected": false, - "type": "genericNode", - "width": 320 - }, - { - "data": { - "id": "LanguageModelComponent-htMuI", - "node": { - "base_classes": [ - "LanguageModel", - "Message" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Runs a language model given a specified provider.", - "display_name": "Language Model", - "documentation": "", - "edited": false, - "field_order": [ - "provider", - "model_name", - "api_key", - "input_value", - "system_message", - "stream", - "temperature" - ], - "frozen": false, - "icon": "brain-circuit", - "legacy": false, - "lf_version": "1.4.3", - "metadata": { - "code_hash": "bb5f8714781b", - "dependencies": { - "dependencies": [ - { - "name": "langchain_anthropic", - "version": "0.3.14" - }, - { - "name": "langchain_google_genai", - "version": "2.0.6" - }, - { - "name": "langchain_openai", - "version": "0.3.23" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "keywords": [ - "model", - "llm", - "language model", - "large language model" - ], - "module": "lfx.components.models.language_model.LanguageModelComponent" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "Model Response", - "group_outputs": false, - "method": "text_response", - "name": "text_output", - "selected": "Message", - "tool_mode": true, - "types": [ - "Message" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Language Model", - "group_outputs": false, - "method": "build_model", - "name": "model_output", - "selected": "LanguageModel", - "tool_mode": true, - "types": [ - "LanguageModel" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "priority": 0, - "template": { - "_type": "Component", - "api_key": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "OpenAI API Key", - "dynamic": false, - "info": "Model Provider API key", - "input_types": [], - "load_from_db": true, - "name": "api_key", - "password": true, - "placeholder": "", - "real_time_refresh": true, - "required": false, - "show": true, - "title_case": false, - "type": "str", - "value": "OPENAI_API_KEY" - }, - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from typing import Any\n\nimport requests\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_ibm import ChatWatsonx\nfrom langchain_ollama import ChatOllama\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom lfx.base.models.anthropic_constants import ANTHROPIC_MODELS\nfrom lfx.base.models.google_generative_ai_constants import GOOGLE_GENERATIVE_AI_MODELS\nfrom lfx.base.models.google_generative_ai_model import ChatGoogleGenerativeAIFixed\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.model_utils import get_ollama_models, is_valid_ollama_url\nfrom lfx.base.models.openai_constants import OPENAI_CHAT_MODEL_NAMES, OPENAI_REASONING_MODEL_NAMES\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, MessageTextInput, StrInput\nfrom lfx.io import DropdownInput, MessageInput, MultilineInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.util import transform_localhost_url\n\n# IBM watsonx.ai constants\nIBM_WATSONX_DEFAULT_MODELS = [\"ibm/granite-3-2b-instruct\", \"ibm/granite-3-8b-instruct\", \"ibm/granite-13b-instruct-v2\"]\nIBM_WATSONX_URLS = [\n \"https://us-south.ml.cloud.ibm.com\",\n \"https://eu-de.ml.cloud.ibm.com\",\n \"https://eu-gb.ml.cloud.ibm.com\",\n \"https://au-syd.ml.cloud.ibm.com\",\n \"https://jp-tok.ml.cloud.ibm.com\",\n \"https://ca-tor.ml.cloud.ibm.com\",\n]\n\n# Ollama API constants\nHTTP_STATUS_OK = 200\nJSON_MODELS_KEY = \"models\"\nJSON_NAME_KEY = \"name\"\nJSON_CAPABILITIES_KEY = \"capabilities\"\nDESIRED_CAPABILITY = \"completion\"\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\n priority = 0 # Set priority to 0 to make it appear first\n\n @staticmethod\n def fetch_ibm_models(base_url: str) -> list[str]:\n \"\"\"Fetch available models from the watsonx.ai API.\"\"\"\n try:\n endpoint = f\"{base_url}/ml/v1/foundation_model_specs\"\n params = {\"version\": \"2024-09-16\", \"filters\": \"function_text_chat,!lifecycle_withdrawn\"}\n response = requests.get(endpoint, params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n models = [model[\"model_id\"] for model in data.get(\"resources\", [])]\n return sorted(models)\n except Exception: # noqa: BLE001\n logger.exception(\"Error fetching IBM watsonx models. Using default models.\")\n return IBM_WATSONX_DEFAULT_MODELS\n\n inputs = [\n DropdownInput(\n name=\"provider\",\n display_name=\"Model Provider\",\n options=[\"OpenAI\", \"Anthropic\", \"Google\", \"IBM watsonx.ai\", \"Ollama\"],\n value=\"OpenAI\",\n info=\"Select the model provider\",\n real_time_refresh=True,\n options_metadata=[\n {\"icon\": \"OpenAI\"},\n {\"icon\": \"Anthropic\"},\n {\"icon\": \"GoogleGenerativeAI\"},\n {\"icon\": \"WatsonxAI\"},\n {\"icon\": \"Ollama\"},\n ],\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES,\n value=OPENAI_CHAT_MODEL_NAMES[0],\n info=\"Select the model to use\",\n real_time_refresh=True,\n refresh_button=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n show=False,\n real_time_refresh=True,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"watsonx Project ID\",\n info=\"The project ID associated with the foundation model (IBM watsonx.ai only)\",\n show=False,\n required=False,\n ),\n MessageTextInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n load_from_db=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n provider = self.provider\n model_name = self.model_name\n temperature = self.temperature\n stream = self.stream\n\n if provider == \"OpenAI\":\n if not self.api_key:\n msg = \"OpenAI API key is required when using OpenAI provider\"\n raise ValueError(msg)\n\n if model_name in OPENAI_REASONING_MODEL_NAMES:\n # reasoning models do not support temperature (yet)\n temperature = None\n\n return ChatOpenAI(\n model_name=model_name,\n temperature=temperature,\n streaming=stream,\n openai_api_key=self.api_key,\n )\n if provider == \"Anthropic\":\n if not self.api_key:\n msg = \"Anthropic API key is required when using Anthropic provider\"\n raise ValueError(msg)\n return ChatAnthropic(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n anthropic_api_key=self.api_key,\n )\n if provider == \"Google\":\n if not self.api_key:\n msg = \"Google API key is required when using Google provider\"\n raise ValueError(msg)\n return ChatGoogleGenerativeAIFixed(\n model=model_name,\n temperature=temperature,\n streaming=stream,\n google_api_key=self.api_key,\n )\n if provider == \"IBM watsonx.ai\":\n if not self.api_key:\n msg = \"IBM API key is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.base_url_ibm_watsonx:\n msg = \"IBM watsonx API Endpoint is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n if not self.project_id:\n msg = \"IBM watsonx Project ID is required when using IBM watsonx.ai provider\"\n raise ValueError(msg)\n return ChatWatsonx(\n apikey=SecretStr(self.api_key).get_secret_value(),\n url=self.base_url_ibm_watsonx,\n project_id=self.project_id,\n model_id=model_name,\n params={\n \"temperature\": temperature,\n },\n streaming=stream,\n )\n if provider == \"Ollama\":\n if not self.ollama_base_url:\n msg = \"Ollama API URL is required when using Ollama provider\"\n raise ValueError(msg)\n if not model_name:\n msg = \"Model name is required when using Ollama provider\"\n raise ValueError(msg)\n\n transformed_base_url = transform_localhost_url(self.ollama_base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n return ChatOllama(\n base_url=transformed_base_url,\n model=model_name,\n temperature=temperature,\n )\n msg = f\"Unknown provider: {provider}\"\n raise ValueError(msg)\n\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name == \"provider\":\n if field_value == \"OpenAI\":\n build_config[\"model_name\"][\"options\"] = OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES\n build_config[\"model_name\"][\"value\"] = OPENAI_CHAT_MODEL_NAMES[0]\n build_config[\"api_key\"][\"display_name\"] = \"OpenAI API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Anthropic\":\n build_config[\"model_name\"][\"options\"] = ANTHROPIC_MODELS\n build_config[\"model_name\"][\"value\"] = ANTHROPIC_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Anthropic API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Google\":\n build_config[\"model_name\"][\"options\"] = GOOGLE_GENERATIVE_AI_MODELS\n build_config[\"model_name\"][\"value\"] = GOOGLE_GENERATIVE_AI_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"Google API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"IBM watsonx.ai\":\n build_config[\"model_name\"][\"options\"] = IBM_WATSONX_DEFAULT_MODELS\n build_config[\"model_name\"][\"value\"] = IBM_WATSONX_DEFAULT_MODELS[0]\n build_config[\"api_key\"][\"display_name\"] = \"IBM API Key\"\n build_config[\"api_key\"][\"show\"] = True\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = True\n build_config[\"project_id\"][\"show\"] = True\n build_config[\"ollama_base_url\"][\"show\"] = False\n elif field_value == \"Ollama\":\n # Fetch Ollama models from the API\n build_config[\"api_key\"][\"show\"] = False\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = False\n build_config[\"project_id\"][\"show\"] = False\n build_config[\"ollama_base_url\"][\"show\"] = True\n\n # Try multiple sources to get the URL (in order of preference):\n # 1. Instance attribute (already resolved from global/db)\n # 2. Build config value (may be a global variable reference)\n # 3. Default value\n ollama_url = getattr(self, \"ollama_base_url\", None)\n if not ollama_url:\n config_value = build_config[\"ollama_base_url\"].get(\"value\", DEFAULT_OLLAMA_URL)\n # If config_value looks like a variable name (all caps with underscores), use default\n is_variable_ref = (\n config_value\n and isinstance(config_value, str)\n and config_value.isupper()\n and \"_\" in config_value\n )\n if is_variable_ref:\n await logger.adebug(\n f\"Config value appears to be a variable reference: {config_value}, using default\"\n )\n ollama_url = DEFAULT_OLLAMA_URL\n else:\n ollama_url = config_value\n\n await logger.adebug(f\"Fetching Ollama models for provider switch. URL: {ollama_url}\")\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n except ValueError:\n await logger.awarning(\"Failed to fetch Ollama models. Setting empty options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n await logger.awarning(f\"Invalid Ollama URL: {ollama_url}\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n elif (\n field_name == \"base_url_ibm_watsonx\"\n and field_value\n and hasattr(self, \"provider\")\n and self.provider == \"IBM watsonx.ai\"\n ):\n # Fetch IBM models when base_url changes\n try:\n models = self.fetch_ibm_models(base_url=field_value)\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else IBM_WATSONX_DEFAULT_MODELS[0]\n info_message = f\"Updated model options: {len(models)} models found in {field_value}\"\n logger.info(info_message)\n except Exception: # noqa: BLE001\n logger.exception(\"Error updating IBM model options.\")\n elif field_name == \"ollama_base_url\":\n # Fetch Ollama models when ollama_base_url changes\n # Use the field_value directly since this is triggered when the field changes\n logger.debug(\n f\"Fetching Ollama models from updated URL: {build_config['ollama_base_url']} \\\n and value {self.ollama_base_url}\",\n )\n await logger.adebug(f\"Fetching Ollama models from updated URL: {self.ollama_base_url}\")\n if await is_valid_ollama_url(url=self.ollama_base_url):\n try:\n models = await get_ollama_models(\n base_url_value=self.ollama_base_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model_name\"][\"options\"] = models\n build_config[\"model_name\"][\"value\"] = models[0] if models else \"\"\n info_message = f\"Updated model options: {len(models)} models found in {self.ollama_base_url}\"\n await logger.ainfo(info_message)\n except ValueError:\n await logger.awarning(\"Error updating Ollama model options.\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n else:\n await logger.awarning(f\"Invalid Ollama URL: {self.ollama_base_url}\")\n build_config[\"model_name\"][\"options\"] = []\n build_config[\"model_name\"][\"value\"] = \"\"\n elif field_name == \"model_name\":\n # Refresh Ollama models when model_name field is accessed\n if hasattr(self, \"provider\") and self.provider == \"Ollama\":\n ollama_url = getattr(self, \"ollama_base_url\", DEFAULT_OLLAMA_URL)\n if await is_valid_ollama_url(url=ollama_url):\n try:\n models = await get_ollama_models(\n base_url_value=ollama_url,\n desired_capability=DESIRED_CAPABILITY,\n json_models_key=JSON_MODELS_KEY,\n json_name_key=JSON_NAME_KEY,\n json_capabilities_key=JSON_CAPABILITIES_KEY,\n )\n build_config[\"model_name\"][\"options\"] = models\n except ValueError:\n await logger.awarning(\"Failed to refresh Ollama models.\")\n build_config[\"model_name\"][\"options\"] = []\n else:\n build_config[\"model_name\"][\"options\"] = []\n\n # Hide system_message for o1 models - currently unsupported\n if field_value and field_value.startswith(\"o1\") and hasattr(self, \"provider\") and self.provider == \"OpenAI\":\n if \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = False\n elif \"system_message\" in build_config:\n build_config[\"system_message\"][\"show\"] = True\n return build_config\n" - }, - "input_value": { - "_input_type": "MessageInput", - "advanced": false, - "display_name": "Input", - "dynamic": false, - "info": "The input text to send to the model", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "input_value", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - }, - "model_name": { - "_input_type": "DropdownInput", - "advanced": false, - "combobox": false, - "dialog_inputs": {}, - "display_name": "Model Name", - "dynamic": false, - "info": "Select the model to use", - "name": "model_name", - "options": [ - "gpt-4o-mini", - "gpt-4o", - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4.1-nano", - "gpt-4-turbo", - "gpt-4-turbo-preview", - "gpt-4", - "gpt-3.5-turbo", - "gpt-5", - "gpt-5-mini", - "gpt-5-nano", - "gpt-5-chat-latest", - "o1", - "o3-mini", - "o3", - "o3-pro", - "o4-mini", - "o4-mini-high" - ], - "options_metadata": [], - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "toggle": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "str", - "value": "gpt-4o-mini" - }, - "provider": { - "_input_type": "DropdownInput", - "advanced": false, - "combobox": false, - "dialog_inputs": {}, - "display_name": "Model Provider", - "dynamic": false, - "info": "Select the model provider", - "name": "provider", - "options": [ - "OpenAI", - "Anthropic", - "Google" - ], - "options_metadata": [ - { - "icon": "OpenAI" - }, - { - "icon": "Anthropic" - }, - { - "icon": "GoogleGenerativeAI" - } - ], - "placeholder": "", - "real_time_refresh": true, - "required": false, - "show": true, - "title_case": false, - "toggle": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "str", - "value": "OpenAI" - }, - "stream": { - "_input_type": "BoolInput", - "advanced": true, - "display_name": "Stream", - "dynamic": false, - "info": "Whether to stream the response", - "list": false, - "list_add_label": "Add More", - "name": "stream", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "bool", - "value": false - }, - "system_message": { - "_input_type": "MultilineInput", - "advanced": false, - "copy_field": false, - "display_name": "System Message", - "dynamic": false, - "info": "A system message that helps set the behavior of the assistant", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "multiline": true, - "name": "system_message", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "type": "str", - "value": "" - }, - "temperature": { - "_input_type": "SliderInput", - "advanced": true, - "display_name": "Temperature", - "dynamic": false, - "info": "Controls randomness in responses", - "max_label": "", - "max_label_icon": "", - "min_label": "", - "min_label_icon": "", - "name": "temperature", - "placeholder": "", - "range_spec": { - "max": 1, - "min": 0, - "step": 0.01, - "step_type": "float" - }, - "required": false, - "show": true, - "slider_buttons": false, - "slider_buttons_options": [], - "slider_input": false, - "title_case": false, - "tool_mode": false, - "type": "slider", - "value": 0.1 - } - }, - "tool_mode": false - }, - "showNode": true, - "type": "LanguageModelComponent" - }, - "dragging": false, - "id": "LanguageModelComponent-htMuI", - "measured": { - "height": 532, - "width": 320 - }, - "position": { - "x": 1256.4027532477426, - "y": -116.62150897037591 - }, - "selected": false, - "type": "genericNode" - }, - { - "data": { - "id": "File-b2gOG", - "node": { - "base_classes": [ - "DataFrame" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Loads and returns the content from uploaded files.", - "display_name": "File", - "documentation": "", - "edited": false, - "field_order": [ - "path", - "file_path", - "separator", - "silent_errors", - "delete_server_file_after_processing", - "ignore_unsupported_extensions", - "ignore_unspecified_files", - "use_multithreading", - "concurrency_multithreading" - ], - "frozen": false, - "icon": "file-text", - "legacy": false, - "lf_version": "1.4.3", - "metadata": { - "code_hash": "1d81b3a4d764", - "dependencies": { - "dependencies": [ - { - "name": "lfx", - "version": null - }, - { - "name": "langchain_core", - "version": "0.3.80" - }, - { - "name": "pydantic", - "version": "2.11.10" - } - ], - "total_dependencies": 3 - }, - "module": "lfx.components.files_and_knowledge.file.FileComponent" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "Raw Content", - "group_outputs": false, - "method": "load_files_message", - "name": "message", - "selected": "Message", - "tool_mode": true, - "types": [ - "Message" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "advanced_mode": { - "_input_type": "BoolInput", - "advanced": false, - "display_name": "Advanced Parser", - "dynamic": false, - "info": "Enable advanced document processing and export with Docling for PDFs, images, and office documents. Note that advanced document processing can consume significant resources.", - "list": false, - "list_add_label": "Add More", - "name": "advanced_mode", - "placeholder": "", - "real_time_refresh": true, - "required": false, - "show": false, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "bool", - "value": false - }, - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "\"\"\"Enhanced file component with Docling support and process isolation.\n\nNotes:\n-----\n- ALL Docling parsing/export runs in a separate OS process to prevent memory\n growth and native library state from impacting the main Langflow process.\n- Standard text/structured parsing continues to use existing BaseFileComponent\n utilities (and optional threading via `parallel_load_data`).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport json\nimport subprocess\nimport sys\nimport textwrap\nfrom copy import deepcopy\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\nfrom typing import Any\n\nfrom lfx.base.data.base_file import BaseFileComponent\nfrom lfx.base.data.storage_utils import parse_storage_path, validate_image_content_type\nfrom lfx.base.data.utils import TEXT_FILE_TYPES, parallel_load_data, parse_text_file_to_data\nfrom lfx.inputs.inputs import DropdownInput, MessageTextInput, StrInput\nfrom lfx.io import BoolInput, FileInput, IntInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame # noqa: TC001\nfrom lfx.schema.message import Message\nfrom lfx.services.deps import get_settings_service, get_storage_service\nfrom lfx.utils.async_helpers import run_until_complete\n\n\nclass FileComponent(BaseFileComponent):\n \"\"\"File component with optional Docling processing (isolated in a subprocess).\"\"\"\n\n display_name = \"Read File\"\n # description is now a dynamic property - see get_tool_description()\n _base_description = \"Loads content from one or more files.\"\n documentation: str = \"https://docs.langflow.org/read-file\"\n icon = \"file-text\"\n name = \"File\"\n add_tool_output = True # Enable tool mode toggle without requiring tool_mode inputs\n\n # Extensions that can be processed without Docling (using standard text parsing)\n TEXT_EXTENSIONS = TEXT_FILE_TYPES\n\n # Extensions that require Docling for processing (images, advanced office formats, etc.)\n DOCLING_ONLY_EXTENSIONS = [\n \"adoc\",\n \"asciidoc\",\n \"asc\",\n \"bmp\",\n \"dotx\",\n \"dotm\",\n \"docm\",\n \"jpg\",\n \"jpeg\",\n \"png\",\n \"potx\",\n \"ppsx\",\n \"pptm\",\n \"potm\",\n \"ppsm\",\n \"pptx\",\n \"tiff\",\n \"xls\",\n \"xlsx\",\n \"xhtml\",\n \"webp\",\n ]\n\n # Docling-supported/compatible extensions; TEXT_FILE_TYPES are supported by the base loader.\n VALID_EXTENSIONS = [\n *TEXT_EXTENSIONS,\n *DOCLING_ONLY_EXTENSIONS,\n ]\n\n # Fixed export settings used when markdown export is requested.\n EXPORT_FORMAT = \"Markdown\"\n IMAGE_MODE = \"placeholder\"\n\n _base_inputs = deepcopy(BaseFileComponent.get_base_inputs())\n\n for input_item in _base_inputs:\n if isinstance(input_item, FileInput) and input_item.name == \"path\":\n input_item.real_time_refresh = True\n input_item.tool_mode = False # Disable tool mode for file upload input\n input_item.required = False # Make it optional so it doesn't error in tool mode\n break\n\n inputs = [\n *_base_inputs,\n StrInput(\n name=\"file_path_str\",\n display_name=\"File Path\",\n info=(\n \"Path to the file to read. Used when component is called as a tool. \"\n \"If not provided, will use the uploaded file from 'path' input.\"\n ),\n show=False,\n advanced=True,\n tool_mode=True, # Required for Toolset toggle, but _get_tools() ignores this parameter\n required=False,\n ),\n BoolInput(\n name=\"advanced_mode\",\n display_name=\"Advanced Parser\",\n value=False,\n real_time_refresh=True,\n info=(\n \"Enable advanced document processing and export with Docling for PDFs, images, and office documents. \"\n \"Note that advanced document processing can consume significant resources.\"\n ),\n show=True,\n ),\n DropdownInput(\n name=\"pipeline\",\n display_name=\"Pipeline\",\n info=\"Docling pipeline to use\",\n options=[\"standard\", \"vlm\"],\n value=\"standard\",\n advanced=True,\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"ocr_engine\",\n display_name=\"OCR Engine\",\n info=\"OCR engine to use. Only available when pipeline is set to 'standard'.\",\n options=[\"None\", \"easyocr\"],\n value=\"easyocr\",\n show=False,\n advanced=True,\n ),\n StrInput(\n name=\"md_image_placeholder\",\n display_name=\"Image placeholder\",\n info=\"Specify the image placeholder for markdown exports.\",\n value=\"\",\n advanced=True,\n show=False,\n ),\n StrInput(\n name=\"md_page_break_placeholder\",\n display_name=\"Page break placeholder\",\n info=\"Add this placeholder between pages in the markdown output.\",\n value=\"\",\n advanced=True,\n show=False,\n ),\n MessageTextInput(\n name=\"doc_key\",\n display_name=\"Doc Key\",\n info=\"The key to use for the DoclingDocument column.\",\n value=\"doc\",\n advanced=True,\n show=False,\n ),\n # Deprecated input retained for backward-compatibility.\n BoolInput(\n name=\"use_multithreading\",\n display_name=\"[Deprecated] Use Multithreading\",\n advanced=True,\n value=True,\n info=\"Set 'Processing Concurrency' greater than 1 to enable multithreading.\",\n ),\n IntInput(\n name=\"concurrency_multithreading\",\n display_name=\"Processing Concurrency\",\n advanced=True,\n info=\"When multiple files are being processed, the number of files to process concurrently.\",\n value=1,\n ),\n BoolInput(\n name=\"markdown\",\n display_name=\"Markdown Export\",\n info=\"Export processed documents to Markdown format. Only available when advanced mode is enabled.\",\n value=False,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Raw Content\", name=\"message\", method=\"load_files_message\", tool_mode=True),\n ]\n\n # ------------------------------ Tool description with file names --------------\n\n def get_tool_description(self) -> str:\n \"\"\"Return a dynamic description that includes the names of uploaded files.\n\n This helps the Agent understand which files are available to read.\n \"\"\"\n base_description = \"Loads and returns the content from uploaded files.\"\n\n # Get the list of uploaded file paths\n file_paths = getattr(self, \"path\", None)\n if not file_paths:\n return base_description\n\n # Ensure it's a list\n if not isinstance(file_paths, list):\n file_paths = [file_paths]\n\n # Extract just the file names from the paths\n file_names = []\n for fp in file_paths:\n if fp:\n name = Path(fp).name\n file_names.append(name)\n\n if file_names:\n files_str = \", \".join(file_names)\n return f\"{base_description} Available files: {files_str}. Call this tool to read these files.\"\n\n return base_description\n\n @property\n def description(self) -> str:\n \"\"\"Dynamic description property that includes uploaded file names.\"\"\"\n return self.get_tool_description()\n\n async def _get_tools(self) -> list:\n \"\"\"Override to create a tool without parameters.\n\n The Read File component should use the files already uploaded via UI,\n not accept file paths from the Agent (which wouldn't know the internal paths).\n \"\"\"\n from langchain_core.tools import StructuredTool\n from pydantic import BaseModel\n\n # Empty schema - no parameters needed\n class EmptySchema(BaseModel):\n \"\"\"No parameters required - uses pre-uploaded files.\"\"\"\n\n async def read_files_tool() -> str:\n \"\"\"Read the content of uploaded files.\"\"\"\n try:\n result = self.load_files_message()\n if hasattr(result, \"get_text\"):\n return result.get_text()\n if hasattr(result, \"text\"):\n return result.text\n return str(result)\n except (FileNotFoundError, ValueError, OSError, RuntimeError) as e:\n return f\"Error reading files: {e}\"\n\n description = self.get_tool_description()\n\n tool = StructuredTool(\n name=\"load_files_message\",\n description=description,\n coroutine=read_files_tool,\n args_schema=EmptySchema,\n handle_tool_error=True,\n tags=[\"load_files_message\"],\n metadata={\n \"display_name\": \"Read File\",\n \"display_description\": description,\n },\n )\n\n return [tool]\n\n # ------------------------------ UI helpers --------------------------------------\n\n def _path_value(self, template: dict) -> list[str]:\n \"\"\"Return the list of currently selected file paths from the template.\"\"\"\n return template.get(\"path\", {}).get(\"file_path\", [])\n\n def update_build_config(\n self,\n build_config: dict[str, Any],\n field_value: Any,\n field_name: str | None = None,\n ) -> dict[str, Any]:\n \"\"\"Show/hide Advanced Parser and related fields based on selection context.\"\"\"\n if field_name == \"path\":\n paths = self._path_value(build_config)\n\n # If all files can be processed by docling, do so\n allow_advanced = all(not file_path.endswith((\".csv\", \".xlsx\", \".parquet\")) for file_path in paths)\n build_config[\"advanced_mode\"][\"show\"] = allow_advanced\n if not allow_advanced:\n build_config[\"advanced_mode\"][\"value\"] = False\n for f in (\"pipeline\", \"ocr_engine\", \"doc_key\", \"md_image_placeholder\", \"md_page_break_placeholder\"):\n if f in build_config:\n build_config[f][\"show\"] = False\n\n # Docling Processing\n elif field_name == \"advanced_mode\":\n for f in (\"pipeline\", \"ocr_engine\", \"doc_key\", \"md_image_placeholder\", \"md_page_break_placeholder\"):\n if f in build_config:\n build_config[f][\"show\"] = bool(field_value)\n if f == \"pipeline\":\n build_config[f][\"advanced\"] = not bool(field_value)\n\n elif field_name == \"pipeline\":\n if field_value == \"standard\":\n build_config[\"ocr_engine\"][\"show\"] = True\n build_config[\"ocr_engine\"][\"value\"] = \"easyocr\"\n else:\n build_config[\"ocr_engine\"][\"show\"] = False\n build_config[\"ocr_engine\"][\"value\"] = \"None\"\n\n return build_config\n\n def update_outputs(self, frontend_node: dict[str, Any], field_name: str, field_value: Any) -> dict[str, Any]: # noqa: ARG002\n \"\"\"Dynamically show outputs based on file count/type and advanced mode.\"\"\"\n if field_name not in [\"path\", \"advanced_mode\", \"pipeline\"]:\n return frontend_node\n\n template = frontend_node.get(\"template\", {})\n paths = self._path_value(template)\n if not paths:\n return frontend_node\n\n frontend_node[\"outputs\"] = []\n if len(paths) == 1:\n file_path = paths[0] if field_name == \"path\" else frontend_node[\"template\"][\"path\"][\"file_path\"][0]\n if file_path.endswith((\".csv\", \".xlsx\", \".parquet\")):\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"Structured Content\",\n name=\"dataframe\",\n method=\"load_files_structured\",\n tool_mode=True,\n ),\n )\n elif file_path.endswith(\".json\"):\n frontend_node[\"outputs\"].append(\n Output(display_name=\"Structured Content\", name=\"json\", method=\"load_files_json\", tool_mode=True),\n )\n\n advanced_mode = frontend_node.get(\"template\", {}).get(\"advanced_mode\", {}).get(\"value\", False)\n if advanced_mode:\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"Structured Output\",\n name=\"advanced_dataframe\",\n method=\"load_files_dataframe\",\n tool_mode=True,\n ),\n )\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"Markdown\", name=\"advanced_markdown\", method=\"load_files_markdown\", tool_mode=True\n ),\n )\n frontend_node[\"outputs\"].append(\n Output(display_name=\"File Path\", name=\"path\", method=\"load_files_path\", tool_mode=True),\n )\n else:\n frontend_node[\"outputs\"].append(\n Output(display_name=\"Raw Content\", name=\"message\", method=\"load_files_message\", tool_mode=True),\n )\n frontend_node[\"outputs\"].append(\n Output(display_name=\"File Path\", name=\"path\", method=\"load_files_path\", tool_mode=True),\n )\n else:\n # Multiple files => DataFrame output; advanced parser disabled\n frontend_node[\"outputs\"].append(\n Output(display_name=\"Files\", name=\"dataframe\", method=\"load_files\", tool_mode=True)\n )\n\n return frontend_node\n\n # ------------------------------ Core processing ----------------------------------\n\n def _validate_and_resolve_paths(self) -> list[BaseFileComponent.BaseFile]:\n \"\"\"Override to handle file_path_str input from tool mode.\n\n When called as a tool, the file_path_str parameter can be set.\n If not provided, it will fall back to using the path FileInput (uploaded file).\n Priority:\n 1. file_path_str (if provided by the tool call)\n 2. path (uploaded file from UI)\n \"\"\"\n # Check if file_path_str is provided (from tool mode)\n file_path_str = getattr(self, \"file_path_str\", None)\n if file_path_str:\n # Use the string path from tool mode\n from pathlib import Path\n\n from lfx.schema.data import Data\n\n resolved_path = Path(self.resolve_path(file_path_str))\n if not resolved_path.exists():\n msg = f\"File or directory not found: {file_path_str}\"\n self.log(msg)\n if not self.silent_errors:\n raise ValueError(msg)\n return []\n\n data_obj = Data(data={self.SERVER_FILE_PATH_FIELDNAME: str(resolved_path)})\n return [BaseFileComponent.BaseFile(data_obj, resolved_path, delete_after_processing=False)]\n\n # Otherwise use the default implementation (uses path FileInput)\n return super()._validate_and_resolve_paths()\n\n def _is_docling_compatible(self, file_path: str) -> bool:\n \"\"\"Lightweight extension gate for Docling-compatible types.\"\"\"\n docling_exts = (\n \".adoc\",\n \".asciidoc\",\n \".asc\",\n \".bmp\",\n \".csv\",\n \".dotx\",\n \".dotm\",\n \".docm\",\n \".docx\",\n \".htm\",\n \".html\",\n \".jpg\",\n \".jpeg\",\n \".json\",\n \".md\",\n \".pdf\",\n \".png\",\n \".potx\",\n \".ppsx\",\n \".pptm\",\n \".potm\",\n \".ppsm\",\n \".pptx\",\n \".tiff\",\n \".txt\",\n \".xls\",\n \".xlsx\",\n \".xhtml\",\n \".xml\",\n \".webp\",\n )\n return file_path.lower().endswith(docling_exts)\n\n async def _get_local_file_for_docling(self, file_path: str) -> tuple[str, bool]:\n \"\"\"Get a local file path for Docling processing, downloading from S3 if needed.\n\n Args:\n file_path: Either a local path or S3 key (format \"flow_id/filename\")\n\n Returns:\n tuple[str, bool]: (local_path, should_delete) where should_delete indicates\n if this is a temporary file that should be cleaned up\n \"\"\"\n settings = get_settings_service().settings\n if settings.storage_type == \"local\":\n return file_path, False\n\n # S3 storage - download to temp file\n parsed = parse_storage_path(file_path)\n if not parsed:\n msg = f\"Invalid S3 path format: {file_path}. Expected 'flow_id/filename'\"\n raise ValueError(msg)\n\n storage_service = get_storage_service()\n flow_id, filename = parsed\n\n # Get file content from S3\n content = await storage_service.get_file(flow_id, filename)\n\n suffix = Path(filename).suffix\n with NamedTemporaryFile(mode=\"wb\", suffix=suffix, delete=False) as tmp_file:\n tmp_file.write(content)\n temp_path = tmp_file.name\n\n return temp_path, True\n\n def _process_docling_in_subprocess(self, file_path: str) -> Data | None:\n \"\"\"Run Docling in a separate OS process and map the result to a Data object.\n\n We avoid multiprocessing pickling by launching `python -c \"