mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 16:09:31 +08:00
Merge remote-tracking branch 'origin/release-1.8.3'
# Conflicts: # src/backend/tests/unit/test_unified_models.py
This commit is contained in:
@ -6041,56 +6041,21 @@
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155",
|
||||
"is_verified": false,
|
||||
"line_number": 69
|
||||
"line_number": 1171
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a",
|
||||
"is_verified": false,
|
||||
"line_number": 87
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a",
|
||||
"is_verified": false,
|
||||
"line_number": 96
|
||||
"line_number": 1182
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030",
|
||||
"is_verified": false,
|
||||
"line_number": 106
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "a19ee5a9fdc191092796cd9bfa97c55fe5631695",
|
||||
"is_verified": false,
|
||||
"line_number": 380
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "b780a23b530ef6bb5c3b722a0c9c5e3abf222e8b",
|
||||
"is_verified": false,
|
||||
"line_number": 381
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "ed96761b106155514463a9f82d02b14042e75b61",
|
||||
"is_verified": false,
|
||||
"line_number": 382
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/lfx/src/lfx/base/models/unified_models.py",
|
||||
"hashed_secret": "ac579e82cacca3f9c1a8b18bdfdad78e8b6140f8",
|
||||
"is_verified": false,
|
||||
"line_number": 383
|
||||
"line_number": 1196
|
||||
}
|
||||
],
|
||||
"src/lfx/src/lfx/cli/serve_app.py": [
|
||||
@ -6373,5 +6338,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2026-03-04T23:52:18Z"
|
||||
"generated_at": "2026-03-25T17:32:51Z"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "langflow"
|
||||
version = "1.8.2"
|
||||
version = "1.8.3"
|
||||
description = "A Python package with a built-in web application"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
license = "MIT"
|
||||
@ -17,7 +17,7 @@ maintainers = [
|
||||
]
|
||||
# Define your main dependencies here
|
||||
dependencies = [
|
||||
"langflow-base[complete]~=0.8.2",
|
||||
"langflow-base[complete]~=0.8.3",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -137,6 +137,7 @@ async def upload_user_file(
|
||||
settings_service: Annotated[SettingsService, Depends(get_settings_service)],
|
||||
*,
|
||||
append: bool = False,
|
||||
ephemeral: bool = False,
|
||||
) -> UploadFileResponse:
|
||||
"""Upload a file for the current user and track it in the database."""
|
||||
# Get the max allowed file size from settings (in MB)
|
||||
@ -232,6 +233,12 @@ async def upload_user_file(
|
||||
# General error saving file or getting file size
|
||||
raise HTTPException(status_code=500, detail=f"Error accessing file: {e}") from e
|
||||
|
||||
if ephemeral:
|
||||
# Ephemeral uploads: file is saved to storage (servable for chat history)
|
||||
# but no UserFile record is created (won't appear in "My Files")
|
||||
file_path = f"{current_user.id}/{stored_file_name}"
|
||||
return UploadFileResponse(id=file_id, name=root_filename, path=file_path, size=file_size)
|
||||
|
||||
if append and existing_file:
|
||||
existing_file.size = file_size
|
||||
session.add(existing_file)
|
||||
|
||||
@ -2123,7 +2123,7 @@
|
||||
"legacy": false,
|
||||
"lf_version": "1.4.2",
|
||||
"metadata": {
|
||||
"code_hash": "2bd7a064d724",
|
||||
"code_hash": "3e55e36d0692",
|
||||
"dependencies": {
|
||||
"dependencies": [
|
||||
{
|
||||
@ -2167,7 +2167,7 @@
|
||||
"advanced": true,
|
||||
"display_name": "Allow Dangerous Deserialization",
|
||||
"dynamic": false,
|
||||
"info": "Set to True to allow loading pickle files from untrusted sources. Only enable this if you trust the source of the data.",
|
||||
"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.",
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"name": "allow_dangerous_deserialization",
|
||||
@ -2178,7 +2178,7 @@
|
||||
"tool_mode": false,
|
||||
"trace_as_metadata": true,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
"value": false
|
||||
},
|
||||
"code": {
|
||||
"advanced": true,
|
||||
@ -2196,7 +2196,7 @@
|
||||
"show": true,
|
||||
"title_case": false,
|
||||
"type": "code",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"embedding": {
|
||||
"_input_type": "HandleInput",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "langflow-base"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
description = "A Python package with a built-in web application"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
license = "MIT"
|
||||
@ -17,7 +17,7 @@ maintainers = [
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"lfx~=0.3.2",
|
||||
"lfx~=0.3.3",
|
||||
"fastapi>=0.135.0,<1.0.0",
|
||||
"httpx[http2]>=0.27,<1.0.0",
|
||||
"aiofile>=3.9.0,<4.0.0",
|
||||
|
||||
@ -149,6 +149,77 @@ async def test_upload_file(files_client, files_created_api_key):
|
||||
assert "id" in response_json
|
||||
|
||||
|
||||
async def test_should_not_persist_in_my_files_when_upload_is_ephemeral(files_client, files_created_api_key):
|
||||
"""Ephemeral uploads save the file to storage but do NOT create a UserFile DB record.
|
||||
|
||||
This is the expected behavior for chat playground uploads in Desktop,
|
||||
where the file must be servable (for chat history) but should not
|
||||
appear in the user's 'My Files' list.
|
||||
"""
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
# Upload with ephemeral=true
|
||||
response = await files_client.post(
|
||||
"api/v2/files",
|
||||
files={"file": ("playground_image.png", b"fake image content")},
|
||||
params={"ephemeral": "true"},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201, f"Expected 201, got {response.status_code}: {response.text}"
|
||||
|
||||
upload_response = response.json()
|
||||
assert "path" in upload_response
|
||||
|
||||
# The file must NOT appear in the user's file list
|
||||
list_response = await files_client.get("api/v2/files", headers=headers)
|
||||
assert list_response.status_code == 200
|
||||
file_names = [f["name"] for f in list_response.json()]
|
||||
assert "playground_image" not in file_names, (
|
||||
f"Ephemeral file should not appear in My Files, but found: {file_names}"
|
||||
)
|
||||
|
||||
# The file is saved in storage and the response includes a valid path
|
||||
file_path = upload_response["path"]
|
||||
assert file_path, "Ephemeral upload should return a non-empty path"
|
||||
# Path format: {user_id}/{stored_file_name}
|
||||
parts = file_path.split("/")
|
||||
assert len(parts) == 2, f"Expected path format 'user_id/filename', got: {file_path}"
|
||||
|
||||
|
||||
async def test_should_return_path_with_forward_slashes_when_uploading_file(files_client, files_created_api_key):
|
||||
"""Upload response path must use forward slashes on all platforms.
|
||||
|
||||
On Windows, pathlib.Path serializes with backslashes, but the GET list
|
||||
endpoint returns the raw DB string with forward slashes. If the POST
|
||||
response uses backslashes, the frontend cannot match them with
|
||||
selectedFiles.includes(file.path), leaving checkboxes unchecked.
|
||||
"""
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
response = await files_client.post(
|
||||
"api/v2/files",
|
||||
files={"file": ("test_path.txt", b"path test content")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
upload_path = response.json()["path"]
|
||||
assert "\\" not in upload_path, (
|
||||
f"Upload response path contains backslashes: '{upload_path}'. "
|
||||
"Path must use forward slashes on all platforms for frontend compatibility."
|
||||
)
|
||||
|
||||
# Verify the upload path matches what GET /files returns
|
||||
list_response = await files_client.get("api/v2/files", headers=headers)
|
||||
assert list_response.status_code == 200
|
||||
|
||||
listed_paths = [f["path"] for f in list_response.json()]
|
||||
assert upload_path in listed_paths, (
|
||||
f"Upload path '{upload_path}' not found in listed paths {listed_paths}. "
|
||||
"POST and GET must return identical path strings."
|
||||
)
|
||||
|
||||
|
||||
async def test_download_file(files_client, files_created_api_key):
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
|
||||
102
src/backend/tests/unit/test_credential_resolution.py
Normal file
102
src/backend/tests/unit/test_credential_resolution.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Tests for credential resolution in the unified models system.
|
||||
|
||||
Bug: Desktop Global Variable OPENAI_API_KEY not injected at runtime.
|
||||
When api_key parameter is None (Agent component default), get_api_key_for_provider
|
||||
only attempts DB lookup but has no os.getenv() fallback and no error handling
|
||||
for ValueError from get_variable(). This causes failures in Desktop where the
|
||||
env var is not set and the DB lookup is the only path.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class TestGetApiKeyForProviderDbFallback:
|
||||
"""Tests for get_api_key_for_provider when api_key param is None (second path)."""
|
||||
|
||||
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
|
||||
@patch("lfx.base.models.unified_models.run_until_complete")
|
||||
def test_should_fallback_to_env_when_db_lookup_raises_value_error(self, mock_run, mock_mapping, monkeypatch):
|
||||
"""When variable_service.get_variable raises ValueError (variable not found in DB).
|
||||
|
||||
get_api_key_for_provider should fall back to os.getenv() instead of returning None.
|
||||
"""
|
||||
from lfx.base.models.unified_models import get_api_key_for_provider
|
||||
|
||||
user_id = str(uuid4())
|
||||
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
|
||||
mock_run.side_effect = ValueError("OPENAI_API_KEY variable not found.")
|
||||
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-env-key")
|
||||
|
||||
result = get_api_key_for_provider(user_id, "OpenAI", None)
|
||||
|
||||
assert result == "sk-test-env-key"
|
||||
|
||||
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
|
||||
@patch("lfx.base.models.unified_models.run_until_complete")
|
||||
def test_should_fallback_to_env_when_db_lookup_returns_empty_string(self, mock_run, mock_mapping, monkeypatch):
|
||||
"""When decryption fails, get_variable returns empty string.
|
||||
|
||||
get_api_key_for_provider should fall back to os.getenv().
|
||||
"""
|
||||
from lfx.base.models.unified_models import get_api_key_for_provider
|
||||
|
||||
user_id = str(uuid4())
|
||||
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
|
||||
mock_run.return_value = ""
|
||||
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-env-key")
|
||||
|
||||
result = get_api_key_for_provider(user_id, "OpenAI", None)
|
||||
|
||||
assert result == "sk-test-env-key"
|
||||
|
||||
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
|
||||
@patch("lfx.base.models.unified_models.run_until_complete")
|
||||
def test_should_fallback_to_env_when_variable_service_is_none(self, mock_run, mock_mapping, monkeypatch):
|
||||
"""When variable_service is None (service not available in thread context).
|
||||
|
||||
get_api_key_for_provider should fall back to os.getenv().
|
||||
"""
|
||||
from lfx.base.models.unified_models import get_api_key_for_provider
|
||||
|
||||
user_id = str(uuid4())
|
||||
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
|
||||
mock_run.return_value = None
|
||||
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-env-key")
|
||||
|
||||
result = get_api_key_for_provider(user_id, "OpenAI", None)
|
||||
|
||||
assert result == "sk-test-env-key"
|
||||
|
||||
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
|
||||
@patch("lfx.base.models.unified_models.run_until_complete")
|
||||
def test_should_return_none_when_both_db_and_env_unavailable(self, mock_run, mock_mapping, monkeypatch):
|
||||
"""When both DB lookup and env var are unavailable, should return None."""
|
||||
from lfx.base.models.unified_models import get_api_key_for_provider
|
||||
|
||||
user_id = str(uuid4())
|
||||
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
|
||||
mock_run.return_value = None
|
||||
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
|
||||
result = get_api_key_for_provider(user_id, "OpenAI", None)
|
||||
|
||||
assert result is None
|
||||
|
||||
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
|
||||
@patch("lfx.base.models.unified_models.run_until_complete")
|
||||
def test_should_return_db_value_when_db_lookup_succeeds(self, mock_run, mock_mapping):
|
||||
"""When DB lookup succeeds, should return the DB value (no env fallback needed)."""
|
||||
from lfx.base.models.unified_models import get_api_key_for_provider
|
||||
|
||||
user_id = str(uuid4())
|
||||
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
|
||||
mock_run.return_value = "sk-from-database"
|
||||
|
||||
result = get_api_key_for_provider(user_id, "OpenAI", None)
|
||||
|
||||
assert result == "sk-from-database"
|
||||
@ -1,6 +1,11 @@
|
||||
from unittest.mock import MagicMock
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from lfx.base.models.unified_models import get_unified_models_detailed, update_model_options_in_build_config
|
||||
from lfx.base.models.unified_models import (
|
||||
apply_provider_variable_config_to_build_config,
|
||||
get_unified_models_detailed,
|
||||
update_model_options_in_build_config,
|
||||
)
|
||||
|
||||
|
||||
def _flatten_models(result):
|
||||
@ -81,16 +86,16 @@ def test_filter_by_model_type_embeddings():
|
||||
|
||||
|
||||
def test_update_model_options_with_custom_field_name():
|
||||
"""Test that update_model_options_in_build_config works with custom field names."""
|
||||
"""Test that update_model_options_in_build_config populates the 'model' field with embedding options."""
|
||||
# Create mock component
|
||||
mock_component = MagicMock()
|
||||
mock_component.user_id = "test-user-123"
|
||||
mock_component.cache = {}
|
||||
mock_component.log = MagicMock()
|
||||
|
||||
# Create build_config with custom field name
|
||||
# Create build_config using the standard 'model' field (used for embeddings too)
|
||||
build_config = {
|
||||
"embedding_model": {
|
||||
"model": {
|
||||
"options": [],
|
||||
"value": "",
|
||||
"input_types": ["Embeddings"],
|
||||
@ -104,25 +109,24 @@ def test_update_model_options_with_custom_field_name():
|
||||
{"name": "embed-english-v3.0", "provider": "Cohere"},
|
||||
]
|
||||
|
||||
# Call with custom field name
|
||||
# Call with embedding cache key prefix
|
||||
result = update_model_options_in_build_config(
|
||||
component=mock_component,
|
||||
build_config=build_config,
|
||||
cache_key_prefix="test_embedding_options",
|
||||
cache_key_prefix="embedding_model_options",
|
||||
get_options_func=mock_get_options,
|
||||
field_name=None,
|
||||
field_value="",
|
||||
model_field_name="embedding_model",
|
||||
)
|
||||
|
||||
# Verify options were populated in the custom field
|
||||
assert "embedding_model" in result
|
||||
assert len(result["embedding_model"]["options"]) == 2
|
||||
assert result["embedding_model"]["options"][0]["name"] == "text-embedding-ada-002"
|
||||
assert result["embedding_model"]["options"][1]["provider"] == "Cohere"
|
||||
# Verify options were populated in the model field
|
||||
assert "model" in result
|
||||
assert len(result["model"]["options"]) == 2
|
||||
assert result["model"]["options"][0]["name"] == "text-embedding-ada-002"
|
||||
assert result["model"]["options"][1]["provider"] == "Cohere"
|
||||
|
||||
# Verify default value was set
|
||||
assert result["embedding_model"]["value"] == [result["embedding_model"]["options"][0]]
|
||||
assert result["model"]["value"] == [result["model"]["options"][0]]
|
||||
|
||||
|
||||
def test_update_model_options_default_field_name():
|
||||
@ -160,3 +164,227 @@ def test_update_model_options_default_field_name():
|
||||
assert "model" in result
|
||||
assert len(result["model"]["options"]) == 1
|
||||
assert result["model"]["options"][0]["name"] == "gpt-4"
|
||||
|
||||
|
||||
# Tests for apply_provider_variable_config_to_build_config
|
||||
|
||||
|
||||
def test_apply_provider_config_auto_sets_env_var_for_empty_field():
|
||||
"""Test that env var is auto-set when field is empty."""
|
||||
mock_provider_vars = [
|
||||
{
|
||||
"variable_key": "OPENAI_API_KEY",
|
||||
"component_metadata": {
|
||||
"mapping_field": "openai_api_key",
|
||||
"required": True,
|
||||
"advanced": False,
|
||||
"info": "OpenAI API Key",
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
build_config = {
|
||||
"openai_api_key": {
|
||||
"value": "",
|
||||
"show": False,
|
||||
"required": False,
|
||||
"advanced": False,
|
||||
"load_from_db": False,
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("lfx.base.models.unified_models.get_provider_all_variables", return_value=mock_provider_vars),
|
||||
patch("lfx.base.models.unified_models._get_all_provider_specific_field_names", return_value=["openai_api_key"]),
|
||||
patch.dict(os.environ, {"OPENAI_API_KEY": "test-key-123"}), # pragma: allowlist secret
|
||||
):
|
||||
result = apply_provider_variable_config_to_build_config(build_config, "OpenAI")
|
||||
|
||||
# Should auto-set the env var key for empty field
|
||||
assert result["openai_api_key"]["value"] == "OPENAI_API_KEY"
|
||||
assert result["openai_api_key"]["load_from_db"] is True
|
||||
assert result["openai_api_key"]["show"] is True
|
||||
assert result["openai_api_key"]["required"] is True
|
||||
|
||||
|
||||
def test_apply_provider_config_preserves_user_selected_value():
|
||||
"""Test that user-selected values are NOT overwritten."""
|
||||
mock_provider_vars = [
|
||||
{
|
||||
"variable_key": "OPENAI_API_KEY",
|
||||
"component_metadata": {
|
||||
"mapping_field": "openai_api_key",
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# User has already selected a different global variable
|
||||
build_config = {
|
||||
"openai_api_key": {
|
||||
"value": "MY_CUSTOM_API_KEY",
|
||||
"load_from_db": True,
|
||||
"show": False,
|
||||
"required": False,
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("lfx.base.models.unified_models.get_provider_all_variables", return_value=mock_provider_vars),
|
||||
patch("lfx.base.models.unified_models._get_all_provider_specific_field_names", return_value=["openai_api_key"]),
|
||||
patch.dict(os.environ, {"OPENAI_API_KEY": "test-key-123"}), # pragma: allowlist secret
|
||||
):
|
||||
result = apply_provider_variable_config_to_build_config(build_config, "OpenAI")
|
||||
|
||||
# Should preserve user's selection
|
||||
assert result["openai_api_key"]["value"] == "MY_CUSTOM_API_KEY"
|
||||
assert result["openai_api_key"]["load_from_db"] is True
|
||||
|
||||
|
||||
def test_apply_provider_config_replaces_hardcoded_with_env_var():
|
||||
"""Test that hardcoded values are replaced with env var key for global variable usage.
|
||||
|
||||
Expected behavior: When load_from_db=False (hardcoded value), the function
|
||||
replaces it with the env var key and sets load_from_db=True to enable
|
||||
global variable functionality.
|
||||
"""
|
||||
mock_provider_vars = [
|
||||
{
|
||||
"variable_key": "OPENAI_API_KEY",
|
||||
"component_metadata": {
|
||||
"mapping_field": "openai_api_key",
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# User has hardcoded a value (not loading from db)
|
||||
build_config = {
|
||||
"openai_api_key": {
|
||||
"value": "sk-hardcoded-key",
|
||||
"load_from_db": False,
|
||||
"show": False,
|
||||
"required": False,
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("lfx.base.models.unified_models.get_provider_all_variables", return_value=mock_provider_vars),
|
||||
patch("lfx.base.models.unified_models._get_all_provider_specific_field_names", return_value=["openai_api_key"]),
|
||||
patch.dict(os.environ, {"OPENAI_API_KEY": "test-key-123"}), # pragma: allowlist secret
|
||||
):
|
||||
result = apply_provider_variable_config_to_build_config(build_config, "OpenAI")
|
||||
|
||||
# Should overwrite with env var key since load_from_db was False
|
||||
assert result["openai_api_key"]["value"] == "OPENAI_API_KEY"
|
||||
assert result["openai_api_key"]["load_from_db"] is True
|
||||
|
||||
|
||||
def test_apply_provider_config_skips_when_env_var_not_set():
|
||||
"""Test that nothing is set when env var doesn't exist."""
|
||||
mock_provider_vars = [
|
||||
{
|
||||
"variable_key": "OPENAI_API_KEY",
|
||||
"component_metadata": {
|
||||
"mapping_field": "openai_api_key",
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
build_config = {
|
||||
"openai_api_key": {
|
||||
"value": "",
|
||||
"show": False,
|
||||
"required": False,
|
||||
"load_from_db": False,
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("lfx.base.models.unified_models.get_provider_all_variables", return_value=mock_provider_vars),
|
||||
patch("lfx.base.models.unified_models._get_all_provider_specific_field_names", return_value=["openai_api_key"]),
|
||||
patch.dict(os.environ, {}, clear=True),
|
||||
):
|
||||
result = apply_provider_variable_config_to_build_config(build_config, "OpenAI")
|
||||
|
||||
# Should not set anything when env var doesn't exist
|
||||
assert result["openai_api_key"]["value"] == ""
|
||||
assert result["openai_api_key"]["load_from_db"] is False
|
||||
|
||||
|
||||
def test_apply_provider_config_applies_component_metadata():
|
||||
"""Test that component metadata (required, advanced, info) is applied."""
|
||||
mock_provider_vars = [
|
||||
{
|
||||
"variable_key": "OPENAI_API_KEY",
|
||||
"component_metadata": {
|
||||
"mapping_field": "openai_api_key",
|
||||
"required": True,
|
||||
"advanced": False,
|
||||
"info": "OpenAI API Key",
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
build_config = {
|
||||
"openai_api_key": {
|
||||
"value": "",
|
||||
"show": False,
|
||||
"required": False,
|
||||
"advanced": False,
|
||||
"load_from_db": False,
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("lfx.base.models.unified_models.get_provider_all_variables", return_value=mock_provider_vars),
|
||||
patch("lfx.base.models.unified_models._get_all_provider_specific_field_names", return_value=["openai_api_key"]),
|
||||
patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}), # pragma: allowlist secret
|
||||
):
|
||||
result = apply_provider_variable_config_to_build_config(build_config, "OpenAI")
|
||||
|
||||
# Check metadata was applied
|
||||
assert result["openai_api_key"]["required"] is True
|
||||
assert result["openai_api_key"]["advanced"] is False
|
||||
assert result["openai_api_key"]["info"] == "OpenAI API Key"
|
||||
assert result["openai_api_key"]["show"] is True
|
||||
|
||||
|
||||
def test_apply_provider_config_idempotent_when_already_set():
|
||||
"""Test idempotent behavior when value already matches env var key.
|
||||
|
||||
When a field already has the env var key as its value and load_from_db=True,
|
||||
the function should be a no-op (no changes made). This documents the
|
||||
idempotent behavior of the function.
|
||||
"""
|
||||
mock_provider_vars = [
|
||||
{
|
||||
"variable_key": "OPENAI_API_KEY",
|
||||
"component_metadata": {
|
||||
"mapping_field": "openai_api_key",
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# Field already configured with env var key
|
||||
build_config = {
|
||||
"openai_api_key": {
|
||||
"value": "OPENAI_API_KEY",
|
||||
"load_from_db": True,
|
||||
"show": False,
|
||||
"required": False,
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("lfx.base.models.unified_models.get_provider_all_variables", return_value=mock_provider_vars),
|
||||
patch("lfx.base.models.unified_models._get_all_provider_specific_field_names", return_value=["openai_api_key"]),
|
||||
patch.dict(os.environ, {"OPENAI_API_KEY": "test-key-123"}), # pragma: allowlist secret
|
||||
):
|
||||
result = apply_provider_variable_config_to_build_config(build_config, "OpenAI")
|
||||
|
||||
# Should remain unchanged (idempotent)
|
||||
assert result["openai_api_key"]["value"] == "OPENAI_API_KEY"
|
||||
assert result["openai_api_key"]["load_from_db"] is True
|
||||
|
||||
4
src/frontend/package-lock.json
generated
4
src/frontend/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "langflow",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langflow",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.3",
|
||||
"dependencies": {
|
||||
"@chakra-ui/number-input": "^2.1.2",
|
||||
"@chakra-ui/system": "^2.6.2",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langflow",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.3",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lfx"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
description = "Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
"icon": "FAISS",
|
||||
"legacy": false,
|
||||
"metadata": {
|
||||
"code_hash": "2bd7a064d724",
|
||||
"code_hash": "3e55e36d0692",
|
||||
"dependencies": {
|
||||
"dependencies": [
|
||||
{
|
||||
@ -85,7 +85,7 @@
|
||||
"advanced": true,
|
||||
"display_name": "Allow Dangerous Deserialization",
|
||||
"dynamic": false,
|
||||
"info": "Set to True to allow loading pickle files from untrusted sources. Only enable this if you trust the source of the data.",
|
||||
"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.",
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"name": "allow_dangerous_deserialization",
|
||||
@ -98,7 +98,7 @@
|
||||
"trace_as_metadata": true,
|
||||
"track_in_telemetry": true,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
"value": false
|
||||
},
|
||||
"code": {
|
||||
"advanced": true,
|
||||
@ -116,7 +116,7 @@
|
||||
"show": true,
|
||||
"title_case": false,
|
||||
"type": "code",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"embedding": {
|
||||
"_input_type": "HandleInput",
|
||||
@ -118191,6 +118191,6 @@
|
||||
"num_components": 359,
|
||||
"num_modules": 97
|
||||
},
|
||||
"sha256": "c2c714d9bec52ae89ed53341db3be1653da036cb8259013f204df4707bdbf757",
|
||||
"version": "0.3.1"
|
||||
"sha256": "8fc3c7cb1bdf0873d2374de24e6e6e9a0bed598adbd0ef95964db455b2c937b6",
|
||||
"version": "0.3.3"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -280,14 +280,26 @@ def apply_provider_variable_config_to_build_config(
|
||||
env_var_key = var_info.get("variable_key")
|
||||
if env_var_key:
|
||||
env_value = os.environ.get(env_var_key)
|
||||
# Only set the value if the field is currently empty or not already set to load from db
|
||||
# This prevents overwriting user-selected global variables
|
||||
current_value = field_config.get("value")
|
||||
current_load_from_db = field_config.get("load_from_db", False)
|
||||
|
||||
if env_value and str(env_value).strip():
|
||||
field_config["value"] = env_var_key
|
||||
field_config["load_from_db"] = True
|
||||
logger.debug(
|
||||
"Set field %s to env var name %s (value resolved at runtime)",
|
||||
field_name,
|
||||
env_var_key,
|
||||
)
|
||||
# Only auto-set if field is empty or not already loading from db
|
||||
if not current_value or (not current_load_from_db):
|
||||
field_config["value"] = env_var_key
|
||||
field_config["load_from_db"] = True
|
||||
logger.debug(
|
||||
"Set field %s to env var name %s (value resolved at runtime)",
|
||||
field_name,
|
||||
env_var_key,
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"Skipping auto-set for field %s - user has already selected a value (load_from_db=True)",
|
||||
field_name,
|
||||
)
|
||||
|
||||
return build_config
|
||||
|
||||
@ -503,20 +515,31 @@ def get_api_key_for_provider(user_id: UUID | str | None, provider: str, api_key:
|
||||
if not variable_name:
|
||||
return None
|
||||
|
||||
# Try to get from global variables
|
||||
# Try to get from global variables, fall back to environment
|
||||
async def _get_variable():
|
||||
async with session_scope() as session:
|
||||
variable_service = get_variable_service()
|
||||
if variable_service is None:
|
||||
return None
|
||||
return await variable_service.get_variable(
|
||||
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
|
||||
name=variable_name,
|
||||
field="",
|
||||
session=session,
|
||||
)
|
||||
try:
|
||||
return await variable_service.get_variable(
|
||||
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
|
||||
name=variable_name,
|
||||
field="",
|
||||
session=session,
|
||||
)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return run_until_complete(_get_variable())
|
||||
try:
|
||||
api_key = run_until_complete(_get_variable())
|
||||
except (ValueError, Exception): # noqa: BLE001
|
||||
api_key = None
|
||||
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
return os.getenv(variable_name)
|
||||
|
||||
|
||||
def get_all_variables_for_provider(user_id: UUID | str | None, provider: str) -> dict[str, str]:
|
||||
|
||||
@ -31,10 +31,10 @@ class FaissVectorStoreComponent(LCVectorStoreComponent):
|
||||
BoolInput(
|
||||
name="allow_dangerous_deserialization",
|
||||
display_name="Allow Dangerous Deserialization",
|
||||
info="Set to True to allow loading pickle files from untrusted sources. "
|
||||
"Only enable this if you trust the source of the data.",
|
||||
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.",
|
||||
advanced=True,
|
||||
value=True,
|
||||
value=False,
|
||||
),
|
||||
HandleInput(name="embedding", display_name="Embedding", input_types=["Embeddings"]),
|
||||
IntInput(
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
"""Regression tests for FaissVectorStoreComponent security defaults."""
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("langchain_community")
|
||||
|
||||
from lfx.components.FAISS.faiss import FaissVectorStoreComponent
|
||||
from lfx.io import BoolInput
|
||||
|
||||
|
||||
class TestFaissVectorStoreComponentDefaults:
|
||||
"""Regression tests ensuring FAISS component security defaults do not regress."""
|
||||
|
||||
def test_allow_dangerous_deserialization_defaults_to_false(self):
|
||||
"""Regression test: allow_dangerous_deserialization must default to False.
|
||||
|
||||
This guards against accidentally re-enabling unsafe pickle deserialization
|
||||
(RCE vulnerability PVR0699083). Do not change this default to True.
|
||||
"""
|
||||
input_def = next(
|
||||
inp
|
||||
for inp in FaissVectorStoreComponent.inputs
|
||||
if isinstance(inp, BoolInput) and inp.name == "allow_dangerous_deserialization"
|
||||
)
|
||||
assert input_def.value is False, (
|
||||
"allow_dangerous_deserialization must default to False to prevent RCE via malicious pickle files. "
|
||||
"See security issue PVR0699083."
|
||||
)
|
||||
|
||||
def test_allow_dangerous_deserialization_is_advanced(self):
|
||||
"""allow_dangerous_deserialization should be an advanced field to reduce accidental enabling."""
|
||||
input_def = next(
|
||||
inp
|
||||
for inp in FaissVectorStoreComponent.inputs
|
||||
if isinstance(inp, BoolInput) and inp.name == "allow_dangerous_deserialization"
|
||||
)
|
||||
assert input_def.advanced is True
|
||||
9
uv.lock
generated
9
uv.lock
generated
@ -3945,6 +3945,7 @@ dependencies = [
|
||||
{ name = "griffecli" },
|
||||
{ name = "griffelib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/56/28a0accac339c164b52a92c6cfc45a903acc0c174caa5c1713803467b533/griffe-2.0.0.tar.gz", hash = "sha256:c68979cd8395422083a51ea7cf02f9c119d889646d99b7b656ee43725de1b80f", size = 293906, upload-time = "2026-03-23T21:06:53.402Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" },
|
||||
]
|
||||
@ -3957,6 +3958,7 @@ dependencies = [
|
||||
{ name = "colorama" },
|
||||
{ name = "griffelib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/f8/2e129fd4a86e52e58eefe664de05e7d502decf766e7316cc9e70fdec3e18/griffecli-2.0.0.tar.gz", hash = "sha256:312fa5ebb4ce6afc786356e2d0ce85b06c1c20d45abc42d74f0cda65e159f6ef", size = 56213, upload-time = "2026-03-23T21:06:54.8Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" },
|
||||
]
|
||||
@ -3965,6 +3967,7 @@ wheels = [
|
||||
name = "griffelib"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
|
||||
]
|
||||
@ -5738,7 +5741,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langflow"
|
||||
version = "1.8.2"
|
||||
version = "1.8.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langflow-base", extra = ["complete"] },
|
||||
@ -5916,7 +5919,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langflow-base"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = { editable = "src/backend/base" }
|
||||
dependencies = [
|
||||
{ name = "aiofile" },
|
||||
@ -7096,7 +7099,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lfx"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = { editable = "src/lfx" }
|
||||
dependencies = [
|
||||
{ name = "ag-ui-protocol" },
|
||||
|
||||
Reference in New Issue
Block a user