feat(workflow): add Jinja2 template rendering node

This commit is contained in:
mengyonghao
2025-12-25 16:48:21 +08:00
parent 0b141fb952
commit eeb7a3a68d
11 changed files with 102 additions and 24 deletions

View File

@@ -8,10 +8,11 @@ from app.core.workflow.nodes.agent import AgentNode
from app.core.workflow.nodes.assigner import AssignerNode from app.core.workflow.nodes.assigner import AssignerNode
from app.core.workflow.nodes.base_node import BaseNode, WorkflowState from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
from app.core.workflow.nodes.end import EndNode from app.core.workflow.nodes.end import EndNode
from app.core.workflow.nodes.http_request import HttpRequestNode
from app.core.workflow.nodes.if_else import IfElseNode from app.core.workflow.nodes.if_else import IfElseNode
from app.core.workflow.nodes.jinja_render import JinjaRenderNode
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNode from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNode
from app.core.workflow.nodes.llm import LLMNode from app.core.workflow.nodes.llm import LLMNode
from app.core.workflow.nodes.http_request import HttpRequestNode
from app.core.workflow.nodes.node_factory import NodeFactory, WorkflowNode from app.core.workflow.nodes.node_factory import NodeFactory, WorkflowNode
from app.core.workflow.nodes.start import StartNode from app.core.workflow.nodes.start import StartNode
from app.core.workflow.nodes.transform import TransformNode from app.core.workflow.nodes.transform import TransformNode
@@ -29,5 +30,6 @@ __all__ = [
"WorkflowNode", "WorkflowNode",
"KnowledgeRetrievalNode", "KnowledgeRetrievalNode",
"AssignerNode", "AssignerNode",
"HttpRequestNode" "HttpRequestNode",
"JinjaRenderNode",
] ]

View File

@@ -7,10 +7,12 @@
import asyncio import asyncio
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, TypedDict, Annotated
from operator import add from operator import add
from langchain_core.messages import AnyMessage, HumanMessage, AIMessage from typing import Any
from langchain_core.messages import AnyMessage, AIMessage
from langgraph.config import get_stream_writer from langgraph.config import get_stream_writer
from typing_extensions import TypedDict, Annotated
from app.core.workflow.variable_pool import VariablePool from app.core.workflow.variable_pool import VariablePool

View File

@@ -3,20 +3,21 @@
所有节点的配置类都在这里导出,方便使用。 所有节点的配置类都在这里导出,方便使用。
""" """
from app.core.workflow.nodes.agent.config import AgentNodeConfig
from app.core.workflow.nodes.assigner.config import AssignerNodeConfig
from app.core.workflow.nodes.base_config import ( from app.core.workflow.nodes.base_config import (
BaseNodeConfig, BaseNodeConfig,
VariableDefinition, VariableDefinition,
VariableType, VariableType,
) )
from app.core.workflow.nodes.start.config import StartNodeConfig
from app.core.workflow.nodes.end.config import EndNodeConfig from app.core.workflow.nodes.end.config import EndNodeConfig
from app.core.workflow.nodes.llm.config import LLMNodeConfig, MessageConfig
from app.core.workflow.nodes.agent.config import AgentNodeConfig
from app.core.workflow.nodes.transform.config import TransformNodeConfig
from app.core.workflow.nodes.if_else.config import IfElseNodeConfig
from app.core.workflow.nodes.knowledge.config import KnowledgeRetrievalNodeConfig
from app.core.workflow.nodes.http_request.config import HttpRequestNodeConfig from app.core.workflow.nodes.http_request.config import HttpRequestNodeConfig
from app.core.workflow.nodes.assigner.config import AssignerNodeConfig from app.core.workflow.nodes.if_else.config import IfElseNodeConfig
from app.core.workflow.nodes.jinja_render.config import JinjaRenderNodeConfig
from app.core.workflow.nodes.knowledge.config import KnowledgeRetrievalNodeConfig
from app.core.workflow.nodes.llm.config import LLMNodeConfig, MessageConfig
from app.core.workflow.nodes.start.config import StartNodeConfig
from app.core.workflow.nodes.transform.config import TransformNodeConfig
__all__ = [ __all__ = [
# 基础类 # 基础类
@@ -33,5 +34,6 @@ __all__ = [
"IfElseNodeConfig", "IfElseNodeConfig",
"KnowledgeRetrievalNodeConfig", "KnowledgeRetrievalNodeConfig",
"AssignerNodeConfig", "AssignerNodeConfig",
"HttpRequestNodeConfig" "HttpRequestNodeConfig",
"JinjaRenderNodeConfig",
] ]

View File

@@ -24,6 +24,7 @@ class NodeType(StrEnum):
TOOL = "tool" TOOL = "tool"
AGENT = "agent" AGENT = "agent"
ASSIGNER = "assigner" ASSIGNER = "assigner"
JINJARENDER = "jinja-render"
class ComparisonOperator(StrEnum): class ComparisonOperator(StrEnum):

View File

@@ -7,15 +7,12 @@ import httpx
# import filetypes # TODO: File support (Feature) # import filetypes # TODO: File support (Feature)
from httpx import AsyncClient, Response, Timeout from httpx import AsyncClient, Response, Timeout
from app.core.workflow.nodes import BaseNode, WorkflowState from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
from app.core.workflow.nodes.enums import HttpRequestMethod, HttpErrorHandle, HttpAuthType, HttpContentType from app.core.workflow.nodes.enums import HttpRequestMethod, HttpErrorHandle, HttpAuthType, HttpContentType
from app.core.workflow.nodes.http_request.config import HttpRequestNodeConfig, HttpRequestNodeOutput from app.core.workflow.nodes.http_request.config import HttpRequestNodeConfig, HttpRequestNodeOutput
logger = logging.getLogger(__file__) logger = logging.getLogger(__file__)
DEFAULT_USER_AGENT = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36")
class HttpRequestNode(BaseNode): class HttpRequestNode(BaseNode):
""" """
@@ -91,9 +88,7 @@ class HttpRequestNode(BaseNode):
Both header keys and values support runtime template rendering. Both header keys and values support runtime template rendering.
""" """
headers = { headers = {}
"user-agent": DEFAULT_USER_AGENT
}
for key, value in self.typed_config.headers.items(): for key, value in self.typed_config.headers.items():
headers[self._render_template(key, state)] = self._render_template(value, state) headers[self._render_template(key, state)] = self._render_template(value, state)
return headers return headers

View File

@@ -0,0 +1,4 @@
from app.core.workflow.nodes.jinja_render.config import JinjaRenderNodeConfig
from app.core.workflow.nodes.jinja_render.node import JinjaRenderNode
__all__ = ["JinjaRenderNode", "JinjaRenderNodeConfig"]

View File

@@ -0,0 +1,24 @@
from pydantic import Field, BaseModel
from app.core.workflow.nodes.base_config import BaseNodeConfig
class VariablesMappingConfig(BaseModel):
name: str = Field(
...,
description="The variable name to be rendered"
)
value: str = Field(
...,
description="The corresponding value from the workflow"
)
class JinjaRenderNodeConfig(BaseNodeConfig):
template: str = Field(
...,
description="The Jinja2 template string to be rendered"
)
mapping: list[VariablesMappingConfig] = Field(
...,
description="Mapping configuration for variables used in the template"
)

View File

@@ -0,0 +1,45 @@
from typing import Any
from app.core.workflow.nodes import WorkflowState
from app.core.workflow.nodes.base_node import BaseNode
from app.core.workflow.nodes.jinja_render.config import JinjaRenderNodeConfig
from app.core.workflow.template_renderer import TemplateRenderer
class JinjaRenderNode(BaseNode):
def __init__(self, node_config: dict[str, Any], workflow_config: dict[str, Any]):
super().__init__(node_config, workflow_config)
self.typed_config = JinjaRenderNodeConfig(**self.config)
async def execute(self, state: WorkflowState) -> Any:
"""
Execute the node: render the Jinja2 template with mapped variables.
The rendered result is returned in a structure compatible with WorkflowState
merging, so that downstream nodes can access it via node_outputs.
Args:
state (WorkflowState): Current workflow state containing variables,
node outputs, and runtime variables.
Returns:
dict[str, Any]: Node output dictionary containing the rendered result
under `node_outputs[self.node_id]["output"]["rendered"]` and a
status flag "completed".
Raises:
RuntimeError: If Jinja2 template rendering fails due to invalid template
syntax or missing variables.
"""
render = TemplateRenderer(strict=False)
context = {}
for variable in self.typed_config.mapping:
context[variable.name] = self._render_template(variable.value, state)
try:
res = render.env.from_string(self.typed_config.template).render(**context)
except Exception as e:
raise RuntimeError(f"JinjaRender Node {self.node_name} render failed: {e}") from e
return res

View File

@@ -7,17 +7,18 @@
import logging import logging
from typing import Any, Union from typing import Any, Union
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNode
from app.core.workflow.nodes.http_request import HttpRequestNode
from app.core.workflow.nodes.agent import AgentNode from app.core.workflow.nodes.agent import AgentNode
from app.core.workflow.nodes.assigner import AssignerNode
from app.core.workflow.nodes.base_node import BaseNode from app.core.workflow.nodes.base_node import BaseNode
from app.core.workflow.nodes.end import EndNode from app.core.workflow.nodes.end import EndNode
from app.core.workflow.nodes.enums import NodeType from app.core.workflow.nodes.enums import NodeType
from app.core.workflow.nodes.http_request import HttpRequestNode
from app.core.workflow.nodes.if_else import IfElseNode from app.core.workflow.nodes.if_else import IfElseNode
from app.core.workflow.nodes.jinja_render import JinjaRenderNode
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNode
from app.core.workflow.nodes.llm import LLMNode from app.core.workflow.nodes.llm import LLMNode
from app.core.workflow.nodes.start import StartNode from app.core.workflow.nodes.start import StartNode
from app.core.workflow.nodes.transform import TransformNode from app.core.workflow.nodes.transform import TransformNode
from app.core.workflow.nodes.assigner import AssignerNode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -32,6 +33,7 @@ WorkflowNode = Union[
AssignerNode, AssignerNode,
HttpRequestNode, HttpRequestNode,
KnowledgeRetrievalNode, KnowledgeRetrievalNode,
JinjaRenderNode,
] ]
@@ -52,6 +54,7 @@ class NodeFactory:
NodeType.KNOWLEDGE_RETRIEVAL: KnowledgeRetrievalNode, NodeType.KNOWLEDGE_RETRIEVAL: KnowledgeRetrievalNode,
NodeType.ASSIGNER: AssignerNode, NodeType.ASSIGNER: AssignerNode,
NodeType.HTTP_REQUEST: HttpRequestNode, NodeType.HTTP_REQUEST: HttpRequestNode,
NodeType.JINJARENDER: JinjaRenderNode,
} }
@classmethod @classmethod

View File

@@ -7,7 +7,7 @@
import logging import logging
from typing import Any from typing import Any
from jinja2 import Template, TemplateSyntaxError, UndefinedError, Environment, StrictUndefined from jinja2 import TemplateSyntaxError, UndefinedError, Environment, StrictUndefined, Undefined
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,7 +22,7 @@ class TemplateRenderer:
strict: 是否使用严格模式(未定义变量会抛出异常) strict: 是否使用严格模式(未定义变量会抛出异常)
""" """
self.env = Environment( self.env = Environment(
undefined=StrictUndefined if strict else None, undefined=StrictUndefined if strict else Undefined,
autoescape=False # 不自动转义,因为我们处理的是文本而非 HTML autoescape=False # 不自动转义,因为我们处理的是文本而非 HTML
) )