diff --git a/api/core/workflow/node_factory.py b/api/core/workflow/node_factory.py index 3fcf250c6e..85faea6ef3 100644 --- a/api/core/workflow/node_factory.py +++ b/api/core/workflow/node_factory.py @@ -6,11 +6,11 @@ from functools import lru_cache from typing import TYPE_CHECKING, Any, cast, final, override from sqlalchemy import select -from sqlalchemy.orm import Session from configs import dify_config from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext from core.app.llm.model_access import build_dify_model_access, fetch_model_config +from core.db.session_factory import session_factory from core.helper.code_executor.code_executor import ( CodeExecutionError, CodeExecutor, @@ -39,7 +39,6 @@ from core.workflow.nodes.agent.plugin_strategy_adapter import ( from core.workflow.nodes.agent.runtime_support import AgentRuntimeSupport from core.workflow.system_variables import SystemVariableKey, get_system_text, system_variable_selector from core.workflow.template_rendering import CodeExecutorJinja2TemplateRenderer -from extensions.ext_database import db from graphon.entities.base_node_data import BaseNodeData from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter from graphon.enums import BuiltinNodeTypes, NodeType @@ -229,10 +228,14 @@ def fetch_memory( node_data_memory: MemoryConfig | None, model_instance: ModelInstance, ) -> TokenBufferMemory | None: + """Build prompt memory for node construction without requiring Flask-local state.""" if not node_data_memory or not conversation_id: return None - with Session(db.engine, expire_on_commit=False) as session: + # Node construction can happen in graph initialization paths where Flask's + # app context is not active. Use the app-configured session factory instead + # of resolving db.engine through Flask-SQLAlchemy's current_app proxy. + with session_factory.create_session() as session: stmt = select(Conversation).where(Conversation.app_id == app_id, Conversation.id == conversation_id) conversation = session.scalar(stmt) if not conversation: diff --git a/api/tests/unit_tests/core/workflow/test_node_factory.py b/api/tests/unit_tests/core/workflow/test_node_factory.py index 9a7fcf166f..352c83758d 100644 --- a/api/tests/unit_tests/core/workflow/test_node_factory.py +++ b/api/tests/unit_tests/core/workflow/test_node_factory.py @@ -109,9 +109,8 @@ class TestFetchMemory: def scalar(self, _stmt): return None - monkeypatch.setattr(node_factory, "db", SimpleNamespace(engine=sentinel.engine)) + monkeypatch.setattr(node_factory, "session_factory", SimpleNamespace(create_session=FakeSession)) monkeypatch.setattr(node_factory, "select", MagicMock(return_value=FakeSelect())) - monkeypatch.setattr(node_factory, "Session", FakeSession) result = node_factory.fetch_memory( conversation_id="conversation-id", @@ -144,9 +143,8 @@ class TestFetchMemory: return conversation token_buffer_memory = MagicMock(return_value=memory) - monkeypatch.setattr(node_factory, "db", SimpleNamespace(engine=sentinel.engine)) + monkeypatch.setattr(node_factory, "session_factory", SimpleNamespace(create_session=FakeSession)) monkeypatch.setattr(node_factory, "select", MagicMock(return_value=FakeSelect())) - monkeypatch.setattr(node_factory, "Session", FakeSession) monkeypatch.setattr(node_factory, "TokenBufferMemory", token_buffer_memory) result = node_factory.fetch_memory( @@ -162,6 +160,41 @@ class TestFetchMemory: model_instance=sentinel.model_instance, ) + def test_uses_configured_session_factory_without_flask_app_context(self, monkeypatch: pytest.MonkeyPatch): + class FakeSelect: + def where(self, *_args): + return self + + class FakeSession: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def scalar(self, _stmt): + return sentinel.conversation + + class RaisingDB: + @property + def engine(self): + raise RuntimeError("Working outside of application context.") + + token_buffer_memory = MagicMock(return_value=sentinel.memory) + monkeypatch.setattr(node_factory, "db", RaisingDB(), raising=False) + monkeypatch.setattr(node_factory, "session_factory", SimpleNamespace(create_session=FakeSession)) + monkeypatch.setattr(node_factory, "select", MagicMock(return_value=FakeSelect())) + monkeypatch.setattr(node_factory, "TokenBufferMemory", token_buffer_memory) + + result = node_factory.fetch_memory( + conversation_id="conversation-id", + app_id="app-id", + node_data_memory=object(), + model_instance=sentinel.model_instance, + ) + + assert result is sentinel.memory + class TestDifyGraphInitContext: def test_to_graph_init_params_preserves_explicit_values(self):