Skip to content

Commit 13bc05f

Browse files
committed
Merge release-1.11.0 into fix/remove-default-lfx-bundles
2 parents 97902f2 + 5ad6611 commit 13bc05f

33 files changed

Lines changed: 1251 additions & 113 deletions

File tree

docs/docs/Components/bundles-valkey.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import PartialParams from '@site/docs/_partial-hidden-params.mdx';
88
import PartialConditionalParams from '@site/docs/_partial-conditional-params.mdx';
99
import PartialVectorSearchResults from '@site/docs/_partial-vector-search-results.mdx';
1010
import PartialVectorStoreInstance from '@site/docs/_partial-vector-store-instance.mdx';
11-
import PartialLfxBundlesInstall from '@site/docs/_partial-bundle-lfx-bundles-install.mdx';
11+
import { GraduatedBundleInstall } from '@site/docs/_partial-bundle-graduated-install.mdx';
1212

13-
<PartialLfxBundlesInstall />
13+
<GraduatedBundleInstall packageName="valkey" />
1414

1515
<Icon name="Blocks" aria-hidden="true" /> [**Bundles**](/components-bundle-components) contain custom components that support specific third-party integrations with Langflow.
1616

docs/docs/_partial-bundle-graduated-install.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const GraduatedBundleInstall = ({ packageName }) => {
1818
'openai-compatible': { name: 'OpenAI Compatible', coreSupport: false },
1919
'oracle': { name: 'Oracle', coreSupport: false },
2020
'paddle': { name: 'PaddleOCR', coreSupport: false },
21+
'valkey': { name: 'Valkey', coreSupport: false },
2122
'vllm': { name: 'vLLM', coreSupport: false },
2223
};
2324

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies = [
4545
"lfx-empiriolabs>=0.1.0,<1.0.0",
4646
"lfx-openai-compatible>=0.1.0,<1.0.0",
4747
"lfx-exa>=0.1.0,<1.0.0",
48+
"lfx-valkey>=0.1.0,<1.0.0",
4849
# langflow-extensions:bundle-deps-end
4950
]
5051

@@ -120,6 +121,7 @@ lfx-vllm = { workspace = true }
120121
lfx-empiriolabs = { workspace = true }
121122
lfx-openai-compatible = { workspace = true }
122123
lfx-exa = { workspace = true }
124+
lfx-valkey = { workspace = true }
123125
# langflow-extensions:bundle-sources-end
124126
torch = { index = "pytorch-cpu" }
125127
torchvision = { index = "pytorch-cpu" }
@@ -150,6 +152,7 @@ members = [
150152
"src/bundles/empiriolabs",
151153
"src/bundles/openai-compatible",
152154
"src/bundles/exa",
155+
"src/bundles/valkey",
153156
# langflow-extensions:bundle-members-end
154157
]
155158

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def is_permanent_storage_failure(error: Exception) -> bool:
4646
if isinstance(error, FileNotFoundError):
4747
return True
4848

49-
# Check for S3 error codes (boto3/aioboto3)
49+
# Check for S3 error codes (boto3/aiobotocore)
5050
# S3 errors have a 'response' attribute with Error.Code
5151
if hasattr(error, "response"):
5252
response = error.response

src/backend/base/langflow/core/celeryconfig.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
# celeryconfig.py
22
import os
33

4+
langflow_valkey_host = os.environ.get("LANGFLOW_VALKEY_HOST")
5+
langflow_valkey_port = os.environ.get("LANGFLOW_VALKEY_PORT")
46
langflow_redis_host = os.environ.get("LANGFLOW_REDIS_HOST")
57
langflow_redis_port = os.environ.get("LANGFLOW_REDIS_PORT")
68
# broker default user
79

8-
if langflow_redis_host and langflow_redis_port:
10+
if langflow_valkey_host and langflow_valkey_port:
11+
# Valkey is wire-compatible with Redis. Celery/kombu registers redis://,
12+
# not valkey://, as the transport scheme.
13+
broker_url = f"redis://{langflow_valkey_host}:{langflow_valkey_port}/0"
14+
result_backend = f"redis://{langflow_valkey_host}:{langflow_valkey_port}/0"
15+
elif langflow_redis_host and langflow_redis_port:
916
broker_url = f"redis://{langflow_redis_host}:{langflow_redis_port}/0"
1017
result_backend = f"redis://{langflow_redis_host}:{langflow_redis_port}/0"
1118
else:

src/backend/base/langflow/initial_setup/starter_projects/Content Aggregator.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2516,7 +2516,7 @@
25162516
},
25172517
{
25182518
"name": "boto3",
2519-
"version": "1.40.61"
2519+
"version": "1.43.46"
25202520
},
25212521
{
25222522
"name": "googleapiclient",

src/backend/base/langflow/services/storage/s3.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""S3-based storage service implementation using async boto3.
1+
"""S3-based storage service implementation using aiobotocore.
22
33
This service handles file storage operations with AWS S3, including
44
file upload, download, deletion, and listing operations.
@@ -22,7 +22,7 @@
2222

2323

2424
class S3StorageService(StorageService):
25-
"""A service class for handling S3 storage operations using aioboto3."""
25+
"""A service class for handling S3 storage operations using aiobotocore."""
2626

2727
def __init__(self, session_service: SessionService, settings_service: SettingsService) -> None:
2828
"""Initialize the S3 storage service with session and settings services.
@@ -32,7 +32,7 @@ def __init__(self, session_service: SessionService, settings_service: SettingsSe
3232
settings_service: The settings service instance
3333
3434
Raises:
35-
ImportError: If aioboto3 is not installed
35+
ImportError: If aiobotocore is not installed
3636
ValueError: If required S3 configuration is missing
3737
"""
3838
super().__init__(session_service, settings_service)
@@ -50,13 +50,13 @@ def __init__(self, session_service: SessionService, settings_service: SettingsSe
5050
self.tags = settings_service.settings.object_storage_tags or {}
5151

5252
try:
53-
import aioboto3
53+
from aiobotocore.session import get_session
5454
except ImportError as exc:
55-
msg = "aioboto3 is required for S3 storage. Install it with: uv pip install aioboto3"
55+
msg = "aiobotocore is required for S3 storage. Install it with: uv pip install aiobotocore"
5656
raise ImportError(msg) from exc
5757

5858
# Create session - AWS credentials are picked up from environment variables
59-
self.session = aioboto3.Session()
59+
self.session = get_session()
6060
self._client = None
6161

6262
self.set_ready()
@@ -157,7 +157,7 @@ def resolve_component_path(self, logical_path: str) -> str:
157157

158158
def _get_client(self):
159159
"""Get or create an S3 client using the async context manager."""
160-
return self.session.client("s3")
160+
return self.session.create_client("s3")
161161

162162
async def save_file(self, flow_id: str, file_name: str, data: bytes, *, append: bool = False) -> None:
163163
"""Save a file to S3.
@@ -384,7 +384,7 @@ async def get_file_size(self, flow_id: str, file_name: str) -> int:
384384
async def teardown(self) -> None:
385385
"""Perform any cleanup operations when the service is being torn down.
386386
387-
For S3, we don't need to do anything as aioboto3 handles cleanup
387+
For S3, we don't need to do anything as aiobotocore handles cleanup
388388
via context managers.
389389
"""
390390
logger.info("S3 storage service teardown complete")

src/backend/base/pyproject.toml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,8 @@ huggingface = ["huggingface-hub[inference]>=1.0.0,<2.0.0"]
297297
metaphor = ["exa-py>=1.14,<3"]
298298
metal = ["metal_sdk==2.5.1"]
299299
markup = ["MarkupSafe==3.0.2"]
300-
# Deliberately retained after the lfx-amazon graduation: boto3 also backs
301-
# langflow-base's own S3 storage backend (services/storage/s3.py, api/v2/files.py)
302-
# and lfx's S3 ingestion sources -- it is not exclusive to the amazon components.
300+
# Deliberately retained after the lfx-amazon graduation: boto3 backs lfx's
301+
# synchronous S3 file helpers. The backend storage service uses aiobotocore.
303302
aws = ["boto3>=1.34.162,<2.0.0"]
304303
numexpr = ["numexpr>=2.10.2"]
305304
qianfan = ["qianfan==0.3.5"]
@@ -313,8 +312,8 @@ litellm = [
313312
"litellm>=1.85.1,<2.0.0; python_version < '3.14'",
314313
# Runtime deps from litellm[proxy] that langflow-ide hits when logging
315314
# to langfuse via litellm.proxy.proxy_server (issue #12228). The full
316-
# [proxy] extra pins boto3==1.43.1, which conflicts with our aioboto3
317-
# transitive cap, so we pull in just the missing modules instead.
315+
# Pull in only the modules Langflow uses from litellm[proxy] rather than
316+
# its much larger dependency surface.
318317
"apscheduler>=3.10.4,<4.0.0",
319318
"cryptography",
320319
]
@@ -414,8 +413,12 @@ mlx = [
414413
# MCP
415414
fastmcp = ["fastmcp>=3.2.0"]
416415

417-
# AWS extras
418-
aioboto3 = ["aioboto3>=15.2.0,<16.0.0"]
416+
# AWS async client. S3 storage uses aiobotocore directly so boto3 is no longer
417+
# constrained by aioboto3's exact pin to aiobotocore 2.25.1.
418+
aiobotocore = ["aiobotocore>=3.7.0,<4.0.0"]
419+
# Preserve the legacy extra name so existing langflow-base[aioboto3] install
420+
# commands continue to enable Langflow's async S3 backend.
421+
aioboto3 = ["aiobotocore>=3.7.0,<4.0.0"]
419422

420423
# Astra
421424
astrapy = ["astrapy>=2.1.0,<3.0.0"]
@@ -463,7 +466,7 @@ complete = [
463466
"langflow-base[mcp]",
464467
# Orchestration / deployment adapters
465468
"langflow-base[kubernetes]",
466-
"langflow-base[aioboto3]",
469+
"langflow-base[aiobotocore]",
467470
"langflow-base[ibm-watsonx-clients]",
468471
# lfx-core component features: the dependency now lives in lfx (these base
469472
# extras re-export the matching lfx extra), keeping a single source of truth

src/backend/tests/unit/core/test_celeryconfig.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
"""Unit tests for langflow.core.celeryconfig module."""
22

3+
import importlib
4+
5+
import pytest
6+
37
# Import the module to test
48
from langflow.core import celeryconfig
59

610

11+
@pytest.fixture(autouse=True)
12+
def _restore_celeryconfig_after_test(monkeypatch):
13+
yield
14+
monkeypatch.undo()
15+
importlib.reload(celeryconfig)
16+
17+
718
class TestCeleryConfigAcceptContent:
819
"""Unit tests for accept_content configuration."""
920

@@ -93,3 +104,51 @@ def test_result_backend_contains_host(self):
93104
if "://" in result_backend:
94105
host_part = result_backend.split("://")[1]
95106
assert len(host_part) > 0
107+
108+
109+
class TestCeleryConfigValkey:
110+
"""Unit tests for Valkey broker configuration."""
111+
112+
def test_valkey_env_vars_set_broker_and_backend(self, monkeypatch):
113+
monkeypatch.setenv("LANGFLOW_VALKEY_HOST", "valkey-host")
114+
monkeypatch.setenv("LANGFLOW_VALKEY_PORT", "6380")
115+
monkeypatch.delenv("LANGFLOW_REDIS_HOST", raising=False)
116+
monkeypatch.delenv("LANGFLOW_REDIS_PORT", raising=False)
117+
118+
importlib.reload(celeryconfig)
119+
120+
assert celeryconfig.broker_url == "redis://valkey-host:6380/0"
121+
assert celeryconfig.result_backend == "redis://valkey-host:6380/0"
122+
123+
def test_valkey_takes_precedence_over_redis(self, monkeypatch):
124+
monkeypatch.setenv("LANGFLOW_VALKEY_HOST", "valkey-host")
125+
monkeypatch.setenv("LANGFLOW_VALKEY_PORT", "6380")
126+
monkeypatch.setenv("LANGFLOW_REDIS_HOST", "redis-host")
127+
monkeypatch.setenv("LANGFLOW_REDIS_PORT", "6379")
128+
129+
importlib.reload(celeryconfig)
130+
131+
assert celeryconfig.broker_url == "redis://valkey-host:6380/0"
132+
assert celeryconfig.result_backend == "redis://valkey-host:6380/0"
133+
134+
def test_without_valkey_preserves_redis_behavior(self, monkeypatch):
135+
monkeypatch.delenv("LANGFLOW_VALKEY_HOST", raising=False)
136+
monkeypatch.delenv("LANGFLOW_VALKEY_PORT", raising=False)
137+
monkeypatch.setenv("LANGFLOW_REDIS_HOST", "redis-host")
138+
monkeypatch.setenv("LANGFLOW_REDIS_PORT", "6379")
139+
140+
importlib.reload(celeryconfig)
141+
142+
assert celeryconfig.broker_url == "redis://redis-host:6379/0"
143+
assert celeryconfig.result_backend == "redis://redis-host:6379/0"
144+
145+
def test_incomplete_valkey_configuration_falls_back_to_redis(self, monkeypatch):
146+
monkeypatch.setenv("LANGFLOW_VALKEY_HOST", "valkey-host")
147+
monkeypatch.delenv("LANGFLOW_VALKEY_PORT", raising=False)
148+
monkeypatch.setenv("LANGFLOW_REDIS_HOST", "redis-host")
149+
monkeypatch.setenv("LANGFLOW_REDIS_PORT", "6379")
150+
151+
importlib.reload(celeryconfig)
152+
153+
assert celeryconfig.broker_url == "redis://redis-host:6379/0"
154+
assert celeryconfig.result_backend == "redis://redis-host:6379/0"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Packaging tests for the asynchronous S3 storage dependency."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
if sys.version_info >= (3, 11):
9+
import tomllib
10+
else:
11+
import tomli as tomllib
12+
13+
14+
def test_async_s3_extras_use_aiobotocore_directly() -> None:
15+
pyproject_path = Path(__file__).resolve().parents[4] / "base" / "pyproject.toml"
16+
with pyproject_path.open("rb") as pyproject_file:
17+
optional = tomllib.load(pyproject_file)["project"]["optional-dependencies"]
18+
19+
expected = ["aiobotocore>=3.7.0,<4.0.0"]
20+
assert optional["aiobotocore"] == expected
21+
assert optional["aioboto3"] == expected
22+
assert "langflow-base[aiobotocore]" in optional["complete"]

0 commit comments

Comments
 (0)