diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json index c788647dd1..f91e5b0e02 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json @@ -665,7 +665,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json index 0853ec8e26..e3992a65f3 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json @@ -626,7 +626,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json index 8e4cceb967..165761b34c 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json @@ -373,7 +373,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json index 9eb3659e04..33b7107867 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json @@ -2452,7 +2452,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", 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 868a094c98..b1df62cab9 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 @@ -420,7 +420,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1320,7 +1320,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" }, { "name": "pydantic", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json b/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json index d733c554ea..bdaa839e81 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json @@ -134,7 +134,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json b/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json index cdac09ec7f..728d21fb43 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json @@ -678,7 +678,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json index e69b046f81..c9c09ce316 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json @@ -460,7 +460,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json index 1f859d87cc..89d9976402 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json @@ -961,7 +961,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1910,7 +1910,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1918,7 +1918,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json b/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json index fe412f72a4..4fb1b814fc 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json @@ -347,7 +347,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1187,7 +1187,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1195,7 +1195,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json b/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json index fb4fc59062..895f8b3e5a 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json @@ -414,7 +414,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -703,7 +703,7 @@ }, { "name": "cryptography", - "version": "48.0.0" + "version": "48.0.1" }, { "name": "langchain_chroma", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json index 295e7092ce..d35735ef3b 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json @@ -456,7 +456,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1197,7 +1197,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1205,7 +1205,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json b/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json index c902ce4d0a..610d98fc4b 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json @@ -689,7 +689,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -969,7 +969,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1249,7 +1249,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json index 1d77138722..d8da643090 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json @@ -429,7 +429,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json b/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json index 1eff9d4724..1d8803cf9c 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json @@ -917,7 +917,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1205,7 +1205,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1213,7 +1213,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -1799,7 +1799,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json b/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json index 6b79f447f9..1e0883dbed 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json @@ -517,7 +517,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -805,7 +805,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -813,7 +813,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -2113,7 +2113,7 @@ "dependencies": [ { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" }, { "name": "pydantic", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json index 3c6f24be8c..345f00005b 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json @@ -400,7 +400,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -771,7 +771,7 @@ "key": "APIRequest", "legacy": false, "metadata": { - "code_hash": "716a753c256e", + "code_hash": "71438e194131", "dependencies": { "dependencies": [ { @@ -889,7 +889,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nimport re\nimport tempfile\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse\n\nimport aiofiles\nimport aiofiles.os as aiofiles_os\nimport httpx\nimport validators\n\nfrom lfx.base.curl.parse import parse_context\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import TabInput\nfrom lfx.io import (\n BoolInput,\n DataInput,\n DropdownInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_advanced, set_field_display\n\n# SSRF Protection imports - for preventing Server-Side Request Forgery attacks\nfrom lfx.utils.ssrf_protection import (\n SSRFProtectionError,\n is_ssrf_protection_enabled,\n validate_and_resolve_url,\n)\nfrom lfx.utils.ssrf_transport import create_ssrf_protected_client\n\n# Define fields for each mode\nMODE_FIELDS = {\n \"URL\": [\n \"url_input\",\n \"method\",\n ],\n \"cURL\": [\"curl_input\"],\n}\n\n# Fields that should always be visible\nDEFAULT_FIELDS = [\"mode\"]\n\n# HTTP redirect status codes (RFC 9110).\nHTTP_MOVED_PERMANENTLY = 301\nHTTP_FOUND = 302\nHTTP_SEE_OTHER = 303\nHTTP_TEMPORARY_REDIRECT = 307\nHTTP_PERMANENT_REDIRECT = 308\n\n# Maximum number of redirects to follow when re-validating each hop (matches httpx's default).\nMAX_REDIRECTS = 20\n\n# HTTP status codes that represent a redirect carrying a Location header.\nREDIRECT_STATUS_CODES = frozenset(\n {\n HTTP_MOVED_PERMANENTLY,\n HTTP_FOUND,\n HTTP_SEE_OTHER,\n HTTP_TEMPORARY_REDIRECT,\n HTTP_PERMANENT_REDIRECT,\n }\n)\n\n# Default ports per scheme, used to compare redirect origins.\nDEFAULT_SCHEME_PORTS = {\"http\": 80, \"https\": 443}\n\n\nclass APIRequestComponent(Component):\n display_name = \"API Request\"\n description = \"Make HTTP requests using URL or cURL commands.\"\n documentation: str = \"https://docs.langflow.org/api-request\"\n icon = \"Globe\"\n name = \"APIRequest\"\n\n inputs = [\n MessageTextInput(\n name=\"url_input\",\n display_name=\"URL\",\n info=\"Enter the URL for the request.\",\n advanced=False,\n tool_mode=True,\n ),\n MultilineInput(\n name=\"curl_input\",\n display_name=\"cURL\",\n info=(\n \"Paste a curl command to populate the fields. \"\n \"This will fill in the dictionary fields for headers and body.\"\n ),\n real_time_refresh=True,\n tool_mode=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Method\",\n options=[\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"],\n value=\"GET\",\n info=\"The HTTP method to use.\",\n real_time_refresh=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"URL\", \"cURL\"],\n value=\"URL\",\n info=\"Enable cURL mode to populate fields from a cURL command.\",\n real_time_refresh=True,\n ),\n DataInput(\n name=\"query_params\",\n display_name=\"Query Parameters\",\n info=\"The query parameters to append to the URL.\",\n advanced=True,\n ),\n TableInput(\n name=\"body\",\n display_name=\"Body\",\n info=\"The body to send with the request as a dictionary (for POST, PATCH, PUT).\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Parameter name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"description\": \"Parameter value\",\n },\n ],\n value=[],\n input_types=[\"Data\", \"JSON\"],\n advanced=True,\n real_time_refresh=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": \"Langflow/1.0\"}],\n advanced=True,\n input_types=[\"Data\", \"JSON\"],\n real_time_refresh=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n value=30,\n info=\"The timeout to use for the request.\",\n advanced=True,\n ),\n BoolInput(\n name=\"follow_redirects\",\n display_name=\"Follow Redirects\",\n value=False,\n info=(\n \"Whether to follow HTTP redirects. \"\n \"WARNING: Enabling redirects may allow SSRF bypass attacks where a public URL \"\n \"redirects to internal resources. Only enable if you trust the target server. \"\n \"See OWASP SSRF Prevention Cheat Sheet for details.\"\n ),\n advanced=True,\n ),\n BoolInput(\n name=\"save_to_file\",\n display_name=\"Save to File\",\n value=False,\n info=\"Save the API response to a temporary file\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_httpx_metadata\",\n display_name=\"Include HTTPx Metadata\",\n value=False,\n info=(\n \"Include properties such as headers, status_code, response_headers, \"\n \"and redirection_history in the output.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"API Response\", name=\"data\", method=\"make_api_request\"),\n ]\n\n def _parse_json_value(self, value: Any) -> Any:\n \"\"\"Parse a value that might be a JSON string.\"\"\"\n if not isinstance(value, str):\n return value\n\n try:\n parsed = json.loads(value)\n except json.JSONDecodeError:\n return value\n else:\n return parsed\n\n def _process_body(self, body: Any) -> dict:\n \"\"\"Process the body input into a valid dictionary.\"\"\"\n if body is None:\n return {}\n if hasattr(body, \"data\"):\n body = body.data\n if isinstance(body, dict):\n return self._process_dict_body(body)\n if isinstance(body, str):\n return self._process_string_body(body)\n if isinstance(body, list):\n return self._process_list_body(body)\n return {}\n\n def _process_dict_body(self, body: dict) -> dict:\n \"\"\"Process dictionary body by parsing JSON values.\"\"\"\n return {k: self._parse_json_value(v) for k, v in body.items()}\n\n def _process_string_body(self, body: str) -> dict:\n \"\"\"Process string body by attempting JSON parse.\"\"\"\n try:\n return self._process_body(json.loads(body))\n except json.JSONDecodeError:\n return {\"data\": body}\n\n def _process_list_body(self, body: list) -> dict:\n \"\"\"Process list body by converting to key-value dictionary.\"\"\"\n processed_dict = {}\n try:\n for item in body:\n # Unwrap Data objects\n current_item = item\n if hasattr(item, \"data\"):\n unwrapped_data = item.data\n # If the unwrapped data is a dict but not key-value format, use it directly\n if isinstance(unwrapped_data, dict) and not self._is_valid_key_value_item(unwrapped_data):\n return unwrapped_data\n current_item = unwrapped_data\n if not self._is_valid_key_value_item(current_item):\n continue\n key = current_item[\"key\"]\n value = self._parse_json_value(current_item[\"value\"])\n processed_dict[key] = value\n except (KeyError, TypeError, ValueError) as e:\n self.log(f\"Failed to process body list: {e}\")\n return {}\n return processed_dict\n\n def _is_valid_key_value_item(self, item: Any) -> bool:\n \"\"\"Check if an item is a valid key-value dictionary.\"\"\"\n return isinstance(item, dict) and \"key\" in item and \"value\" in item\n\n def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:\n \"\"\"Parse a cURL command and update build configuration.\"\"\"\n try:\n parsed = parse_context(curl)\n\n # Update basic configuration\n url = parsed.url\n # Normalize URL before setting it\n url = self._normalize_url(url)\n\n build_config[\"url_input\"][\"value\"] = url\n build_config[\"method\"][\"value\"] = parsed.method.upper()\n\n # Process headers\n headers_list = [{\"key\": k, \"value\": v} for k, v in parsed.headers.items()]\n build_config[\"headers\"][\"value\"] = headers_list\n\n # Process body data\n if not parsed.data:\n build_config[\"body\"][\"value\"] = []\n elif parsed.data:\n try:\n json_data = json.loads(parsed.data)\n if isinstance(json_data, dict):\n body_list = [\n {\"key\": k, \"value\": json.dumps(v) if isinstance(v, dict | list) else str(v)}\n for k, v in json_data.items()\n ]\n build_config[\"body\"][\"value\"] = body_list\n else:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": json.dumps(json_data)}]\n except json.JSONDecodeError:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": parsed.data}]\n\n except Exception as exc:\n msg = f\"Error parsing curl: {exc}\"\n self.log(msg)\n raise ValueError(msg) from exc\n\n return build_config\n\n def _normalize_url(self, url: str) -> str:\n \"\"\"Normalize URL by adding https:// if no protocol is specified.\"\"\"\n if not url or not isinstance(url, str):\n msg = \"URL cannot be empty\"\n raise ValueError(msg)\n\n url = url.strip()\n if url.startswith((\"http://\", \"https://\")):\n return url\n return f\"https://{url}\"\n\n async def make_request(\n self,\n client: httpx.AsyncClient,\n method: str,\n url: str,\n headers: dict | None = None,\n body: Any = None,\n timeout: int = 5,\n *,\n follow_redirects: bool = False,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n redirection_history = []\n\n try:\n # Prepare request parameters\n request_params = {\n \"method\": method,\n \"url\": url,\n \"headers\": headers,\n \"timeout\": timeout,\n \"follow_redirects\": follow_redirects,\n }\n # Only include body for methods that support it (GET must not have a body per HTTP spec)\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n response = await client.request(**request_params)\n\n redirection_history = [\n {\n \"url\": redirect.headers.get(\"Location\", str(redirect.url)),\n \"status_code\": redirect.status_code,\n }\n for redirect in response.history\n ]\n\n return await self._build_response_data(\n response,\n url,\n headers,\n redirection_history,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n async def _build_response_data(\n self,\n response: httpx.Response,\n source_url: str,\n headers: dict | None,\n redirection_history: list,\n *,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n \"\"\"Turn an httpx response into the component's ``Data`` output.\n\n Shared by the standard request path (``make_request``) and the redirect\n re-validation path (``_follow_redirects_with_validation``) so both produce\n identical metadata, optional file saving, and body decoding.\n \"\"\"\n is_binary, file_path = await self._response_info(response, with_file_path=save_to_file)\n response_headers = self._headers_to_dict(response.headers)\n\n # Base metadata\n metadata = {\n \"source\": source_url,\n \"status_code\": response.status_code,\n \"response_headers\": response_headers,\n }\n\n if redirection_history:\n metadata[\"redirection_history\"] = redirection_history\n\n if save_to_file:\n mode = \"wb\" if is_binary else \"w\"\n encoding = response.encoding if mode == \"w\" else None\n if file_path:\n await aiofiles_os.makedirs(file_path.parent, exist_ok=True)\n if is_binary:\n async with aiofiles.open(file_path, \"wb\") as f:\n await f.write(response.content)\n await f.flush()\n else:\n async with aiofiles.open(file_path, \"w\", encoding=encoding) as f:\n await f.write(response.text)\n await f.flush()\n metadata[\"file_path\"] = str(file_path)\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n return Data(data=metadata)\n\n # Handle response content\n if is_binary:\n result = response.content\n else:\n try:\n result = response.json()\n except json.JSONDecodeError:\n self.log(\"Failed to decode JSON response\")\n result = response.text.encode(\"utf-8\")\n\n metadata[\"result\"] = result\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n\n return Data(data=metadata)\n\n def add_query_params(self, url: str, params: dict) -> str:\n \"\"\"Add query parameters to URL efficiently.\"\"\"\n if not params:\n return url\n url_parts = list(urlparse(url))\n query = dict(parse_qsl(url_parts[4]))\n query.update(params)\n url_parts[4] = urlencode(query)\n return urlunparse(url_parts)\n\n def _headers_to_dict(self, headers: httpx.Headers) -> dict[str, str]:\n \"\"\"Convert HTTP headers to a dictionary with lowercased keys.\"\"\"\n return {k.lower(): v for k, v in headers.items()}\n\n def _process_headers(self, headers: Any) -> dict:\n \"\"\"Process the headers input into a valid dictionary.\"\"\"\n if headers is None:\n return {}\n if isinstance(headers, dict):\n return headers\n if isinstance(headers, list):\n return {item[\"key\"]: item[\"value\"] for item in headers if self._is_valid_key_value_item(item)}\n return {}\n\n async def make_api_request(self) -> Data:\n \"\"\"Make HTTP request with SSRF protection and DNS pinning.\n\n This method implements comprehensive SSRF (Server-Side Request Forgery) protection\n using DNS pinning to prevent DNS rebinding attacks. The protection works by:\n 1. Validating the URL and resolving DNS during security check\n 2. Pinning the validated IP address\n 3. Forcing the HTTP client to use the pinned IP for the actual request\n 4. Ignoring any subsequent DNS changes (prevents rebinding attacks)\n\n Returns:\n Data: Response data from the HTTP request\n\n Raises:\n ValueError: If URL is invalid or blocked by SSRF protection\n \"\"\"\n # Extract request parameters\n method = self.method\n url = self.url_input.strip() if isinstance(self.url_input, str) else \"\"\n headers = self.headers or {}\n body = self.body or {}\n timeout = self.timeout\n follow_redirects = self.follow_redirects\n save_to_file = self.save_to_file\n include_httpx_metadata = self.include_httpx_metadata\n\n # Security warning: HTTP redirects can bypass SSRF protection\n # A public URL could redirect to an internal resource\n if follow_redirects:\n self.log(\n \"Security Warning: HTTP redirects are enabled. This may allow SSRF bypass attacks \"\n \"where a public URL redirects to internal resources (e.g., cloud metadata endpoints). \"\n \"Only enable this if you trust the target server.\"\n )\n\n # Normalize URL (add https:// if no protocol specified)\n url = self._normalize_url(url)\n\n # Basic URL format validation\n if not validators.url(url):\n msg = f\"Invalid URL provided: {url}\"\n raise ValueError(msg)\n\n # ============================================================================\n # SSRF Protection with DNS Pinning\n # ============================================================================\n # This prevents DNS rebinding attacks by:\n # 1. Resolving DNS and validating IPs during security check\n # 2. Pinning the validated IP address\n # 3. Using a custom HTTP transport that forces use of the pinned IP\n # 4. Ignoring any new DNS resolutions (prevents rebinding)\n #\n # Without DNS pinning, an attacker could:\n # - First DNS lookup: returns public IP (passes validation)\n # - Second DNS lookup: returns internal IP (bypasses protection)\n # - Attack succeeds: accesses internal services\n #\n # With DNS pinning:\n # - First DNS lookup: returns public IP (passes validation)\n # - IP is pinned: \"example.com = 93.184.216.34\"\n # - HTTP request: uses pinned IP directly (no new DNS lookup)\n # - Attack fails: even if DNS changes, we use the validated IP\n # ============================================================================\n\n try:\n # Validate URL and get validated IPs for DNS pinning\n _validated_url, validated_ips = validate_and_resolve_url(url)\n\n # Log DNS pinning information for security auditing\n if validated_ips:\n self.log(f\"SSRF Protection: Using DNS pinning with {len(validated_ips)} validated IP(s)\")\n\n except SSRFProtectionError as e:\n # SSRF protection blocked the request (private IP, internal network, etc.)\n msg = f\"SSRF Protection: {e}\"\n raise ValueError(msg) from e\n\n # Process query parameters (from string or Data object)\n if isinstance(self.query_params, str):\n query_params = dict(parse_qsl(self.query_params))\n else:\n query_params = self.query_params.data if self.query_params else {}\n\n # Process headers and body into proper format\n headers = self._process_headers(headers)\n body = self._process_body(body)\n url = self.add_query_params(url, query_params)\n\n # ============================================================================\n # Execute the request (re-validating any redirects when SSRF protection is on)\n # ============================================================================\n # When SSRF protection is enabled we must NOT let httpx auto-follow redirects:\n # a validated public URL can redirect to an internal address (loopback, RFC1918,\n # link-local / cloud metadata) that was never checked, bypassing both the initial\n # validation and DNS pinning. Instead we follow redirects manually so every hop\n # is re-validated with the same denylist + DNS pinning. When protection is\n # disabled, we preserve the previous behavior and let httpx handle redirects.\n if is_ssrf_protection_enabled() and follow_redirects:\n result = await self._follow_redirects_with_validation(\n method,\n url,\n headers,\n body,\n timeout,\n validated_ips,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n else:\n # No redirect re-validation needed:\n # - SSRF protection is disabled (user opted out), or\n # - redirects are disabled, so httpx makes a single request.\n # DNS pinning still applies to the single request when protection is enabled\n # and the host resolved to validated IPs.\n async with self._build_http_client(url, validated_ips) as client:\n result = await self.make_request(\n client,\n method,\n url,\n headers,\n body,\n timeout,\n follow_redirects=follow_redirects,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n\n self.status = result\n return result\n\n def _build_http_client(self, url: str, validated_ips: list[str]) -> httpx.AsyncClient:\n \"\"\"Create an HTTP client, pinning DNS to validated IPs when SSRF protection applies.\n\n Args:\n url: The request URL whose hostname will be pinned.\n validated_ips: IPs validated by ``validate_and_resolve_url`` for this hop.\n\n Returns:\n httpx.AsyncClient: A client that pins DNS to ``validated_ips`` (preventing\n rebinding) when SSRF protection is enabled and the hop has validated IPs;\n otherwise a standard client (protection disabled, allowlisted host, or\n hostname extraction failure).\n \"\"\"\n if is_ssrf_protection_enabled() and validated_ips:\n # Extract hostname from the URL so the custom transport can pin it while\n # preserving the Host header for virtual hosting / TLS SNI.\n hostname = urlparse(url).hostname\n if hostname:\n # The custom transport tries validated IPs in order (dual-stack / LB).\n return create_ssrf_protected_client(hostname=hostname, validated_ips=validated_ips)\n return httpx.AsyncClient()\n\n @staticmethod\n def _method_for_redirect(method: str, status_code: int) -> str:\n \"\"\"Return the HTTP method to use after a redirect, mirroring httpx semantics.\n\n A 303 (See Other) always becomes GET; 301/302 downgrade POST to GET for\n browser compatibility; 307/308 preserve the original method (and body).\n \"\"\"\n method = method.upper()\n if status_code == HTTP_SEE_OTHER and method != \"HEAD\":\n return \"GET\"\n if status_code in (HTTP_MOVED_PERMANENTLY, HTTP_FOUND) and method == \"POST\":\n return \"GET\"\n return method\n\n @staticmethod\n def _headers_for_redirect(headers: dict | None, current_url: str, next_url: str) -> dict | None:\n \"\"\"Drop sensitive headers when a redirect crosses to a different origin.\n\n Mirrors httpx's auto-follow behavior so manually following redirects does not\n leak credentials (Authorization / Cookie) to an origin other than the one the\n caller intended them for. Headers are kept only when the redirect stays on the\n same origin (scheme, host, port) or is a direct https upgrade of the same host\n on default ports - the exact cases where httpx keeps the Authorization header.\n \"\"\"\n if not headers:\n return headers\n current, nxt = urlparse(current_url), urlparse(next_url)\n current_port = current.port or DEFAULT_SCHEME_PORTS.get(current.scheme)\n next_port = nxt.port or DEFAULT_SCHEME_PORTS.get(nxt.scheme)\n same_origin = (current.scheme, current.hostname, current_port) == (nxt.scheme, nxt.hostname, next_port)\n https_upgrade = (\n current.hostname == nxt.hostname\n and current.scheme == \"http\"\n and nxt.scheme == \"https\"\n and current_port == DEFAULT_SCHEME_PORTS[\"http\"]\n and next_port == DEFAULT_SCHEME_PORTS[\"https\"]\n )\n if same_origin or https_upgrade:\n return headers\n sensitive = {\"authorization\", \"proxy-authorization\", \"cookie\"}\n return {k: v for k, v in headers.items() if k.lower() not in sensitive}\n\n async def _follow_redirects_with_validation(\n self,\n method: str,\n url: str,\n headers: dict | None,\n body: Any,\n timeout: int,\n validated_ips: list[str],\n *,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n \"\"\"Make the request and follow redirects manually, re-validating every hop.\n\n This closes an SSRF bypass: with ``follow_redirects`` enabled, httpx would\n otherwise auto-follow a redirect from a validated public URL to an internal\n address that was never checked. Here each redirect ``Location`` is resolved\n (relative locations included) and re-validated with ``validate_and_resolve_url``\n — the same private/loopback/link-local denylist and DNS pinning applied to the\n initial request — before any connection to it is made. A blocked hop raises\n ``ValueError``; the number of redirects is capped at ``MAX_REDIRECTS``.\n \"\"\"\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n current_url = url\n current_ips = validated_ips\n redirection_history: list[dict] = []\n\n for _ in range(MAX_REDIRECTS + 1):\n request_params: dict[str, Any] = {\n \"method\": method,\n \"url\": current_url,\n \"headers\": headers,\n \"timeout\": timeout,\n # Never let httpx follow redirects itself; each hop is validated below.\n \"follow_redirects\": False,\n }\n # Only include body for methods that support it (GET must not have a body).\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n\n try:\n async with self._build_http_client(current_url, current_ips) as client:\n response = await client.request(**request_params)\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {current_url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n location = response.headers.get(\"Location\")\n if response.status_code in REDIRECT_STATUS_CODES and location:\n # Resolve relative redirects against the current URL.\n next_url = urljoin(current_url, location)\n redirection_history.append({\"url\": location, \"status_code\": response.status_code})\n\n # Re-validate the redirect target with the same SSRF denylist + DNS pinning.\n # Non-http(s) schemes, private/loopback/link-local hosts, and hostnames that\n # resolve to blocked IPs all raise SSRFProtectionError here.\n try:\n _validated_url, current_ips = validate_and_resolve_url(next_url)\n except SSRFProtectionError as e:\n msg = f\"SSRF Protection: blocked redirect to {next_url}: {e}\"\n raise ValueError(msg) from e\n\n method = self._method_for_redirect(method, response.status_code)\n headers = self._headers_for_redirect(headers, current_url, next_url)\n current_url = next_url\n continue\n\n # Not a redirect (or no Location header) - this is the final response.\n return await self._build_response_data(\n response,\n url,\n headers,\n redirection_history,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n\n msg = f\"SSRF Protection: exceeded the maximum of {MAX_REDIRECTS} redirects while requesting {url}\"\n raise ValueError(msg)\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n \"\"\"Update the build config based on the selected mode.\"\"\"\n if field_name != \"mode\":\n if field_name == \"curl_input\" and self.mode == \"cURL\" and self.curl_input:\n return self.parse_curl(self.curl_input, build_config)\n return build_config\n\n if field_value == \"cURL\":\n set_field_display(build_config, \"curl_input\", value=True)\n if build_config[\"curl_input\"][\"value\"]:\n try:\n build_config = self.parse_curl(build_config[\"curl_input\"][\"value\"], build_config)\n except ValueError as e:\n self.log(f\"Failed to parse cURL input: {e}\")\n else:\n set_field_display(build_config, \"curl_input\", value=False)\n\n return set_current_fields(\n build_config=build_config,\n action_fields=MODE_FIELDS,\n selected_action=field_value,\n default_fields=DEFAULT_FIELDS,\n func=set_field_advanced,\n default_value=True,\n )\n\n async def _response_info(\n self, response: httpx.Response, *, with_file_path: bool = False\n ) -> tuple[bool, Path | None]:\n \"\"\"Determine the file path and whether the response content is binary.\n\n Args:\n response (Response): The HTTP response object.\n with_file_path (bool): Whether to save the response content to a file.\n\n Returns:\n Tuple[bool, Path | None]:\n A tuple containing a boolean indicating if the content is binary and the full file path (if applicable).\n \"\"\"\n content_type = response.headers.get(\"Content-Type\", \"\")\n is_binary = \"application/octet-stream\" in content_type or \"application/binary\" in content_type\n\n if not with_file_path:\n return is_binary, None\n\n component_temp_dir = Path(tempfile.gettempdir()) / self.__class__.__name__\n\n # Create directory asynchronously\n await aiofiles_os.makedirs(component_temp_dir, exist_ok=True)\n\n filename = None\n if \"Content-Disposition\" in response.headers:\n content_disposition = response.headers[\"Content-Disposition\"]\n filename_match = re.search(r'filename=\"(.+?)\"', content_disposition)\n if filename_match:\n extracted_filename = filename_match.group(1)\n filename = extracted_filename\n\n # Step 3: Infer file extension or use part of the request URL if no filename\n if not filename:\n # Extract the last segment of the URL path\n url_path = urlparse(str(response.request.url) if response.request else \"\").path\n base_name = Path(url_path).name # Get the last segment of the path\n if not base_name: # If the path ends with a slash or is empty\n base_name = \"response\"\n\n # Infer file extension\n content_type_to_extension = {\n \"text/plain\": \".txt\",\n \"application/json\": \".json\",\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"application/octet-stream\": \".bin\",\n }\n extension = content_type_to_extension.get(content_type, \".bin\" if is_binary else \".txt\")\n filename = f\"{base_name}{extension}\"\n\n # Step 4: Define the full file path\n file_path = component_temp_dir / filename\n\n # Step 5: Check if file exists asynchronously and handle accordingly\n try:\n # Try to create the file exclusively (x mode) to check existence\n async with aiofiles.open(file_path, \"x\") as _:\n pass # File created successfully, we can use this path\n except FileExistsError:\n # If file exists, append a timestamp to the filename\n timestamp = datetime.now(timezone.utc).strftime(\"%Y%m%d%H%M%S%f\")\n file_path = component_temp_dir / f\"{timestamp}-{filename}\"\n\n return is_binary, file_path\n" + "value": "import json\nimport re\nimport tempfile\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse\n\nimport aiofiles\nimport aiofiles.os as aiofiles_os\nimport httpx\nimport validators\n\nfrom lfx.base.curl.parse import parse_context\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import TabInput\nfrom lfx.io import (\n BoolInput,\n DataInput,\n DropdownInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_advanced, set_field_display\n\n# SSRF Protection imports - for preventing Server-Side Request Forgery attacks\nfrom lfx.utils.ssrf_protection import (\n SSRFProtectionError,\n is_ssrf_protection_enabled,\n validate_and_resolve_url,\n)\nfrom lfx.utils.ssrf_transport import create_ssrf_protected_client\n\n# Define fields for each mode\nMODE_FIELDS = {\n \"URL\": [\n \"url_input\",\n \"method\",\n ],\n \"cURL\": [\"curl_input\"],\n}\n\n# Fields that should always be visible\nDEFAULT_FIELDS = [\"mode\"]\n\n# HTTP redirect status codes (RFC 9110).\nHTTP_MOVED_PERMANENTLY = 301\nHTTP_FOUND = 302\nHTTP_SEE_OTHER = 303\nHTTP_TEMPORARY_REDIRECT = 307\nHTTP_PERMANENT_REDIRECT = 308\n\n# Maximum number of redirects to follow when re-validating each hop (matches httpx's default).\nMAX_REDIRECTS = 20\n\n# HTTP status codes that represent a redirect carrying a Location header.\nREDIRECT_STATUS_CODES = frozenset(\n {\n HTTP_MOVED_PERMANENTLY,\n HTTP_FOUND,\n HTTP_SEE_OTHER,\n HTTP_TEMPORARY_REDIRECT,\n HTTP_PERMANENT_REDIRECT,\n }\n)\n\n# Default ports per scheme, used to compare redirect origins.\nDEFAULT_SCHEME_PORTS = {\"http\": 80, \"https\": 443}\n\n\nclass APIRequestComponent(Component):\n display_name = \"API Request\"\n description = \"Make HTTP requests using URL or cURL commands.\"\n documentation: str = \"https://docs.langflow.org/api-request\"\n icon = \"Globe\"\n name = \"APIRequest\"\n\n inputs = [\n MessageTextInput(\n name=\"url_input\",\n display_name=\"URL\",\n info=\"Enter the URL for the request.\",\n advanced=False,\n tool_mode=True,\n ),\n MultilineInput(\n name=\"curl_input\",\n display_name=\"cURL\",\n info=(\n \"Paste a curl command to populate the fields. \"\n \"This will fill in the dictionary fields for headers and body.\"\n ),\n real_time_refresh=True,\n tool_mode=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Method\",\n options=[\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"],\n value=\"GET\",\n info=\"The HTTP method to use.\",\n real_time_refresh=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"URL\", \"cURL\"],\n value=\"URL\",\n info=\"Enable cURL mode to populate fields from a cURL command.\",\n real_time_refresh=True,\n ),\n DataInput(\n name=\"query_params\",\n display_name=\"Query Parameters\",\n info=\"The query parameters to append to the URL.\",\n advanced=True,\n ),\n TableInput(\n name=\"body\",\n display_name=\"Body\",\n info=\"The body to send with the request as a dictionary (for POST, PATCH, PUT).\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Parameter name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"description\": \"Parameter value\",\n },\n ],\n value=[],\n input_types=[\"Data\", \"JSON\"],\n advanced=True,\n real_time_refresh=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": \"Langflow/1.0\"}],\n advanced=True,\n input_types=[\"Data\", \"JSON\"],\n real_time_refresh=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n value=30,\n info=\"The timeout to use for the request.\",\n advanced=True,\n ),\n BoolInput(\n name=\"follow_redirects\",\n display_name=\"Follow Redirects\",\n value=False,\n info=(\n \"Whether to follow HTTP redirects. \"\n \"WARNING: Enabling redirects may allow SSRF bypass attacks where a public URL \"\n \"redirects to internal resources. Only enable if you trust the target server. \"\n \"See OWASP SSRF Prevention Cheat Sheet for details.\"\n ),\n advanced=True,\n ),\n BoolInput(\n name=\"save_to_file\",\n display_name=\"Save to File\",\n value=False,\n info=\"Save the API response to a temporary file\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_httpx_metadata\",\n display_name=\"Include HTTPx Metadata\",\n value=False,\n info=(\n \"Include properties such as headers, status_code, response_headers, \"\n \"and redirection_history in the output.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"API Response\", name=\"data\", method=\"make_api_request\"),\n ]\n\n def _parse_json_value(self, value: Any) -> Any:\n \"\"\"Parse a value that might be a JSON string.\"\"\"\n if not isinstance(value, str):\n return value\n\n try:\n parsed = json.loads(value)\n except json.JSONDecodeError:\n return value\n else:\n return parsed\n\n def _process_body(self, body: Any) -> dict:\n \"\"\"Process the body input into a valid dictionary.\"\"\"\n if body is None:\n return {}\n if hasattr(body, \"data\"):\n body = body.data\n if isinstance(body, dict):\n return self._process_dict_body(body)\n if isinstance(body, str):\n return self._process_string_body(body)\n if isinstance(body, list):\n return self._process_list_body(body)\n return {}\n\n def _process_dict_body(self, body: dict) -> dict:\n \"\"\"Process dictionary body by parsing JSON values.\"\"\"\n return {k: self._parse_json_value(v) for k, v in body.items()}\n\n def _process_string_body(self, body: str) -> dict:\n \"\"\"Process string body by attempting JSON parse.\"\"\"\n try:\n return self._process_body(json.loads(body))\n except json.JSONDecodeError:\n return {\"data\": body}\n\n def _process_list_body(self, body: list) -> dict:\n \"\"\"Process list body by converting to key-value dictionary.\"\"\"\n processed_dict = {}\n try:\n for item in body:\n # Unwrap Data objects\n current_item = item\n if hasattr(item, \"data\"):\n unwrapped_data = item.data\n # If the unwrapped data is a dict but not key-value format, use it directly\n if isinstance(unwrapped_data, dict) and not self._is_valid_key_value_item(unwrapped_data):\n return unwrapped_data\n current_item = unwrapped_data\n if not self._is_valid_key_value_item(current_item):\n continue\n key = current_item[\"key\"]\n value = self._parse_json_value(current_item[\"value\"])\n processed_dict[key] = value\n except (KeyError, TypeError, ValueError) as e:\n self.log(f\"Failed to process body list: {e}\")\n return {}\n return processed_dict\n\n def _is_valid_key_value_item(self, item: Any) -> bool:\n \"\"\"Check if an item is a valid key-value dictionary.\"\"\"\n return isinstance(item, dict) and \"key\" in item and \"value\" in item\n\n def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:\n \"\"\"Parse a cURL command and update build configuration.\"\"\"\n try:\n parsed = parse_context(curl)\n\n # Update basic configuration\n url = parsed.url\n # Normalize URL before setting it\n url = self._normalize_url(url)\n\n build_config[\"url_input\"][\"value\"] = url\n build_config[\"method\"][\"value\"] = parsed.method.upper()\n\n # Process headers\n headers_list = [{\"key\": k, \"value\": v} for k, v in parsed.headers.items()]\n build_config[\"headers\"][\"value\"] = headers_list\n\n # Process body data\n if not parsed.data:\n build_config[\"body\"][\"value\"] = []\n elif parsed.data:\n try:\n json_data = json.loads(parsed.data)\n if isinstance(json_data, dict):\n body_list = [\n {\"key\": k, \"value\": json.dumps(v) if isinstance(v, dict | list) else str(v)}\n for k, v in json_data.items()\n ]\n build_config[\"body\"][\"value\"] = body_list\n else:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": json.dumps(json_data)}]\n except json.JSONDecodeError:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": parsed.data}]\n\n except Exception as exc:\n msg = f\"Error parsing curl: {exc}\"\n self.log(msg)\n raise ValueError(msg) from exc\n\n return build_config\n\n def _normalize_url(self, url: str) -> str:\n \"\"\"Normalize URL by adding https:// if no protocol is specified.\"\"\"\n if not url or not isinstance(url, str):\n msg = \"URL cannot be empty\"\n raise ValueError(msg)\n\n url = url.strip()\n if url.startswith((\"http://\", \"https://\")):\n return url\n return f\"https://{url}\"\n\n async def make_request(\n self,\n client: httpx.AsyncClient,\n method: str,\n url: str,\n headers: dict | None = None,\n body: Any = None,\n timeout: int = 5,\n *,\n follow_redirects: bool = False,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n redirection_history = []\n\n try:\n # Prepare request parameters\n request_params = {\n \"method\": method,\n \"url\": url,\n \"headers\": headers,\n \"timeout\": timeout,\n \"follow_redirects\": follow_redirects,\n }\n # Only include body for methods that support it (GET must not have a body per HTTP spec)\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n response = await client.request(**request_params)\n\n redirection_history = [\n {\n \"url\": redirect.headers.get(\"Location\", str(redirect.url)),\n \"status_code\": redirect.status_code,\n }\n for redirect in response.history\n ]\n\n return await self._build_response_data(\n response,\n url,\n headers,\n redirection_history,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n async def _build_response_data(\n self,\n response: httpx.Response,\n source_url: str,\n headers: dict | None,\n redirection_history: list,\n *,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n \"\"\"Turn an httpx response into the component's ``Data`` output.\n\n Shared by the standard request path (``make_request``) and the redirect\n re-validation path (``_follow_redirects_with_validation``) so both produce\n identical metadata, optional file saving, and body decoding.\n \"\"\"\n is_binary, file_path = await self._response_info(response, with_file_path=save_to_file)\n response_headers = self._headers_to_dict(response.headers)\n\n # Base metadata\n metadata = {\n \"source\": source_url,\n \"status_code\": response.status_code,\n \"response_headers\": response_headers,\n }\n\n if redirection_history:\n metadata[\"redirection_history\"] = redirection_history\n\n if save_to_file:\n mode = \"wb\" if is_binary else \"w\"\n encoding = response.encoding if mode == \"w\" else None\n if file_path:\n await aiofiles_os.makedirs(file_path.parent, exist_ok=True)\n if is_binary:\n async with aiofiles.open(file_path, \"wb\") as f:\n await f.write(response.content)\n await f.flush()\n else:\n async with aiofiles.open(file_path, \"w\", encoding=encoding) as f:\n await f.write(response.text)\n await f.flush()\n metadata[\"file_path\"] = str(file_path)\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n return Data(data=metadata)\n\n # Handle response content\n if is_binary:\n result = response.content\n else:\n try:\n result = response.json()\n except json.JSONDecodeError:\n self.log(\"Failed to decode JSON response\")\n result = response.text.encode(\"utf-8\")\n\n metadata[\"result\"] = result\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n\n return Data(data=metadata)\n\n def add_query_params(self, url: str, params: dict) -> str:\n \"\"\"Add query parameters to URL efficiently.\"\"\"\n if not params:\n return url\n url_parts = list(urlparse(url))\n query = dict(parse_qsl(url_parts[4]))\n query.update(params)\n url_parts[4] = urlencode(query)\n return urlunparse(url_parts)\n\n def _headers_to_dict(self, headers: httpx.Headers) -> dict[str, str]:\n \"\"\"Convert HTTP headers to a dictionary with lowercased keys.\"\"\"\n return {k.lower(): v for k, v in headers.items()}\n\n def _process_headers(self, headers: Any) -> dict:\n \"\"\"Process the headers input into a valid dictionary.\"\"\"\n if headers is None:\n return {}\n if isinstance(headers, dict):\n return headers\n if isinstance(headers, list):\n return {item[\"key\"]: item[\"value\"] for item in headers if self._is_valid_key_value_item(item)}\n return {}\n\n async def make_api_request(self) -> Data:\n \"\"\"Make HTTP request with SSRF protection and DNS pinning.\n\n This method implements comprehensive SSRF (Server-Side Request Forgery) protection\n using DNS pinning to prevent DNS rebinding attacks. The protection works by:\n 1. Validating the URL and resolving DNS during security check\n 2. Pinning the validated IP address\n 3. Forcing the HTTP client to use the pinned IP for the actual request\n 4. Ignoring any subsequent DNS changes (prevents rebinding attacks)\n\n Returns:\n Data: Response data from the HTTP request\n\n Raises:\n ValueError: If URL is invalid or blocked by SSRF protection\n \"\"\"\n # Extract request parameters\n method = self.method\n url = self.url_input.strip() if isinstance(self.url_input, str) else \"\"\n headers = self.headers or {}\n body = self.body or {}\n timeout = self.timeout\n follow_redirects = self.follow_redirects\n save_to_file = self.save_to_file\n include_httpx_metadata = self.include_httpx_metadata\n\n # Security warning: HTTP redirects can bypass SSRF protection\n # A public URL could redirect to an internal resource\n if follow_redirects:\n self.log(\n \"Security Warning: HTTP redirects are enabled. This may allow SSRF bypass attacks \"\n \"where a public URL redirects to internal resources (e.g., cloud metadata endpoints). \"\n \"Only enable this if you trust the target server.\"\n )\n\n # Normalize URL (add https:// if no protocol specified)\n url = self._normalize_url(url)\n\n # Basic URL format validation\n if not validators.url(url):\n msg = f\"Invalid URL provided: {url}\"\n raise ValueError(msg)\n\n # ============================================================================\n # SSRF Protection with DNS Pinning\n # ============================================================================\n # This prevents DNS rebinding attacks by:\n # 1. Resolving DNS and validating IPs during security check\n # 2. Pinning the validated IP address\n # 3. Using a custom HTTP transport that forces use of the pinned IP\n # 4. Ignoring any new DNS resolutions (prevents rebinding)\n #\n # Without DNS pinning, an attacker could:\n # - First DNS lookup: returns public IP (passes validation)\n # - Second DNS lookup: returns internal IP (bypasses protection)\n # - Attack succeeds: accesses internal services\n #\n # With DNS pinning:\n # - First DNS lookup: returns public IP (passes validation)\n # - IP is pinned: \"example.com = 93.184.216.34\"\n # - HTTP request: uses pinned IP directly (no new DNS lookup)\n # - Attack fails: even if DNS changes, we use the validated IP\n # ============================================================================\n\n try:\n # Validate URL and get validated IPs for DNS pinning\n _validated_url, validated_ips = validate_and_resolve_url(url)\n\n # Log DNS pinning information for security auditing\n if validated_ips:\n self.log(f\"SSRF Protection: Using DNS pinning with {len(validated_ips)} validated IP(s)\")\n\n except SSRFProtectionError as e:\n # SSRF protection blocked the request (private IP, internal network, etc.)\n msg = f\"SSRF Protection: {e}\"\n raise ValueError(msg) from e\n\n # Process query parameters (from string or Data object)\n if isinstance(self.query_params, str):\n query_params = dict(parse_qsl(self.query_params))\n else:\n query_params = self.query_params.data if self.query_params else {}\n\n # Process headers and body into proper format\n headers = self._process_headers(headers)\n body = self._process_body(body)\n url = self.add_query_params(url, query_params)\n\n # ============================================================================\n # Execute the request (re-validating any redirects when SSRF protection is on)\n # ============================================================================\n # When SSRF protection is enabled we must NOT let httpx auto-follow redirects:\n # a validated public URL can redirect to an internal address (loopback, RFC1918,\n # link-local / cloud metadata) that was never checked, bypassing both the initial\n # validation and DNS pinning. Instead we follow redirects manually so every hop\n # is re-validated with the same denylist + DNS pinning. When protection is\n # disabled, we preserve the previous behavior and let httpx handle redirects.\n if is_ssrf_protection_enabled() and follow_redirects:\n result = await self._follow_redirects_with_validation(\n method,\n url,\n headers,\n body,\n timeout,\n validated_ips,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n else:\n # No redirect re-validation needed:\n # - SSRF protection is disabled (user opted out), or\n # - redirects are disabled, so httpx makes a single request.\n # DNS pinning still applies to the single request when protection is enabled\n # and the host resolved to validated IPs.\n async with self._build_http_client(url, validated_ips) as client:\n result = await self.make_request(\n client,\n method,\n url,\n headers,\n body,\n timeout,\n follow_redirects=follow_redirects,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n\n self.status = result\n return result\n\n def _build_http_client(self, url: str, validated_ips: list[str]) -> httpx.AsyncClient:\n \"\"\"Create an HTTP client, pinning DNS to validated IPs when SSRF protection applies.\n\n Args:\n url: The request URL whose hostname will be pinned.\n validated_ips: IPs validated by ``validate_and_resolve_url`` for this hop.\n\n Returns:\n httpx.AsyncClient: A client that pins DNS to ``validated_ips`` (preventing\n rebinding) when SSRF protection is enabled and the hop has validated IPs;\n otherwise a standard client (protection disabled, allowlisted host, or\n hostname extraction failure).\n \"\"\"\n if is_ssrf_protection_enabled() and validated_ips:\n # Extract hostname from the URL so the custom transport can pin it while\n # preserving the Host header for virtual hosting / TLS SNI.\n hostname = urlparse(url).hostname\n if hostname:\n # The custom transport tries validated IPs in order (dual-stack / LB).\n return create_ssrf_protected_client(hostname=hostname, validated_ips=validated_ips)\n return httpx.AsyncClient()\n\n @staticmethod\n def _method_for_redirect(method: str, status_code: int) -> str:\n \"\"\"Return the HTTP method to use after a redirect, mirroring httpx semantics.\n\n A 303 (See Other) always becomes GET; 301/302 downgrade POST to GET for\n browser compatibility; 307/308 preserve the original method (and body).\n \"\"\"\n method = method.upper()\n if status_code == HTTP_SEE_OTHER and method != \"HEAD\":\n return \"GET\"\n if status_code in (HTTP_MOVED_PERMANENTLY, HTTP_FOUND) and method == \"POST\":\n return \"GET\"\n return method\n\n @staticmethod\n def _headers_for_redirect(headers: dict | None, current_url: str, next_url: str) -> dict | None:\n \"\"\"Drop sensitive headers when a redirect crosses to a different origin.\n\n Mirrors httpx's auto-follow behavior so manually following redirects does not\n leak credentials (Authorization / Cookie) to an origin other than the one the\n caller intended them for. Headers are kept only when the redirect stays on the\n same origin (scheme, host, port) or is a direct https upgrade of the same host\n on default ports - the exact cases where httpx keeps the Authorization header.\n \"\"\"\n if not headers:\n return headers\n current, nxt = urlparse(current_url), urlparse(next_url)\n current_port = current.port or DEFAULT_SCHEME_PORTS.get(current.scheme)\n next_port = nxt.port or DEFAULT_SCHEME_PORTS.get(nxt.scheme)\n same_origin = (current.scheme, current.hostname, current_port) == (nxt.scheme, nxt.hostname, next_port)\n https_upgrade = (\n current.hostname == nxt.hostname\n and current.scheme == \"http\"\n and nxt.scheme == \"https\"\n and current_port == DEFAULT_SCHEME_PORTS[\"http\"]\n and next_port == DEFAULT_SCHEME_PORTS[\"https\"]\n )\n if same_origin or https_upgrade:\n return headers\n sensitive = {\"authorization\", \"proxy-authorization\", \"cookie\"}\n return {k: v for k, v in headers.items() if k.lower() not in sensitive}\n\n async def _follow_redirects_with_validation(\n self,\n method: str,\n url: str,\n headers: dict | None,\n body: Any,\n timeout: int,\n validated_ips: list[str],\n *,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n \"\"\"Make the request and follow redirects manually, re-validating every hop.\n\n This closes an SSRF bypass: with ``follow_redirects`` enabled, httpx would\n otherwise auto-follow a redirect from a validated public URL to an internal\n address that was never checked. Here each redirect ``Location`` is resolved\n (relative locations included) and re-validated with ``validate_and_resolve_url``\n — the same private/loopback/link-local denylist and DNS pinning applied to the\n initial request — before any connection to it is made. A blocked hop raises\n ``ValueError``; the number of redirects is capped at ``MAX_REDIRECTS``.\n \"\"\"\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n current_url = url\n current_ips = validated_ips\n redirection_history: list[dict] = []\n\n for _ in range(MAX_REDIRECTS + 1):\n request_params: dict[str, Any] = {\n \"method\": method,\n \"url\": current_url,\n \"headers\": headers,\n \"timeout\": timeout,\n # Never let httpx follow redirects itself; each hop is validated below.\n \"follow_redirects\": False,\n }\n # Only include body for methods that support it (GET must not have a body).\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n\n try:\n async with self._build_http_client(current_url, current_ips) as client:\n response = await client.request(**request_params)\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {current_url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n location = response.headers.get(\"Location\")\n if response.status_code in REDIRECT_STATUS_CODES and location:\n # Resolve relative redirects against the current URL.\n next_url = urljoin(current_url, location)\n redirection_history.append({\"url\": location, \"status_code\": response.status_code})\n\n # Re-validate the redirect target with the same SSRF denylist + DNS pinning.\n # Non-http(s) schemes, private/loopback/link-local hosts, and hostnames that\n # resolve to blocked IPs all raise SSRFProtectionError here.\n try:\n _validated_url, current_ips = validate_and_resolve_url(next_url)\n except SSRFProtectionError as e:\n msg = f\"SSRF Protection: blocked redirect to {next_url}: {e}\"\n raise ValueError(msg) from e\n\n method = self._method_for_redirect(method, response.status_code)\n headers = self._headers_for_redirect(headers, current_url, next_url)\n current_url = next_url\n continue\n\n # Not a redirect (or no Location header) - this is the final response.\n return await self._build_response_data(\n response,\n url,\n headers,\n redirection_history,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n\n msg = f\"SSRF Protection: exceeded the maximum of {MAX_REDIRECTS} redirects while requesting {url}\"\n raise ValueError(msg)\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n \"\"\"Update the build config based on the selected mode.\"\"\"\n if field_name != \"mode\":\n if field_name == \"curl_input\" and self.mode == \"cURL\" and self.curl_input:\n return self.parse_curl(self.curl_input, build_config)\n return build_config\n\n if field_value == \"cURL\":\n set_field_display(build_config, \"curl_input\", value=True)\n if build_config[\"curl_input\"][\"value\"]:\n try:\n build_config = self.parse_curl(build_config[\"curl_input\"][\"value\"], build_config)\n except ValueError as e:\n self.log(f\"Failed to parse cURL input: {e}\")\n else:\n set_field_display(build_config, \"curl_input\", value=False)\n\n return set_current_fields(\n build_config=build_config,\n action_fields=MODE_FIELDS,\n selected_action=field_value,\n default_fields=DEFAULT_FIELDS,\n func=set_field_advanced,\n default_value=True,\n )\n\n async def _response_info(\n self, response: httpx.Response, *, with_file_path: bool = False\n ) -> tuple[bool, Path | None]:\n \"\"\"Determine the file path and whether the response content is binary.\n\n Args:\n response (Response): The HTTP response object.\n with_file_path (bool): Whether to save the response content to a file.\n\n Returns:\n Tuple[bool, Path | None]:\n A tuple containing a boolean indicating if the content is binary and the full file path (if applicable).\n \"\"\"\n content_type = response.headers.get(\"Content-Type\", \"\")\n is_binary = \"application/octet-stream\" in content_type or \"application/binary\" in content_type\n\n if not with_file_path:\n return is_binary, None\n\n component_temp_dir = Path(tempfile.gettempdir()) / self.__class__.__name__\n\n # Create directory asynchronously\n await aiofiles_os.makedirs(component_temp_dir, exist_ok=True)\n\n filename = None\n if \"Content-Disposition\" in response.headers:\n content_disposition = response.headers[\"Content-Disposition\"]\n filename_match = re.search(r'filename=\"(.+?)\"', content_disposition)\n if filename_match:\n # Reduce to the basename to prevent path traversal: the response\n # (and therefore this header) is fully attacker-influenced, so a\n # value like filename=\"../../../../tmp/evil.sh\" must not escape\n # component_temp_dir. Path(...).name strips any directory parts.\n # Normalize backslashes to forward slashes first so Windows-style\n # separators (e.g. filename=\"..\\..\\tmp\\evil.sh\") are also stripped\n # on POSIX, where \"\\\\\" is a valid filename character that\n # Path(...).name would otherwise leave intact. NUL bytes are\n # stripped too: they survive Path(...).name and would otherwise\n # make the later .resolve() raise a cryptic \"embedded null\n # character\" ValueError instead of failing cleanly here.\n normalized_filename = filename_match.group(1).replace(\"\\\\\", \"/\").replace(\"\\x00\", \"\")\n extracted_filename = Path(normalized_filename).name\n if extracted_filename and extracted_filename not in (\".\", \"..\"):\n filename = extracted_filename\n\n # Step 3: Infer file extension or use part of the request URL if no filename\n if not filename:\n # Extract the last segment of the URL path\n url_path = urlparse(str(response.request.url) if response.request else \"\").path\n base_name = Path(url_path).name # Get the last segment of the path\n if not base_name: # If the path ends with a slash or is empty\n base_name = \"response\"\n\n # Infer file extension\n content_type_to_extension = {\n \"text/plain\": \".txt\",\n \"application/json\": \".json\",\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"application/octet-stream\": \".bin\",\n }\n extension = content_type_to_extension.get(content_type, \".bin\" if is_binary else \".txt\")\n filename = f\"{base_name}{extension}\"\n\n # Step 4: Define the full file path\n file_path = component_temp_dir / filename\n\n # Defense-in-depth: ensure the resolved path stays within the component\n # temp dir even if filename derivation above is ever changed.\n if not file_path.resolve().is_relative_to(component_temp_dir.resolve()):\n msg = \"Resolved output path escapes the component temporary directory\"\n raise ValueError(msg)\n\n # Step 5: Check if file exists asynchronously and handle accordingly\n try:\n # Try to create the file exclusively (x mode) to check existence\n async with aiofiles.open(file_path, \"x\") as _:\n pass # File created successfully, we can use this path\n except FileExistsError:\n # If file exists, append a timestamp to the filename\n timestamp = datetime.now(timezone.utc).strftime(\"%Y%m%d%H%M%S%f\")\n file_path = component_temp_dir / f\"{timestamp}-{filename}\"\n\n return is_binary, file_path\n" }, "curl_input": { "_input_type": "MultilineInput", @@ -1246,7 +1246,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1254,7 +1254,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json index 75ee5c7e40..0fd2c6d3d8 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json @@ -162,7 +162,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -775,7 +775,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" }, { "name": "pydantic", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json b/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json index aab306c3fa..91b54b3c2e 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json @@ -424,7 +424,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1640,7 +1640,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1648,7 +1648,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json index 3b88c0541a..72cae37c92 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json @@ -1773,7 +1773,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -2816,7 +2816,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -2824,7 +2824,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json index ef049f82e6..d5473f51ee 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json @@ -127,7 +127,9 @@ "id": "LoopComponent-GtPZT", "inputTypes": [ "DataFrame", - "Table" + "Table", + "Data", + "Message" ], "type": "other" } @@ -136,7 +138,7 @@ "source": "ArXivComponent-tAHR5", "sourceHandle": "{œdataTypeœ: œArXivComponentœ, œidœ: œArXivComponent-tAHR5œ, œnameœ: œdataframeœ, œoutput_typesœ: [œTableœ]}", "target": "LoopComponent-GtPZT", - "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œLoopComponent-GtPZTœ, œinputTypesœ: [œDataFrameœ, œTableœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œLoopComponent-GtPZTœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œMessageœ], œtypeœ: œotherœ}" }, { "className": "", @@ -376,7 +378,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1327,7 +1329,7 @@ "icon": "infinity", "legacy": false, "metadata": { - "code_hash": "0b08e58a5c70", + "code_hash": "2712a1de7210", "dependencies": { "dependencies": [ { @@ -1355,6 +1357,7 @@ "selected": "JSON", "tool_mode": true, "types": [ + "Data", "JSON" ], "value": "__UNDEFINED__" @@ -1369,6 +1372,7 @@ "selected": "Table", "tool_mode": true, "types": [ + "DataFrame", "Table" ], "value": "__UNDEFINED__" @@ -1393,17 +1397,19 @@ "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=\"The initial DataFrame to iterate over.\",\n input_types=[\"DataFrame\", \"Table\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Item\",\n name=\"item\",\n method=\"item_output\",\n allows_loop=True,\n loop_types=[\"Message\"],\n group_outputs=True,\n ),\n Output(display_name=\"Done\", name=\"done\", method=\"done_output\", group_outputs=True),\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 return a list of Data objects.\"\"\"\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" + "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": "The initial DataFrame to iterate over.", + "info": "Data to iterate over. Accepts DataFrame, Table, Data, or Message.", "input_types": [ "DataFrame", - "Table" + "Table", + "Data", + "Message" ], "list": false, "list_add_label": "Add More", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json index d529d9de2b..569e32ac41 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json @@ -633,7 +633,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json index abe456925b..4b80c91d08 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json @@ -409,7 +409,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -898,7 +898,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -906,7 +906,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json index 3d55a6a108..867cba15c4 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json @@ -575,7 +575,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -947,7 +947,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -955,7 +955,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json index c2b34b6599..aab4e89431 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json @@ -362,7 +362,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -370,7 +370,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -956,7 +956,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -964,7 +964,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -2408,7 +2408,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -2416,7 +2416,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -3814,7 +3814,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json index d660a1b44b..3edc677cf3 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json @@ -658,7 +658,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -945,7 +945,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -953,7 +953,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json index 2f975ee312..b22aa592e7 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json @@ -152,7 +152,7 @@ "dependencies": [ { "name": "apify_client", - "version": "3.0.2" + "version": "3.0.3" }, { "name": "langchain_community", @@ -384,7 +384,7 @@ "dependencies": [ { "name": "apify_client", - "version": "3.0.2" + "version": "3.0.3" }, { "name": "langchain_community", @@ -983,7 +983,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1297,7 +1297,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1305,7 +1305,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Structured Data Analysis Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Structured Data Analysis Agent.json index 048102a124..d61c806f31 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Structured Data Analysis Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Structured Data Analysis Agent.json @@ -464,7 +464,7 @@ }, { "name": "OpenDsStar", - "version": null + "version": "1.0.26" } ], "total_dependencies": 4 @@ -1093,7 +1093,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" }, { "name": "pydantic", @@ -2101,7 +2101,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -5231,7 +5231,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json index c7e95d1311..d25b2fc6ca 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json @@ -831,7 +831,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1113,7 +1113,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -2637,7 +2637,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" }, { "name": "pydantic", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json index 810ace5ec9..a45fdfb24d 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json @@ -507,7 +507,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1727,7 +1727,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -1735,7 +1735,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -2316,7 +2316,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -2324,7 +2324,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -2905,7 +2905,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -2913,7 +2913,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json index 9516a16f95..c8ff529537 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json @@ -405,7 +405,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json index d577f26dd3..2eff0475c1 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json @@ -732,7 +732,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx", @@ -1620,7 +1620,7 @@ }, { "name": "cryptography", - "version": "48.0.0" + "version": "48.0.1" }, { "name": "langchain_chroma", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json index 71fe7e0d7c..9d7256639e 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json @@ -505,7 +505,7 @@ "dependencies": [ { "name": "langchain", - "version": "1.2.18" + "version": "1.3.11" }, { "name": "lfx", @@ -513,7 +513,7 @@ }, { "name": "langchain_core", - "version": "1.4.0" + "version": "1.4.8" } ], "total_dependencies": 3 @@ -1303,7 +1303,7 @@ }, { "name": "fastapi", - "version": "0.136.3" + "version": "0.138.0" }, { "name": "lfx",