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.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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
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 (
|
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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user