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.base_node import BaseNode, WorkflowState
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.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.http_request import HttpRequestNode
from app.core.workflow.nodes.node_factory import NodeFactory, WorkflowNode
from app.core.workflow.nodes.start import StartNode
from app.core.workflow.nodes.transform import TransformNode
@@ -29,5 +30,6 @@ __all__ = [
"WorkflowNode",
"KnowledgeRetrievalNode",
"AssignerNode",
"HttpRequestNode"
"HttpRequestNode",
"JinjaRenderNode",
]

View File

@@ -7,10 +7,12 @@
import asyncio
import logging
from abc import ABC, abstractmethod
from typing import Any, TypedDict, Annotated
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 typing_extensions import TypedDict, Annotated
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 (
BaseNodeConfig,
VariableDefinition,
VariableType,
)
from app.core.workflow.nodes.start.config import StartNodeConfig
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.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__ = [
# 基础类
@@ -33,5 +34,6 @@ __all__ = [
"IfElseNodeConfig",
"KnowledgeRetrievalNodeConfig",
"AssignerNodeConfig",
"HttpRequestNodeConfig"
"HttpRequestNodeConfig",
"JinjaRenderNodeConfig",
]

View File

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

View File

@@ -7,15 +7,12 @@ import httpx
# import filetypes # TODO: File support (Feature)
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.http_request.config import HttpRequestNodeConfig, HttpRequestNodeOutput
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):
"""
@@ -91,9 +88,7 @@ class HttpRequestNode(BaseNode):
Both header keys and values support runtime template rendering.
"""
headers = {
"user-agent": DEFAULT_USER_AGENT
}
headers = {}
for key, value in self.typed_config.headers.items():
headers[self._render_template(key, state)] = self._render_template(value, state)
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
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.assigner import AssignerNode
from app.core.workflow.nodes.base_node import BaseNode
from app.core.workflow.nodes.end import EndNode
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.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.start import StartNode
from app.core.workflow.nodes.transform import TransformNode
from app.core.workflow.nodes.assigner import AssignerNode
logger = logging.getLogger(__name__)
@@ -32,6 +33,7 @@ WorkflowNode = Union[
AssignerNode,
HttpRequestNode,
KnowledgeRetrievalNode,
JinjaRenderNode,
]
@@ -52,6 +54,7 @@ class NodeFactory:
NodeType.KNOWLEDGE_RETRIEVAL: KnowledgeRetrievalNode,
NodeType.ASSIGNER: AssignerNode,
NodeType.HTTP_REQUEST: HttpRequestNode,
NodeType.JINJARENDER: JinjaRenderNode,
}
@classmethod

View File

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