vectorvein 0.2.64__tar.gz → 0.2.66__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {vectorvein-0.2.64 → vectorvein-0.2.66}/PKG-INFO +1 -1
  2. {vectorvein-0.2.64 → vectorvein-0.2.66}/pyproject.toml +1 -1
  3. vectorvein-0.2.66/src/vectorvein/workflow/graph/workflow.py +339 -0
  4. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/utils/json_to_code.py +19 -1
  5. vectorvein-0.2.64/src/vectorvein/workflow/graph/workflow.py +0 -170
  6. {vectorvein-0.2.64 → vectorvein-0.2.66}/README.md +0 -0
  7. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/__init__.py +0 -0
  8. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/api/__init__.py +0 -0
  9. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/api/client.py +0 -0
  10. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/api/exceptions.py +0 -0
  11. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/api/models.py +0 -0
  12. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/__init__.py +0 -0
  13. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/anthropic_client.py +0 -0
  14. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/baichuan_client.py +0 -0
  15. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/base_client.py +0 -0
  16. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/deepseek_client.py +0 -0
  17. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/ernie_client.py +0 -0
  18. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/gemini_client.py +0 -0
  19. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/groq_client.py +0 -0
  20. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/local_client.py +0 -0
  21. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/minimax_client.py +0 -0
  22. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/mistral_client.py +0 -0
  23. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/moonshot_client.py +0 -0
  24. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/openai_client.py +0 -0
  25. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/openai_compatible_client.py +0 -0
  26. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/py.typed +0 -0
  27. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/qwen_client.py +0 -0
  28. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/stepfun_client.py +0 -0
  29. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/utils.py +0 -0
  30. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/xai_client.py +0 -0
  31. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/yi_client.py +0 -0
  32. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/chat_clients/zhipuai_client.py +0 -0
  33. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/py.typed +0 -0
  34. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/server/token_server.py +0 -0
  35. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/settings/__init__.py +0 -0
  36. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/settings/py.typed +0 -0
  37. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/types/__init__.py +0 -0
  38. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/types/defaults.py +0 -0
  39. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/types/enums.py +0 -0
  40. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/types/exception.py +0 -0
  41. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/types/llm_parameters.py +0 -0
  42. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/types/py.typed +0 -0
  43. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/types/settings.py +0 -0
  44. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/utilities/media_processing.py +0 -0
  45. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/utilities/rate_limiter.py +0 -0
  46. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/utilities/retry.py +0 -0
  47. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/graph/edge.py +0 -0
  48. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/graph/node.py +0 -0
  49. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/graph/port.py +0 -0
  50. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/__init__.py +0 -0
  51. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/audio_generation.py +0 -0
  52. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/control_flows.py +0 -0
  53. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/file_processing.py +0 -0
  54. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/image_generation.py +0 -0
  55. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/llms.py +0 -0
  56. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/media_editing.py +0 -0
  57. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/media_processing.py +0 -0
  58. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/output.py +0 -0
  59. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/relational_db.py +0 -0
  60. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/text_processing.py +0 -0
  61. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/tools.py +0 -0
  62. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/triggers.py +0 -0
  63. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/vector_db.py +0 -0
  64. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/video_generation.py +0 -0
  65. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/nodes/web_crawlers.py +0 -0
  66. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/utils/check.py +0 -0
  67. {vectorvein-0.2.64 → vectorvein-0.2.66}/src/vectorvein/workflow/utils/layout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vectorvein
3
- Version: 0.2.64
3
+ Version: 0.2.66
4
4
  Summary: VectorVein Python SDK
5
5
  Author-Email: Anderson <andersonby@163.com>
6
6
  License: MIT
@@ -17,7 +17,7 @@ description = "VectorVein Python SDK"
17
17
  name = "vectorvein"
18
18
  readme = "README.md"
19
19
  requires-python = ">=3.10"
20
- version = "0.2.64"
20
+ version = "0.2.66"
21
21
 
22
22
  [project.license]
23
23
  text = "MIT"
@@ -0,0 +1,339 @@
1
+ import json
2
+ from typing import List, Union, Dict, Any, Optional
3
+
4
+ from .node import Node
5
+ from .edge import Edge
6
+ from .port import InputPort, OutputPort
7
+ from ..utils.layout import layout
8
+ from ..utils.check import (
9
+ WorkflowCheckResult,
10
+ check_dag,
11
+ check_ui,
12
+ check_useless_nodes,
13
+ check_required_ports,
14
+ check_override_ports,
15
+ )
16
+
17
+
18
+ class Workflow:
19
+ def __init__(self) -> None:
20
+ self.nodes: List[Node] = []
21
+ self.edges: List[Edge] = []
22
+
23
+ def add_node(self, node: Node):
24
+ self.nodes.append(node)
25
+
26
+ def add_nodes(self, nodes: List[Node]):
27
+ self.nodes.extend(nodes)
28
+
29
+ def add_edge(self, edge: Edge):
30
+ self.edges.append(edge)
31
+
32
+ def connect(
33
+ self,
34
+ source_node: Union[str, Node],
35
+ source_port: str,
36
+ target_node: Union[str, Node],
37
+ target_port: str,
38
+ ):
39
+ # 获取源节点ID
40
+ if isinstance(source_node, Node):
41
+ source_node_id = source_node.id
42
+ else:
43
+ source_node_id = source_node
44
+
45
+ # 获取目标节点ID
46
+ if isinstance(target_node, Node):
47
+ target_node_id = target_node.id
48
+ else:
49
+ target_node_id = target_node
50
+
51
+ # 检查源节点是否存在
52
+ source_node_exists = any(node.id == source_node_id for node in self.nodes)
53
+ if not source_node_exists:
54
+ raise ValueError(f"Source node not found: {source_node_id}")
55
+
56
+ # 检查目标节点是否存在
57
+ target_node_exists = any(node.id == target_node_id for node in self.nodes)
58
+ if not target_node_exists:
59
+ raise ValueError(f"Target node not found: {target_node_id}")
60
+
61
+ # 检查源节点的端口是否存在
62
+ source_node_obj = next(node for node in self.nodes if node.id == source_node_id)
63
+ if not source_node_obj.has_output_port(source_port):
64
+ raise ValueError(f"Source node {source_node_id} has no output port: {source_port}")
65
+
66
+ # 检查目标节点的端口是否存在
67
+ target_node_obj = next(node for node in self.nodes if node.id == target_node_id)
68
+ if not target_node_obj.has_input_port(target_port):
69
+ raise ValueError(f"Target node {target_node_id} has no input port: {target_port}")
70
+
71
+ # 检查目标端口是否已有被连接的线
72
+ for edge in self.edges:
73
+ if edge.target == target_node_id and edge.target_handle == target_port:
74
+ raise ValueError(
75
+ f"The input port {target_port} of the target node {target_node_id} is already connected: {edge.source}({edge.source_handle}) → {edge.target}({edge.target_handle})"
76
+ )
77
+
78
+ # 创建并添加边
79
+ edge_id = f"vueflow__edge-{source_node_id}{source_port}-{target_node_id}{target_port}"
80
+ edge = Edge(edge_id, source_node_id, source_port, target_node_id, target_port)
81
+ self.add_edge(edge)
82
+
83
+ def to_dict(self):
84
+ return {
85
+ "nodes": [node.to_dict() for node in self.nodes],
86
+ "edges": [edge.to_dict() for edge in self.edges],
87
+ "viewport": {"x": 0, "y": 0, "zoom": 1},
88
+ }
89
+
90
+ def to_json(self, ensure_ascii=False):
91
+ return json.dumps(self.to_dict(), ensure_ascii=ensure_ascii)
92
+
93
+ def to_mermaid(self) -> str:
94
+ """生成 Mermaid 格式的流程图。
95
+
96
+ Returns:
97
+ str: Mermaid 格式的流程图文本
98
+ """
99
+ lines = ["flowchart TD"]
100
+
101
+ # 创建节点类型到序号的映射
102
+ type_counters = {}
103
+ node_id_to_label = {}
104
+
105
+ # 首先为所有节点生成标签
106
+ for node in self.nodes:
107
+ node_type = node.type.lower()
108
+ if node_type not in type_counters:
109
+ type_counters[node_type] = 0
110
+ node_label = f"{node_type}_{type_counters[node_type]}"
111
+ node_id_to_label[node.id] = node_label
112
+ type_counters[node_type] += 1
113
+
114
+ # 添加节点定义
115
+ for node in self.nodes:
116
+ node_label = node_id_to_label[node.id]
117
+ lines.append(f' {node_label}["{node_label} ({node.type})"]')
118
+
119
+ lines.append("") # 添加一个空行分隔节点和边的定义
120
+
121
+ # 添加边的定义
122
+ for edge in self.edges:
123
+ source_label = node_id_to_label[edge.source]
124
+ target_label = node_id_to_label[edge.target]
125
+ label = f"{edge.source_handle} → {edge.target_handle}"
126
+ lines.append(f" {source_label} -->|{label}| {target_label}")
127
+
128
+ return "\n".join(lines)
129
+
130
+ def check(self) -> WorkflowCheckResult:
131
+ """检查流程图的有效性。
132
+
133
+ Returns:
134
+ WorkflowCheckResult: 包含各种检查结果的字典
135
+ """
136
+ dag_check = check_dag(self) # 检查流程图是否为有向无环图,并检测是否存在孤立节点。
137
+ ui_check = check_ui(self)
138
+ useless_nodes = check_useless_nodes(self)
139
+ required_ports = check_required_ports(self)
140
+ override_ports = check_override_ports(self)
141
+
142
+ # 合并结果
143
+ result: WorkflowCheckResult = {
144
+ "no_cycle": dag_check["no_cycle"],
145
+ "no_isolated_nodes": dag_check["no_isolated_nodes"],
146
+ "ui_warnings": ui_check,
147
+ "useless_nodes": useless_nodes,
148
+ "required_ports": required_ports,
149
+ "override_ports": override_ports,
150
+ }
151
+
152
+ return result
153
+
154
+ def layout(self, options: Optional[Dict[str, Any]] = None) -> "Workflow":
155
+ """对工作流中的节点进行自动布局,计算并更新每个节点的位置。
156
+
157
+ 此方法实现了一个简单的分层布局算法,将节点按照有向图的拓扑结构进行排列。
158
+
159
+ Args:
160
+ options: 布局选项,包括:
161
+ - direction: 布局方向 ('TB', 'BT', 'LR', 'RL'),默认 'LR'
162
+ - node_spacing: 同一层级节点间的间距,默认 500
163
+ - layer_spacing: 不同层级间的间距,默认 400
164
+ - margin_x: 图形左右边距,默认 20
165
+ - margin_y: 图形上下边距,默认 20
166
+
167
+ Returns:
168
+ 布局后的工作流对象
169
+ """
170
+ layout(self.nodes, self.edges, options)
171
+ return self
172
+
173
+ @classmethod
174
+ def from_json(cls, json_str: str) -> "Workflow":
175
+ """从 JSON 字符串创建工作流对象。
176
+
177
+ Args:
178
+ json_str: JSON 字符串
179
+
180
+ Returns:
181
+ Workflow: 工作流对象
182
+ """
183
+ workflow = cls()
184
+ data = json.loads(json_str)
185
+
186
+ # 创建节点
187
+ for node_data in data.get("nodes", []):
188
+ node_type = node_data["type"]
189
+ category = node_data["category"]
190
+ task_name = node_data["data"]["task_name"]
191
+
192
+ # 尝试动态导入节点类
193
+ NodeClass = None
194
+ try:
195
+ # 如果task_name包含分类信息
196
+ if "." in task_name:
197
+ category, _ = task_name.split(".")
198
+ module_path = f"vectorvein.workflow.nodes.{category}"
199
+ module = __import__(module_path, fromlist=[node_type])
200
+ if hasattr(module, node_type):
201
+ NodeClass = getattr(module, node_type)
202
+ except (ImportError, AttributeError):
203
+ pass
204
+
205
+ if not NodeClass:
206
+ raise ValueError(f"Node class not found: {node_type}")
207
+ # 创建节点实例以获取默认值
208
+ node_instance = NodeClass()
209
+
210
+ # 使用节点实例的基本属性
211
+ node = Node(
212
+ node_type=node_type,
213
+ category=category,
214
+ task_name=task_name,
215
+ description=node_data["data"].get(
216
+ "description", node_instance.description if hasattr(node_instance, "description") else ""
217
+ ),
218
+ node_id=node_data["id"],
219
+ position=node_data.get("position", {"x": 0, "y": 0}),
220
+ seleted_workflow_title=node_data["data"].get("seleted_workflow_title", ""),
221
+ is_template=node_data["data"].get("is_template", False),
222
+ initialized=node_data.get("initialized", False),
223
+ can_add_input_ports=node_data["data"].get("has_inputs", False),
224
+ can_add_output_ports=node_data["data"].get("has_outputs", False),
225
+ )
226
+
227
+ # 处理端口
228
+ for port_name, port_data in node_data["data"].get("template", {}).items():
229
+ # 如果端口已存在于节点实例中,直接修改其属性
230
+ if port_name in node_instance.ports:
231
+ # 直接修改原始端口的属性,而不是创建新端口
232
+ port = node_instance.ports[port_name]
233
+
234
+ # 更新端口的属性
235
+ if "field_type" in port_data:
236
+ port.port_type = port_data["field_type"]
237
+ if "required" in port_data:
238
+ port.required = port_data["required"]
239
+ if "show" in port_data:
240
+ port.show = port_data["show"]
241
+ if "value" in port_data:
242
+ port.value = port_data["value"]
243
+ if "options" in port_data:
244
+ port.options = port_data["options"]
245
+ if "type" in port_data:
246
+ port.field_type = port_data["type"]
247
+ if "max_length" in port_data:
248
+ port.max_length = port_data["max_length"]
249
+ if "support_file_types" in port_data and port_data["support_file_types"]:
250
+ port.support_file_types = port_data["support_file_types"].split(", ")
251
+ if "multiple" in port_data:
252
+ port.multiple = port_data["multiple"]
253
+ if "group" in port_data:
254
+ port.group = port_data["group"]
255
+ if "group_collpased" in port_data:
256
+ port.group_collpased = port_data["group_collpased"]
257
+ if "has_tooltip" in port_data:
258
+ port.has_tooltip = port_data["has_tooltip"]
259
+ if "max" in port_data:
260
+ port.max = port_data["max"]
261
+ if "min" in port_data:
262
+ port.min = port_data["min"]
263
+ if "list" in port_data:
264
+ port.list = port_data["list"]
265
+ else:
266
+ # 对于新添加的端口,检查是否允许添加
267
+ port_type = port_data.get("field_type", "text")
268
+ is_output = port_data.get("is_output", False)
269
+
270
+ # 检查节点是否允许添加该类型的端口
271
+ if (is_output and not node.can_add_output_ports) or (
272
+ not is_output and not node.can_add_input_ports
273
+ ):
274
+ # 如果不允许添加,跳过该端口
275
+ continue
276
+
277
+ # 创建并添加新端口
278
+ if is_output:
279
+ port = OutputPort(
280
+ name=port_name,
281
+ port_type=port_type,
282
+ required=port_data.get("required", False),
283
+ show=port_data.get("show", False),
284
+ value=port_data.get("value"),
285
+ options=port_data.get("options"),
286
+ field_type=port_data.get("type"),
287
+ max_length=port_data.get("max_length"),
288
+ support_file_types=port_data.get("support_file_types", "").split(", ")
289
+ if port_data.get("support_file_types")
290
+ else None,
291
+ multiple=port_data.get("multiple"),
292
+ group=port_data.get("group"),
293
+ group_collpased=port_data.get("group_collpased", False),
294
+ has_tooltip=port_data.get("has_tooltip", False),
295
+ max=port_data.get("max"),
296
+ min=port_data.get("min"),
297
+ list=port_data.get("list", False),
298
+ )
299
+ else:
300
+ port = InputPort(
301
+ name=port_name,
302
+ port_type=port_type,
303
+ required=port_data.get("required", True),
304
+ show=port_data.get("show", False),
305
+ value=port_data.get("value"),
306
+ options=port_data.get("options"),
307
+ field_type=port_data.get("type"),
308
+ max_length=port_data.get("max_length"),
309
+ support_file_types=port_data.get("support_file_types", "").split(", ")
310
+ if port_data.get("support_file_types")
311
+ else None,
312
+ multiple=port_data.get("multiple"),
313
+ group=port_data.get("group"),
314
+ group_collpased=port_data.get("group_collpased", False),
315
+ has_tooltip=port_data.get("has_tooltip", False),
316
+ max=port_data.get("max"),
317
+ min=port_data.get("min"),
318
+ list=port_data.get("list", False),
319
+ )
320
+
321
+ # 添加新端口到节点
322
+ node.ports[port_name] = port
323
+
324
+ workflow.add_node(node)
325
+
326
+ # 创建边
327
+ for edge_data in data.get("edges", []):
328
+ edge = Edge(
329
+ id=edge_data["id"],
330
+ source=edge_data["source"],
331
+ source_handle=edge_data["sourceHandle"],
332
+ target=edge_data["target"],
333
+ target_handle=edge_data["targetHandle"],
334
+ animated=edge_data.get("animated", True),
335
+ type=edge_data.get("type", "default"),
336
+ )
337
+ workflow.add_edge(edge)
338
+
339
+ return workflow
@@ -86,6 +86,16 @@ def generate_python_code(
86
86
  node_imports = set()
87
87
  node_instances = {}
88
88
 
89
+ # 收集所有连接的端口
90
+ connected_ports = set()
91
+ for edge in workflow_data["edges"]:
92
+ source_id = edge["source"]
93
+ target_id = edge["target"]
94
+ source_handle = edge["sourceHandle"]
95
+ target_handle = edge["targetHandle"]
96
+ connected_ports.add((source_id, source_handle))
97
+ connected_ports.add((target_id, target_handle))
98
+
89
99
  # 解析节点并生成导入语句
90
100
  for node in workflow_data["nodes"]:
91
101
  node_type = node["type"]
@@ -125,7 +135,13 @@ def generate_python_code(
125
135
  port_value = port["value"]
126
136
  default_value = node_instance.ports[port_name].value if port_name in node_instance.ports else None
127
137
 
128
- if port_value and port_value != default_value:
138
+ # 判断端口是否有值且值与默认不同,并且端口满足以下条件之一:有连接、是输入端口、在UI上显示
139
+ port_is_connected = (node["id"], port["name"]) in connected_ports
140
+ if (
141
+ port_value
142
+ and port_value != default_value
143
+ and (port_is_connected or not port.get("is_output", False) or port.get("show", False))
144
+ ):
129
145
  values.append(port)
130
146
 
131
147
  node_instances[node["id"]] = {
@@ -156,6 +172,8 @@ def generate_python_code(
156
172
  code.append("")
157
173
  for node_info in node_instances.values():
158
174
  for port in node_info["add_ports"]:
175
+ if "name" not in port:
176
+ continue
159
177
  params = [
160
178
  f"name={to_python_str(port['name'])}",
161
179
  f"port_type={to_python_str(port['field_type'])}",
@@ -1,170 +0,0 @@
1
- import json
2
- from typing import List, Union, Dict, Any, Optional
3
-
4
- from .node import Node
5
- from .edge import Edge
6
- from ..utils.layout import layout
7
- from ..utils.check import (
8
- WorkflowCheckResult,
9
- check_dag,
10
- check_ui,
11
- check_useless_nodes,
12
- check_required_ports,
13
- check_override_ports,
14
- )
15
-
16
-
17
- class Workflow:
18
- def __init__(self) -> None:
19
- self.nodes: List[Node] = []
20
- self.edges: List[Edge] = []
21
-
22
- def add_node(self, node: Node):
23
- self.nodes.append(node)
24
-
25
- def add_nodes(self, nodes: List[Node]):
26
- self.nodes.extend(nodes)
27
-
28
- def add_edge(self, edge: Edge):
29
- self.edges.append(edge)
30
-
31
- def connect(
32
- self,
33
- source_node: Union[str, Node],
34
- source_port: str,
35
- target_node: Union[str, Node],
36
- target_port: str,
37
- ):
38
- # 获取源节点ID
39
- if isinstance(source_node, Node):
40
- source_node_id = source_node.id
41
- else:
42
- source_node_id = source_node
43
-
44
- # 获取目标节点ID
45
- if isinstance(target_node, Node):
46
- target_node_id = target_node.id
47
- else:
48
- target_node_id = target_node
49
-
50
- # 检查源节点是否存在
51
- source_node_exists = any(node.id == source_node_id for node in self.nodes)
52
- if not source_node_exists:
53
- raise ValueError(f"Source node not found: {source_node_id}")
54
-
55
- # 检查目标节点是否存在
56
- target_node_exists = any(node.id == target_node_id for node in self.nodes)
57
- if not target_node_exists:
58
- raise ValueError(f"Target node not found: {target_node_id}")
59
-
60
- # 检查源节点的端口是否存在
61
- source_node_obj = next(node for node in self.nodes if node.id == source_node_id)
62
- if not source_node_obj.has_output_port(source_port):
63
- raise ValueError(f"Source node {source_node_id} has no output port: {source_port}")
64
-
65
- # 检查目标节点的端口是否存在
66
- target_node_obj = next(node for node in self.nodes if node.id == target_node_id)
67
- if not target_node_obj.has_input_port(target_port):
68
- raise ValueError(f"Target node {target_node_id} has no input port: {target_port}")
69
-
70
- # 检查目标端口是否已有被连接的线
71
- for edge in self.edges:
72
- if edge.target == target_node_id and edge.target_handle == target_port:
73
- raise ValueError(
74
- f"The input port {target_port} of the target node {target_node_id} is already connected: {edge.source}({edge.source_handle}) → {edge.target}({edge.target_handle})"
75
- )
76
-
77
- # 创建并添加边
78
- edge_id = f"vueflow__edge-{source_node_id}{source_port}-{target_node_id}{target_port}"
79
- edge = Edge(edge_id, source_node_id, source_port, target_node_id, target_port)
80
- self.add_edge(edge)
81
-
82
- def to_dict(self):
83
- return {
84
- "nodes": [node.to_dict() for node in self.nodes],
85
- "edges": [edge.to_dict() for edge in self.edges],
86
- "viewport": {"x": 0, "y": 0, "zoom": 1},
87
- }
88
-
89
- def to_json(self, ensure_ascii=False):
90
- return json.dumps(self.to_dict(), ensure_ascii=ensure_ascii)
91
-
92
- def to_mermaid(self) -> str:
93
- """生成 Mermaid 格式的流程图。
94
-
95
- Returns:
96
- str: Mermaid 格式的流程图文本
97
- """
98
- lines = ["flowchart TD"]
99
-
100
- # 创建节点类型到序号的映射
101
- type_counters = {}
102
- node_id_to_label = {}
103
-
104
- # 首先为所有节点生成标签
105
- for node in self.nodes:
106
- node_type = node.type.lower()
107
- if node_type not in type_counters:
108
- type_counters[node_type] = 0
109
- node_label = f"{node_type}_{type_counters[node_type]}"
110
- node_id_to_label[node.id] = node_label
111
- type_counters[node_type] += 1
112
-
113
- # 添加节点定义
114
- for node in self.nodes:
115
- node_label = node_id_to_label[node.id]
116
- lines.append(f' {node_label}["{node_label} ({node.type})"]')
117
-
118
- lines.append("") # 添加一个空行分隔节点和边的定义
119
-
120
- # 添加边的定义
121
- for edge in self.edges:
122
- source_label = node_id_to_label[edge.source]
123
- target_label = node_id_to_label[edge.target]
124
- label = f"{edge.source_handle} → {edge.target_handle}"
125
- lines.append(f" {source_label} -->|{label}| {target_label}")
126
-
127
- return "\n".join(lines)
128
-
129
- def check(self) -> WorkflowCheckResult:
130
- """检查流程图的有效性。
131
-
132
- Returns:
133
- WorkflowCheckResult: 包含各种检查结果的字典
134
- """
135
- dag_check = check_dag(self) # 检查流程图是否为有向无环图,并检测是否存在孤立节点。
136
- ui_check = check_ui(self)
137
- useless_nodes = check_useless_nodes(self)
138
- required_ports = check_required_ports(self)
139
- override_ports = check_override_ports(self)
140
-
141
- # 合并结果
142
- result: WorkflowCheckResult = {
143
- "no_cycle": dag_check["no_cycle"],
144
- "no_isolated_nodes": dag_check["no_isolated_nodes"],
145
- "ui_warnings": ui_check,
146
- "useless_nodes": useless_nodes,
147
- "required_ports": required_ports,
148
- "override_ports": override_ports,
149
- }
150
-
151
- return result
152
-
153
- def layout(self, options: Optional[Dict[str, Any]] = None) -> "Workflow":
154
- """对工作流中的节点进行自动布局,计算并更新每个节点的位置。
155
-
156
- 此方法实现了一个简单的分层布局算法,将节点按照有向图的拓扑结构进行排列。
157
-
158
- Args:
159
- options: 布局选项,包括:
160
- - direction: 布局方向 ('TB', 'BT', 'LR', 'RL'),默认 'LR'
161
- - node_spacing: 同一层级节点间的间距,默认 500
162
- - layer_spacing: 不同层级间的间距,默认 400
163
- - margin_x: 图形左右边距,默认 20
164
- - margin_y: 图形上下边距,默认 20
165
-
166
- Returns:
167
- 布局后的工作流对象
168
- """
169
- layout(self.nodes, self.edges, options)
170
- return self
File without changes