feat(workflow): add Jinja2 template rendering node
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
0
api/app/core/workflow/nodes/code/__init__.py
Normal file
0
api/app/core/workflow/nodes/code/__init__.py
Normal 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",
|
||||
]
|
||||
|
||||
@@ -24,6 +24,7 @@ class NodeType(StrEnum):
|
||||
TOOL = "tool"
|
||||
AGENT = "agent"
|
||||
ASSIGNER = "assigner"
|
||||
JINJARENDER = "jinja-render"
|
||||
|
||||
|
||||
class ComparisonOperator(StrEnum):
|
||||
|
||||
@@ -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
|
||||
|
||||
4
api/app/core/workflow/nodes/jinja_render/__init__.py
Normal file
4
api/app/core/workflow/nodes/jinja_render/__init__.py
Normal 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"]
|
||||
24
api/app/core/workflow/nodes/jinja_render/config.py
Normal file
24
api/app/core/workflow/nodes/jinja_render/config.py
Normal 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"
|
||||
)
|
||||
45
api/app/core/workflow/nodes/jinja_render/node.py
Normal file
45
api/app/core/workflow/nodes/jinja_render/node.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user