{ "data": { "edges": [ { "animated": false, "className": "", "data": { "sourceHandle": { "dataType": "ChatInput", "id": "ChatInput-MBlfD", "name": "message", "output_types": [ "Message" ] }, "targetHandle": { "fieldName": "search_query", "id": "ArXivComponent-zGqWh", "inputTypes": [ "Message" ], "type": "str" } }, "id": "reactflow__edge-ChatInput-MBlfD{œdataTypeœ:œChatInputœ,œidœ:œChatInput-MBlfDœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-ArXivComponent-zGqWh{œfieldNameœ:œsearch_queryœ,œidœ:œArXivComponent-zGqWhœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "ChatInput-MBlfD", "sourceHandle": "{œdataTypeœ: œChatInputœ, œidœ: œChatInput-MBlfDœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", "target": "ArXivComponent-zGqWh", "targetHandle": "{œfieldNameœ: œsearch_queryœ, œidœ: œArXivComponent-zGqWhœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, "className": "", "data": { "sourceHandle": { "dataType": "LoopComponent", "id": "LoopComponent-E7Ur0", "name": "item", "output_types": [ "JSON" ] }, "targetHandle": { "fieldName": "input_data", "id": "ParserComponent-V2o48", "inputTypes": [ "DataFrame", "Table", "Data", "JSON" ], "type": "other" } }, "id": "reactflow__edge-LoopComponent-E7Ur0{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-E7Ur0œ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ]}-ParserComponent-V2o48{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-V2o48œ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "LoopComponent-E7Ur0", "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-E7Ur0œ, œnameœ: œitemœ, œoutput_typesœ: [œJSONœ]}", "target": "ParserComponent-V2o48", "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-V2o48œ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "animated": false, "className": "", "data": { "sourceHandle": { "dataType": "ParserComponent", "id": "ParserComponent-V2o48", "name": "parsed_text", "output_types": [ "Message" ] }, "targetHandle": { "fieldName": "input_value", "id": "LanguageModelComponent-ps5yd", "inputTypes": [ "Message" ], "type": "str" } }, "id": "reactflow__edge-ParserComponent-V2o48{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-V2o48œ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-LanguageModelComponent-ps5yd{œfieldNameœ:œinput_valueœ,œidœ:œLanguageModelComponent-ps5ydœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "ParserComponent-V2o48", "sourceHandle": "{œdataTypeœ: œParserComponentœ, œidœ: œParserComponent-V2o48œ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", "target": "LanguageModelComponent-ps5yd", "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œLanguageModelComponent-ps5ydœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, "className": "", "data": { "sourceHandle": { "dataType": "LanguageModelComponent", "id": "LanguageModelComponent-ps5yd", "name": "text_output", "output_types": [ "Message" ] }, "targetHandle": { "dataType": "LoopComponent", "id": "LoopComponent-E7Ur0", "name": "item", "output_types": [ "Data", "Message" ] } }, "id": "reactflow__edge-LanguageModelComponent-ps5yd{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-ps5ydœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-LoopComponent-E7Ur0{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-E7Ur0œ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ,œMessageœ]}", "selected": false, "source": "LanguageModelComponent-ps5yd", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-ps5ydœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "LoopComponent-E7Ur0", "targetHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-E7Ur0œ, œnameœ: œitemœ, œoutput_typesœ: [œDataœ, œMessageœ]}" }, { "animated": false, "className": "", "data": { "sourceHandle": { "dataType": "ArXivComponent", "id": "ArXivComponent-zGqWh", "name": "dataframe", "output_types": [ "Table" ] }, "targetHandle": { "fieldName": "data", "id": "LoopComponent-E7Ur0", "inputTypes": [ "DataFrame", "Table", "Data", "Message" ], "type": "other" } }, "id": "reactflow__edge-ArXivComponent-zGqWh{œdataTypeœ:œArXivComponentœ,œidœ:œArXivComponent-zGqWhœ,œnameœ:œdataframeœ,œoutput_typesœ:[œTableœ]}-LoopComponent-E7Ur0{œfieldNameœ:œdataœ,œidœ:œLoopComponent-E7Ur0œ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "ArXivComponent-zGqWh", "sourceHandle": "{œdataTypeœ: œArXivComponentœ, œidœ: œArXivComponent-zGqWhœ, œnameœ: œdataframeœ, œoutput_typesœ: [œTableœ]}", "target": "LoopComponent-E7Ur0", "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œLoopComponent-E7Ur0œ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, "className": "", "data": { "sourceHandle": { "dataType": "LoopComponent", "id": "LoopComponent-E7Ur0", "name": "done", "output_types": [ "Table" ] }, "targetHandle": { "fieldName": "input_value", "id": "ChatOutput-scvYG", "inputTypes": [ "Data", "JSON", "DataFrame", "Table", "Message" ], "type": "other" } }, "id": "reactflow__edge-LoopComponent-E7Ur0{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-E7Ur0œ,œnameœ:œdoneœ,œoutput_typesœ:[œDataFrameœ]}-ChatOutput-scvYG{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-scvYGœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "LoopComponent-E7Ur0", "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-E7Ur0œ, œnameœ: œdoneœ, œoutput_typesœ: [œTableœ]}", "target": "ChatOutput-scvYG", "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-scvYGœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], "nodes": [ { "data": { "id": "ArXivComponent-zGqWh", "node": { "base_classes": [ "DataFrame", "Table" ], "beta": false, "conditional_paths": [], "custom_fields": {}, "description": "Search and retrieve papers from arXiv.org.", "display_name": "arXiv", "documentation": "", "edited": false, "field_order": [ "search_query", "search_type", "max_results" ], "frozen": false, "icon": "arXiv", "legacy": false, "lf_version": "1.7.0", "metadata": { "code_hash": "9475ee946974", "dependencies": { "dependencies": [ { "name": "defusedxml", "version": "0.7.1" }, { "name": "lfx", "version": null } ], "total_dependencies": 2 }, "module": "lfx_arxiv.components.arxiv.arxiv.ArXivComponent" }, "minimized": false, "output_types": [], "outputs": [ { "allows_loop": false, "cache": true, "display_name": "Table", "group_outputs": false, "method": "search_papers_dataframe", "name": "dataframe", "selected": "Table", "tool_mode": true, "types": [ "Table" ], "value": "__UNDEFINED__" } ], "pinned": false, "template": { "_type": "Component", "code": { "advanced": true, "api_editable": false, "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": "import urllib\nfrom xml.etree.ElementTree import Element\n\nfrom defusedxml.ElementTree import fromstring\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\nARXIV_REQUEST_TIMEOUT_SECONDS = 10\n\n\nclass ArXivComponent(Component):\n display_name = \"arXiv\"\n description = \"Search and retrieve papers from arXiv.org.\"\n icon = \"arXiv\"\n\n inputs = [\n MessageTextInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n info=\"The search query for arXiv papers (e.g., 'quantum computing')\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_type\",\n display_name=\"Search Field\",\n info=\"The field to search in\",\n options=[\"all\", \"title\", \"abstract\", \"author\", \"cat\"], # cat is for category\n value=\"all\",\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"Maximum number of results to return\",\n value=10,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"search_papers_dataframe\"),\n ]\n\n def build_query_url(self) -> str:\n \"\"\"Build the arXiv API query URL.\"\"\"\n base_url = \"http://export.arxiv.org/api/query?\"\n\n # Build the search query based on search type\n if self.search_type == \"all\":\n search_query = self.search_query # No prefix for all fields\n else:\n # Map dropdown values to ArXiv API prefixes\n prefix_map = {\"title\": \"ti\", \"abstract\": \"abs\", \"author\": \"au\", \"cat\": \"cat\"}\n prefix = prefix_map.get(self.search_type, \"\")\n search_query = f\"{prefix}:{self.search_query}\"\n\n # URL parameters\n params = {\n \"search_query\": search_query,\n \"max_results\": str(self.max_results),\n }\n\n # Convert params to URL query string\n query_string = \"&\".join([f\"{k}={urllib.parse.quote(str(v))}\" for k, v in params.items()])\n\n return base_url + query_string\n\n def parse_atom_response(self, response_text: str) -> list[dict]:\n \"\"\"Parse the Atom XML response from arXiv.\"\"\"\n # Parse XML safely using defusedxml\n root = fromstring(response_text)\n\n # Define namespace dictionary for XML parsing\n ns = {\"atom\": \"http://www.w3.org/2005/Atom\", \"arxiv\": \"http://arxiv.org/schemas/atom\"}\n\n papers = []\n # Process each entry (paper)\n for entry in root.findall(\"atom:entry\", ns):\n paper = {\n \"id\": self._get_text(entry, \"atom:id\", ns),\n \"title\": self._get_text(entry, \"atom:title\", ns),\n \"summary\": self._get_text(entry, \"atom:summary\", ns),\n \"published\": self._get_text(entry, \"atom:published\", ns),\n \"updated\": self._get_text(entry, \"atom:updated\", ns),\n \"authors\": [author.find(\"atom:name\", ns).text for author in entry.findall(\"atom:author\", ns)],\n \"arxiv_url\": self._get_link(entry, \"alternate\", ns),\n \"pdf_url\": self._get_link(entry, \"related\", ns),\n \"comment\": self._get_text(entry, \"arxiv:comment\", ns),\n \"journal_ref\": self._get_text(entry, \"arxiv:journal_ref\", ns),\n \"primary_category\": self._get_category(entry, ns),\n \"categories\": [cat.get(\"term\") for cat in entry.findall(\"atom:category\", ns)],\n }\n papers.append(paper)\n\n return papers\n\n def _get_text(self, element: Element, path: str, ns: dict) -> str | None:\n \"\"\"Safely extract text from an XML element.\"\"\"\n el = element.find(path, ns)\n return el.text.strip() if el is not None and el.text else None\n\n def _get_link(self, element: Element, rel: str, ns: dict) -> str | None:\n \"\"\"Get link URL based on relation type.\"\"\"\n for link in element.findall(\"atom:link\", ns):\n if link.get(\"rel\") == rel:\n return link.get(\"href\")\n return None\n\n def _get_category(self, element: Element, ns: dict) -> str | None:\n \"\"\"Get primary category.\"\"\"\n cat = element.find(\"arxiv:primary_category\", ns)\n return cat.get(\"term\") if cat is not None else None\n\n def run_model(self) -> DataFrame:\n return self.search_papers_dataframe()\n\n def search_papers(self) -> list[Data]:\n \"\"\"Search arXiv and return results.\"\"\"\n try:\n # Build the query URL\n url = self.build_query_url()\n\n # Validate URL scheme and host\n parsed_url = urllib.parse.urlparse(url)\n if parsed_url.scheme not in {\"http\", \"https\"}:\n error_msg = f\"Invalid URL scheme: {parsed_url.scheme}\"\n raise ValueError(error_msg)\n if parsed_url.hostname != \"export.arxiv.org\":\n error_msg = f\"Invalid host: {parsed_url.hostname}\"\n raise ValueError(error_msg)\n\n # Create a custom opener that only allows http/https schemes\n class RestrictedHTTPHandler(urllib.request.HTTPHandler):\n def http_open(self, req):\n return super().http_open(req)\n\n class RestrictedHTTPSHandler(urllib.request.HTTPSHandler):\n def https_open(self, req):\n return super().https_open(req)\n\n # Build opener with restricted handlers\n opener = urllib.request.build_opener(RestrictedHTTPHandler, RestrictedHTTPSHandler)\n urllib.request.install_opener(opener)\n\n # Make the request with validated URL using restricted opener\n response = opener.open(url, timeout=ARXIV_REQUEST_TIMEOUT_SECONDS)\n response_text = response.read().decode(\"utf-8\")\n\n # Parse the response\n papers = self.parse_atom_response(response_text)\n\n # Convert to Data objects\n results = [Data(data=paper) for paper in papers]\n self.status = results\n except (TimeoutError, urllib.error.URLError, ValueError) as e:\n error_data = Data(data={\"error\": f\"Request error: {e!s}\"})\n self.status = error_data\n return [error_data]\n else:\n return results\n\n def search_papers_dataframe(self) -> DataFrame:\n \"\"\"Convert the Arxiv search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.search_papers()\n return DataFrame(data)\n" }, "max_results": { "_input_type": "IntInput", "advanced": false, "display_name": "Max Results", "dynamic": false, "info": "Maximum number of results to return", "list": false, "list_add_label": "Add More", "load_from_db": false, "name": "max_results", "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "type": "int", "value": 3 }, "search_query": { "_input_type": "MessageTextInput", "advanced": false, "display_name": "Search Query", "dynamic": false, "info": "The search query for arXiv papers (e.g., 'quantum computing')", "input_types": [ "Message" ], "list": false, "list_add_label": "Add More", "load_from_db": false, "name": "search_query", "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": true, "trace_as_input": true, "trace_as_metadata": true, "type": "str", "value": "" }, "search_type": { "_input_type": "DropdownInput", "advanced": false, "combobox": false, "dialog_inputs": {}, "display_name": "Search Field", "dynamic": false, "info": "The field to search in", "name": "search_type", "options": [ "all", "title", "abstract", "author", "cat" ], "options_metadata": [], "placeholder": "", "required": false, "show": true, "title_case": false, "toggle": false, "tool_mode": false, "trace_as_metadata": true, "type": "str", "value": "all" } }, "tool_mode": false }, "showNode": true, "type": "ArXivComponent" }, "dragging": false, "id": "ArXivComponent-zGqWh", "measured": { "height": 371, "width": 320 }, "position": { "x": 81.59312530546094, "y": 3.9397854556273906 }, "selected": false, "type": "genericNode" }, { "data": { "id": "ChatOutput-scvYG", "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", "context_id", "data_template", "clean_data" ], "frozen": false, "icon": "MessagesSquare", "legacy": false, "lf_version": "1.7.0", "metadata": { "code_hash": "0aab1c78e1dc", "component_class_name": "ChatOutput", "dependencies": { "dependencies": [ { "name": "orjson", "version": "3.11.9" }, { "name": "fastapi", "version": "0.139.2" }, { "name": "lfx", "version": null } ], "total_dependencies": 3 }, "module": "lfx.components.input_output.chat_output.ChatOutput" }, "minimized": true, "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, "api_editable": false, "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\", \"JSON\", \"DataFrame\", \"Table\", \"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 # Preserve existing session_id from the incoming message if it exists\n existing_session_id = message.session_id\n else:\n message = Message(text=text)\n existing_session_id = None\n\n # Set message properties\n message.sender = self.sender\n message.sender_name = self.sender_name\n # Preserve session_id from incoming message, or use component/graph session_id\n message.session_id = (\n self.session_id or existing_session_id or (self.graph.session_id if hasattr(self, \"graph\") else None) or \"\"\n )\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 # Set accumulated token usage from all upstream LLM vertices.\n # This must happen AFTER send_message() because streaming captures\n # usage from chunks and would overwrite accumulated totals.\n if hasattr(self, \"_vertex\") and self._vertex is not None:\n accumulated_usage = self._vertex._accumulate_upstream_token_usage() # noqa: SLF001\n if accumulated_usage:\n message.properties.usage = accumulated_usage\n if self.should_store_message and message.get_id():\n message = await self._update_stored_message(message)\n await self._send_message_event(message, id_=message.get_id())\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 clean_data: bool = getattr(self, \"clean_data\", False)\n if isinstance(self.input_value, list):\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, clean_data=clean_data)\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, "list_add_label": "Add More", "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": "HandleInput", "advanced": false, "display_name": "Inputs", "dynamic": false, "info": "Message to be passed as output.", "input_types": [ "Data", "JSON", "DataFrame", "Table", "Message" ], "list": false, "list_add_label": "Add More", "name": "input_value", "placeholder": "", "required": true, "show": true, "title_case": false, "trace_as_metadata": true, "type": "other", "value": "" }, "sender": { "_input_type": "DropdownInput", "advanced": true, "combobox": false, "dialog_inputs": {}, "display_name": "Sender Type", "dynamic": false, "info": "Type of sender.", "name": "sender", "options": [ "Machine", "User" ], "options_metadata": [], "placeholder": "", "required": false, "show": true, "title_case": false, "toggle": 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, "list_add_label": "Add More", "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, "list_add_label": "Add More", "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, "list_add_label": "Add More", "name": "should_store_message", "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "type": "bool", "value": true } }, "tool_mode": false }, "showNode": false, "type": "ChatOutput" }, "dragging": false, "id": "ChatOutput-scvYG", "measured": { "height": 52, "width": 192 }, "position": { "x": 1161.842067639501, "y": 380.6300719873473 }, "selected": false, "type": "genericNode" }, { "data": { "id": "ChatInput-MBlfD", "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", "should_store_message", "sender", "sender_name", "session_id", "context_id", "files" ], "frozen": false, "icon": "MessagesSquare", "legacy": false, "lf_version": "1.7.0", "metadata": { "code_hash": "45974e6b1909", "component_class_name": "ChatInput", "dependencies": { "dependencies": [ { "name": "lfx", "version": null } ], "total_dependencies": 1 }, "module": "lfx.components.input_output.chat.ChatInput" }, "minimized": true, "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, "api_editable": false, "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.image import get_file_paths\nfrom lfx.schema.message import Message\nfrom lfx.services.deps import get_settings_service\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\nfrom lfx.utils.file_path_security import (\n component_file_access_scopes,\n enforce_local_file_access,\n is_local_file_access_restricted,\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 # Build endpoints inject caller-controlled file references directly into\n # ChatInput. Resolve and validate local references before the Message can\n # later read them while constructing model attachment content.\n if files:\n settings_service = get_settings_service()\n storage_type = getattr(getattr(settings_service, \"settings\", None), \"storage_type\", \"local\")\n # The storage factory falls back to local storage for unsupported\n # values, so only the explicitly configured S3 backend may bypass\n # filesystem containment.\n if str(storage_type).lower() != \"s3\" and is_local_file_access_restricted():\n scope_ids = list(component_file_access_scopes(self))\n # Public executions replace graph.flow_id with a per-visitor\n # virtual ID. Only the server-populated source provenance may\n # restore the already-validated public attachment namespace.\n source_flow_id = getattr(self.graph, \"source_flow_id\", None)\n if source_flow_id:\n scope_ids.append(source_flow_id)\n for file_path in get_file_paths(files):\n enforce_local_file_access(file_path, scope_ids=scope_ids)\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": { "_input_type": "FileInput", "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, "list_add_label": "Add More", "name": "files", "placeholder": "", "required": false, "show": true, "temp_file": true, "title_case": false, "trace_as_metadata": true, "type": "file", "value": "" }, "input_value": { "_input_type": "MultilineInput", "advanced": false, "copy_field": false, "display_name": "Input Text", "dynamic": false, "info": "Message to be passed as input.", "input_types": [], "list": false, "list_add_label": "Add More", "load_from_db": false, "multiline": true, "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": "ai" }, "sender": { "_input_type": "DropdownInput", "advanced": true, "combobox": false, "dialog_inputs": {}, "display_name": "Sender Type", "dynamic": false, "info": "Type of sender.", "name": "sender", "options": [ "Machine", "User" ], "options_metadata": [], "placeholder": "", "required": false, "show": true, "title_case": false, "toggle": false, "tool_mode": false, "trace_as_metadata": true, "type": "str", "value": "User" }, "sender_name": { "_input_type": "MessageTextInput", "advanced": true, "display_name": "Sender Name", "dynamic": false, "info": "Name of the sender.", "input_types": [ "Message" ], "list": false, "list_add_label": "Add More", "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": "User" }, "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, "list_add_label": "Add More", "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, "list_add_label": "Add More", "name": "should_store_message", "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "type": "bool", "value": true } }, "tool_mode": false }, "showNode": true, "type": "ChatInput" }, "dragging": false, "id": "ChatInput-MBlfD", "measured": { "height": 207, "width": 320 }, "position": { "x": -303.21079570415833, "y": 107.75353484470551 }, "selected": false, "type": "genericNode" }, { "data": { "id": "note-Hiyq7", "node": { "description": "# 📖 README\nThis template translates research paper summaries on ArXiv into Portuguese and summarizes them. \n Using **Langflow’s looping mechanism**, the template iterates through multiple research papers, translates them with the **OpenAI** model component, and outputs an aggregated version of all translated papers. \n\n## Quick start\n1. Configure your **Model Provider** with your API credentials. \n2. In the **Playground**, enter a query related to a research topic (for example, “Quantum Computing Advancements”). \n\n The flow fetches a list of research papers from ArXiv matching the query. Each paper in the retrieved list is processed one-by-one using the Langflow **Loop component**. \n\n The abstract of each paper is translated into Portuguese by the **OpenAI** model component. \n\n Once all papers are translated, the system aggregates them into a **single structured output**.", "display_name": "", "documentation": "", "i18n_key": "template_notes.research_translation_loop.60bb882f", "template": {} }, "type": "note" }, "dragging": false, "height": 647, "id": "note-Hiyq7", "measured": { "height": 647, "width": 576 }, "position": { "x": -890.9006297459302, "y": -233.44894493951168 }, "resizing": false, "selected": false, "type": "noteNode", "width": 576 }, { "data": { "id": "LanguageModelComponent-ps5yd", "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": "https://docs.langflow.org/components-models", "edited": false, "field_order": [ "model", "model_name", "provider", "api_key", "base_url_ibm_watsonx", "project_id", "ollama_base_url", "input_value", "system_message", "stream", "temperature", "max_tokens" ], "frozen": false, "icon": "brain-circuit", "last_updated": "2026-06-26T12:11:44.628Z", "legacy": false, "lf_version": "1.7.0", "metadata": { "code_hash": "1bfa98a19c39", "component_class_name": "LanguageModelComponent", "dependencies": { "dependencies": [ { "name": "lfx", "version": null } ], "total_dependencies": 1 }, "keywords": [ "model", "llm", "language model", "large language model" ], "model_provider_policy_mode": "delegate", "module": "lfx.components.models_and_agents.language_model.LanguageModelComponent" }, "minimized": false, "output_types": [], "outputs": [ { "allows_loop": false, "cache": true, "display_name": "Model Response", "group_outputs": false, "hidden": null, "loop_types": null, "method": "text_response", "name": "text_output", "options": null, "required_inputs": null, "selected": "Message", "tool_mode": true, "types": [ "Message" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, "display_name": "Language Model", "group_outputs": false, "hidden": null, "loop_types": null, "method": "build_model", "name": "model_output", "options": null, "required_inputs": null, "selected": "LanguageModel", "tool_mode": true, "types": [ "LanguageModel" ], "value": "__UNDEFINED__" } ], "pinned": false, "priority": 0, "template": { "_frontend_node_flow_id": { "value": "7eb7c081-888c-4f25-ba81-c84e6aab2778" }, "_frontend_node_folder_id": { "value": "511db528-db06-4bf9-bdf0-f13221918e76" }, "_type": "Component", "api_key": { "_input_type": "SecretStrInput", "advanced": true, "display_name": "API Key", "dynamic": false, "info": "Overrides global provider settings. Leave blank to use your pre-configured API Key.", "input_types": [], "load_from_db": false, "name": "api_key", "override_skip": false, "password": true, "placeholder": "", "real_time_refresh": true, "required": false, "show": false, "title_case": false, "track_in_telemetry": false, "type": "str", "value": "" }, "base_url_ibm_watsonx": { "_input_type": "DropdownInput", "advanced": false, "combobox": false, "dialog_inputs": {}, "display_name": "watsonx API Endpoint", "dynamic": false, "external_options": {}, "info": "The base URL of the API (IBM watsonx.ai only)", "name": "base_url_ibm_watsonx", "options": [ "https://us-south.ml.cloud.ibm.com", "https://eu-de.ml.cloud.ibm.com", "https://eu-gb.ml.cloud.ibm.com", "https://au-syd.ml.cloud.ibm.com", "https://jp-tok.ml.cloud.ibm.com", "https://ca-tor.ml.cloud.ibm.com" ], "options_metadata": [], "override_skip": false, "placeholder": "", "real_time_refresh": true, "required": false, "show": false, "title_case": false, "toggle": false, "tool_mode": false, "trace_as_metadata": true, "track_in_telemetry": true, "type": "str", "value": "https://us-south.ml.cloud.ibm.com" }, "code": { "advanced": true, "api_editable": false, "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.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_llm,\n handle_model_input_update,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.models_and_agents.model_selection import apply_model_overrides\nfrom lfx.field_typing.constants import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\n\nclass LanguageModelComponent(LCModelComponent):\n model_provider_policy_mode = \"delegate\"\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\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Language Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n ),\n StrInput(\n name=\"model_name\",\n display_name=\"Model Name Override\",\n info=(\n \"Optional model name to use instead of the selected model. \"\n \"Can be set from a global variable for runtime model selection.\"\n ),\n advanced=True,\n load_from_db=False,\n ),\n StrInput(\n name=\"provider\",\n display_name=\"Provider Override\",\n info=(\n \"Optional provider to use with Model Name Override. Leave blank to use the selected model's provider.\"\n ),\n advanced=True,\n load_from_db=False,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Overrides global provider settings. Leave blank to use your pre-configured API Key.\",\n required=False,\n show=True,\n real_time_refresh=True,\n advanced=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 combobox=True,\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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API (Ollama only)\",\n show=False,\n real_time_refresh=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 IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Set to 0 for no explicit limit. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=0, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n model = apply_model_overrides(\n self.model,\n model_name=getattr(self, \"model_name\", None),\n provider=getattr(self, \"provider\", None),\n user_id=self.user_id,\n get_options=get_language_model_options,\n )\n return get_llm(\n model=model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return handle_model_input_update(self, build_config, field_value, field_name)\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", "override_skip": false, "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, "track_in_telemetry": false, "type": "str", "value": "" }, "is_refresh": false, "model": { "_input_type": "ModelInput", "advanced": false, "display_name": "Language Model", "dynamic": false, "external_options": { "fields": { "data": { "node": { "display_name": "Connect other models", "icon": "CornerDownLeft", "name": "connect_other_models" } } } }, "info": "Select your model provider", "input_types": [ "LanguageModel" ], "list": false, "list_add_label": "Add More", "model_type": "language", "name": "model", "options": [], "override_skip": false, "placeholder": "Setup Provider", "real_time_refresh": true, "refresh_button": true, "required": true, "show": true, "title_case": false, "tool_mode": false, "trace_as_input": true, "track_in_telemetry": false, "type": "model", "value": "" }, "model_name": { "_input_type": "StrInput", "advanced": true, "display_name": "Model Name Override", "dynamic": false, "info": "Optional model name to use instead of the selected model. Can be set from a global variable for runtime model selection.", "list": false, "list_add_label": "Add More", "load_from_db": false, "name": "model_name", "override_skip": false, "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "track_in_telemetry": false, "type": "str", "value": "" }, "ollama_base_url": { "_input_type": "StrInput", "advanced": false, "display_name": "Ollama API URL", "dynamic": false, "info": "Endpoint of the Ollama API (Ollama only)", "input_types": [], "list": false, "list_add_label": "Add More", "load_from_db": false, "name": "ollama_base_url", "override_skip": false, "placeholder": "", "real_time_refresh": true, "required": false, "show": false, "title_case": false, "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, "track_in_telemetry": false, "type": "str", "value": "" }, "project_id": { "_input_type": "StrInput", "advanced": false, "display_name": "watsonx Project ID", "dynamic": false, "info": "The project ID associated with the foundation model (IBM watsonx.ai only)", "list": false, "list_add_label": "Add More", "load_from_db": false, "name": "project_id", "override_skip": false, "placeholder": "", "required": false, "show": false, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "track_in_telemetry": false, "type": "str", "value": "" }, "provider": { "_input_type": "StrInput", "advanced": true, "display_name": "Provider Override", "dynamic": false, "info": "Optional provider to use with Model Name Override. Leave blank to use the selected model's provider.", "list": false, "list_add_label": "Add More", "load_from_db": false, "name": "provider", "override_skip": false, "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "track_in_telemetry": false, "type": "str", "value": "" }, "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", "override_skip": false, "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "track_in_telemetry": true, "type": "bool", "value": false }, "system_message": { "_input_type": "MultilineInput", "advanced": false, "ai_enabled": 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", "override_skip": false, "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, "track_in_telemetry": false, "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", "override_skip": false, "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, "track_in_telemetry": false, "type": "slider", "value": 0.1 } }, "tool_mode": false }, "selected_output": "text_output", "showNode": true, "type": "LanguageModelComponent" }, "dragging": false, "id": "LanguageModelComponent-ps5yd", "measured": { "height": 371, "width": 320 }, "position": { "x": 1630.638479427766, "y": 116.0160835115366 }, "selected": false, "type": "genericNode" }, { "data": { "id": "LoopComponent-E7Ur0", "node": { "base_classes": [ "Data", "DataFrame", "JSON", "Table" ], "beta": false, "conditional_paths": [], "custom_fields": {}, "description": "Iterates through Data or Message objects, processing items individually and aggregating results from loop inputs.", "display_name": "Loop", "documentation": "https://docs.langflow.org/loop", "edited": false, "field_order": [ "data" ], "frozen": false, "icon": "infinity", "legacy": false, "metadata": { "code_hash": "2712a1de7210", "component_class_name": "LoopComponent", "dependencies": { "dependencies": [ { "name": "lfx", "version": null } ], "total_dependencies": 1 }, "module": "lfx.components.flow_controls.loop.LoopComponent" }, "minimized": false, "output_types": [], "outputs": [ { "allows_loop": true, "cache": true, "display_name": "Item", "group_outputs": true, "loop_types": [ "Message" ], "method": "item_output", "name": "item", "selected": "JSON", "tool_mode": true, "types": [ "Data", "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, "display_name": "Done", "group_outputs": true, "method": "done_output", "name": "done", "selected": "Table", "tool_mode": true, "types": [ "DataFrame", "Table" ], "value": "__UNDEFINED__" } ], "pinned": false, "template": { "_type": "Component", "code": { "advanced": true, "api_editable": false, "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.flow_controls.loop_utils import (\n execute_loop_body,\n extract_loop_output,\n get_loop_body_start_edge,\n get_loop_body_start_vertex,\n get_loop_body_vertices,\n validate_data_input,\n)\nfrom lfx.components.processing.converter import convert_to_data\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass LoopComponent(Component):\n display_name = \"Loop\"\n description = (\n \"Iterates through Data or Message objects, processing items individually \"\n \"and aggregating results from loop inputs.\"\n )\n documentation: str = \"https://docs.langflow.org/loop\"\n icon = \"infinity\"\n\n inputs = [\n HandleInput(\n name=\"data\",\n display_name=\"Inputs\",\n info=\"Data to iterate over. Accepts DataFrame, Table, Data, or Message.\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"Message\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Item\",\n name=\"item\",\n method=\"item_output\",\n types=[\"Data\"],\n allows_loop=True,\n loop_types=[\"Message\"],\n group_outputs=True,\n ),\n Output(\n display_name=\"Done\",\n name=\"done\",\n method=\"done_output\",\n types=[\"DataFrame\", \"Table\"],\n group_outputs=True,\n ),\n ]\n\n def initialize_data(self) -> None:\n \"\"\"Initialize the data list and context index.\n\n Seeds the input list and index counter in ctx. The aggregated results\n are owned by `_iterate`, which writes them once the subgraph finishes.\n \"\"\"\n if self.ctx.get(f\"{self._id}_initialized\", False):\n return\n\n # Ensure data is a list of Data objects\n data_list = self._validate_data(self.data)\n\n # Store the initial data and context variables\n self.update_ctx(\n {\n f\"{self._id}_data\": data_list,\n f\"{self._id}_index\": 0,\n f\"{self._id}_initialized\": True,\n }\n )\n\n def _convert_message_to_data(self, message: Message) -> Data:\n \"\"\"Convert a Message object to a Data object using Type Convert logic.\"\"\"\n return convert_to_data(message, auto_parse=False)\n\n def _validate_data(self, data):\n \"\"\"Validate and normalize the input to a flat list of Data objects.\n\n Coerces each accepted input type into Data so `validate_data_input`\n always receives a homogeneous payload:\n - Message -> a clean Data via `_convert_message_to_data` (just the\n message payload, not the full envelope). `Message` subclasses\n `Data`, so without this it would slip through as a single Data\n carrying all of the message metadata.\n - DataFrame/Table -> expanded to its rows.\n - Data / list[Data] -> passed through unchanged.\n\n Lists are normalized item-by-item so a mixed `[Message, DataFrame,\n Data]` still yields `list[Data]` instead of being rejected as a\n mixed-type list (`validate_data_input` requires every list item to be\n a Data, and DataFrame is not a Data subclass).\n \"\"\"\n if isinstance(data, Message):\n data = self._convert_message_to_data(data)\n elif isinstance(data, list):\n normalized: list = []\n for item in data:\n if isinstance(item, Message):\n normalized.append(self._convert_message_to_data(item))\n elif isinstance(item, DataFrame):\n normalized.extend(item.to_data_list())\n else:\n normalized.append(item)\n data = normalized\n return validate_data_input(data)\n\n def get_loop_body_vertices(self) -> set[str]:\n \"\"\"Identify vertices in this loop's body via graph traversal.\n\n Traverses from the loop's \"item\" output to the vertex that feeds back\n to the loop's \"item\" input, collecting all vertices in between.\n This naturally handles nested loops by stopping at this loop's feedback edge.\n\n Returns:\n Set of vertex IDs that form this loop's body\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return set()\n\n return get_loop_body_vertices(\n vertex=self._vertex,\n graph=self.graph,\n get_incoming_edge_by_target_param_fn=self.get_incoming_edge_by_target_param,\n )\n\n def _get_loop_body_start_vertex(self) -> str | None:\n \"\"\"Get the first vertex in the loop body (connected to loop's item output).\n\n Returns:\n The vertex ID of the first vertex in the loop body, or None if not found\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return None\n\n return get_loop_body_start_vertex(vertex=self._vertex)\n\n def _extract_loop_output(self, results: list) -> Data:\n \"\"\"Extract the output from subgraph execution results.\n\n Args:\n results: List of VertexBuildResult objects from subgraph execution\n\n Returns:\n Data object containing the loop iteration output\n \"\"\"\n # Get the vertex ID that feeds back to the item input (end of loop body)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n return extract_loop_output(results=results, end_vertex_id=end_vertex_id)\n\n async def execute_loop_body(self, data_list: list[Data], event_manager=None) -> list[Data]:\n \"\"\"Execute loop body for each data item.\n\n Creates an isolated subgraph for the loop body and executes it\n for each item in the data list, collecting results.\n\n Args:\n data_list: List of Data objects to iterate over\n event_manager: Optional event manager to pass to subgraph execution for UI events\n\n Returns:\n List of Data objects containing results from each iteration\n \"\"\"\n # Get the loop body configuration once\n loop_body_vertex_ids = self.get_loop_body_vertices()\n start_vertex_id = self._get_loop_body_start_vertex()\n start_edge = get_loop_body_start_edge(self._vertex)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n\n return await execute_loop_body(\n graph=self.graph,\n data_list=data_list,\n loop_body_vertex_ids=loop_body_vertex_ids,\n start_vertex_id=start_vertex_id,\n start_edge=start_edge,\n end_vertex_id=end_vertex_id,\n event_manager=event_manager,\n )\n\n async def _iterate(self) -> list[Data]:\n \"\"\"Run the loop body subgraph once and cache the aggregated results.\n\n Both `item_output` and `done_output` may be called during the same\n vertex build (if both outputs have downstream consumers), so this\n helper is idempotent: it caches the aggregated results in ctx under\n `{self._id}_aggregated` and guards re-entry with `{self._id}_iterated`.\n If a prior call raised, the exception is cached in\n `{self._id}_iteration_error` and re-raised on subsequent calls so\n repeat invocations surface the same failure instead of silently\n returning empty data or re-running the subgraph.\n\n Production constructs a fresh Graph per request (via\n `Graph.from_payload`), so `ctx` is effectively per-run and the\n cached `_iterated` flag does not leak across executions.\n \"\"\"\n if self.ctx.get(f\"{self._id}_iterated\", False):\n cached_error = self.ctx.get(f\"{self._id}_iteration_error\")\n if cached_error is not None:\n raise cached_error\n return self.ctx.get(f\"{self._id}_aggregated\", [])\n\n import time\n\n started_at = time.perf_counter()\n try:\n self.initialize_data()\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n self.log(f\"Starting loop over {len(data_list)} item(s)\", name=\"Start\")\n\n if not data_list:\n self.update_ctx({f\"{self._id}_aggregated\": [], f\"{self._id}_iterated\": True})\n self.log(\"No items to iterate, skipping loop body\", name=\"Skipped\")\n return []\n\n aggregated_results = await self.execute_loop_body(data_list, event_manager=self._event_manager)\n except Exception as exc:\n from lfx.log.logger import logger\n\n elapsed = time.perf_counter() - started_at\n self.log(f\"Loop failed after {elapsed:.3f}s: {exc}\", name=\"Error\")\n await logger.aexception(f\"Loop {self._id} failed while executing loop body\")\n self.update_ctx({f\"{self._id}_iteration_error\": exc, f\"{self._id}_iterated\": True})\n raise\n\n elapsed = time.perf_counter() - started_at\n self.log(\n f\"Completed {len(aggregated_results)} iteration(s) in {elapsed:.3f}s\",\n name=\"Complete\",\n )\n self.update_ctx({f\"{self._id}_aggregated\": aggregated_results, f\"{self._id}_iterated\": True})\n return aggregated_results\n\n async def item_output(self) -> Data:\n \"\"\"Display the inputs dispatched to the loop body.\n\n Also triggers the iteration (idempotent) when `done` has no\n downstream consumer, so the loop still runs if only the Item\n output is connected. When `done` IS connected we leave iteration\n to `done_output`. The `item` branch is stopped in both cases so\n downstream vertices aren't executed by the outer graph (the loop\n body runs internally via the subgraph in `_iterate`).\n\n Returns a `Data` envelope so the outer item edge remains\n compatible with Data-typed consumers in the loop body. The\n wrapped payload exposes the iterated rows for inspection.\n \"\"\"\n self.stop(\"item\")\n if self._vertex is not None and \"done\" not in self._vertex.edges_source_names:\n await self._iterate()\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n return Data(data={\"count\": len(data_list), \"items\": [d.data for d in data_list]})\n\n async def done_output(self) -> DataFrame:\n \"\"\"Return the aggregated results from the loop iteration.\n\n Triggers the iteration if it hasn't run yet (for example when only\n `done` is connected and `item_output` never executed). In the common\n case where `item` is also connected, `_iterate` has already cached\n the aggregated results in ctx and this call is a cheap read, so the\n order in which the two outputs are evaluated does not matter.\n \"\"\"\n aggregated_results = await self._iterate()\n return DataFrame(aggregated_results)\n" }, "data": { "_input_type": "HandleInput", "advanced": false, "display_name": "Inputs", "dynamic": false, "info": "Data to iterate over. Accepts DataFrame, Table, Data, or Message.", "input_types": [ "DataFrame", "Table", "Data", "Message" ], "list": false, "list_add_label": "Add More", "name": "data", "override_skip": false, "placeholder": "", "required": false, "show": true, "title_case": false, "trace_as_metadata": true, "track_in_telemetry": false, "type": "other", "value": "" } }, "tool_mode": false }, "showNode": true, "type": "LoopComponent" }, "dragging": false, "id": "LoopComponent-E7Ur0", "measured": { "height": 245, "width": 320 }, "position": { "x": 581.346082615804, "y": 105.19270617605812 }, "selected": false, "type": "genericNode" }, { "data": { "id": "ParserComponent-V2o48", "node": { "base_classes": [ "Message" ], "beta": false, "category": "models_and_agents", "conditional_paths": [], "custom_fields": {}, "description": "Extracts text using a template.", "display_name": "Parser", "documentation": "https://docs.langflow.org/parser", "edited": false, "field_order": [ "input_data", "mode", "pattern", "sep" ], "frozen": false, "icon": "braces", "legacy": false, "metadata": { "code_hash": "7ab5fd0e0a10", "component_class_name": "ParserComponent", "dependencies": { "dependencies": [ { "name": "lfx", "version": null } ], "total_dependencies": 1 }, "module": "lfx.components.processing.parser.ParserComponent" }, "minimized": false, "output_types": [], "outputs": [ { "allows_loop": false, "cache": true, "display_name": "Parsed Text", "group_outputs": false, "method": "parse_combined_text", "name": "parsed_text", "selected": "Message", "tool_mode": true, "types": [ "Message" ], "value": "__UNDEFINED__" } ], "pinned": false, "template": { "_type": "Component", "code": { "advanced": true, "api_editable": false, "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.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass _DefaultDict(dict):\n def __init__(self, data_item: Data):\n super().__init__(data_item.data)\n self.default_value = data_item.default_value\n\n def __missing__(self, key):\n return self.default_value if self.default_value is not None else \"\"\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts a DataFrame, a Data object, or a list of Data objects.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self) -> tuple[DataFrame | None, Data | list[Data] | None]:\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n return None, input_data\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame, Data, or list[Data].\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n data_items = data if isinstance(data, list) else [data]\n for data_item in data_items:\n formatted_text = self.pattern.format_map(_DefaultDict(data_item))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n clean_data = getattr(self, \"clean_data\", False)\n if isinstance(self.input_data, list):\n result = \"\\n\".join(safe_convert(item, clean_data=clean_data) for item in self.input_data)\n else:\n result = safe_convert(self.input_data, clean_data=clean_data)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, "display_name": "JSON or Table", "dynamic": false, "info": "Accepts a DataFrame, a Data object, or a list of Data objects.", "input_types": [ "DataFrame", "Table", "Data", "JSON" ], "list": false, "list_add_label": "Add More", "name": "input_data", "override_skip": false, "placeholder": "", "required": true, "show": true, "title_case": false, "trace_as_metadata": true, "track_in_telemetry": false, "type": "other", "value": "" }, "mode": { "_input_type": "TabInput", "advanced": false, "display_name": "Mode", "dynamic": false, "info": "Convert into raw string instead of using a template.", "name": "mode", "options": [ "Parser", "Stringify" ], "override_skip": false, "placeholder": "", "real_time_refresh": true, "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_metadata": true, "track_in_telemetry": true, "type": "tab", "value": "Parser" }, "pattern": { "_input_type": "MultilineInput", "advanced": false, "ai_enabled": false, "copy_field": false, "display_name": "Template", "dynamic": true, "info": "Use variables within curly brackets to extract column values for DataFrames or key values for Data.For example: `Name: {Name}, Age: {Age}, Country: {Country}`", "input_types": [ "Message" ], "list": false, "list_add_label": "Add More", "load_from_db": false, "multiline": true, "name": "pattern", "override_skip": false, "placeholder": "", "required": true, "show": true, "title_case": false, "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, "track_in_telemetry": false, "type": "str", "value": "Title: {title}\nSummary: {summary}" }, "sep": { "_input_type": "MessageTextInput", "advanced": true, "display_name": "Separator", "dynamic": false, "info": "String used to separate rows/items.", "input_types": [ "Message" ], "list": false, "list_add_label": "Add More", "load_from_db": false, "name": "sep", "override_skip": false, "placeholder": "", "required": false, "show": true, "title_case": false, "tool_mode": false, "trace_as_input": true, "trace_as_metadata": true, "track_in_telemetry": false, "type": "str", "value": "\n" } }, "tool_mode": false }, "showNode": true, "type": "ParserComponent" }, "dragging": false, "id": "ParserComponent-V2o48", "measured": { "height": 331, "width": 320 }, "position": { "x": 1102.2949192976482, "y": -80.12506181353082 }, "selected": false, "type": "genericNode" } ], "viewport": { "x": 362.1507908140667, "y": 244.92208036288463, "zoom": 0.39415259018753734 } }, "description": "This template iterates over search results using LoopComponent and translates each result into Portuguese automatically. 🚀", "endpoint_name": null, "id": "7eb7c081-888c-4f25-ba81-c84e6aab2778", "is_component": false, "last_tested_version": "1.10.1", "locked": false, "name": "Research Translation Loop", "tags": [ "chatbots", "content-generation" ] }