diff --git a/docs/docs/Configuration/environment-variables.mdx b/docs/docs/Configuration/environment-variables.mdx
index a6482453a..275d16ad3 100644
--- a/docs/docs/Configuration/environment-variables.mdx
+++ b/docs/docs/Configuration/environment-variables.mdx
@@ -194,6 +194,7 @@ The following table lists the environment variables supported by Langflow.
| LANGFLOW_COMPONENTS_PATH | String | `langflow/components` | Path to the directory containing custom components.
See [`--components-path` option](./configuration-cli.mdx#run-components-path). |
| LANGFLOW_CONFIG_DIR | String | See description | Set the Langflow configuration directory where files, logs, and the Langflow database are stored. Defaults: **Linux/WSL**: `~/.cache/langflow/`
**macOS**: `/Users//Library/Caches/langflow/`
**Windows**: `%LOCALAPPDATA%\langflow\langflow\Cache`|
| LANGFLOW_DATABASE_URL | String | Not set | Set the database URL for Langflow. If not provided, Langflow uses a SQLite database. |
+| LANGFLOW_USE_NOOP_DATABASE | Boolean | `false` | Use a no-op database, which avoids database connections and operations. Useful for running flows without a database. |
| LANGFLOW_DATABASE_CONNECTION_RETRY | Boolean | `false` | If True, Langflow tries to connect to the database again if it fails. |
| LANGFLOW_DB_POOL_SIZE | Integer | `10` | **DEPRECATED:** Use LANGFLOW_DB_CONNECTION_SETTINGS instead. The number of connections to keep open in the connection pool. |
| LANGFLOW_DB_MAX_OVERFLOW | Integer | `20` | **DEPRECATED:** Use LANGFLOW_DB_CONNECTION_SETTINGS instead. The number of connections to allow that can be opened beyond the pool size. |
diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py
index 9ad6c0fc7..769474327 100644
--- a/src/backend/base/langflow/api/v1/endpoints.py
+++ b/src/backend/base/langflow/api/v1/endpoints.py
@@ -682,7 +682,9 @@ async def custom_component_update(
):
"""Update an existing custom component with new code and configuration.
- Processes the provided code and template updates, applies parameter changes (including those loaded from the database), updates the component's build configuration, and validates outputs. Returns the updated component node as a JSON-serializable dictionary.
+ Processes the provided code and template updates, applies parameter changes (including those loaded from the
+ database), updates the component's build configuration, and validates outputs. Returns the updated component node as
+ a JSON-serializable dictionary.
Raises:
HTTPException: If an error occurs during component building or updating.
diff --git a/src/backend/base/langflow/services/database/service.py b/src/backend/base/langflow/services/database/service.py
index 0fd33acec..85f0dbd53 100644
--- a/src/backend/base/langflow/services/database/service.py
+++ b/src/backend/base/langflow/services/database/service.py
@@ -27,6 +27,7 @@ from langflow.initial_setup.constants import STARTER_FOLDER_NAME
from langflow.services.base import Service
from langflow.services.database import models
from langflow.services.database.models.user.crud import get_user_by_username
+from langflow.services.database.session import NoopSession
from langflow.services.database.utils import Result, TableResults
from langflow.services.deps import get_settings_service
from langflow.services.utils import teardown_superuser
@@ -182,14 +183,17 @@ class DatabaseService(Service):
@asynccontextmanager
async def with_session(self):
- async with AsyncSession(self.engine, expire_on_commit=False) as session:
- # Start of Selection
- try:
- yield session
- except exc.SQLAlchemyError as db_exc:
- logger.error(f"Database error during session scope: {db_exc}")
- await session.rollback()
- raise
+ if self.settings_service.settings.use_noop_database:
+ yield NoopSession()
+ else:
+ async with AsyncSession(self.engine, expire_on_commit=False) as session:
+ # Start of Selection
+ try:
+ yield session
+ except exc.SQLAlchemyError as db_exc:
+ logger.error(f"Database error during session scope: {db_exc}")
+ await session.rollback()
+ raise
async def assign_orphaned_flows_to_superuser(self) -> None:
"""Assign orphaned flows to the default superuser when auto login is enabled."""
diff --git a/src/backend/base/langflow/services/database/session.py b/src/backend/base/langflow/services/database/session.py
new file mode 100644
index 000000000..28c6182d6
--- /dev/null
+++ b/src/backend/base/langflow/services/database/session.py
@@ -0,0 +1,62 @@
+class NoopSession:
+ class NoopBind:
+ class NoopConnect:
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb):
+ pass
+
+ async def run_sync(self, fn, *args, **kwargs): # noqa: ARG002
+ return None
+
+ def connect(self):
+ return self.NoopConnect()
+
+ bind = NoopBind()
+
+ async def add(self, *args, **kwargs):
+ pass
+
+ async def commit(self):
+ pass
+
+ async def rollback(self):
+ pass
+
+ async def execute(self, *args, **kwargs): # noqa: ARG002
+ return None
+
+ async def query(self, *args, **kwargs): # noqa: ARG002
+ return []
+
+ async def close(self):
+ pass
+
+ async def refresh(self, *args, **kwargs):
+ pass
+
+ async def delete(self, *args, **kwargs):
+ pass
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb):
+ pass
+
+ async def get(self, *args, **kwargs): # noqa: ARG002
+ return None
+
+ async def exec(self, *args, **kwargs): # noqa: ARG002
+ class _NoopResult:
+ def first(self):
+ return None
+
+ def all(self):
+ return []
+
+ def one_or_none(self):
+ return None
+
+ return _NoopResult()
diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py
index 6f97401c6..d9a6d6538 100644
--- a/src/backend/base/langflow/services/settings/base.py
+++ b/src/backend/base/langflow/services/settings/base.py
@@ -126,6 +126,10 @@ class Settings(BaseSettings):
- echo: Enable SQL query logging (development only)
"""
+ use_noop_database: bool = False
+ """If True, disables all database operations and uses a no-op session.
+ Controlled by LANGFLOW_USE_NOOP_DATABASE env variable."""
+
# cache configuration
cache_type: Literal["async", "redis", "memory", "disk"] = "async"
"""The cache type can be 'async' or 'redis'."""
@@ -268,6 +272,13 @@ class Settings(BaseSettings):
update_starter_projects: bool = True
"""If set to True, Langflow will update starter projects."""
+ @field_validator("use_noop_database", mode="before")
+ @classmethod
+ def set_use_noop_database(cls, value):
+ if value:
+ logger.info("Running with NOOP database session. All DB operations are disabled.")
+ return value
+
@field_validator("event_delivery", mode="before")
@classmethod
def set_event_delivery(cls, value, info):
diff --git a/src/backend/base/langflow/services/utils.py b/src/backend/base/langflow/services/utils.py
index a61cc5f3b..5f10df36c 100644
--- a/src/backend/base/langflow/services/utils.py
+++ b/src/backend/base/langflow/services/utils.py
@@ -29,7 +29,8 @@ async def get_or_create_super_user(session: AsyncSession, username, password, is
from langflow.services.database.models.user.model import User
stmt = select(User).where(User.username == username)
- user = (await session.exec(stmt)).first()
+ result = await session.exec(stmt)
+ user = result.first()
if user and user.is_superuser:
return None # Superuser already exists
@@ -114,7 +115,8 @@ async def teardown_superuser(settings_service, session: AsyncSession) -> None:
from langflow.services.database.models.user.model import User
stmt = select(User).where(User.username == username)
- user = (await session.exec(stmt)).first()
+ result = await session.exec(stmt)
+ user = result.first()
# Check if super was ever logged in, if not delete it
# if it has logged in, it means the user is using it to login
if user and user.is_superuser is True and not user.last_login_at:
diff --git a/src/backend/tests/conftest.py b/src/backend/tests/conftest.py
index dd60d49b5..a144d52a7 100644
--- a/src/backend/tests/conftest.py
+++ b/src/backend/tests/conftest.py
@@ -76,6 +76,10 @@ def blockbuster(request):
.can_block_in("rich/traceback.py", "_render_stack")
.can_block_in("langchain_core/_api/internal.py", "is_caller_internal")
.can_block_in("langchain_core/runnables/utils.py", "get_function_nonlocals")
+ .can_block_in("alembic/versions", "_load_revisions")
+ .can_block_in("dotenv/main.py", "find_dotenv")
+ .can_block_in("alembic/script/base.py", "_load_revisions")
+ .can_block_in("alembic/env.py", "_do_run_migrations")
)
for func in ["os.stat", "os.path.abspath", "os.scandir", "os.listdir"]:
@@ -366,6 +370,16 @@ def deactivate_tracing(monkeypatch):
monkeypatch.undo()
+@pytest.fixture
+def use_noop_session(monkeypatch):
+ monkeypatch.setenv("LANGFLOW_USE_NOOP_DATABASE", "1")
+ # Optionally patch the Settings object if needed
+ # from langflow.services.settings.base import Settings
+ # monkeypatch.setattr(Settings, "use_noop_database", True)
+ yield
+ monkeypatch.undo()
+
+
@pytest.fixture(name="client")
async def client_fixture(
session: Session, # noqa: ARG001
diff --git a/src/backend/tests/unit/components/agents/test_agent_component.py b/src/backend/tests/unit/components/agents/test_agent_component.py
index 5f4d7baac..dacd28e3d 100644
--- a/src/backend/tests/unit/components/agents/test_agent_component.py
+++ b/src/backend/tests/unit/components/agents/test_agent_component.py
@@ -1,5 +1,6 @@
import os
from typing import Any
+from unittest.mock import AsyncMock, patch
from uuid import uuid4
import pytest
@@ -14,6 +15,7 @@ from langflow.base.models.openai_constants import (
from langflow.components.agents.agent import AgentComponent
from langflow.components.tools.calculator import CalculatorToolComponent
from langflow.custom import Component
+from langflow.services.database.session import NoopSession
from tests.base import ComponentTestBaseWithClient, ComponentTestBaseWithoutClient
from tests.unit.mock_language_model import MockLanguageModel
@@ -21,7 +23,7 @@ from tests.unit.mock_language_model import MockLanguageModel
# Load environment variables from .env file
-class TestAgentComponent(ComponentTestBaseWithoutClient):
+class TestAgentComponentWithoutClient(ComponentTestBaseWithoutClient):
@pytest.fixture
def component_class(self):
return AgentComponent
@@ -99,16 +101,6 @@ class TestAgentComponent(ComponentTestBaseWithoutClient):
# Verify model_name field is cleared for Custom
assert "model_name" not in updated_config
-
-class TestAgentComponentWithClient(ComponentTestBaseWithClient):
- @pytest.fixture
- def component_class(self):
- return AgentComponent
-
- @pytest.fixture
- def file_names_mapping(self):
- return []
-
@pytest.mark.api_key_required
@pytest.mark.no_blockbuster
async def test_agent_component_with_calculator(self):
@@ -124,13 +116,19 @@ class TestAgentComponentWithClient(ComponentTestBaseWithClient):
tools=tools,
input_value=input_value,
api_key=api_key,
- model_name="gpt-4o",
- agent_llm="OpenAI",
+ model_name="gpt-4.1-nano",
+ llm_type="OpenAI",
temperature=temperature,
_session_id=str(uuid4()),
)
- response = await agent.message_response()
+ with (
+ patch.object(NoopSession, "add", new_callable=AsyncMock) as mock_add,
+ patch.object(NoopSession, "commit", new_callable=AsyncMock) as mock_commit,
+ ):
+ response = await agent.message_response()
+ assert mock_add.called
+ assert mock_commit.called
assert "4" in response.data.get("text")
@pytest.mark.api_key_required
@@ -141,22 +139,122 @@ class TestAgentComponentWithClient(ComponentTestBaseWithClient):
input_value = "What is 2 + 2?"
# Iterate over all OpenAI models
- failed_models = []
+ failed_models = {}
for model_name in OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES:
- # Initialize the AgentComponent with mocked inputs
- tools = [CalculatorToolComponent().build_tool()] # Use the Calculator component as a tool
- agent = AgentComponent(
- tools=tools,
- input_value=input_value,
- api_key=api_key,
- model_name=model_name,
- agent_llm="OpenAI",
- _session_id=str(uuid4()),
- )
+ try:
+ # Initialize the AgentComponent with mocked inputs
+ tools = [CalculatorToolComponent().build_tool()] # Use the Calculator component as a tool
+ agent = AgentComponent(
+ tools=tools,
+ input_value=input_value,
+ api_key=api_key,
+ model_name=model_name,
+ agent_llm=None,
+ llm_type="OpenAI",
+ temperature=0.1,
+ _session_id=str(uuid4()),
+ )
- response = await agent.message_response()
- if "4" not in response.data.get("text"):
- failed_models.append(model_name)
+ response = await agent.message_response()
+ if "4" not in response.data.get("text"):
+ failed_models[model_name] = f"Expected '4' in response but got: {response.data.get('text')}"
+ except Exception as e: # noqa: BLE001
+ failed_models[model_name] = f"Exception occurred: {e!s}"
+
+ assert not failed_models, f"The following models failed the test: {failed_models}"
+
+ @pytest.mark.api_key_required
+ @pytest.mark.no_blockbuster
+ async def test_agent_component_with_all_anthropic_models(self):
+ # Mock inputs
+ api_key = os.getenv("ANTHROPIC_API_KEY")
+ input_value = "What is 2 + 2?"
+
+ # Iterate over all Anthropic models
+ failed_models = {}
+
+ for model_name in ANTHROPIC_MODELS:
+ try:
+ # Initialize the AgentComponent with mocked inputs
+ tools = [CalculatorToolComponent().build_tool()]
+ agent = AgentComponent(
+ tools=tools,
+ input_value=input_value,
+ api_key=api_key,
+ model_name=model_name,
+ agent_llm="Anthropic",
+ _session_id=str(uuid4()),
+ )
+
+ response = await agent.message_response()
+ response_text = response.data.get("text", "")
+
+ if "4" not in response_text:
+ failed_models[model_name] = f"Expected '4' in response but got: {response_text}"
+
+ except Exception as e: # noqa: BLE001
+ failed_models[model_name] = f"Exception occurred: {e!s}"
+
+ assert not failed_models, "The following models failed the test:\n" + "\n".join(
+ f"{model}: {error}" for model, error in failed_models.items()
+ )
+
+
+class TestAgentComponentWithClient(ComponentTestBaseWithClient):
+ @pytest.fixture
+ def component_class(self):
+ return AgentComponent
+
+ @pytest.fixture
+ def file_names_mapping(self):
+ return []
+
+ @pytest.mark.api_key_required
+ @pytest.mark.no_blockbuster
+ async def test_agent_component_with_calculator(self):
+ api_key = os.getenv("OPENAI_API_KEY")
+ tools = [CalculatorToolComponent().build_tool()]
+ input_value = "What is 2 + 2?"
+
+ temperature = 0.1
+
+ # Initialize the AgentComponent with mocked inputs
+ agent = AgentComponent(
+ tools=tools,
+ input_value=input_value,
+ api_key=api_key,
+ model_name="gpt-4o",
+ agent_llm="OpenAI",
+ temperature=temperature,
+ _session_id=str(uuid4()),
+ )
+ response = await agent.message_response()
+ assert "4" in response.data.get("text")
+
+ @pytest.mark.api_key_required
+ @pytest.mark.no_blockbuster
+ async def test_agent_component_with_all_openai_models(self):
+ api_key = os.getenv("OPENAI_API_KEY")
+ input_value = "What is 2 + 2?"
+
+ # Iterate over all OpenAI models
+ failed_models = {}
+ for model_name in OPENAI_CHAT_MODEL_NAMES + OPENAI_REASONING_MODEL_NAMES:
+ try:
+ tools = [CalculatorToolComponent().build_tool()]
+ agent = AgentComponent(
+ tools=tools,
+ input_value=input_value,
+ api_key=api_key,
+ model_name=model_name,
+ agent_llm="OpenAI",
+ _session_id=str(uuid4()),
+ )
+ response = await agent.message_response()
+ if "4" not in response.data.get("text"):
+ failed_models[model_name] = f"Expected '4' in response but got: {response.data.get('text')}"
+ except Exception as e: # noqa: BLE001
+ failed_models[model_name] = f"Exception occurred: {e!s}"
assert not failed_models, f"The following models failed the test: {failed_models}"
diff --git a/src/backend/tests/unit/custom/custom_component/test_component.py b/src/backend/tests/unit/custom/custom_component/test_component.py
index d5c935682..649963599 100644
--- a/src/backend/tests/unit/custom/custom_component/test_component.py
+++ b/src/backend/tests/unit/custom/custom_component/test_component.py
@@ -1,13 +1,16 @@
from typing import Any
+from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from langflow.components.crewai import CrewAIAgentComponent, SequentialTaskComponent
from langflow.components.custom_component import CustomComponent
from langflow.components.input_output import ChatInput, ChatOutput
+from langflow.custom.custom_component.component import Component
from langflow.custom.utils import update_component_build_config
from langflow.schema import dotdict
+from langflow.schema.message import Message
+from langflow.services.database.session import NoopSession
from langflow.template import Output
-from typing_extensions import override
crewai_available = False
try:
@@ -25,7 +28,7 @@ def test_set_invalid_output():
chatoutput.set(input_value=chatinput.build_config)
-@pytest.mark.skipif(not crewai_available, reason="CrewAI is not installed")
+@pytest.mark.xfail(reason="CrewAI is not outdated")
def test_set_component():
crewai_agent = CrewAIAgentComponent()
task = SequentialTaskComponent()
@@ -75,12 +78,11 @@ def _assert_all_outputs_have_different_required_inputs(outputs: list[Output]):
async def test_update_component_build_config_sync():
class TestComponent(CustomComponent):
- @override
def update_build_config(
self,
build_config: dotdict,
- field_value: Any,
- field_name: str | None = None,
+ field_value: Any, # noqa: ARG002
+ field_name: str | None = None, # noqa: ARG002
):
build_config["foo"] = "bar"
return build_config
@@ -93,12 +95,11 @@ async def test_update_component_build_config_sync():
async def test_update_component_build_config_async():
class TestComponent(CustomComponent):
- @override
async def update_build_config(
self,
build_config: dotdict,
- field_value: Any,
- field_name: str | None = None,
+ field_value: Any, # noqa: ARG002
+ field_name: str | None = None, # noqa: ARG002
):
build_config["foo"] = "bar"
return build_config
@@ -107,3 +108,55 @@ async def test_update_component_build_config_async():
build_config = dotdict()
build_config = await update_component_build_config(component, build_config, "", "")
assert build_config["foo"] == "bar"
+
+
+@pytest.mark.usefixtures("use_noop_session")
+@pytest.mark.asyncio
+async def test_send_message_without_database(monkeypatch): # noqa: ARG001
+ component = Component()
+ event_manager = MagicMock()
+ component._event_manager = event_manager
+ message = Message(text="Hello", session_id="session", flow_id=None, sender="User", sender_name="Test")
+ with (
+ patch.object(NoopSession, "add", new_callable=AsyncMock) as mock_add,
+ patch.object(NoopSession, "commit", new_callable=AsyncMock) as mock_commit,
+ ):
+ result = await component.send_message(message)
+ assert isinstance(result, Message)
+ assert result.text == "Hello"
+ assert result.sender == "User"
+ assert result.sender_name == "Test"
+ # Optionally, check that add/commit were called (if you want to enforce this)
+ assert mock_add.called
+ assert mock_commit.called
+ assert event_manager.on_message.called
+
+
+@pytest.mark.usefixtures("use_noop_session")
+@pytest.mark.asyncio
+async def test_agent_component_send_message_events(monkeypatch): # noqa: ARG001
+ from langflow.components.agents.agent import AgentComponent
+
+ event_manager = MagicMock()
+ agent = AgentComponent(
+ agent_llm="OpenAI",
+ input_value="Hello",
+ system_prompt="You are a helpful assistant.",
+ tools=[],
+ _session_id="test-session",
+ )
+ agent._event_manager = event_manager
+ message = Message(text="Hello", session_id="test-session", flow_id=None, sender="User", sender_name="Test")
+ with (
+ patch.object(NoopSession, "add", new_callable=AsyncMock) as mock_add,
+ patch.object(NoopSession, "commit", new_callable=AsyncMock) as mock_commit,
+ ):
+ result = await agent.send_message(message)
+ assert isinstance(result, Message)
+ assert result.text == "Hello"
+ assert result.sender == "User"
+ assert result.sender_name == "Test"
+ # Optionally, check that add/commit were called (if you want to enforce this)
+ assert mock_add.called
+ assert mock_commit.called
+ assert event_manager.on_message.called