vectorvein 0.2.49__tar.gz → 0.2.51__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.
- {vectorvein-0.2.49 → vectorvein-0.2.51}/PKG-INFO +1 -1
- {vectorvein-0.2.49 → vectorvein-0.2.51}/pyproject.toml +1 -1
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/graph/node.py +2 -6
- vectorvein-0.2.51/src/vectorvein/workflow/graph/workflow.py +154 -0
- vectorvein-0.2.51/src/vectorvein/workflow/utils/check.py +159 -0
- vectorvein-0.2.51/src/vectorvein/workflow/utils/layout.py +114 -0
- vectorvein-0.2.49/src/vectorvein/workflow/graph/workflow.py +0 -285
- {vectorvein-0.2.49 → vectorvein-0.2.51}/README.md +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/__init__.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/api/__init__.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/api/client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/api/exceptions.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/api/models.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/__init__.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/anthropic_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/baichuan_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/base_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/deepseek_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/ernie_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/gemini_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/groq_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/local_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/minimax_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/mistral_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/moonshot_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/openai_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/openai_compatible_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/py.typed +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/qwen_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/stepfun_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/utils.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/xai_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/yi_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/zhipuai_client.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/py.typed +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/server/token_server.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/settings/__init__.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/settings/py.typed +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/types/__init__.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/types/defaults.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/types/enums.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/types/exception.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/types/llm_parameters.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/types/py.typed +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/types/settings.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/utilities/media_processing.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/utilities/rate_limiter.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/utilities/retry.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/graph/edge.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/graph/port.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/__init__.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/audio_generation.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/control_flows.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/file_processing.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/image_generation.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/llms.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/media_editing.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/media_processing.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/output.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/relational_db.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/text_processing.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/tools.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/triggers.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/vector_db.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/video_generation.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/nodes/web_crawlers.py +0 -0
- {vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/workflow/utils/json_to_code.py +0 -0
@@ -10,15 +10,13 @@ class PortsDict(Dict[str, Port]):
|
|
10
10
|
def __init__(self, owner_node: "Node", *args, **kwargs):
|
11
11
|
super().__init__(*args, **kwargs)
|
12
12
|
self._owner_node = owner_node
|
13
|
-
self._initializing = True
|
13
|
+
self._initializing = True
|
14
14
|
|
15
15
|
def __setitem__(self, key: str, value: Port) -> None:
|
16
|
-
# 初始化阶段或端口已存在时,允许直接添加/更新
|
17
16
|
if self._initializing or key in self:
|
18
17
|
super().__setitem__(key, value)
|
19
18
|
return
|
20
19
|
|
21
|
-
# 对于新端口,检查添加权限
|
22
20
|
if isinstance(value, OutputPort) and not self._owner_node.can_add_output_ports:
|
23
21
|
raise ValueError(
|
24
22
|
f"Node<{self._owner_node.id}> '{self._owner_node.type}' does not allow adding output ports"
|
@@ -58,13 +56,11 @@ class Node:
|
|
58
56
|
self.description: str = description
|
59
57
|
self.can_add_input_ports: bool = can_add_input_ports
|
60
58
|
self.can_add_output_ports: bool = can_add_output_ports
|
61
|
-
|
59
|
+
|
62
60
|
self.ports = PortsDict(self)
|
63
|
-
# 如果提供了初始端口,将它们添加到字典中
|
64
61
|
if ports:
|
65
62
|
for name, port in ports.items():
|
66
63
|
self.ports[name] = port
|
67
|
-
# 结束初始化阶段
|
68
64
|
self.ports.finish_initialization()
|
69
65
|
|
70
66
|
self.position: Dict[str, float] = position or {"x": 0, "y": 0}
|
@@ -0,0 +1,154 @@
|
|
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 WorkflowCheckResult, check_dag, check_ui
|
8
|
+
|
9
|
+
|
10
|
+
class Workflow:
|
11
|
+
def __init__(self) -> None:
|
12
|
+
self.nodes: List[Node] = []
|
13
|
+
self.edges: List[Edge] = []
|
14
|
+
|
15
|
+
def add_node(self, node: Node):
|
16
|
+
self.nodes.append(node)
|
17
|
+
|
18
|
+
def add_nodes(self, nodes: List[Node]):
|
19
|
+
self.nodes.extend(nodes)
|
20
|
+
|
21
|
+
def add_edge(self, edge: Edge):
|
22
|
+
self.edges.append(edge)
|
23
|
+
|
24
|
+
def connect(
|
25
|
+
self,
|
26
|
+
source_node: Union[str, Node],
|
27
|
+
source_port: str,
|
28
|
+
target_node: Union[str, Node],
|
29
|
+
target_port: str,
|
30
|
+
):
|
31
|
+
# 获取源节点ID
|
32
|
+
if isinstance(source_node, Node):
|
33
|
+
source_node_id = source_node.id
|
34
|
+
else:
|
35
|
+
source_node_id = source_node
|
36
|
+
|
37
|
+
# 获取目标节点ID
|
38
|
+
if isinstance(target_node, Node):
|
39
|
+
target_node_id = target_node.id
|
40
|
+
else:
|
41
|
+
target_node_id = target_node
|
42
|
+
|
43
|
+
# 检查源节点是否存在
|
44
|
+
source_node_exists = any(node.id == source_node_id for node in self.nodes)
|
45
|
+
if not source_node_exists:
|
46
|
+
raise ValueError(f"Source node not found: {source_node_id}")
|
47
|
+
|
48
|
+
# 检查目标节点是否存在
|
49
|
+
target_node_exists = any(node.id == target_node_id for node in self.nodes)
|
50
|
+
if not target_node_exists:
|
51
|
+
raise ValueError(f"Target node not found: {target_node_id}")
|
52
|
+
|
53
|
+
# 检查源节点的端口是否存在
|
54
|
+
source_node_obj = next(node for node in self.nodes if node.id == source_node_id)
|
55
|
+
if not source_node_obj.has_output_port(source_port):
|
56
|
+
raise ValueError(f"Source node {source_node_id} has no output port: {source_port}")
|
57
|
+
|
58
|
+
# 检查目标节点的端口是否存在
|
59
|
+
target_node_obj = next(node for node in self.nodes if node.id == target_node_id)
|
60
|
+
if not target_node_obj.has_input_port(target_port):
|
61
|
+
raise ValueError(f"Target node {target_node_id} has no input port: {target_port}")
|
62
|
+
|
63
|
+
# 检查目标端口是否已有被连接的线
|
64
|
+
for edge in self.edges:
|
65
|
+
if edge.target == target_node_id and edge.target_handle == target_port:
|
66
|
+
raise ValueError(
|
67
|
+
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})"
|
68
|
+
)
|
69
|
+
|
70
|
+
# 创建并添加边
|
71
|
+
edge_id = f"vueflow__edge-{source_node_id}{source_port}-{target_node_id}{target_port}"
|
72
|
+
edge = Edge(edge_id, source_node_id, source_port, target_node_id, target_port)
|
73
|
+
self.add_edge(edge)
|
74
|
+
|
75
|
+
def to_dict(self):
|
76
|
+
return {
|
77
|
+
"nodes": [node.to_dict() for node in self.nodes],
|
78
|
+
"edges": [edge.to_dict() for edge in self.edges],
|
79
|
+
"viewport": {"x": 0, "y": 0, "zoom": 1},
|
80
|
+
}
|
81
|
+
|
82
|
+
def to_json(self, ensure_ascii=False):
|
83
|
+
return json.dumps(self.to_dict(), ensure_ascii=ensure_ascii)
|
84
|
+
|
85
|
+
def to_mermaid(self) -> str:
|
86
|
+
"""生成 Mermaid 格式的流程图。
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
str: Mermaid 格式的流程图文本
|
90
|
+
"""
|
91
|
+
lines = ["flowchart TD"]
|
92
|
+
|
93
|
+
# 创建节点类型到序号的映射
|
94
|
+
type_counters = {}
|
95
|
+
node_id_to_label = {}
|
96
|
+
|
97
|
+
# 首先为所有节点生成标签
|
98
|
+
for node in self.nodes:
|
99
|
+
node_type = node.type.lower()
|
100
|
+
if node_type not in type_counters:
|
101
|
+
type_counters[node_type] = 0
|
102
|
+
node_label = f"{node_type}_{type_counters[node_type]}"
|
103
|
+
node_id_to_label[node.id] = node_label
|
104
|
+
type_counters[node_type] += 1
|
105
|
+
|
106
|
+
# 添加节点定义
|
107
|
+
for node in self.nodes:
|
108
|
+
node_label = node_id_to_label[node.id]
|
109
|
+
lines.append(f' {node_label}["{node_label} ({node.type})"]')
|
110
|
+
|
111
|
+
lines.append("") # 添加一个空行分隔节点和边的定义
|
112
|
+
|
113
|
+
# 添加边的定义
|
114
|
+
for edge in self.edges:
|
115
|
+
source_label = node_id_to_label[edge.source]
|
116
|
+
target_label = node_id_to_label[edge.target]
|
117
|
+
label = f"{edge.source_handle} → {edge.target_handle}"
|
118
|
+
lines.append(f" {source_label} -->|{label}| {target_label}")
|
119
|
+
|
120
|
+
return "\n".join(lines)
|
121
|
+
|
122
|
+
def check(self) -> WorkflowCheckResult:
|
123
|
+
"""检查流程图的有效性。
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
WorkflowCheckResult: 包含各种检查结果的字典
|
127
|
+
"""
|
128
|
+
dag_check = check_dag(self) # 检查流程图是否为有向无环图,并检测是否存在孤立节点。
|
129
|
+
ui_check = check_ui(self)
|
130
|
+
|
131
|
+
# 合并结果
|
132
|
+
result = dag_check
|
133
|
+
result["ui_warnings"] = ui_check
|
134
|
+
|
135
|
+
return result
|
136
|
+
|
137
|
+
def layout(self, options: Optional[Dict[str, Any]] = None) -> "Workflow":
|
138
|
+
"""对工作流中的节点进行自动布局,计算并更新每个节点的位置。
|
139
|
+
|
140
|
+
此方法实现了一个简单的分层布局算法,将节点按照有向图的拓扑结构进行排列。
|
141
|
+
|
142
|
+
Args:
|
143
|
+
options: 布局选项,包括:
|
144
|
+
- direction: 布局方向 ('TB', 'BT', 'LR', 'RL'),默认 'LR'
|
145
|
+
- node_spacing: 同一层级节点间的间距,默认 500
|
146
|
+
- layer_spacing: 不同层级间的间距,默认 400
|
147
|
+
- margin_x: 图形左右边距,默认 20
|
148
|
+
- margin_y: 图形上下边距,默认 20
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
布局后的工作流对象
|
152
|
+
"""
|
153
|
+
layout(self.nodes, self.edges, options)
|
154
|
+
return self
|
@@ -0,0 +1,159 @@
|
|
1
|
+
from typing import TypedDict, TYPE_CHECKING
|
2
|
+
|
3
|
+
from ..graph.port import InputPort
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from ..graph.workflow import Workflow
|
7
|
+
|
8
|
+
|
9
|
+
class UIWarning(TypedDict, total=False):
|
10
|
+
"""UI警告类型。"""
|
11
|
+
|
12
|
+
input_ports_shown_but_connected: list[dict] # 显示的输入端口但被连接
|
13
|
+
has_shown_input_ports: bool # 是否存在显示的输入端口
|
14
|
+
has_output_nodes: bool # 是否存在输出节点
|
15
|
+
|
16
|
+
|
17
|
+
class WorkflowCheckResult(TypedDict, total=False):
|
18
|
+
"""工作流检查结果类型。"""
|
19
|
+
|
20
|
+
no_cycle: bool # 工作流是否不包含环
|
21
|
+
no_isolated_nodes: bool # 工作流是否不包含孤立节点
|
22
|
+
ui_warnings: UIWarning # UI相关警告
|
23
|
+
|
24
|
+
|
25
|
+
def check_dag(workflow: "Workflow") -> WorkflowCheckResult:
|
26
|
+
"""检查流程图是否为有向无环图,并检测是否存在孤立节点。
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
WorkflowCheckResult: 包含检查结果的字典
|
30
|
+
- no_cycle (bool): 如果流程图是有向无环图返回 True,否则返回 False
|
31
|
+
- no_isolated_nodes (bool): 如果不存在孤立节点返回 True,否则返回 False
|
32
|
+
"""
|
33
|
+
result: WorkflowCheckResult = {"no_cycle": True, "no_isolated_nodes": True}
|
34
|
+
|
35
|
+
# 过滤掉触发器节点和辅助节点
|
36
|
+
trigger_nodes = [
|
37
|
+
node.id
|
38
|
+
for node in workflow.nodes
|
39
|
+
if hasattr(node, "category") and (node.category == "triggers" or node.category == "assistedNodes")
|
40
|
+
]
|
41
|
+
|
42
|
+
# 获取需要检查的节点和边
|
43
|
+
regular_nodes = [node.id for node in workflow.nodes if node.id not in trigger_nodes]
|
44
|
+
regular_edges = [
|
45
|
+
edge for edge in workflow.edges if edge.source not in trigger_nodes and edge.target not in trigger_nodes
|
46
|
+
]
|
47
|
+
|
48
|
+
# ---------- 检查有向图是否有环 ----------
|
49
|
+
# 构建邻接表
|
50
|
+
adjacency = {node_id: [] for node_id in regular_nodes}
|
51
|
+
for edge in regular_edges:
|
52
|
+
if edge.source in adjacency: # 确保节点在字典中
|
53
|
+
adjacency[edge.source].append(edge.target)
|
54
|
+
|
55
|
+
# 三种状态: 0 = 未访问, 1 = 正在访问, 2 = 已访问完成
|
56
|
+
visited = {node_id: 0 for node_id in regular_nodes}
|
57
|
+
|
58
|
+
def dfs_cycle_detection(node_id):
|
59
|
+
# 如果节点正在被访问,说明找到了环
|
60
|
+
if visited[node_id] == 1:
|
61
|
+
return False
|
62
|
+
|
63
|
+
# 如果节点已经访问完成,无需再次访问
|
64
|
+
if visited[node_id] == 2:
|
65
|
+
return True
|
66
|
+
|
67
|
+
# 标记为正在访问
|
68
|
+
visited[node_id] = 1
|
69
|
+
|
70
|
+
# 访问所有邻居
|
71
|
+
for neighbor in adjacency[node_id]:
|
72
|
+
if neighbor in visited and not dfs_cycle_detection(neighbor):
|
73
|
+
return False
|
74
|
+
|
75
|
+
# 标记为已访问完成
|
76
|
+
visited[node_id] = 2
|
77
|
+
return True
|
78
|
+
|
79
|
+
# 对每个未访问的节点进行 DFS 检测环
|
80
|
+
for node_id in regular_nodes:
|
81
|
+
if visited[node_id] == 0:
|
82
|
+
if not dfs_cycle_detection(node_id):
|
83
|
+
result["no_cycle"] = False
|
84
|
+
break
|
85
|
+
|
86
|
+
# ---------- 检查是否存在孤立节点 ----------
|
87
|
+
# 构建无向图邻接表
|
88
|
+
undirected_adjacency = {node_id: [] for node_id in regular_nodes}
|
89
|
+
for edge in regular_edges:
|
90
|
+
if edge.source in undirected_adjacency and edge.target in undirected_adjacency:
|
91
|
+
undirected_adjacency[edge.source].append(edge.target)
|
92
|
+
undirected_adjacency[edge.target].append(edge.source)
|
93
|
+
|
94
|
+
# 深度优先搜索来检测连通分量
|
95
|
+
undirected_visited = set()
|
96
|
+
|
97
|
+
def dfs_connected_components(node_id):
|
98
|
+
undirected_visited.add(node_id)
|
99
|
+
for neighbor in undirected_adjacency[node_id]:
|
100
|
+
if neighbor not in undirected_visited:
|
101
|
+
dfs_connected_components(neighbor)
|
102
|
+
|
103
|
+
# 计算连通分量数量
|
104
|
+
connected_components_count = 0
|
105
|
+
for node_id in regular_nodes:
|
106
|
+
if node_id not in undirected_visited:
|
107
|
+
connected_components_count += 1
|
108
|
+
dfs_connected_components(node_id)
|
109
|
+
|
110
|
+
# 如果连通分量数量大于1,说明存在孤立节点
|
111
|
+
if connected_components_count > 1 and len(regular_nodes) > 0:
|
112
|
+
result["no_isolated_nodes"] = False
|
113
|
+
|
114
|
+
return result
|
115
|
+
|
116
|
+
|
117
|
+
def check_ui(workflow: "Workflow") -> UIWarning:
|
118
|
+
"""
|
119
|
+
检查工作流的 UI 情况。
|
120
|
+
以下情况会警告:
|
121
|
+
1. 某个输入端口的 show=True,但是又有连线连接到该端口(实际运行时会被覆盖)。
|
122
|
+
2. 整个工作流没有任何输入端口是 show=True 的,说明没有让用户输入的地方。
|
123
|
+
3. 整个工作流没有任何输出节点,这样工作流结果无法呈现。
|
124
|
+
"""
|
125
|
+
warnings: UIWarning = {
|
126
|
+
"input_ports_shown_but_connected": [],
|
127
|
+
"has_shown_input_ports": False,
|
128
|
+
"has_output_nodes": False,
|
129
|
+
}
|
130
|
+
|
131
|
+
# 检查是否有任何显示的输入端口
|
132
|
+
has_shown_input_ports = False
|
133
|
+
|
134
|
+
# 找出所有连接的目标端口
|
135
|
+
connected_ports = {(edge.target, edge.target_handle) for edge in workflow.edges}
|
136
|
+
|
137
|
+
# 遍历所有节点
|
138
|
+
for node in workflow.nodes:
|
139
|
+
# 检查是否为输出节点
|
140
|
+
if hasattr(node, "category") and node.category == "outputs":
|
141
|
+
warnings["has_output_nodes"] = True
|
142
|
+
|
143
|
+
# 检查节点的输入端口
|
144
|
+
for port_name in node.ports.keys() if hasattr(node, "ports") else []:
|
145
|
+
port = node.ports.get(port_name)
|
146
|
+
# 确保是输入端口且设置为显示
|
147
|
+
if hasattr(port, "show") and getattr(port, "show", False) and isinstance(port, InputPort):
|
148
|
+
has_shown_input_ports = True
|
149
|
+
|
150
|
+
# 检查显示的端口是否也被连接
|
151
|
+
if (node.id, port_name) in connected_ports:
|
152
|
+
warnings["input_ports_shown_but_connected"].append(
|
153
|
+
{"node_id": node.id, "node_type": node.type, "port_name": port_name}
|
154
|
+
)
|
155
|
+
|
156
|
+
# 如果没有任何显示的输入端口
|
157
|
+
warnings["has_shown_input_ports"] = has_shown_input_ports
|
158
|
+
|
159
|
+
return warnings
|
@@ -0,0 +1,114 @@
|
|
1
|
+
from typing import Optional, Dict, Any, List, TYPE_CHECKING
|
2
|
+
|
3
|
+
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from vectorvein.workflow.graph.node import Node
|
6
|
+
from vectorvein.workflow.graph.edge import Edge
|
7
|
+
|
8
|
+
|
9
|
+
def layout(nodes: List["Node"], edges: List["Edge"], options: Optional[Dict[str, Any]] = None):
|
10
|
+
"""对工作流中的节点进行自动布局,计算并更新每个节点的位置。
|
11
|
+
|
12
|
+
此方法实现了一个简单的分层布局算法,将节点按照有向图的拓扑结构进行排列。
|
13
|
+
|
14
|
+
Args:
|
15
|
+
options: 布局选项,包括:
|
16
|
+
- direction: 布局方向 ('TB', 'BT', 'LR', 'RL'),默认 'TB'
|
17
|
+
- node_spacing: 同一层级节点间的间距,默认 150
|
18
|
+
- layer_spacing: 不同层级间的间距,默认 100
|
19
|
+
- margin_x: 图形左右边距,默认 20
|
20
|
+
- margin_y: 图形上下边距,默认 20
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
布局后的工作流对象
|
24
|
+
"""
|
25
|
+
# 设置默认选项
|
26
|
+
default_options = {
|
27
|
+
"direction": "LR", # 从上到下的布局
|
28
|
+
"node_spacing": 400, # 同一层级节点间的间距
|
29
|
+
"layer_spacing": 500, # 不同层级间的间距
|
30
|
+
"margin_x": 20, # 图形左右边距
|
31
|
+
"margin_y": 20, # 图形上下边距
|
32
|
+
}
|
33
|
+
|
34
|
+
# 合并用户提供的选项
|
35
|
+
if options:
|
36
|
+
default_options.update(options)
|
37
|
+
|
38
|
+
# 构建邻接表
|
39
|
+
adjacency = {node.id: [] for node in nodes}
|
40
|
+
in_degree = {node.id: 0 for node in nodes}
|
41
|
+
|
42
|
+
for edge in edges:
|
43
|
+
if edge.source in adjacency:
|
44
|
+
adjacency[edge.source].append(edge.target)
|
45
|
+
in_degree[edge.target] = in_degree.get(edge.target, 0) + 1
|
46
|
+
|
47
|
+
# 找出所有入度为0的节点(根节点)
|
48
|
+
roots = [node_id for node_id, degree in in_degree.items() if degree == 0]
|
49
|
+
|
50
|
+
# 如果没有根节点,选择第一个节点作为起点
|
51
|
+
if not roots and nodes:
|
52
|
+
roots = [nodes[0].id]
|
53
|
+
|
54
|
+
# 按层级排列节点
|
55
|
+
layers = []
|
56
|
+
visited = set()
|
57
|
+
|
58
|
+
current_layer = roots
|
59
|
+
while current_layer:
|
60
|
+
layers.append(current_layer)
|
61
|
+
next_layer = []
|
62
|
+
for node_id in current_layer:
|
63
|
+
visited.add(node_id)
|
64
|
+
for neighbor in adjacency.get(node_id, []):
|
65
|
+
if neighbor not in visited and all(
|
66
|
+
parent in visited for parent in [e.source for e in edges if e.target == neighbor]
|
67
|
+
):
|
68
|
+
next_layer.append(neighbor)
|
69
|
+
current_layer = next_layer
|
70
|
+
|
71
|
+
# 还有未访问的节点(可能是孤立节点或环的一部分)
|
72
|
+
remaining = [node.id for node in nodes if node.id not in visited]
|
73
|
+
if remaining:
|
74
|
+
layers.append(remaining)
|
75
|
+
|
76
|
+
# 根据层级信息设置节点位置
|
77
|
+
layer_spacing = default_options["layer_spacing"]
|
78
|
+
node_spacing = default_options["node_spacing"]
|
79
|
+
margin_x = default_options["margin_x"]
|
80
|
+
margin_y = default_options["margin_y"]
|
81
|
+
|
82
|
+
# 布局方向
|
83
|
+
is_vertical = default_options["direction"] in ["TB", "BT"]
|
84
|
+
is_reversed = default_options["direction"] in ["BT", "RL"]
|
85
|
+
|
86
|
+
for layer_idx, layer in enumerate(layers):
|
87
|
+
for node_idx, node_id in enumerate(layer):
|
88
|
+
# 根据布局方向计算位置
|
89
|
+
if is_vertical:
|
90
|
+
# 垂直布局 (TB 或 BT)
|
91
|
+
x = node_idx * node_spacing + margin_x
|
92
|
+
y = layer_idx * layer_spacing + margin_y
|
93
|
+
if is_reversed: # BT 布局需要反转 y 坐标
|
94
|
+
y = (len(layers) - 1 - layer_idx) * layer_spacing + margin_y
|
95
|
+
else:
|
96
|
+
# 水平布局 (LR 或 RL)
|
97
|
+
x = layer_idx * layer_spacing + margin_x
|
98
|
+
y = node_idx * node_spacing + margin_y
|
99
|
+
if is_reversed: # RL 布局需要反转 x 坐标
|
100
|
+
x = (len(layers) - 1 - layer_idx) * layer_spacing + margin_x
|
101
|
+
|
102
|
+
# 找到节点对象并设置位置
|
103
|
+
for node in nodes:
|
104
|
+
if node.id == node_id:
|
105
|
+
# 确保节点有 position 属性
|
106
|
+
if not hasattr(node, "position"):
|
107
|
+
node.position = {"x": x, "y": y}
|
108
|
+
else:
|
109
|
+
# 如果已经有 position 属性,更新它
|
110
|
+
if isinstance(node.position, dict):
|
111
|
+
node.position.update({"x": x, "y": y})
|
112
|
+
else:
|
113
|
+
node.position = {"x": x, "y": y}
|
114
|
+
break
|
@@ -1,285 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from typing import List, Union, TypedDict
|
3
|
-
|
4
|
-
from .node import Node
|
5
|
-
from .edge import Edge
|
6
|
-
from .port import InputPort
|
7
|
-
|
8
|
-
|
9
|
-
class UIWarning(TypedDict, total=False):
|
10
|
-
"""UI警告类型。"""
|
11
|
-
|
12
|
-
input_ports_shown_but_connected: list[dict] # 显示的输入端口但被连接
|
13
|
-
has_shown_input_ports: bool # 是否存在显示的输入端口
|
14
|
-
has_output_nodes: bool # 是否存在输出节点
|
15
|
-
|
16
|
-
|
17
|
-
class WorkflowCheckResult(TypedDict, total=False):
|
18
|
-
"""工作流检查结果类型。"""
|
19
|
-
|
20
|
-
no_cycle: bool # 工作流是否不包含环
|
21
|
-
no_isolated_nodes: bool # 工作流是否不包含孤立节点
|
22
|
-
ui_warnings: UIWarning # UI相关警告
|
23
|
-
|
24
|
-
|
25
|
-
class Workflow:
|
26
|
-
def __init__(self) -> None:
|
27
|
-
self.nodes: List[Node] = []
|
28
|
-
self.edges: List[Edge] = []
|
29
|
-
|
30
|
-
def add_node(self, node: Node):
|
31
|
-
self.nodes.append(node)
|
32
|
-
|
33
|
-
def add_nodes(self, nodes: List[Node]):
|
34
|
-
self.nodes.extend(nodes)
|
35
|
-
|
36
|
-
def add_edge(self, edge: Edge):
|
37
|
-
self.edges.append(edge)
|
38
|
-
|
39
|
-
def connect(
|
40
|
-
self,
|
41
|
-
source_node: Union[str, Node],
|
42
|
-
source_port: str,
|
43
|
-
target_node: Union[str, Node],
|
44
|
-
target_port: str,
|
45
|
-
):
|
46
|
-
# 获取源节点ID
|
47
|
-
if isinstance(source_node, Node):
|
48
|
-
source_node_id = source_node.id
|
49
|
-
else:
|
50
|
-
source_node_id = source_node
|
51
|
-
|
52
|
-
# 获取目标节点ID
|
53
|
-
if isinstance(target_node, Node):
|
54
|
-
target_node_id = target_node.id
|
55
|
-
else:
|
56
|
-
target_node_id = target_node
|
57
|
-
|
58
|
-
# 检查源节点是否存在
|
59
|
-
source_node_exists = any(node.id == source_node_id for node in self.nodes)
|
60
|
-
if not source_node_exists:
|
61
|
-
raise ValueError(f"Source node not found: {source_node_id}")
|
62
|
-
|
63
|
-
# 检查目标节点是否存在
|
64
|
-
target_node_exists = any(node.id == target_node_id for node in self.nodes)
|
65
|
-
if not target_node_exists:
|
66
|
-
raise ValueError(f"Target node not found: {target_node_id}")
|
67
|
-
|
68
|
-
# 检查源节点的端口是否存在
|
69
|
-
source_node_obj = next(node for node in self.nodes if node.id == source_node_id)
|
70
|
-
if not source_node_obj.has_output_port(source_port):
|
71
|
-
raise ValueError(f"Source node {source_node_id} has no output port: {source_port}")
|
72
|
-
|
73
|
-
# 检查目标节点的端口是否存在
|
74
|
-
target_node_obj = next(node for node in self.nodes if node.id == target_node_id)
|
75
|
-
if not target_node_obj.has_input_port(target_port):
|
76
|
-
raise ValueError(f"Target node {target_node_id} has no input port: {target_port}")
|
77
|
-
|
78
|
-
# 检查目标端口是否已有被连接的线
|
79
|
-
for edge in self.edges:
|
80
|
-
if edge.target == target_node_id and edge.target_handle == target_port:
|
81
|
-
raise ValueError(
|
82
|
-
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})"
|
83
|
-
)
|
84
|
-
|
85
|
-
# 创建并添加边
|
86
|
-
edge_id = f"vueflow__edge-{source_node_id}{source_port}-{target_node_id}{target_port}"
|
87
|
-
edge = Edge(edge_id, source_node_id, source_port, target_node_id, target_port)
|
88
|
-
self.add_edge(edge)
|
89
|
-
|
90
|
-
def to_dict(self):
|
91
|
-
return {
|
92
|
-
"nodes": [node.to_dict() for node in self.nodes],
|
93
|
-
"edges": [edge.to_dict() for edge in self.edges],
|
94
|
-
"viewport": {"x": 0, "y": 0, "zoom": 1},
|
95
|
-
}
|
96
|
-
|
97
|
-
def to_json(self, ensure_ascii=False):
|
98
|
-
return json.dumps(self.to_dict(), ensure_ascii=ensure_ascii)
|
99
|
-
|
100
|
-
def to_mermaid(self) -> str:
|
101
|
-
"""生成 Mermaid 格式的流程图。
|
102
|
-
|
103
|
-
Returns:
|
104
|
-
str: Mermaid 格式的流程图文本
|
105
|
-
"""
|
106
|
-
lines = ["flowchart TD"]
|
107
|
-
|
108
|
-
# 创建节点类型到序号的映射
|
109
|
-
type_counters = {}
|
110
|
-
node_id_to_label = {}
|
111
|
-
|
112
|
-
# 首先为所有节点生成标签
|
113
|
-
for node in self.nodes:
|
114
|
-
node_type = node.type.lower()
|
115
|
-
if node_type not in type_counters:
|
116
|
-
type_counters[node_type] = 0
|
117
|
-
node_label = f"{node_type}_{type_counters[node_type]}"
|
118
|
-
node_id_to_label[node.id] = node_label
|
119
|
-
type_counters[node_type] += 1
|
120
|
-
|
121
|
-
# 添加节点定义
|
122
|
-
for node in self.nodes:
|
123
|
-
node_label = node_id_to_label[node.id]
|
124
|
-
lines.append(f' {node_label}["{node_label} ({node.type})"]')
|
125
|
-
|
126
|
-
lines.append("") # 添加一个空行分隔节点和边的定义
|
127
|
-
|
128
|
-
# 添加边的定义
|
129
|
-
for edge in self.edges:
|
130
|
-
source_label = node_id_to_label[edge.source]
|
131
|
-
target_label = node_id_to_label[edge.target]
|
132
|
-
label = f"{edge.source_handle} → {edge.target_handle}"
|
133
|
-
lines.append(f" {source_label} -->|{label}| {target_label}")
|
134
|
-
|
135
|
-
return "\n".join(lines)
|
136
|
-
|
137
|
-
def _check_dag(self) -> WorkflowCheckResult:
|
138
|
-
"""检查流程图是否为有向无环图,并检测是否存在孤立节点。
|
139
|
-
|
140
|
-
Returns:
|
141
|
-
WorkflowCheckResult: 包含检查结果的字典
|
142
|
-
- no_cycle (bool): 如果流程图是有向无环图返回 True,否则返回 False
|
143
|
-
- no_isolated_nodes (bool): 如果不存在孤立节点返回 True,否则返回 False
|
144
|
-
"""
|
145
|
-
result: WorkflowCheckResult = {"no_cycle": True, "no_isolated_nodes": True}
|
146
|
-
|
147
|
-
# 过滤掉触发器节点和辅助节点
|
148
|
-
trigger_nodes = [
|
149
|
-
node.id
|
150
|
-
for node in self.nodes
|
151
|
-
if hasattr(node, "category") and (node.category == "triggers" or node.category == "assistedNodes")
|
152
|
-
]
|
153
|
-
|
154
|
-
# 获取需要检查的节点和边
|
155
|
-
regular_nodes = [node.id for node in self.nodes if node.id not in trigger_nodes]
|
156
|
-
regular_edges = [
|
157
|
-
edge for edge in self.edges if edge.source not in trigger_nodes and edge.target not in trigger_nodes
|
158
|
-
]
|
159
|
-
|
160
|
-
# ---------- 检查有向图是否有环 ----------
|
161
|
-
# 构建邻接表
|
162
|
-
adjacency = {node_id: [] for node_id in regular_nodes}
|
163
|
-
for edge in regular_edges:
|
164
|
-
if edge.source in adjacency: # 确保节点在字典中
|
165
|
-
adjacency[edge.source].append(edge.target)
|
166
|
-
|
167
|
-
# 三种状态: 0 = 未访问, 1 = 正在访问, 2 = 已访问完成
|
168
|
-
visited = {node_id: 0 for node_id in regular_nodes}
|
169
|
-
|
170
|
-
def dfs_cycle_detection(node_id):
|
171
|
-
# 如果节点正在被访问,说明找到了环
|
172
|
-
if visited[node_id] == 1:
|
173
|
-
return False
|
174
|
-
|
175
|
-
# 如果节点已经访问完成,无需再次访问
|
176
|
-
if visited[node_id] == 2:
|
177
|
-
return True
|
178
|
-
|
179
|
-
# 标记为正在访问
|
180
|
-
visited[node_id] = 1
|
181
|
-
|
182
|
-
# 访问所有邻居
|
183
|
-
for neighbor in adjacency[node_id]:
|
184
|
-
if neighbor in visited and not dfs_cycle_detection(neighbor):
|
185
|
-
return False
|
186
|
-
|
187
|
-
# 标记为已访问完成
|
188
|
-
visited[node_id] = 2
|
189
|
-
return True
|
190
|
-
|
191
|
-
# 对每个未访问的节点进行 DFS 检测环
|
192
|
-
for node_id in regular_nodes:
|
193
|
-
if visited[node_id] == 0:
|
194
|
-
if not dfs_cycle_detection(node_id):
|
195
|
-
result["no_cycle"] = False
|
196
|
-
break
|
197
|
-
|
198
|
-
# ---------- 检查是否存在孤立节点 ----------
|
199
|
-
# 构建无向图邻接表
|
200
|
-
undirected_adjacency = {node_id: [] for node_id in regular_nodes}
|
201
|
-
for edge in regular_edges:
|
202
|
-
if edge.source in undirected_adjacency and edge.target in undirected_adjacency:
|
203
|
-
undirected_adjacency[edge.source].append(edge.target)
|
204
|
-
undirected_adjacency[edge.target].append(edge.source)
|
205
|
-
|
206
|
-
# 深度优先搜索来检测连通分量
|
207
|
-
undirected_visited = set()
|
208
|
-
|
209
|
-
def dfs_connected_components(node_id):
|
210
|
-
undirected_visited.add(node_id)
|
211
|
-
for neighbor in undirected_adjacency[node_id]:
|
212
|
-
if neighbor not in undirected_visited:
|
213
|
-
dfs_connected_components(neighbor)
|
214
|
-
|
215
|
-
# 计算连通分量数量
|
216
|
-
connected_components_count = 0
|
217
|
-
for node_id in regular_nodes:
|
218
|
-
if node_id not in undirected_visited:
|
219
|
-
connected_components_count += 1
|
220
|
-
dfs_connected_components(node_id)
|
221
|
-
|
222
|
-
# 如果连通分量数量大于1,说明存在孤立节点
|
223
|
-
if connected_components_count > 1 and len(regular_nodes) > 0:
|
224
|
-
result["no_isolated_nodes"] = False
|
225
|
-
|
226
|
-
return result
|
227
|
-
|
228
|
-
def _check_ui(self) -> UIWarning:
|
229
|
-
"""
|
230
|
-
检查工作流的 UI 情况。
|
231
|
-
以下情况会警告:
|
232
|
-
1. 某个输入端口的 show=True,但是又有连线连接到该端口(实际运行时会被覆盖)。
|
233
|
-
2. 整个工作流没有任何输入端口是 show=True 的,说明没有让用户输入的地方。
|
234
|
-
3. 整个工作流没有任何输出节点,这样工作流结果无法呈现。
|
235
|
-
"""
|
236
|
-
warnings: UIWarning = {
|
237
|
-
"input_ports_shown_but_connected": [],
|
238
|
-
"has_shown_input_ports": False,
|
239
|
-
"has_output_nodes": False,
|
240
|
-
}
|
241
|
-
|
242
|
-
# 检查是否有任何显示的输入端口
|
243
|
-
has_shown_input_ports = False
|
244
|
-
|
245
|
-
# 找出所有连接的目标端口
|
246
|
-
connected_ports = {(edge.target, edge.target_handle) for edge in self.edges}
|
247
|
-
|
248
|
-
# 遍历所有节点
|
249
|
-
for node in self.nodes:
|
250
|
-
# 检查是否为输出节点
|
251
|
-
if hasattr(node, "category") and node.category == "outputs":
|
252
|
-
warnings["has_output_nodes"] = True
|
253
|
-
|
254
|
-
# 检查节点的输入端口
|
255
|
-
for port_name in node.ports.keys() if hasattr(node, "ports") else []:
|
256
|
-
port = node.ports.get(port_name)
|
257
|
-
# 确保是输入端口且设置为显示
|
258
|
-
if hasattr(port, "show") and getattr(port, "show", False) and isinstance(port, InputPort):
|
259
|
-
has_shown_input_ports = True
|
260
|
-
|
261
|
-
# 检查显示的端口是否也被连接
|
262
|
-
if (node.id, port_name) in connected_ports:
|
263
|
-
warnings["input_ports_shown_but_connected"].append(
|
264
|
-
{"node_id": node.id, "node_type": node.type, "port_name": port_name}
|
265
|
-
)
|
266
|
-
|
267
|
-
# 如果没有任何显示的输入端口
|
268
|
-
warnings["has_shown_input_ports"] = has_shown_input_ports
|
269
|
-
|
270
|
-
return warnings
|
271
|
-
|
272
|
-
def check(self) -> WorkflowCheckResult:
|
273
|
-
"""检查流程图的有效性。
|
274
|
-
|
275
|
-
Returns:
|
276
|
-
WorkflowCheckResult: 包含各种检查结果的字典
|
277
|
-
"""
|
278
|
-
dag_check = self._check_dag()
|
279
|
-
ui_check = self._check_ui()
|
280
|
-
|
281
|
-
# 合并结果
|
282
|
-
result: WorkflowCheckResult = dag_check
|
283
|
-
result["ui_warnings"] = ui_check
|
284
|
-
|
285
|
-
return result
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{vectorvein-0.2.49 → vectorvein-0.2.51}/src/vectorvein/chat_clients/openai_compatible_client.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|