feat(database): add noop mode with config flag and update related tests (#9054)

* test: enhance agent component tests and add new fixture for database-less sessions

- Added `use_noop_session` fixture to facilitate testing without a database.
- Expanded `TestAgentComponent` with new tests for agent responses using the Calculator tool and validation across all OpenAI and Anthropic models.
- Updated `test_component.py` to include tests for message sending without a database, ensuring proper event handling and message integrity.
- Refactored existing tests for clarity and consistency.

* fix: enhance user retrieval logic in get_or_create_super_user and teardown_superuser functions

- Updated user retrieval in both functions to handle different result types from the database query, ensuring robustness against unexpected return values.
- Added checks for `first()` method and list type to improve error handling and maintainability.

* feat: add support for no-op database operations in settings

- Introduced `use_noop_database` configuration option to disable all database operations, controlled by the `LANGFLOW_USE_NOOP_DATABASE` environment variable.
- Updated the `use_noop_session` fixture to reflect the new environment variable for testing without a database.

* feat: implement NoopSession for database-less operations

- Added NoopSession class to provide a no-operation database session, allowing for testing and operation without a real database connection.
- Updated DatabaseService to utilize NoopSession when the `use_noop_database` setting is enabled, ensuring all database operations are disabled in this mode.
- Enhanced error handling and logging for session management, improving robustness in scenarios where the database is not in use.

* refactor: update import paths for NoopSession in test files

- Changed import path for NoopSession from `langflow.services.database.service` to `langflow.services.database.session` in both `test_agent_component.py` and `test_component.py`.
- This refactor improves code organization and aligns with recent changes in the project structure.

* feat: enhance NoopSession with result handling methods

- Added an internal _NoopResult class to the NoopSession, providing methods `first()`, `all()`, and `one_or_none()` for better handling of no-operation results.
- This enhancement improves the usability of NoopSession in testing scenarios by mimicking expected database query behaviors.

* refactor: improve logging for NOOP database session

- Changed the log level from warning to info for the NOOP database session message in the DatabaseService class.
- This adjustment enhances clarity in logging, indicating that all DB operations are disabled without implying an error condition.

* docs: improve docstring formatting for custom_component_update function

- Reformatted the docstring for the custom_component_update function to enhance readability by breaking long lines into multiple lines.
- This change improves documentation clarity, making it easier for developers to understand the function's purpose and behavior.

* docs: add LANGFLOW_USE_NOOP_DATABASE environment variable documentation

- Introduced documentation for the new LANGFLOW_USE_NOOP_DATABASE environment variable, which allows users to enable a no-op database mode, avoiding database connections and operations.
- This addition enhances the clarity of configuration options available for Langflow, aiding users in understanding how to run flows without a database.

* refactor: move NOOP database session logging to settings

- Removed the logging statement from the DatabaseService class and added it to the `set_use_noop_database` field validator in the Settings class.
- This change centralizes the logging for the NOOP database session, improving code organization and ensuring that the message is logged whenever the setting is applied.
This commit is contained in:
Gabriel Luiz Freitas Almeida 2025-07-15 13:52:23 -03:00 • committed by GitHub
commit f1e95940a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 294 additions and 47 deletions

View file

@ -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

View file

@ -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}"

View file

@ -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