diff --git a/api/app/core/workflow/nodes/__init__.py b/api/app/core/workflow/nodes/__init__.py index 3744775d..33fe040c 100644 --- a/api/app/core/workflow/nodes/__init__.py +++ b/api/app/core/workflow/nodes/__init__.py @@ -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", ] diff --git a/api/app/core/workflow/nodes/base_node.py b/api/app/core/workflow/nodes/base_node.py index 44c92755..82f3d9b8 100644 --- a/api/app/core/workflow/nodes/base_node.py +++ b/api/app/core/workflow/nodes/base_node.py @@ -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 diff --git a/api/app/core/workflow/nodes/code/__init__.py b/api/app/core/workflow/nodes/code/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/app/core/workflow/nodes/configs.py b/api/app/core/workflow/nodes/configs.py index f904740c..3a87c589 100644 --- a/api/app/core/workflow/nodes/configs.py +++ b/api/app/core/workflow/nodes/configs.py @@ -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", ] diff --git a/api/app/core/workflow/nodes/enums.py b/api/app/core/workflow/nodes/enums.py index b33b64a3..40b9a7ef 100644 --- a/api/app/core/workflow/nodes/enums.py +++ b/api/app/core/workflow/nodes/enums.py @@ -24,6 +24,7 @@ class NodeType(StrEnum): TOOL = "tool" AGENT = "agent" ASSIGNER = "assigner" + JINJARENDER = "jinja-render" class ComparisonOperator(StrEnum): diff --git a/api/app/core/workflow/nodes/http_request/node.py b/api/app/core/workflow/nodes/http_request/node.py index c4223b2c..3b3a8b1a 100644 --- a/api/app/core/workflow/nodes/http_request/node.py +++ b/api/app/core/workflow/nodes/http_request/node.py @@ -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 diff --git a/api/app/core/workflow/nodes/jinja_render/__init__.py b/api/app/core/workflow/nodes/jinja_render/__init__.py new file mode 100644 index 00000000..49f467f5 --- /dev/null +++ b/api/app/core/workflow/nodes/jinja_render/__init__.py @@ -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"] diff --git a/api/app/core/workflow/nodes/jinja_render/config.py b/api/app/core/workflow/nodes/jinja_render/config.py new file mode 100644 index 00000000..5bea2e46 --- /dev/null +++ b/api/app/core/workflow/nodes/jinja_render/config.py @@ -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" + ) diff --git a/api/app/core/workflow/nodes/jinja_render/node.py b/api/app/core/workflow/nodes/jinja_render/node.py new file mode 100644 index 00000000..60beefb6 --- /dev/null +++ b/api/app/core/workflow/nodes/jinja_render/node.py @@ -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 diff --git a/api/app/core/workflow/nodes/node_factory.py b/api/app/core/workflow/nodes/node_factory.py index 98eafbf5..a6d735d0 100644 --- a/api/app/core/workflow/nodes/node_factory.py +++ b/api/app/core/workflow/nodes/node_factory.py @@ -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 diff --git a/api/app/core/workflow/template_renderer.py b/api/app/core/workflow/template_renderer.py index b927bd98..df6053b0 100644 --- a/api/app/core/workflow/template_renderer.py +++ b/api/app/core/workflow/template_renderer.py @@ -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 )