Skip to content

Commit 7f44894

Browse files
committed
Merge remote-tracking branch 'origin/release-1.8.3'
# Conflicts: # src/backend/tests/unit/test_unified_models.py
2 parents 2fa3b36 + af8da56 commit 7f44894

17 files changed

Lines changed: 1605 additions & 451 deletions

File tree

.secrets.baseline

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6041,56 +6041,21 @@
60416041
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
60426042
"hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155",
60436043
"is_verified": false,
6044-
"line_number": 69
6044+
"line_number": 1171
60456045
},
60466046
{
60476047
"type": "Secret Keyword",
60486048
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
60496049
"hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a",
60506050
"is_verified": false,
6051-
"line_number": 87
6052-
},
6053-
{
6054-
"type": "Secret Keyword",
6055-
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
6056-
"hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a",
6057-
"is_verified": false,
6058-
"line_number": 96
6051+
"line_number": 1182
60596052
},
60606053
{
60616054
"type": "Secret Keyword",
60626055
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
60636056
"hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030",
60646057
"is_verified": false,
6065-
"line_number": 106
6066-
},
6067-
{
6068-
"type": "Secret Keyword",
6069-
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
6070-
"hashed_secret": "a19ee5a9fdc191092796cd9bfa97c55fe5631695",
6071-
"is_verified": false,
6072-
"line_number": 380
6073-
},
6074-
{
6075-
"type": "Secret Keyword",
6076-
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
6077-
"hashed_secret": "b780a23b530ef6bb5c3b722a0c9c5e3abf222e8b",
6078-
"is_verified": false,
6079-
"line_number": 381
6080-
},
6081-
{
6082-
"type": "Secret Keyword",
6083-
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
6084-
"hashed_secret": "ed96761b106155514463a9f82d02b14042e75b61",
6085-
"is_verified": false,
6086-
"line_number": 382
6087-
},
6088-
{
6089-
"type": "Secret Keyword",
6090-
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
6091-
"hashed_secret": "ac579e82cacca3f9c1a8b18bdfdad78e8b6140f8",
6092-
"is_verified": false,
6093-
"line_number": 383
6058+
"line_number": 1196
60946059
}
60956060
],
60966061
"src/lfx/src/lfx/cli/serve_app.py": [
@@ -6373,5 +6338,5 @@
63736338
}
63746339
]
63756340
},
6376-
"generated_at": "2026-03-04T23:52:18Z"
6341+
"generated_at": "2026-03-25T17:32:51Z"
63776342
}

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "langflow"
3-
version = "1.8.2"
3+
version = "1.8.3"
44
description = "A Python package with a built-in web application"
55
requires-python = ">=3.10,<3.14"
66
license = "MIT"
@@ -17,7 +17,7 @@ maintainers = [
1717
]
1818
# Define your main dependencies here
1919
dependencies = [
20-
"langflow-base[complete]~=0.8.2",
20+
"langflow-base[complete]~=0.8.3",
2121
]
2222

2323

src/backend/base/langflow/api/v2/files.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ async def upload_user_file(
137137
settings_service: Annotated[SettingsService, Depends(get_settings_service)],
138138
*,
139139
append: bool = False,
140+
ephemeral: bool = False,
140141
) -> UploadFileResponse:
141142
"""Upload a file for the current user and track it in the database."""
142143
# Get the max allowed file size from settings (in MB)
@@ -232,6 +233,12 @@ async def upload_user_file(
232233
# General error saving file or getting file size
233234
raise HTTPException(status_code=500, detail=f"Error accessing file: {e}") from e
234235

236+
if ephemeral:
237+
# Ephemeral uploads: file is saved to storage (servable for chat history)
238+
# but no UserFile record is created (won't appear in "My Files")
239+
file_path = f"{current_user.id}/{stored_file_name}"
240+
return UploadFileResponse(id=file_id, name=root_filename, path=file_path, size=file_size)
241+
235242
if append and existing_file:
236243
existing_file.size = file_size
237244
session.add(existing_file)

src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2123,7 +2123,7 @@
21232123
"legacy": false,
21242124
"lf_version": "1.4.2",
21252125
"metadata": {
2126-
"code_hash": "2bd7a064d724",
2126+
"code_hash": "3e55e36d0692",
21272127
"dependencies": {
21282128
"dependencies": [
21292129
{
@@ -2167,7 +2167,7 @@
21672167
"advanced": true,
21682168
"display_name": "Allow Dangerous Deserialization",
21692169
"dynamic": false,
2170-
"info": "Set to True to allow loading pickle files from untrusted sources. Only enable this if you trust the source of the data.",
2170+
"info": "Set to True to allow loading pickle files. WARNING: Only enable this if you trust the source of the data. Malicious pickle files can execute arbitrary code on your system.",
21712171
"list": false,
21722172
"list_add_label": "Add More",
21732173
"name": "allow_dangerous_deserialization",
@@ -2178,7 +2178,7 @@
21782178
"tool_mode": false,
21792179
"trace_as_metadata": true,
21802180
"type": "bool",
2181-
"value": true
2181+
"value": false
21822182
},
21832183
"code": {
21842184
"advanced": true,
@@ -2196,7 +2196,7 @@
21962196
"show": true,
21972197
"title_case": false,
21982198
"type": "code",
2199-
"value": "from pathlib import Path\n\nfrom langchain_community.vectorstores import FAISS\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.helpers.data import docs_to_data\nfrom lfx.io import BoolInput, HandleInput, IntInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass FaissVectorStoreComponent(LCVectorStoreComponent):\n \"\"\"FAISS Vector Store with search capabilities.\"\"\"\n\n display_name: str = \"FAISS\"\n description: str = \"FAISS Vector Store with search capabilities\"\n name = \"FAISS\"\n icon = \"FAISS\"\n\n inputs = [\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n value=\"langflow_index\",\n ),\n StrInput(\n name=\"persist_directory\",\n display_name=\"Persist Directory\",\n info=\"Path to save the FAISS index. It will be relative to where Langflow is running.\",\n ),\n *LCVectorStoreComponent.inputs,\n BoolInput(\n name=\"allow_dangerous_deserialization\",\n display_name=\"Allow Dangerous Deserialization\",\n info=\"Set to True to allow loading pickle files from untrusted sources. \"\n \"Only enable this if you trust the source of the data.\",\n advanced=True,\n value=True,\n ),\n HandleInput(name=\"embedding\", display_name=\"Embedding\", input_types=[\"Embeddings\"]),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Number of Results\",\n info=\"Number of results to return.\",\n advanced=True,\n value=4,\n ),\n ]\n\n @staticmethod\n def resolve_path(path: str) -> str:\n \"\"\"Resolve the path relative to the Langflow root.\n\n Args:\n path: The path to resolve\n Returns:\n str: The resolved path as a string\n \"\"\"\n return str(Path(path).resolve())\n\n def get_persist_directory(self) -> Path:\n \"\"\"Returns the resolved persist directory path or the current directory if not set.\"\"\"\n if self.persist_directory:\n return Path(self.resolve_path(self.persist_directory))\n return Path()\n\n @check_cached_vector_store\n def build_vector_store(self) -> FAISS:\n \"\"\"Builds the FAISS object.\"\"\"\n path = self.get_persist_directory()\n path.mkdir(parents=True, exist_ok=True)\n\n # Convert DataFrame to Data if needed using parent's method\n self.ingest_data = self._prepare_ingest_data()\n\n documents = []\n for _input in self.ingest_data or []:\n if isinstance(_input, Data):\n documents.append(_input.to_lc_document())\n else:\n documents.append(_input)\n\n faiss = FAISS.from_documents(documents=documents, embedding=self.embedding)\n faiss.save_local(str(path), self.index_name)\n return faiss\n\n def search_documents(self) -> list[Data]:\n \"\"\"Search for documents in the FAISS vector store.\"\"\"\n path = self.get_persist_directory()\n index_path = path / f\"{self.index_name}.faiss\"\n\n if not index_path.exists():\n vector_store = self.build_vector_store()\n else:\n vector_store = FAISS.load_local(\n folder_path=str(path),\n embeddings=self.embedding,\n index_name=self.index_name,\n allow_dangerous_deserialization=self.allow_dangerous_deserialization,\n )\n\n if not vector_store:\n msg = \"Failed to load the FAISS index.\"\n raise ValueError(msg)\n\n if self.search_query and isinstance(self.search_query, str) and self.search_query.strip():\n docs = vector_store.similarity_search(\n query=self.search_query,\n k=self.number_of_results,\n )\n return docs_to_data(docs)\n return []\n"
2199+
"value": "from pathlib import Path\n\nfrom langchain_community.vectorstores import FAISS\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.helpers.data import docs_to_data\nfrom lfx.io import BoolInput, HandleInput, IntInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass FaissVectorStoreComponent(LCVectorStoreComponent):\n \"\"\"FAISS Vector Store with search capabilities.\"\"\"\n\n display_name: str = \"FAISS\"\n description: str = \"FAISS Vector Store with search capabilities\"\n name = \"FAISS\"\n icon = \"FAISS\"\n\n inputs = [\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n value=\"langflow_index\",\n ),\n StrInput(\n name=\"persist_directory\",\n display_name=\"Persist Directory\",\n info=\"Path to save the FAISS index. It will be relative to where Langflow is running.\",\n ),\n *LCVectorStoreComponent.inputs,\n BoolInput(\n name=\"allow_dangerous_deserialization\",\n display_name=\"Allow Dangerous Deserialization\",\n info=\"Set to True to allow loading pickle files. WARNING: Only enable this if you trust the source \"\n \"of the data. Malicious pickle files can execute arbitrary code on your system.\",\n advanced=True,\n value=False,\n ),\n HandleInput(name=\"embedding\", display_name=\"Embedding\", input_types=[\"Embeddings\"]),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Number of Results\",\n info=\"Number of results to return.\",\n advanced=True,\n value=4,\n ),\n ]\n\n @staticmethod\n def resolve_path(path: str) -> str:\n \"\"\"Resolve the path relative to the Langflow root.\n\n Args:\n path: The path to resolve\n Returns:\n str: The resolved path as a string\n \"\"\"\n return str(Path(path).resolve())\n\n def get_persist_directory(self) -> Path:\n \"\"\"Returns the resolved persist directory path or the current directory if not set.\"\"\"\n if self.persist_directory:\n return Path(self.resolve_path(self.persist_directory))\n return Path()\n\n @check_cached_vector_store\n def build_vector_store(self) -> FAISS:\n \"\"\"Builds the FAISS object.\"\"\"\n path = self.get_persist_directory()\n path.mkdir(parents=True, exist_ok=True)\n\n # Convert DataFrame to Data if needed using parent's method\n self.ingest_data = self._prepare_ingest_data()\n\n documents = []\n for _input in self.ingest_data or []:\n if isinstance(_input, Data):\n documents.append(_input.to_lc_document())\n else:\n documents.append(_input)\n\n faiss = FAISS.from_documents(documents=documents, embedding=self.embedding)\n faiss.save_local(str(path), self.index_name)\n return faiss\n\n def search_documents(self) -> list[Data]:\n \"\"\"Search for documents in the FAISS vector store.\"\"\"\n path = self.get_persist_directory()\n index_path = path / f\"{self.index_name}.faiss\"\n\n if not index_path.exists():\n vector_store = self.build_vector_store()\n else:\n vector_store = FAISS.load_local(\n folder_path=str(path),\n embeddings=self.embedding,\n index_name=self.index_name,\n allow_dangerous_deserialization=self.allow_dangerous_deserialization,\n )\n\n if not vector_store:\n msg = \"Failed to load the FAISS index.\"\n raise ValueError(msg)\n\n if self.search_query and isinstance(self.search_query, str) and self.search_query.strip():\n docs = vector_store.similarity_search(\n query=self.search_query,\n k=self.number_of_results,\n )\n return docs_to_data(docs)\n return []\n"
22002200
},
22012201
"embedding": {
22022202
"_input_type": "HandleInput",

src/backend/base/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "langflow-base"
3-
version = "0.8.2"
3+
version = "0.8.3"
44
description = "A Python package with a built-in web application"
55
requires-python = ">=3.10,<3.14"
66
license = "MIT"
@@ -17,7 +17,7 @@ maintainers = [
1717
]
1818

1919
dependencies = [
20-
"lfx~=0.3.2",
20+
"lfx~=0.3.3",
2121
"fastapi>=0.135.0,<1.0.0",
2222
"httpx[http2]>=0.27,<1.0.0",
2323
"aiofile>=3.9.0,<4.0.0",

src/backend/tests/unit/api/v2/test_files.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,77 @@ async def test_upload_file(files_client, files_created_api_key):
149149
assert "id" in response_json
150150

151151

152+
async def test_should_not_persist_in_my_files_when_upload_is_ephemeral(files_client, files_created_api_key):
153+
"""Ephemeral uploads save the file to storage but do NOT create a UserFile DB record.
154+
155+
This is the expected behavior for chat playground uploads in Desktop,
156+
where the file must be servable (for chat history) but should not
157+
appear in the user's 'My Files' list.
158+
"""
159+
headers = {"x-api-key": files_created_api_key.api_key}
160+
161+
# Upload with ephemeral=true
162+
response = await files_client.post(
163+
"api/v2/files",
164+
files={"file": ("playground_image.png", b"fake image content")},
165+
params={"ephemeral": "true"},
166+
headers=headers,
167+
)
168+
assert response.status_code == 201, f"Expected 201, got {response.status_code}: {response.text}"
169+
170+
upload_response = response.json()
171+
assert "path" in upload_response
172+
173+
# The file must NOT appear in the user's file list
174+
list_response = await files_client.get("api/v2/files", headers=headers)
175+
assert list_response.status_code == 200
176+
file_names = [f["name"] for f in list_response.json()]
177+
assert "playground_image" not in file_names, (
178+
f"Ephemeral file should not appear in My Files, but found: {file_names}"
179+
)
180+
181+
# The file is saved in storage and the response includes a valid path
182+
file_path = upload_response["path"]
183+
assert file_path, "Ephemeral upload should return a non-empty path"
184+
# Path format: {user_id}/{stored_file_name}
185+
parts = file_path.split("/")
186+
assert len(parts) == 2, f"Expected path format 'user_id/filename', got: {file_path}"
187+
188+
189+
async def test_should_return_path_with_forward_slashes_when_uploading_file(files_client, files_created_api_key):
190+
"""Upload response path must use forward slashes on all platforms.
191+
192+
On Windows, pathlib.Path serializes with backslashes, but the GET list
193+
endpoint returns the raw DB string with forward slashes. If the POST
194+
response uses backslashes, the frontend cannot match them with
195+
selectedFiles.includes(file.path), leaving checkboxes unchecked.
196+
"""
197+
headers = {"x-api-key": files_created_api_key.api_key}
198+
199+
response = await files_client.post(
200+
"api/v2/files",
201+
files={"file": ("test_path.txt", b"path test content")},
202+
headers=headers,
203+
)
204+
assert response.status_code == 201
205+
206+
upload_path = response.json()["path"]
207+
assert "\\" not in upload_path, (
208+
f"Upload response path contains backslashes: '{upload_path}'. "
209+
"Path must use forward slashes on all platforms for frontend compatibility."
210+
)
211+
212+
# Verify the upload path matches what GET /files returns
213+
list_response = await files_client.get("api/v2/files", headers=headers)
214+
assert list_response.status_code == 200
215+
216+
listed_paths = [f["path"] for f in list_response.json()]
217+
assert upload_path in listed_paths, (
218+
f"Upload path '{upload_path}' not found in listed paths {listed_paths}. "
219+
"POST and GET must return identical path strings."
220+
)
221+
222+
152223
async def test_download_file(files_client, files_created_api_key):
153224
headers = {"x-api-key": files_created_api_key.api_key}
154225

0 commit comments

Comments
 (0)