Merge pull request #515 from SuanmoSuanyangTechnology/feature/workflow-notes

feat(workflow): add support for notes nodes
This commit is contained in:
Mark
2026-03-09 14:14:07 +08:00
committed by GitHub
5 changed files with 101 additions and 50 deletions

View File

@@ -8,34 +8,60 @@ from typing import Any
from urllib.parse import quote from urllib.parse import quote
from app.core.workflow.adapters.base_converter import BaseConverter from app.core.workflow.adapters.base_converter import BaseConverter
from app.core.workflow.adapters.errors import UnsupportVariableType, UnknowModelWarning, ExceptionDefineition, \ from app.core.workflow.adapters.errors import (
UnsupportVariableType,
UnknowModelWarning,
ExceptionDefineition,
ExceptionType ExceptionType
from app.core.workflow.nodes.assigner import AssignerNodeConfig )
from app.core.workflow.nodes.assigner.config import AssignmentItem from app.core.workflow.nodes.assigner.config import AssignmentItem
from app.core.workflow.nodes.base_config import VariableDefinition, BaseNodeConfig from app.core.workflow.nodes.base_config import VariableDefinition, BaseNodeConfig
from app.core.workflow.nodes.code import CodeNodeConfig
from app.core.workflow.nodes.code.config import InputVariable, OutputVariable from app.core.workflow.nodes.code.config import InputVariable, OutputVariable
from app.core.workflow.nodes.configs import StartNodeConfig, LLMNodeConfig from app.core.workflow.nodes.configs import (
from app.core.workflow.nodes.cycle_graph import LoopNodeConfig, IterationNodeConfig StartNodeConfig,
from app.core.workflow.nodes.cycle_graph.config import ConditionDetail as LoopConditionDetail, ConditionsConfig, \ LLMNodeConfig,
AssignerNodeConfig,
CodeNodeConfig,
LoopNodeConfig,
IterationNodeConfig,
EndNodeConfig,
HttpRequestNodeConfig,
IfElseNodeConfig,
JinjaRenderNodeConfig,
KnowledgeRetrievalNodeConfig,
NoteNodeConfig,
ParameterExtractorNodeConfig,
QuestionClassifierNodeConfig,
VariableAggregatorNodeConfig
)
from app.core.workflow.nodes.cycle_graph.config import (
ConditionDetail as LoopConditionDetail,
ConditionsConfig,
CycleVariable CycleVariable
from app.core.workflow.nodes.end import EndNodeConfig )
from app.core.workflow.nodes.enums import ValueInputType, ComparisonOperator, AssignmentOperator, HttpAuthType, \ from app.core.workflow.nodes.enums import (
HttpContentType, HttpErrorHandle ValueInputType,
from app.core.workflow.nodes.http_request import HttpRequestNodeConfig ComparisonOperator,
from app.core.workflow.nodes.http_request.config import HttpAuthConfig, HttpContentTypeConfig, HttpFormData, \ AssignmentOperator,
HttpTimeOutConfig, HttpRetryConfig, HttpErrorDefaultTamplete, HttpErrorHandleConfig HttpAuthType,
from app.core.workflow.nodes.if_else import IfElseNodeConfig HttpContentType,
HttpErrorHandle,
NodeType
)
from app.core.workflow.nodes.http_request.config import (
HttpAuthConfig,
HttpContentTypeConfig,
HttpFormData,
HttpTimeOutConfig,
HttpRetryConfig,
HttpErrorDefaultTamplete,
HttpErrorHandleConfig
)
from app.core.workflow.nodes.if_else.config import ConditionDetail, ConditionBranchConfig from app.core.workflow.nodes.if_else.config import ConditionDetail, ConditionBranchConfig
from app.core.workflow.nodes.jinja_render import JinjaRenderNodeConfig
from app.core.workflow.nodes.jinja_render.config import VariablesMappingConfig from app.core.workflow.nodes.jinja_render.config import VariablesMappingConfig
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig
from app.core.workflow.nodes.llm.config import MemoryWindowSetting, MessageConfig from app.core.workflow.nodes.llm.config import MemoryWindowSetting, MessageConfig
from app.core.workflow.nodes.parameter_extractor import ParameterExtractorNodeConfig
from app.core.workflow.nodes.parameter_extractor.config import ParamsConfig from app.core.workflow.nodes.parameter_extractor.config import ParamsConfig
from app.core.workflow.nodes.question_classifier import QuestionClassifierNodeConfig
from app.core.workflow.nodes.question_classifier.config import ClassifierConfig from app.core.workflow.nodes.question_classifier.config import ClassifierConfig
from app.core.workflow.nodes.variable_aggregator import VariableAggregatorNodeConfig
from app.core.workflow.variable.base_variable import VariableType, DEFAULT_VALUE from app.core.workflow.variable.base_variable import VariableType, DEFAULT_VALUE
@@ -48,24 +74,24 @@ class DifyConverter(BaseConverter):
def __init__(self): def __init__(self):
self.CONFIG_CONVERT_MAP = { self.CONFIG_CONVERT_MAP = {
"start": self.convert_start_node_config, NodeType.START: self.convert_start_node_config,
"llm": self.convert_llm_node_config, NodeType.LLM: self.convert_llm_node_config,
"answer": self.convert_end_node_config, NodeType.END: self.convert_end_node_config,
"if-else": self.convert_if_else_node_config, NodeType.IF_ELSE: self.convert_if_else_node_config,
"loop": self.convert_loop_node_config, NodeType.LOOP: self.convert_loop_node_config,
"iteration": self.convert_iteration_node_config, NodeType.ITERATION: self.convert_iteration_node_config,
"assigner": self.convert_assigner_node_config, NodeType.ASSIGNER: self.convert_assigner_node_config,
"code": self.convert_code_node_config, NodeType.CODE: self.convert_code_node_config,
"http-request": self.convert_http_node_config, NodeType.HTTP_REQUEST: self.convert_http_node_config,
"template-transform": self.convert_jinja_render_node_config, NodeType.JINJARENDER: self.convert_jinja_render_node_config,
"knowledge-retrieval": self.convert_knowledge_node_config, NodeType.KNOWLEDGE_RETRIEVAL: self.convert_knowledge_node_config,
"parameter-extractor": self.convert_parameter_extractor_node_config, NodeType.PARAMETER_EXTRACTOR: self.convert_parameter_extractor_node_config,
"question-classifier": self.convert_question_classifier_node_config, NodeType.QUESTION_CLASSIFIER: self.convert_question_classifier_node_config,
"variable-aggregator": self.convert_variable_aggregator_node_config, NodeType.VAR_AGGREGATOR: self.convert_variable_aggregator_node_config,
"tool": self.convert_tool_node_config, NodeType.TOOL: self.convert_tool_node_config,
"loop-start": lambda x: {}, NodeType.NOTES: self.convert_notes_config,
"iteration-start": lambda x: {}, NodeType.CYCLE_START: lambda x: {},
"loop-end": lambda x: {}, NodeType.BREAK: lambda x: {},
} }
def get_node_convert(self, node_type): def get_node_convert(self, node_type):
@@ -732,3 +758,16 @@ class DifyConverter(BaseConverter):
detail=f"Please reconfigure the tool node.", detail=f"Please reconfigure the tool node.",
)) ))
return {} return {}
@staticmethod
def convert_notes_config(node: dict):
node_data = node["data"]
result = NoteNodeConfig.model_construct(
author=node_data.get("author", ""),
text=node_data.get("text", ""),
width=node_data.get("width", 80),
height=node_data.get("height", 80),
theme=node_data.get("theme", "blue"),
show_author=node_data.get("showAuthor", True)
).model_dump()
return result

View File

@@ -50,7 +50,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):
DifyConverter.__init__(self) DifyConverter.__init__(self)
BasePlatformAdapter.__init__(self, config) BasePlatformAdapter.__init__(self, config)
def get_metadata(self) -> PlatformMetadata: def get_metadata(self) -> PlatformMetadata:
return PlatformMetadata( return PlatformMetadata(
@@ -59,7 +59,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
support_node_types=list(self.NODE_TYPE_MAPPING.keys()) support_node_types=list(self.NODE_TYPE_MAPPING.keys())
) )
def map_node_type(self, platform_node_type) -> str: def map_node_type(self, platform_node_type) -> NodeType:
return self.NODE_TYPE_MAPPING.get(platform_node_type, NodeType.UNKNOWN) return self.NODE_TYPE_MAPPING.get(platform_node_type, NodeType.UNKNOWN)
@property @property
@@ -84,7 +84,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
require_fields = frozenset({'app', 'kind', 'version', 'workflow'}) require_fields = frozenset({'app', 'kind', 'version', 'workflow'})
if not all(field in self.config for field in require_fields): if not all(field in self.config for field in require_fields):
return False return False
if self.config.get("app",{}).get("mode") == "workflow": if self.config.get("app", {}).get("mode") == "workflow":
self.errors.append(ExceptionDefineition( self.errors.append(ExceptionDefineition(
type=ExceptionType.PLATFORM, type=ExceptionType.PLATFORM,
detail="workflow mode is not supported" detail="workflow mode is not supported"
@@ -163,13 +163,14 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
def _convert_node(self, node: dict[str, Any]) -> NodeDefinition | None: def _convert_node(self, node: dict[str, Any]) -> NodeDefinition | None:
node_data = node["data"] node_data = node["data"]
try: try:
node_type = self.map_node_type(node_data["type"])
return NodeDefinition( return NodeDefinition(
id=node["id"], id=node["id"],
type=self.map_node_type(node_data["type"]), type=node_type,
name=node_data.get("title") or "notes", name=node_data.get("title") or "notes",
cycle=node.get("parentId"), cycle=node.get("parentId"),
description=None, description=None,
config=self._convert_node_config(node), config=self._convert_node_config(node_type, node),
position={ position={
"x": node["position"]["x"], "x": node["position"]["x"],
"y": node["position"]["y"] "y": node["position"]["y"]
@@ -183,17 +184,16 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
except Exception as e: except Exception as e:
logger.debug(f"convert node error - {e}", exc_info=True) logger.debug(f"convert node error - {e}", exc_info=True)
def _convert_node_config(self, node: dict): def _convert_node_config(self, node_type: NodeType, node: dict):
node_data = node["data"]
node_type = node_data["type"]
try: try:
node_data = node["data"]
converter = self.get_node_convert(node_type) converter = self.get_node_convert(node_type)
if node_type not in self.CONFIG_CONVERT_MAP: if node_type == NodeType.UNKNOWN:
self.errors.append(ExceptionDefineition( self.errors.append(ExceptionDefineition(
type=ExceptionType.NODE, type=ExceptionType.NODE,
node_id=node["id"], node_id=node["id"],
node_name=node["data"]["title"], node_name=node["data"]["title"],
detail=f"node type {node_type if node_type else 'notes'} is unsupported", detail=f"node type {node_data.get('type')} is unsupported",
)) ))
return converter(node) return converter(node)
except Exception as e: except Exception as e:
@@ -214,7 +214,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
if source in self.branch_node_cache: if source in self.branch_node_cache:
case_id = edge["sourceHandle"] case_id = edge["sourceHandle"]
if case_id == "false": if case_id == "false":
label = f'CASE{len(self.branch_node_cache[source])+1}' label = f'CASE{len(self.branch_node_cache[source]) + 1}'
else: else:
label = f'CASE{self.branch_node_cache[source].index(case_id) + 1}' label = f'CASE{self.branch_node_cache[source].index(case_id) + 1}'
if source in self.error_branch_node_cache: if source in self.error_branch_node_cache:
@@ -257,5 +257,3 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
def _convert_execution(self, execution: dict[str, Any]) -> ExecutionConfig: def _convert_execution(self, execution: dict[str, Any]) -> ExecutionConfig:
return ExecutionConfig() return ExecutionConfig()

View File

@@ -23,6 +23,7 @@ from app.core.workflow.nodes.question_classifier.config import QuestionClassifie
from app.core.workflow.nodes.start.config import StartNodeConfig from app.core.workflow.nodes.start.config import StartNodeConfig
from app.core.workflow.nodes.tool.config import ToolNodeConfig from app.core.workflow.nodes.tool.config import ToolNodeConfig
from app.core.workflow.nodes.variable_aggregator.config import VariableAggregatorNodeConfig from app.core.workflow.nodes.variable_aggregator.config import VariableAggregatorNodeConfig
from app.core.workflow.nodes.notes.config import NoteNodeConfig
__all__ = [ __all__ = [
# 基础类 # 基础类
@@ -47,5 +48,6 @@ __all__ = [
"ToolNodeConfig", "ToolNodeConfig",
"MemoryReadNodeConfig", "MemoryReadNodeConfig",
"MemoryWriteNodeConfig", "MemoryWriteNodeConfig",
"CodeNodeConfig" "CodeNodeConfig",
"NoteNodeConfig"
] ]

View File

@@ -0,0 +1,12 @@
from pydantic import Field
from app.core.workflow.nodes.base_config import BaseNodeConfig
class NoteNodeConfig(BaseNodeConfig):
author: str = Field(default="", description="author")
text: str = Field(default="", description="note content")
width: int = Field(default=80)
height: int = Field(default=80)
theme: str = Field(default="blue")
show_author: bool = Field(default=True)