From ea9e80d063c65330ec96bc0a4c22755f67b97d4f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 4 Jan 2026 20:36:08 +0800 Subject: [PATCH 1/7] Added constructor-based CodeExecutor injection to TemplateTransformNode and threaded it through DifyNodeFactory so TemplateTransform nodes receive the dependency by default, keeping behavior unchanged unless an override is provided. Changes are in `api/core/workflow/nodes/template_transform/template_transform_node.py` and `api/core/workflow/nodes/node_factory.py`. **Commits** - chore(workflow): identify TemplateTransform dependency on CodeExecutor - feat(workflow): add CodeExecutor constructor injection to TemplateTransformNode (defaulting to current behavior) - feat(workflow): inject CodeExecutor from DifyNodeFactory when creating TemplateTransform nodes **Tests** - Not run (not requested) Next step: run `make lint` and `make type-check` if you want to validate the backend checks. --- api/core/workflow/nodes/node_factory.py | 16 +++++++----- .../template_transform_node.py | 26 +++++++++++++++++-- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/api/core/workflow/nodes/node_factory.py b/api/core/workflow/nodes/node_factory.py index 1ba049425982b6..5c04b5f219e7bf 100644 --- a/api/core/workflow/nodes/node_factory.py +++ b/api/core/workflow/nodes/node_factory.py @@ -107,9 +107,13 @@ def create_node(self, node_config: dict[str, object]) -> Node: code_limits=self._code_limits, ) - return node_class( - id=node_id, - config=node_config, - graph_init_params=self.graph_init_params, - graph_runtime_state=self.graph_runtime_state, - ) + node_kwargs: dict[str, object] = { + "id": node_id, + "config": node_config, + "graph_init_params": self.graph_init_params, + "graph_runtime_state": self.graph_runtime_state, + } + if node_type == NodeType.TEMPLATE_TRANSFORM: + node_kwargs["code_executor"] = self._code_executor + + return node_class(**node_kwargs) diff --git a/api/core/workflow/nodes/template_transform/template_transform_node.py b/api/core/workflow/nodes/template_transform/template_transform_node.py index 22743239605c0f..def0fcbb7d898a 100644 --- a/api/core/workflow/nodes/template_transform/template_transform_node.py +++ b/api/core/workflow/nodes/template_transform/template_transform_node.py @@ -1,5 +1,5 @@ from collections.abc import Mapping, Sequence -from typing import Any +from typing import TYPE_CHECKING, Any from configs import dify_config from core.helper.code_executor.code_executor import CodeExecutionError, CodeExecutor, CodeLanguage @@ -8,11 +8,33 @@ from core.workflow.nodes.base.node import Node from core.workflow.nodes.template_transform.entities import TemplateTransformNodeData +if TYPE_CHECKING: + from core.workflow.entities import GraphInitParams + from core.workflow.runtime import GraphRuntimeState + MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH = dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH class TemplateTransformNode(Node[TemplateTransformNodeData]): node_type = NodeType.TEMPLATE_TRANSFORM + _code_executor: type[CodeExecutor] + + def __init__( + self, + id: str, + config: Mapping[str, Any], + graph_init_params: "GraphInitParams", + graph_runtime_state: "GraphRuntimeState", + *, + code_executor: type[CodeExecutor] | None = None, + ) -> None: + super().__init__( + id=id, + config=config, + graph_init_params=graph_init_params, + graph_runtime_state=graph_runtime_state, + ) + self._code_executor = code_executor or CodeExecutor @classmethod def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]: @@ -39,7 +61,7 @@ def _run(self) -> NodeRunResult: variables[variable_name] = value.to_object() if value else None # Run code try: - result = CodeExecutor.execute_workflow_code_template( + result = self._code_executor.execute_workflow_code_template( language=CodeLanguage.JINJA2, code=self.node_data.template, inputs=variables ) except CodeExecutionError as e: From ae6a47d7203151169b49538d6d12e0d636e3cb2d Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 4 Jan 2026 22:21:59 +0800 Subject: [PATCH 2/7] Introduced a Jinja2-specific rendering abstraction and wired TemplateTransform to use it, keeping CodeExecutor as the default adapter while preserving current behavior. Updates are in `api/core/workflow/nodes/template_transform/template_renderer.py`, `api/core/workflow/nodes/template_transform/template_transform_node.py`, `api/core/workflow/nodes/node_factory.py`, and `api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py`. Commit-style summary: - feat(template-transform): add Jinja2 template renderer abstraction with CodeExecutor adapter - refactor(template-transform): use renderer in node/factory and update unit test patches Tests not run (not requested). --- api/core/workflow/nodes/node_factory.py | 8 +++- .../template_transform/template_renderer.py | 40 +++++++++++++++++++ .../template_transform_node.py | 22 +++++----- .../template_transform_node_spec.py | 38 +++++++++--------- 4 files changed, 78 insertions(+), 30 deletions(-) create mode 100644 api/core/workflow/nodes/template_transform/template_renderer.py diff --git a/api/core/workflow/nodes/node_factory.py b/api/core/workflow/nodes/node_factory.py index 5c04b5f219e7bf..ee9373a783df77 100644 --- a/api/core/workflow/nodes/node_factory.py +++ b/api/core/workflow/nodes/node_factory.py @@ -11,6 +11,10 @@ from core.workflow.nodes.base.node import Node from core.workflow.nodes.code.code_node import CodeNode from core.workflow.nodes.code.limits import CodeNodeLimits +from core.workflow.nodes.template_transform.template_renderer import ( + CodeExecutorJinja2TemplateRenderer, + Jinja2TemplateRenderer, +) from libs.typing import is_str, is_str_dict from .node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING @@ -37,6 +41,7 @@ def __init__( code_executor: type[CodeExecutor] | None = None, code_providers: Sequence[type[CodeNodeProvider]] | None = None, code_limits: CodeNodeLimits | None = None, + template_renderer: Jinja2TemplateRenderer | None = None, ) -> None: self.graph_init_params = graph_init_params self.graph_runtime_state = graph_runtime_state @@ -54,6 +59,7 @@ def __init__( max_string_array_length=dify_config.CODE_MAX_STRING_ARRAY_LENGTH, max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH, ) + self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer() @override def create_node(self, node_config: dict[str, object]) -> Node: @@ -114,6 +120,6 @@ def create_node(self, node_config: dict[str, object]) -> Node: "graph_runtime_state": self.graph_runtime_state, } if node_type == NodeType.TEMPLATE_TRANSFORM: - node_kwargs["code_executor"] = self._code_executor + node_kwargs["template_renderer"] = self._template_renderer return node_class(**node_kwargs) diff --git a/api/core/workflow/nodes/template_transform/template_renderer.py b/api/core/workflow/nodes/template_transform/template_renderer.py new file mode 100644 index 00000000000000..1666d17efc6ab6 --- /dev/null +++ b/api/core/workflow/nodes/template_transform/template_renderer.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Protocol + +from core.helper.code_executor.code_executor import CodeExecutionError, CodeExecutor, CodeLanguage + + +class TemplateRenderError(RuntimeError): + """Raised when rendering a Jinja2 template fails.""" + + +class Jinja2TemplateRenderer(Protocol): + """Render Jinja2 templates for template transform nodes.""" + + def render_template(self, template: str, variables: Mapping[str, Any]) -> str: + """Render a Jinja2 template with provided variables.""" + raise NotImplementedError + + +class CodeExecutorJinja2TemplateRenderer: + """Adapter that renders Jinja2 templates via CodeExecutor.""" + + _code_executor: type[CodeExecutor] + + def __init__(self, code_executor: type[CodeExecutor] | None = None) -> None: + self._code_executor = code_executor or CodeExecutor + + def render_template(self, template: str, variables: Mapping[str, Any]) -> str: + try: + result = self._code_executor.execute_workflow_code_template( + language=CodeLanguage.JINJA2, code=template, inputs=variables + ) + except CodeExecutionError as exc: + raise TemplateRenderError(str(exc)) from exc + + rendered = result.get("result") + if not isinstance(rendered, str): + raise TemplateRenderError("Template render result must be a string.") + return rendered diff --git a/api/core/workflow/nodes/template_transform/template_transform_node.py b/api/core/workflow/nodes/template_transform/template_transform_node.py index def0fcbb7d898a..f7e0bccccfdd81 100644 --- a/api/core/workflow/nodes/template_transform/template_transform_node.py +++ b/api/core/workflow/nodes/template_transform/template_transform_node.py @@ -2,11 +2,15 @@ from typing import TYPE_CHECKING, Any from configs import dify_config -from core.helper.code_executor.code_executor import CodeExecutionError, CodeExecutor, CodeLanguage from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus from core.workflow.node_events import NodeRunResult from core.workflow.nodes.base.node import Node from core.workflow.nodes.template_transform.entities import TemplateTransformNodeData +from core.workflow.nodes.template_transform.template_renderer import ( + CodeExecutorJinja2TemplateRenderer, + Jinja2TemplateRenderer, + TemplateRenderError, +) if TYPE_CHECKING: from core.workflow.entities import GraphInitParams @@ -17,7 +21,7 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]): node_type = NodeType.TEMPLATE_TRANSFORM - _code_executor: type[CodeExecutor] + _template_renderer: Jinja2TemplateRenderer def __init__( self, @@ -26,7 +30,7 @@ def __init__( graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", *, - code_executor: type[CodeExecutor] | None = None, + template_renderer: Jinja2TemplateRenderer | None = None, ) -> None: super().__init__( id=id, @@ -34,7 +38,7 @@ def __init__( graph_init_params=graph_init_params, graph_runtime_state=graph_runtime_state, ) - self._code_executor = code_executor or CodeExecutor + self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer() @classmethod def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]: @@ -61,13 +65,11 @@ def _run(self) -> NodeRunResult: variables[variable_name] = value.to_object() if value else None # Run code try: - result = self._code_executor.execute_workflow_code_template( - language=CodeLanguage.JINJA2, code=self.node_data.template, inputs=variables - ) - except CodeExecutionError as e: + rendered = self._template_renderer.render_template(self.node_data.template, variables) + except TemplateRenderError as e: return NodeRunResult(inputs=variables, status=WorkflowNodeExecutionStatus.FAILED, error=str(e)) - if len(result["result"]) > MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH: + if len(rendered) > MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH: return NodeRunResult( inputs=variables, status=WorkflowNodeExecutionStatus.FAILED, @@ -75,7 +77,7 @@ def _run(self) -> NodeRunResult: ) return NodeRunResult( - status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, outputs={"output": result["result"]} + status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, outputs={"output": rendered} ) @classmethod diff --git a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py index 1a67d5c3e3d918..3f2b00818487ea 100644 --- a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py @@ -5,9 +5,9 @@ from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState -from core.helper.code_executor.code_executor import CodeExecutionError from core.workflow.enums import ErrorStrategy, NodeType, WorkflowNodeExecutionStatus from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode +from core.workflow.nodes.template_transform.template_renderer import TemplateRenderError from models.workflow import WorkflowType @@ -127,7 +127,7 @@ def test_version(self): """Test version class method.""" assert TemplateTransformNode.version() == "1" - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_simple_template( self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params ): @@ -145,7 +145,7 @@ def test_run_simple_template( mock_graph_runtime_state.variable_pool.get.side_effect = lambda selector: variable_map.get(tuple(selector)) # Setup mock executor - mock_execute.return_value = {"result": "Hello Alice, you are 30 years old!"} + mock_execute.return_value = "Hello Alice, you are 30 years old!" node = TemplateTransformNode( id="test_node", @@ -162,7 +162,7 @@ def test_run_simple_template( assert result.inputs["name"] == "Alice" assert result.inputs["age"] == 30 - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_with_none_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with None variable values.""" node_data = { @@ -172,7 +172,7 @@ def test_run_with_none_values(self, mock_execute, mock_graph, mock_graph_runtime } mock_graph_runtime_state.variable_pool.get.return_value = None - mock_execute.return_value = {"result": "Value: "} + mock_execute.return_value = "Value: " node = TemplateTransformNode( id="test_node", @@ -187,13 +187,13 @@ def test_run_with_none_values(self, mock_execute, mock_graph, mock_graph_runtime assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.inputs["value"] is None - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_with_code_execution_error( self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params ): """Test _run when code execution fails.""" mock_graph_runtime_state.variable_pool.get.return_value = MagicMock() - mock_execute.side_effect = CodeExecutionError("Template syntax error") + mock_execute.side_effect = TemplateRenderError("Template syntax error") node = TemplateTransformNode( id="test_node", @@ -208,14 +208,14 @@ def test_run_with_code_execution_error( assert result.status == WorkflowNodeExecutionStatus.FAILED assert "Template syntax error" in result.error - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") @patch("core.workflow.nodes.template_transform.template_transform_node.MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH", 10) def test_run_output_length_exceeds_limit( self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params ): """Test _run when output exceeds maximum length.""" mock_graph_runtime_state.variable_pool.get.return_value = MagicMock() - mock_execute.return_value = {"result": "This is a very long output that exceeds the limit"} + mock_execute.return_value = "This is a very long output that exceeds the limit" node = TemplateTransformNode( id="test_node", @@ -230,7 +230,7 @@ def test_run_output_length_exceeds_limit( assert result.status == WorkflowNodeExecutionStatus.FAILED assert "Output length exceeds" in result.error - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_with_complex_jinja2_template( self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params ): @@ -257,7 +257,7 @@ def test_run_with_complex_jinja2_template( ("sys", "show_total"): mock_show_total, } mock_graph_runtime_state.variable_pool.get.side_effect = lambda selector: variable_map.get(tuple(selector)) - mock_execute.return_value = {"result": "apple, banana, orange (Total: 3)"} + mock_execute.return_value = "apple, banana, orange (Total: 3)" node = TemplateTransformNode( id="test_node", @@ -292,7 +292,7 @@ def test_extract_variable_selector_to_variable_mapping(self): assert mapping["node_123.var1"] == ["sys", "input1"] assert mapping["node_123.var2"] == ["sys", "input2"] - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_with_empty_variables(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with no variables (static template).""" node_data = { @@ -301,7 +301,7 @@ def test_run_with_empty_variables(self, mock_execute, mock_graph, mock_graph_run "template": "This is a static message.", } - mock_execute.return_value = {"result": "This is a static message."} + mock_execute.return_value = "This is a static message." node = TemplateTransformNode( id="test_node", @@ -317,7 +317,7 @@ def test_run_with_empty_variables(self, mock_execute, mock_graph, mock_graph_run assert result.outputs["output"] == "This is a static message." assert result.inputs == {} - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_with_numeric_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with numeric variable values.""" node_data = { @@ -339,7 +339,7 @@ def test_run_with_numeric_values(self, mock_execute, mock_graph, mock_graph_runt ("sys", "quantity"): mock_quantity, } mock_graph_runtime_state.variable_pool.get.side_effect = lambda selector: variable_map.get(tuple(selector)) - mock_execute.return_value = {"result": "Total: $31.5"} + mock_execute.return_value = "Total: $31.5" node = TemplateTransformNode( id="test_node", @@ -354,7 +354,7 @@ def test_run_with_numeric_values(self, mock_execute, mock_graph, mock_graph_runt assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.outputs["output"] == "Total: $31.5" - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_with_dict_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with dictionary variable values.""" node_data = { @@ -367,7 +367,7 @@ def test_run_with_dict_values(self, mock_execute, mock_graph, mock_graph_runtime mock_user.to_object.return_value = {"name": "John Doe", "email": "john@example.com"} mock_graph_runtime_state.variable_pool.get.return_value = mock_user - mock_execute.return_value = {"result": "Name: John Doe, Email: john@example.com"} + mock_execute.return_value = "Name: John Doe, Email: john@example.com" node = TemplateTransformNode( id="test_node", @@ -383,7 +383,7 @@ def test_run_with_dict_values(self, mock_execute, mock_graph, mock_graph_runtime assert "John Doe" in result.outputs["output"] assert "john@example.com" in result.outputs["output"] - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutor.execute_workflow_code_template") + @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") def test_run_with_list_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with list variable values.""" node_data = { @@ -396,7 +396,7 @@ def test_run_with_list_values(self, mock_execute, mock_graph, mock_graph_runtime mock_tags.to_object.return_value = ["python", "ai", "workflow"] mock_graph_runtime_state.variable_pool.get.return_value = mock_tags - mock_execute.return_value = {"result": "Tags: #python #ai #workflow "} + mock_execute.return_value = "Tags: #python #ai #workflow " node = TemplateTransformNode( id="test_node", From 1232388e857b8f46b2b59c19a7ce8a2070c68e72 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 4 Jan 2026 22:28:51 +0800 Subject: [PATCH 3/7] Updated TemplateRenderError to inherit from ValueError and switched node creation to return TemplateTransformNode directly for template-transform nodes in `api/core/workflow/nodes/node_factory.py`. Commit-style summary: - refactor(template-transform): derive TemplateRenderError from ValueError - refactor(node-factory): instantiate TemplateTransformNode directly with injected renderer Tests not run (not requested). --- api/core/workflow/nodes/node_factory.py | 22 ++++++++++++------- .../template_transform/template_renderer.py | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/api/core/workflow/nodes/node_factory.py b/api/core/workflow/nodes/node_factory.py index ee9373a783df77..f177aef665b3df 100644 --- a/api/core/workflow/nodes/node_factory.py +++ b/api/core/workflow/nodes/node_factory.py @@ -15,6 +15,7 @@ CodeExecutorJinja2TemplateRenderer, Jinja2TemplateRenderer, ) +from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode from libs.typing import is_str, is_str_dict from .node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING @@ -113,13 +114,18 @@ def create_node(self, node_config: dict[str, object]) -> Node: code_limits=self._code_limits, ) - node_kwargs: dict[str, object] = { - "id": node_id, - "config": node_config, - "graph_init_params": self.graph_init_params, - "graph_runtime_state": self.graph_runtime_state, - } if node_type == NodeType.TEMPLATE_TRANSFORM: - node_kwargs["template_renderer"] = self._template_renderer + return TemplateTransformNode( + id=node_id, + config=node_config, + graph_init_params=self.graph_init_params, + graph_runtime_state=self.graph_runtime_state, + template_renderer=self._template_renderer, + ) - return node_class(**node_kwargs) + return node_class( + id=node_id, + config=node_config, + graph_init_params=self.graph_init_params, + graph_runtime_state=self.graph_runtime_state, + ) diff --git a/api/core/workflow/nodes/template_transform/template_renderer.py b/api/core/workflow/nodes/template_transform/template_renderer.py index 1666d17efc6ab6..7acb9af18985a2 100644 --- a/api/core/workflow/nodes/template_transform/template_renderer.py +++ b/api/core/workflow/nodes/template_transform/template_renderer.py @@ -6,7 +6,7 @@ from core.helper.code_executor.code_executor import CodeExecutionError, CodeExecutor, CodeLanguage -class TemplateRenderError(RuntimeError): +class TemplateRenderError(ValueError): """Raised when rendering a Jinja2 template fails.""" From 45dd2e3fa979d25f7679917f6f2d0f541e1d7cfb Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 4 Jan 2026 22:39:28 +0800 Subject: [PATCH 4/7] chore(lint): ran `make lint` (ruff format updated `api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py`) chore(type-check): ran `make type-check` (basedpyright clean, 0 errors) No errors reported. --- .../template_transform_node_spec.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py index 3f2b00818487ea..66d6c3c56b27f4 100644 --- a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py @@ -6,8 +6,8 @@ from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.enums import ErrorStrategy, NodeType, WorkflowNodeExecutionStatus -from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode from core.workflow.nodes.template_transform.template_renderer import TemplateRenderError +from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode from models.workflow import WorkflowType @@ -127,7 +127,9 @@ def test_version(self): """Test version class method.""" assert TemplateTransformNode.version() == "1" - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_simple_template( self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params ): @@ -162,7 +164,9 @@ def test_run_simple_template( assert result.inputs["name"] == "Alice" assert result.inputs["age"] == 30 - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_with_none_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with None variable values.""" node_data = { @@ -187,7 +191,9 @@ def test_run_with_none_values(self, mock_execute, mock_graph, mock_graph_runtime assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.inputs["value"] is None - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_with_code_execution_error( self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params ): @@ -208,7 +214,9 @@ def test_run_with_code_execution_error( assert result.status == WorkflowNodeExecutionStatus.FAILED assert "Template syntax error" in result.error - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) @patch("core.workflow.nodes.template_transform.template_transform_node.MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH", 10) def test_run_output_length_exceeds_limit( self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params @@ -230,7 +238,9 @@ def test_run_output_length_exceeds_limit( assert result.status == WorkflowNodeExecutionStatus.FAILED assert "Output length exceeds" in result.error - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_with_complex_jinja2_template( self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params ): @@ -292,7 +302,9 @@ def test_extract_variable_selector_to_variable_mapping(self): assert mapping["node_123.var1"] == ["sys", "input1"] assert mapping["node_123.var2"] == ["sys", "input2"] - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_with_empty_variables(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with no variables (static template).""" node_data = { @@ -317,7 +329,9 @@ def test_run_with_empty_variables(self, mock_execute, mock_graph, mock_graph_run assert result.outputs["output"] == "This is a static message." assert result.inputs == {} - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_with_numeric_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with numeric variable values.""" node_data = { @@ -354,7 +368,9 @@ def test_run_with_numeric_values(self, mock_execute, mock_graph, mock_graph_runt assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.outputs["output"] == "Total: $31.5" - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_with_dict_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with dictionary variable values.""" node_data = { @@ -383,7 +399,9 @@ def test_run_with_dict_values(self, mock_execute, mock_graph, mock_graph_runtime assert "John Doe" in result.outputs["output"] assert "john@example.com" in result.outputs["output"] - @patch("core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template") + @patch( + "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" + ) def test_run_with_list_values(self, mock_execute, mock_graph, mock_graph_runtime_state, graph_init_params): """Test _run with list variable values.""" node_data = { From e8ffbfbc1384bcdb70574218d1898ae34d30474e Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 5 Jan 2026 00:23:29 +0800 Subject: [PATCH 5/7] Update api/core/workflow/nodes/template_transform/template_renderer.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- api/core/workflow/nodes/template_transform/template_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/template_transform/template_renderer.py b/api/core/workflow/nodes/template_transform/template_renderer.py index 7acb9af18985a2..a5f06bf2bbd07a 100644 --- a/api/core/workflow/nodes/template_transform/template_renderer.py +++ b/api/core/workflow/nodes/template_transform/template_renderer.py @@ -18,7 +18,7 @@ def render_template(self, template: str, variables: Mapping[str, Any]) -> str: raise NotImplementedError -class CodeExecutorJinja2TemplateRenderer: +class CodeExecutorJinja2TemplateRenderer(Jinja2TemplateRenderer): """Adapter that renders Jinja2 templates via CodeExecutor.""" _code_executor: type[CodeExecutor] From cbb825482f8a4e537e26d2dfdcdfbe1aded48586 Mon Sep 17 00:00:00 2001 From: ofir-frd Date: Tue, 20 Jan 2026 17:34:38 +0200 Subject: [PATCH 6/7] Apply changes for benchmark PR --- api/core/workflow/nodes/node_factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/core/workflow/nodes/node_factory.py b/api/core/workflow/nodes/node_factory.py index f177aef665b3df..72c49d99f2b7da 100644 --- a/api/core/workflow/nodes/node_factory.py +++ b/api/core/workflow/nodes/node_factory.py @@ -8,6 +8,7 @@ from core.helper.code_executor.code_node_provider import CodeNodeProvider from core.workflow.enums import NodeType from core.workflow.graph import NodeFactory +from core.workflow.graph_engine.error_handler import ErrorHandler from core.workflow.nodes.base.node import Node from core.workflow.nodes.code.code_node import CodeNode from core.workflow.nodes.code.limits import CodeNodeLimits From 62d15f5c4948d465146fcabc651dcb8594a334d9 Mon Sep 17 00:00:00 2001 From: ofir-frd Date: Tue, 20 Jan 2026 17:34:42 +0200 Subject: [PATCH 7/7] Add AGENTS.md with compliance rules --- AGENTS.md | 269 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 234 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 782861ad36caf7..d62e6ee49ad942 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,54 +1,253 @@ -# AGENTS.md +# Compliance Rules -## Project Overview +This file contains the compliance and code quality rules for this repository. -Dify is an open-source platform for developing LLM applications with an intuitive interface combining agentic AI workflows, RAG pipelines, agent capabilities, and model management. +## 1. Python Functions Must Include Type Annotations -The codebase is split into: +**Objective:** Ensure type safety and improve code maintainability by requiring explicit type annotations for all function parameters and return values in Python code, as enforced by basedpyright type checking -- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design -- **Frontend Web** (`/web`): Next.js 15 application using TypeScript and React 19 -- **Docker deployment** (`/docker`): Containerized deployment configurations +**Success Criteria:** All Python function definitions include type hints for parameters and return types using Python 3.12+ syntax (e.g., list[str] instead of List[str], int | None instead of Optional[int]) -## Backend Workflow +**Failure Criteria:** Python function definitions lack type annotations for parameters or return values -- Run backend CLI commands through `uv run --project api `. +--- -- Before submission, all backend modifications must pass local checks: `make lint`, `make type-check`, and `uv run --project api --dev dev/pytest/pytest_unit_tests.sh`. +## 2. Python Code Must Follow Ruff Linting Rules -- Use Makefile targets for linting and formatting; `make lint` and `make type-check` cover the required checks. +**Objective:** Maintain consistent code quality and style by adhering to the project's ruff configuration, which enforces rules for code formatting, import ordering, security checks, and best practices -- Integration tests are CI-only and are not expected to run in the local environment. +**Success Criteria:** All Python code passes ruff format and ruff check --fix without errors, following the rules defined in .ruff.toml including line length of 120 characters, proper import sorting, and security rules (S102, S307, S301, S302, S311) -## Frontend Workflow +**Failure Criteria:** Python code violates ruff linting rules such as improper formatting, incorrect import ordering, use of print() instead of logging, or security violations like using exec/eval/pickle -```bash -cd web -pnpm lint:fix -pnpm type-check:tsgo -pnpm test -``` +--- -## Testing & Quality Practices +## 3. Backend Code Must Use Logging Instead of Print Statements -- Follow TDD: red → green → refactor. -- Use `pytest` for backend tests with Arrange-Act-Assert structure. -- Enforce strong typing; avoid `Any` and prefer explicit type annotations. -- Write self-documenting code; only add comments that explain intent. +**Objective:** Enable proper observability and debugging by requiring all output to use the logging module rather than print statements, with logger instances declared at module level -## Language Style +**Success Criteria:** All logging is performed using logger = logging.getLogger(__name__) declared at module top, with no print() statements in production code (tests are exempt) -- **Python**: Keep type hints on functions and attributes, and implement relevant special methods (e.g., `__repr__`, `__str__`). -- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check:tsgo`, and avoid `any` types. +**Failure Criteria:** Code contains print() statements outside of test files, or logging is performed without proper logger initialization -## General Practices +--- -- Prefer editing existing files; add new documentation only when requested. -- Inject dependencies through constructors and preserve clean architecture boundaries. -- Handle errors with domain-specific exceptions at the correct layer. +## 4. Python Code Must Use Modern Type Syntax for Python 3.12+ -## Project Conventions +**Objective:** Leverage modern Python type system features for better code clarity and type safety by using the latest type annotation syntax -- Backend architecture adheres to DDD and Clean Architecture principles. -- Async work runs through Celery with Redis as the broker. -- Frontend user-facing strings must use `web/i18n/en-US/`; avoid hardcoded text. +**Success Criteria:** Type annotations use Python 3.12+ syntax: list[T] instead of List[T], dict[K,V] instead of Dict[K,V], int | None instead of Optional[int], and str | int instead of Union[str, int] + +**Failure Criteria:** Code uses legacy typing imports like List, Dict, Optional, Union from the typing module when modern syntax is available + +--- + +## 5. Python Backend Files Must Not Exceed 800 Lines + +**Objective:** Maintain code readability and modularity by keeping individual Python files under 800 lines, promoting proper code organization and separation of concerns + +**Success Criteria:** All Python files in the backend (api/) contain fewer than 800 lines of code + +**Failure Criteria:** Python files in the backend exceed 800 lines and should be split into multiple files + +--- + +## 6. SQLAlchemy Sessions Must Use Context Managers + +**Objective:** Ensure proper database connection management and prevent resource leaks by requiring all SQLAlchemy sessions to be opened with context managers + +**Success Criteria:** All database operations use 'with Session(db.engine, expire_on_commit=False) as session:' pattern for session management + +**Failure Criteria:** Database sessions are created without context managers or sessions are not properly closed + +--- + +## 7. Database Queries Must Include tenant_id Scoping + +**Objective:** Ensure data isolation and security in multi-tenant architecture by requiring all database queries to be scoped by tenant_id to prevent cross-tenant data access + +**Success Criteria:** All database queries that access tenant-scoped resources include WHERE clauses filtering by tenant_id + +**Failure Criteria:** Database queries access tenant-scoped tables without tenant_id filtering, creating potential data leakage + +--- + +## 8. Python Tests Must Follow pytest AAA Pattern + +**Objective:** Maintain clear and maintainable test structure by requiring all pytest tests to follow the Arrange-Act-Assert pattern for better readability and understanding + +**Success Criteria:** Test functions are structured with three distinct sections: Arrange (setup), Act (execution), Assert (verification), with clear separation between phases + +**Failure Criteria:** Test functions mix setup, execution, and assertion logic without clear separation or organization + +--- + +## 9. TypeScript Must Avoid any Type Annotations + +**Objective:** Maintain type safety in the frontend codebase by avoiding the any type, which bypasses TypeScript's type checking and can lead to runtime errors + +**Success Criteria:** TypeScript code uses specific types or unknown instead of any, with ts/no-explicit-any warnings addressed + +**Failure Criteria:** Code contains 'any' type annotations without justified exceptions or proper type definitions + +--- + +## 10. TypeScript Must Use Type Definitions Instead of Interfaces + +**Objective:** Maintain consistency in type declarations across the codebase by preferring type definitions over interfaces, as enforced by ts/consistent-type-definitions rule + +**Success Criteria:** All TypeScript type declarations use 'type' keyword instead of 'interface' keyword, following the pattern: type MyType = { ... } + +**Failure Criteria:** Code uses 'interface' declarations instead of 'type' definitions + +--- + +## 11. Frontend User-Facing Strings Must Use i18n Translations + +**Objective:** Enable internationalization and localization by requiring all user-facing text in the frontend to be retrieved from i18n translation files rather than hardcoded + +**Success Criteria:** All user-facing strings are defined in web/i18n/en-US/ translation files and accessed via useTranslation hook with proper namespace options, following dify-i18n/require-ns-option rule + +**Failure Criteria:** User-facing strings are hardcoded in component files instead of using translation keys + +--- + +## 12. TypeScript Files Must Follow Strict TypeScript Configuration + +**Objective:** Ensure type safety and catch potential errors at compile time by enabling strict TypeScript compiler options + +**Success Criteria:** All TypeScript code compiles successfully with strict mode enabled in tsconfig.json, including strict type checking and consistent casing enforcement + +**Failure Criteria:** Code contains type errors or inconsistent casing that would fail strict TypeScript compilation + +--- + +## 13. Backend Configuration Must Be Accessed via configs Module + +**Objective:** Centralize configuration management and prevent direct environment variable access by requiring all configuration to be retrieved through the configs module + +**Success Criteria:** Configuration values are accessed through configs.dify_config or related config modules, not via direct os.environ or os.getenv calls + +**Failure Criteria:** Code directly reads environment variables using os.environ or os.getenv instead of using the configs module + +--- + +## 14. Python Backend Must Use Pydantic v2 for Data Validation + +**Objective:** Ensure consistent data validation and serialization using Pydantic v2 models with proper configuration for DTOs and request/response validation + +**Success Criteria:** All data transfer objects use Pydantic v2 BaseModel with ConfigDict(extra='forbid') by default, and use @field_validator/@model_validator for domain rules + +**Failure Criteria:** Data validation uses Pydantic v1 syntax, allows undeclared fields without explicit configuration, or uses custom validation logic instead of Pydantic validators + +--- + +## 15. Backend Errors Must Use Domain-Specific Exceptions + +**Objective:** Provide clear error handling and appropriate HTTP responses by raising domain-specific exceptions from services and translating them to HTTP responses in controllers + +**Success Criteria:** Business logic raises exceptions from services/errors or core/errors modules, and controllers handle these exceptions to return appropriate HTTP responses + +**Failure Criteria:** Services return HTTP responses directly, or generic exceptions are raised without domain context + +--- + +## 16. Python Code Must Use Snake Case for Variables and Functions + +**Objective:** Maintain consistent naming conventions across the Python codebase by using snake_case for variables and functions, PascalCase for classes, and UPPER_CASE for constants + +**Success Criteria:** All Python variables and functions use snake_case naming (e.g., user_name, get_user_data), classes use PascalCase (e.g., UserService), and constants use UPPER_CASE (e.g., MAX_RETRIES) + +**Failure Criteria:** Python code uses camelCase, PascalCase for variables/functions, or inconsistent naming patterns + +--- + +## 17. Frontend ESLint Sonarjs Rules Must Be Followed + +**Objective:** Maintain code quality and prevent common bugs by adhering to SonarJS cognitive complexity and maintainability rules configured in the project + +**Success Criteria:** TypeScript code passes SonarJS linting rules including no-dead-store (error level), max-lines warnings (1000 line limit), and no-variable-usage-before-declaration (error level) + +**Failure Criteria:** Code violates SonarJS rules such as dead stores, files exceeding 1000 lines, or variables used before declaration + +--- + +## 18. Backend Architecture Must Follow Import Layer Constraints + +**Objective:** Maintain clean architecture boundaries by enforcing layer separation through import-linter rules that prevent circular dependencies and upward imports + +**Success Criteria:** Code adheres to import-linter contracts defined in .importlinter, including workflow layer separation (graph_engine → graph → nodes → entities) and domain isolation rules + +**Failure Criteria:** Imports violate architectural layers by importing from higher layers or creating circular dependencies not explicitly allowed in .importlinter + +--- + +## 19. Backend Storage Access Must Use Abstraction Layer + +**Objective:** Ensure consistent and secure storage operations by requiring all storage access to go through extensions.ext_storage.storage abstraction + +**Success Criteria:** All file storage operations use extensions.ext_storage.storage interface instead of direct filesystem or cloud storage APIs + +**Failure Criteria:** Code directly accesses filesystem, S3, Azure Blob, or other storage without using the storage abstraction layer + +--- + +## 20. Backend HTTP Requests Must Use SSRF Proxy Helper + +**Objective:** Prevent Server-Side Request Forgery attacks by requiring all outbound HTTP requests to use the SSRF proxy helper for validation and protection + +**Success Criteria:** All outbound HTTP fetches use core.helper.ssrf_proxy instead of direct httpx, requests, or urllib calls + +**Failure Criteria:** Code makes outbound HTTP requests without using the SSRF proxy helper, potentially exposing internal resources + +--- + +## 21. Python Code Must Not Override Dunder Methods Unnecessarily + +**Objective:** Prevent subtle bugs and maintain expected Python object behavior by avoiding unnecessary overrides of special methods like __init__, __iadd__, etc. + +**Success Criteria:** Special methods (__init__, __iadd__, __str__, __repr__) are only overridden when necessary with proper implementation of relevant special methods documented in coding_style.md + +**Failure Criteria:** Code overrides dunder methods without clear justification or without implementing complementary methods + +--- + +## 22. Backend Must Avoid Security-Risky Functions + +**Objective:** Prevent remote code execution vulnerabilities by prohibiting the use of dangerous built-in functions that can execute arbitrary code + +**Success Criteria:** Python code does not use exec(), eval(), pickle, marshal, or ast.literal_eval() as enforced by ruff security rules S102, S307, S301, S302 + +**Failure Criteria:** Code uses exec(), eval(), pickle.loads(), marshal.loads(), or ast.literal_eval() which can execute arbitrary code + +--- + +## 23. Python Backend Must Use Deterministic Control Flow + +**Objective:** Optimize for observability and debugging by maintaining deterministic control flow with clear logging and actionable errors + +**Success Criteria:** Code avoids clever hacks, maintains readable control flow, includes tenant/app/workflow identifiers in log context, and logs retryable events at warning level and terminal failures at error level + +**Failure Criteria:** Code uses obfuscated logic, lacks proper logging context, or mixes logging levels inappropriately + +--- + +## 24. Backend Async Tasks Must Be Idempotent + +**Objective:** Ensure reliability of background processing by requiring all async tasks to be idempotent and log relevant object identifiers for debugging + +**Success Criteria:** All background tasks in tasks/ are idempotent (can be safely retried), log the relevant object identifiers (tenant_id, app_id, etc.), and specify explicit queue selection + +**Failure Criteria:** Background tasks are not idempotent, lack proper logging identifiers, or don't specify queue configuration + +--- + +## 25. Frontend Code Must Not Use console Statements + +**Objective:** Prevent debug code from reaching production by treating console statements as warnings that should be removed or replaced with proper logging + +**Success Criteria:** Production frontend code avoids console.log, console.warn, console.error statements as enforced by no-console warning rule + +**Failure Criteria:** Code contains console statements that should be removed or replaced with proper logging mechanisms + +---