feat(workflow): support Dify features conversion and file variable migration
This commit is contained in:
@@ -40,6 +40,7 @@ class WorkflowParserResult(BaseModel):
|
|||||||
edges: list[EdgeDefinition] = Field(default_factory=list)
|
edges: list[EdgeDefinition] = Field(default_factory=list)
|
||||||
nodes: list[NodeDefinition] = Field(default_factory=list)
|
nodes: list[NodeDefinition] = Field(default_factory=list)
|
||||||
variables: list[VariableDefinition] = Field(default_factory=list)
|
variables: list[VariableDefinition] = Field(default_factory=list)
|
||||||
|
features: dict[str, Any] = Field(default_factory=dict)
|
||||||
warnings: list[ExceptionDefinition] = Field(default_factory=list)
|
warnings: list[ExceptionDefinition] = Field(default_factory=list)
|
||||||
errors: list[ExceptionDefinition] = Field(default_factory=list)
|
errors: list[ExceptionDefinition] = Field(default_factory=list)
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ class WorkflowImportResult(BaseModel):
|
|||||||
edges: list[EdgeDefinition] = Field(default_factory=list)
|
edges: list[EdgeDefinition] = Field(default_factory=list)
|
||||||
nodes: list[NodeDefinition] = Field(default_factory=list)
|
nodes: list[NodeDefinition] = Field(default_factory=list)
|
||||||
variables: list[VariableDefinition] = Field(default_factory=list)
|
variables: list[VariableDefinition] = Field(default_factory=list)
|
||||||
|
features: dict[str, Any] = Field(default_factory=dict)
|
||||||
warnings: list[ExceptionDefinition] = Field(default_factory=list)
|
warnings: list[ExceptionDefinition] = Field(default_factory=list)
|
||||||
errors: list[ExceptionDefinition] = Field(default_factory=list)
|
errors: list[ExceptionDefinition] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.core.workflow.adapters.errors import (
|
|||||||
ExceptionType
|
ExceptionType
|
||||||
)
|
)
|
||||||
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 as NodeVariableDefinition, BaseNodeConfig
|
||||||
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 (
|
from app.core.workflow.nodes.configs import (
|
||||||
StartNodeConfig,
|
StartNodeConfig,
|
||||||
@@ -36,6 +36,7 @@ from app.core.workflow.nodes.configs import (
|
|||||||
ListOperatorNodeConfig,
|
ListOperatorNodeConfig,
|
||||||
DocExtractorNodeConfig,
|
DocExtractorNodeConfig,
|
||||||
)
|
)
|
||||||
|
from app.schemas.workflow_schema import VariableDefinition as SchemaVariableDefinition
|
||||||
from app.core.workflow.nodes.cycle_graph.config import (
|
from app.core.workflow.nodes.cycle_graph.config import (
|
||||||
ConditionDetail as LoopConditionDetail,
|
ConditionDetail as LoopConditionDetail,
|
||||||
ConditionsConfig,
|
ConditionsConfig,
|
||||||
@@ -98,6 +99,7 @@ class DifyConverter(BaseConverter):
|
|||||||
NodeType.CYCLE_START: lambda x: {},
|
NodeType.CYCLE_START: lambda x: {},
|
||||||
NodeType.BREAK: lambda x: {},
|
NodeType.BREAK: lambda x: {},
|
||||||
}
|
}
|
||||||
|
self._file_vars_to_conv: list[SchemaVariableDefinition] = []
|
||||||
|
|
||||||
def get_node_convert(self, node_type):
|
def get_node_convert(self, node_type):
|
||||||
func = self.CONFIG_CONVERT_MAP.get(node_type, lambda x: {})
|
func = self.CONFIG_CONVERT_MAP.get(node_type, lambda x: {})
|
||||||
@@ -286,19 +288,25 @@ class DifyConverter(BaseConverter):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if var_type in ["file", "array[file]"]:
|
if var_type in [VariableType.FILE, VariableType.ARRAY_FILE]:
|
||||||
self.errors.append(
|
# 开始节点不支持文件变量,转为会话变量
|
||||||
ExceptionDefinition(
|
self._file_vars_to_conv.append(SchemaVariableDefinition(
|
||||||
type=ExceptionType.VARIABLE,
|
name=var["variable"],
|
||||||
node_id=node["id"],
|
type=var_type.value,
|
||||||
node_name=node_data["title"],
|
required=var.get("required", False),
|
||||||
name=var["variable"],
|
default=None,
|
||||||
detail=f"Unsupported Variable type for start node: {var_type}"
|
description=var.get("label", ""),
|
||||||
)
|
))
|
||||||
)
|
self.warnings.append(ExceptionDefinition(
|
||||||
|
type=ExceptionType.VARIABLE,
|
||||||
|
node_id=node["id"],
|
||||||
|
node_name=node_data["title"],
|
||||||
|
name=var["variable"],
|
||||||
|
detail=f"File variable '{var['variable']}' is not supported in start node, moved to conversation variables"
|
||||||
|
))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var_def = VariableDefinition(
|
var_def = NodeVariableDefinition(
|
||||||
name=var["variable"],
|
name=var["variable"],
|
||||||
type=var_type,
|
type=var_type,
|
||||||
required=var["required"],
|
required=var["required"],
|
||||||
@@ -837,3 +845,76 @@ class DifyConverter(BaseConverter):
|
|||||||
).model_dump()
|
).model_dump()
|
||||||
self.config_validate(node["id"], node["data"]["title"], DocExtractorNodeConfig, result)
|
self.config_validate(node["id"], node["data"]["title"], DocExtractorNodeConfig, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_features(features: dict) -> dict:
|
||||||
|
"""Convert Dify features to MemoryBear FeaturesConfigForm format."""
|
||||||
|
if not features:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result: dict = {}
|
||||||
|
|
||||||
|
# opening_statement
|
||||||
|
opening = features.get("opening_statement", "")
|
||||||
|
suggested = features.get("suggested_questions", [])
|
||||||
|
result["opening_statement"] = {
|
||||||
|
"enabled": bool(opening),
|
||||||
|
"statement": opening or None,
|
||||||
|
"suggested_questions": suggested,
|
||||||
|
}
|
||||||
|
|
||||||
|
# citation (对应 Dify retriever_resource)
|
||||||
|
retriever = features.get("retriever_resource", {})
|
||||||
|
result["citation"] = {
|
||||||
|
"enabled": retriever.get("enabled", False) if isinstance(retriever, dict) else False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# file_upload: Dify allowed_file_types 数组 -> 前端扁平字段
|
||||||
|
file_upload = features.get("file_upload", {})
|
||||||
|
allowed_types = file_upload.get("allowed_file_types", []) if file_upload else []
|
||||||
|
allowed_methods = file_upload.get("allowed_file_upload_methods", ["local_file", "remote_url"])
|
||||||
|
if isinstance(allowed_methods, list):
|
||||||
|
if len(allowed_methods) >= 2:
|
||||||
|
transfer_method = "both"
|
||||||
|
elif allowed_methods:
|
||||||
|
transfer_method = allowed_methods[0]
|
||||||
|
else:
|
||||||
|
transfer_method = "both"
|
||||||
|
else:
|
||||||
|
transfer_method = allowed_methods or "both"
|
||||||
|
|
||||||
|
file_config = file_upload.get("fileUploadConfig", {})
|
||||||
|
result["file_upload"] = {
|
||||||
|
"enabled": file_upload.get("enabled", False) if file_upload else False,
|
||||||
|
"image_enabled": "image" in allowed_types,
|
||||||
|
"image_max_size_mb": file_config.get("image_file_size_limit", 10) if file_config else 10,
|
||||||
|
"image_allowed_extensions": ["png", "jpg", "jpeg"],
|
||||||
|
"audio_enabled": "audio" in allowed_types,
|
||||||
|
"audio_max_size_mb": file_config.get("audio_file_size_limit", 50) if file_config else 50,
|
||||||
|
"audio_allowed_extensions": ["mp3", "wav", "m4a"],
|
||||||
|
"document_enabled": "document" in allowed_types,
|
||||||
|
"document_max_size_mb": file_config.get("file_size_limit", 100) if file_config else 100,
|
||||||
|
"document_allowed_extensions": ["pdf", "docx", "doc", "xlsx", "xls", "txt", "csv", "json", "md"],
|
||||||
|
"video_enabled": "video" in allowed_types,
|
||||||
|
"video_max_size_mb": file_config.get("video_file_size_limit", 100) if file_config else 100,
|
||||||
|
"video_allowed_extensions": ["mp4", "mov"],
|
||||||
|
"max_file_count": file_upload.get("number_limits", 1) if file_upload else 1,
|
||||||
|
"allowed_transfer_methods": transfer_method,
|
||||||
|
}
|
||||||
|
|
||||||
|
# text_to_speech
|
||||||
|
tts = features.get("text_to_speech", {})
|
||||||
|
result["text_to_speech"] = {
|
||||||
|
"enabled": tts.get("enabled", False) if isinstance(tts, dict) else False,
|
||||||
|
"voice": tts.get("voice") if isinstance(tts, dict) else None,
|
||||||
|
"language": tts.get("language") if isinstance(tts, dict) else None,
|
||||||
|
"autoplay": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# suggested_questions_after_answer
|
||||||
|
sqa = features.get("suggested_questions_after_answer", {})
|
||||||
|
result["suggested_questions_after_answer"] = {
|
||||||
|
"enabled": sqa.get("enabled", False) if isinstance(sqa, dict) else False,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
@@ -119,9 +119,12 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
|
|||||||
if variable:
|
if variable:
|
||||||
self.conv_variables.append(con_var)
|
self.conv_variables.append(con_var)
|
||||||
|
|
||||||
# for variables in config.get("workflow").get("environment_variables"):
|
# 开始节点的文件变量合并到会话变量
|
||||||
# variable = self._convert_variable(variables)
|
self.conv_variables.extend(self._file_vars_to_conv)
|
||||||
# conv_variables.append(variable)
|
|
||||||
|
features = self.convert_features(
|
||||||
|
self.config.get("workflow", {}).get("features", {})
|
||||||
|
)
|
||||||
|
|
||||||
trigger = self._convert_trigger({})
|
trigger = self._convert_trigger({})
|
||||||
execution_config = self._convert_execution({})
|
execution_config = self._convert_execution({})
|
||||||
@@ -135,6 +138,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
|
|||||||
edges=self.edges,
|
edges=self.edges,
|
||||||
nodes=self.nodes,
|
nodes=self.nodes,
|
||||||
variables=self.conv_variables,
|
variables=self.conv_variables,
|
||||||
|
features=features,
|
||||||
warnings=self.warnings,
|
warnings=self.warnings,
|
||||||
errors=self.errors
|
errors=self.errors
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ class AppService:
|
|||||||
edges=[edge.model_dump() for edge in data.edges] if data.edges else [],
|
edges=[edge.model_dump() for edge in data.edges] if data.edges else [],
|
||||||
variables=[var.model_dump() for var in data.variables] if data.variables else [],
|
variables=[var.model_dump() for var in data.variables] if data.variables else [],
|
||||||
execution_config=data.execution_config.model_dump() if data.execution_config else {},
|
execution_config=data.execution_config.model_dump() if data.execution_config else {},
|
||||||
|
features=data.features if data.features else {},
|
||||||
triggers=[trigger.model_dump() for trigger in data.triggers] if data.triggers else [],
|
triggers=[trigger.model_dump() for trigger in data.triggers] if data.triggers else [],
|
||||||
is_active=True,
|
is_active=True,
|
||||||
created_at=now,
|
created_at=now,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class WorkflowImportService:
|
|||||||
edges=workflow_config.edges,
|
edges=workflow_config.edges,
|
||||||
nodes=workflow_config.nodes,
|
nodes=workflow_config.nodes,
|
||||||
variables=workflow_config.variables,
|
variables=workflow_config.variables,
|
||||||
|
features=workflow_config.features,
|
||||||
warnings=workflow_config.warnings,
|
warnings=workflow_config.warnings,
|
||||||
errors=workflow_config.errors
|
errors=workflow_config.errors
|
||||||
)
|
)
|
||||||
@@ -95,7 +96,8 @@ class WorkflowImportService:
|
|||||||
workflow_config=WorkflowConfigCreate(
|
workflow_config=WorkflowConfigCreate(
|
||||||
nodes=config["nodes"],
|
nodes=config["nodes"],
|
||||||
edges=config["edges"],
|
edges=config["edges"],
|
||||||
variables=config["variables"]
|
variables=config["variables"],
|
||||||
|
features=config.get("features", {})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user