Skip to content

refactor(workflow): add Jinja2 renderer abstraction for template transform - #52

Open
tomerqodo wants to merge 6 commits into
copilot_combined_20260121_qodo_grep_cursor_copilot_1_base_refactorworkflow_add_jinja2_renderer_abstraction_for_template_transform_pr433from
copilot_combined_20260121_qodo_grep_cursor_copilot_1_head_refactorworkflow_add_jinja2_renderer_abstraction_for_template_transform_pr433
Open

refactor(workflow): add Jinja2 renderer abstraction for template transform#52
tomerqodo wants to merge 6 commits into
copilot_combined_20260121_qodo_grep_cursor_copilot_1_base_refactorworkflow_add_jinja2_renderer_abstraction_for_template_transform_pr433from
copilot_combined_20260121_qodo_grep_cursor_copilot_1_head_refactorworkflow_add_jinja2_renderer_abstraction_for_template_transform_pr433

Conversation

@tomerqodo

Copy link
Copy Markdown

Benchmark PR from qodo-benchmark#433

laipz8200 and others added 6 commits January 21, 2026 15:54
…de 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.
…Transform 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).
…ode 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).
…ts/core/workflow/nodes/template_transform/template_transform_node_spec.py`)

chore(type-check): ran `make type-check` (basedpyright clean, 0 errors)

No errors reported.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings January 21, 2026 18:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the template transform workflow node to introduce a Jinja2 renderer abstraction layer. The refactor extracts template rendering logic into a dedicated module with a Protocol-based interface, allowing for better testability and flexibility in rendering implementations.

Changes:

  • Introduces Jinja2TemplateRenderer Protocol and CodeExecutorJinja2TemplateRenderer implementation for template rendering abstraction
  • Updates TemplateTransformNode to accept an injectable renderer dependency instead of directly calling CodeExecutor
  • Modifies DifyNodeFactory to instantiate and inject the template renderer into TemplateTransformNode instances
  • Adds 719 lines of comprehensive test coverage for various Jinja2 template scenarios (filters, loops, conditionals, edge cases)

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 28 comments.

File Description
api/core/workflow/nodes/template_transform/template_renderer.py New module defining the Jinja2TemplateRenderer Protocol, TemplateRenderError exception, and CodeExecutorJinja2TemplateRenderer adapter implementation
api/core/workflow/nodes/template_transform/template_transform_node.py Adds custom __init__ for dependency injection, refactors _run method to use injected renderer, updates exception handling from CodeExecutionError to TemplateRenderError
api/core/workflow/nodes/node_factory.py Adds template_renderer parameter to factory constructor, creates renderer instance with code executor, injects renderer when creating TEMPLATE_TRANSFORM nodes
api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py Updates mocks to patch new renderer class, changes mock return values from dict to string, adds 32 new test methods covering boolean values, nested dicts, filters, loops, special characters, and edge cases
Comments suppressed due to low confidence (13)

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:66

        node = TemplateTransformNode(
            id="test_node",
            config=basic_node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:81

        node = TemplateTransformNode(
            id="test_node",
            config=basic_node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:93

        node = TemplateTransformNode(
            id="test_node",
            config=basic_node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:112

        node = TemplateTransformNode(
            id="test_node",
            config=node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:158

        node = TemplateTransformNode(
            id="test_node",
            config=basic_node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:187

        node = TemplateTransformNode(
            id="test_node",
            config=node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:210

        node = TemplateTransformNode(
            id="test_node",
            config=basic_node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:234

        node = TemplateTransformNode(
            id="test_node",
            config=basic_node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:278

        node = TemplateTransformNode(
            id="test_node",
            config=node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:324

        node = TemplateTransformNode(
            id="test_node",
            config=node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:364

        node = TemplateTransformNode(
            id="test_node",
            config=node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:394

        node = TemplateTransformNode(
            id="test_node",
            config=node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )

api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py:425

        node = TemplateTransformNode(
            id="test_node",
            config=node_data,
            graph_init_params=graph_init_params,
            graph=mock_graph,
            graph_runtime_state=mock_graph_runtime_state,
        )


def render_template(self, template: str, variables: Mapping[str, Any]) -> str:
"""Render a Jinja2 template with provided variables."""
raise NotImplementedError

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Protocol class should not raise NotImplementedError in its method. In Python, Protocol classes are used for structural subtyping (duck typing), and their methods should use ... (Ellipsis) as the body or have no implementation at all. The raise statement here is incorrect for a Protocol definition.

Suggested change
raise NotImplementedError
...

Copilot uses AI. Check for mistakes.
raise TemplateRenderError(str(exc)) from exc

rendered = result.get("result")
if rendered is not None and not isinstance(rendered, str):

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The render_template method should handle the case where result.get("result") returns None. Currently, if rendered is None, it will be returned directly, but this contradicts the return type annotation which specifies 'str', not 'str | None'. Either add a proper None check and raise an error, or update the return type annotation to include None.

Suggested change
if rendered is not None and not isinstance(rendered, str):
if rendered is None:
raise TemplateRenderError("Template render result is missing.")
if not isinstance(rendered, str):

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +40
class CodeExecutorJinja2TemplateRenderer(Jinja2TemplateRenderer):
"""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 rendered is not None and not isinstance(rendered, str):
raise TemplateRenderError("Template render result must be a string.")
return rendered

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new template_renderer.py module lacks dedicated unit tests. While the template_transform_node_spec.py tests the integration, there should be separate unit tests for the CodeExecutorJinja2TemplateRenderer class to cover:

  1. Successful template rendering
  2. Error conversion from CodeExecutionError to TemplateRenderError
  3. Handling of None result values
  4. Type validation for non-string results
  5. Different code_executor injection scenarios

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +41
def __init__(
self,
id: str,
config: Mapping[str, Any],
graph_init_params: "GraphInitParams",
graph_runtime_state: "GraphRuntimeState",
*,
template_renderer: Jinja2TemplateRenderer | None = None,
) -> None:
super().__init__(
id=id,
config=config,
graph_init_params=graph_init_params,
graph_runtime_state=graph_runtime_state,
)
self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer()

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new init method signature doesn't include a graph parameter, but existing tests in this file (lines 58-114 and others not shown in the diff) pass graph=mock_graph when creating TemplateTransformNode instances. These tests will fail with a TypeError for unexpected keyword argument 'graph'. Either the init method should accept and ignore the graph parameter for backward compatibility, or all the existing tests need to be updated to remove the graph parameter.

Copilot uses AI. Check for mistakes.
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(code_executor)

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code_executor parameter is passed to CodeExecutorJinja2TemplateRenderer, but at this point code_executor might still be None (before the default is applied). This should use self._code_executor instead, which has already been resolved to CodeExecutor if code_executor was None (as done on line 49).

Suggested change
self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer(code_executor)
self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer(self._code_executor)

Copilot uses AI. Check for mistakes.
Comment on lines +1020 to +1026
node = TemplateTransformNode(
id="test_node",
config=node_data,
graph_init_params=graph_init_params,
graph=mock_graph,
graph_runtime_state=mock_graph_runtime_state,
)

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword argument 'graph' is not a supported parameter name of TemplateTransformNode.init.

Copilot uses AI. Check for mistakes.
Comment on lines +1050 to +1056
node = TemplateTransformNode(
id="test_node",
config=node_data,
graph_init_params=graph_init_params,
graph=mock_graph,
graph_runtime_state=mock_graph_runtime_state,
)

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword argument 'graph' is not a supported parameter name of TemplateTransformNode.init.

Copilot uses AI. Check for mistakes.
Comment on lines +1080 to +1086
node = TemplateTransformNode(
id="test_node",
config=node_data,
graph_init_params=graph_init_params,
graph=mock_graph,
graph_runtime_state=mock_graph_runtime_state,
)

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword argument 'graph' is not a supported parameter name of TemplateTransformNode.init.

Copilot uses AI. Check for mistakes.
Comment on lines +1110 to +1116
node = TemplateTransformNode(
id="test_node",
config=node_data,
graph_init_params=graph_init_params,
graph=mock_graph,
graph_runtime_state=mock_graph_runtime_state,
)

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword argument 'graph' is not a supported parameter name of TemplateTransformNode.init.

Copilot uses AI. Check for mistakes.
Comment on lines +1140 to +1146
node = TemplateTransformNode(
id="test_node",
config=node_data,
graph_init_params=graph_init_params,
graph=mock_graph,
graph_runtime_state=mock_graph_runtime_state,
)

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword argument 'graph' is not a supported parameter name of TemplateTransformNode.init.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants