dynworkflow 0.1.0__py3-none-any.whl
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.
- dynworkflow/__init__.py +19 -0
- dynworkflow/agent.py +1022 -0
- dynworkflow/cache.py +292 -0
- dynworkflow/flow.py +702 -0
- dynworkflow/py.typed +0 -0
- dynworkflow/reporter.py +250 -0
- dynworkflow/schemas.py +88 -0
- dynworkflow-0.1.0.dist-info/METADATA +93 -0
- dynworkflow-0.1.0.dist-info/RECORD +10 -0
- dynworkflow-0.1.0.dist-info/WHEEL +4 -0
dynworkflow/flow.py
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
from typing import Any, Generator, Callable, NoReturn, TypedDict, cast
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import multiprocessing
|
|
6
|
+
import inspect
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import ast
|
|
13
|
+
import textwrap
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
from .agent import (Agent, AgentClientInterface, MultiAgent, AgentStatus,
|
|
17
|
+
find_agent_group, gen_conn_info_from_env,
|
|
18
|
+
raise_in_thread)
|
|
19
|
+
from .cache import CacheContext, WorkflowCache, default_db_path
|
|
20
|
+
from .reporter import bind_node_print, optional_reporter, unbind_node_print
|
|
21
|
+
|
|
22
|
+
type StructureOutput = (
|
|
23
|
+
dict[str, StructureOutput] |
|
|
24
|
+
list[StructureOutput] |
|
|
25
|
+
str |
|
|
26
|
+
int |
|
|
27
|
+
float |
|
|
28
|
+
bool |
|
|
29
|
+
None
|
|
30
|
+
) # StructureOutput 结构化输出类型
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GraphNode(TypedDict):
|
|
34
|
+
"GraphNode 节点图对象"
|
|
35
|
+
name: str # 节点名称
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Graph(TypedDict):
|
|
39
|
+
"Graph 节点图"
|
|
40
|
+
nodes: dict[str, GraphNode] # 节点字典
|
|
41
|
+
edges: dict[str, set[str]] # 边列表
|
|
42
|
+
start: set[str] # 起始节点
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Result:
|
|
46
|
+
"Result 向 Main Agent 返回结果"
|
|
47
|
+
|
|
48
|
+
def __init__(self, result: Any) -> None:
|
|
49
|
+
self.result_val = result
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return f"Result({self.result_val})"
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return str(self.result)
|
|
56
|
+
|
|
57
|
+
def result(self) -> Any:
|
|
58
|
+
"Result.result() 获取结果"
|
|
59
|
+
return self.result_val
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _Empty:
|
|
63
|
+
"Empty 空对象"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_empty = _Empty() # _empty 空对象
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class NodeStatus(Enum):
|
|
70
|
+
"NodeStatus 节点状态"
|
|
71
|
+
|
|
72
|
+
WAIT = "wait" # 等待
|
|
73
|
+
QUEUE = "queue" # 队列中
|
|
74
|
+
RUNNING = "running" # 运行中
|
|
75
|
+
DONE = "done" # 完成
|
|
76
|
+
ERROR = "error" # 完成
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _describe_node_result(ret: 'NodeResult') -> "dict[str, Any] | None":
|
|
80
|
+
'"_describe_node_result() 把节点返回值序列化为缓存描述符;不可缓存时返回 None'
|
|
81
|
+
|
|
82
|
+
if ret is None:
|
|
83
|
+
return {"kind": "none"}
|
|
84
|
+
if isinstance(ret, Result):
|
|
85
|
+
# value 含不可序列化对象时由 put_node 整体放弃写入
|
|
86
|
+
return {"kind": "result", "value": ret.result()}
|
|
87
|
+
items = list(ret) if isinstance(ret, (list, tuple)) else [ret]
|
|
88
|
+
exec_items: list[dict[str, Any]] = []
|
|
89
|
+
for item in items:
|
|
90
|
+
exec_items.append(
|
|
91
|
+
{"node_id": item._node._node_id, "args": dict(item._args)})
|
|
92
|
+
return {"kind": "executes", "items": exec_items}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Node():
|
|
96
|
+
"Node 节点对象"
|
|
97
|
+
|
|
98
|
+
def __init__(self, node_name: str, node_id: str, func: 'TaskGen') -> None:
|
|
99
|
+
self._node_name = node_name # 节点名称
|
|
100
|
+
self._node_id = node_id # 节点 ID
|
|
101
|
+
self._func = func # 节点生成器
|
|
102
|
+
# === 代码体哈希(参与缓存联合键,函数体变更即失效) ===
|
|
103
|
+
self._source: str | None = None # 节点源码缓存(启动时广播)
|
|
104
|
+
self._code_hash: str | None = self._compute_code_hash()
|
|
105
|
+
# === 节点状态 ===
|
|
106
|
+
self._thread: threading.Thread | None = None # 节点线程
|
|
107
|
+
self._value_lock: threading.Lock = threading.Lock() # 节点值锁
|
|
108
|
+
# === 节点参数缓存 ===
|
|
109
|
+
self._func_cache: dict[str, Any] = {} # 函数调用参数缓存
|
|
110
|
+
self._func_cache_var_keyword: dict[str,
|
|
111
|
+
Any] | None = None # 函数调用可变参数缓存
|
|
112
|
+
self._func_cache_filled_num: int = 0 # 参数已填充数量
|
|
113
|
+
self._func_default_args: dict[str, Any] = {} # 默认参数
|
|
114
|
+
self._init_func_cache() # 初始化缓存
|
|
115
|
+
# === 节点结果 ===
|
|
116
|
+
self.status: NodeStatus = NodeStatus.WAIT # 节点状态
|
|
117
|
+
self.agents: list[AgentStatus] = [] # 节点代理
|
|
118
|
+
|
|
119
|
+
def _compute_code_hash(self) -> str | None:
|
|
120
|
+
"Node._compute_code_hash() 计算函数源码体的 sha256;源码不可得返回 None"
|
|
121
|
+
try:
|
|
122
|
+
source = inspect.getsource(self._func)
|
|
123
|
+
except (OSError, TypeError):
|
|
124
|
+
self._source = None
|
|
125
|
+
return None
|
|
126
|
+
# dedent + strip 归一化缩进,避免同等代码因粘贴缩进不同而误判变化
|
|
127
|
+
normalized = textwrap.dedent(source).strip()
|
|
128
|
+
self._source = normalized
|
|
129
|
+
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
130
|
+
|
|
131
|
+
def _init_func_cache(self) -> None:
|
|
132
|
+
"Node._init_func_cache() 初始化函数调用参数缓存"
|
|
133
|
+
with self._value_lock:
|
|
134
|
+
sig = inspect.signature(self._func)
|
|
135
|
+
for name, param in sig.parameters.items():
|
|
136
|
+
if param.kind == inspect.Parameter.POSITIONAL_ONLY or param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
137
|
+
# 不支持位置参数
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"Cannot use positional only or var positional arguments in node args")
|
|
140
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
141
|
+
# 特殊处理可变关键字参数
|
|
142
|
+
self._func_cache_var_keyword = {}
|
|
143
|
+
continue
|
|
144
|
+
# 填充缓存
|
|
145
|
+
self._func_cache[name] = _empty
|
|
146
|
+
if param.default is not inspect.Parameter.empty:
|
|
147
|
+
# 填充默认值
|
|
148
|
+
self._func_cache[name] = param.default
|
|
149
|
+
self._func_cache_filled_num += 1
|
|
150
|
+
self._func_default_args[name] = param.default
|
|
151
|
+
|
|
152
|
+
def _fill_args(self, **args: Any) -> None:
|
|
153
|
+
"Node.fill_args() 填充参数"
|
|
154
|
+
|
|
155
|
+
with self._value_lock:
|
|
156
|
+
for name, value in args.items():
|
|
157
|
+
if name not in self._func_cache:
|
|
158
|
+
# 参数未定义
|
|
159
|
+
if self._func_cache_var_keyword is None:
|
|
160
|
+
# 不支持可变关键字参数
|
|
161
|
+
raise ValueError(f"Unknown argument: {name}")
|
|
162
|
+
self._func_cache_var_keyword[name] = value
|
|
163
|
+
continue
|
|
164
|
+
if isinstance(self._func_cache[name], _Empty):
|
|
165
|
+
self._func_cache_filled_num += 1
|
|
166
|
+
self._func_cache[name] = value
|
|
167
|
+
|
|
168
|
+
def _check_all_filled(self) -> bool:
|
|
169
|
+
"Node.check_all_filled() 检查所有参数是否已填充"
|
|
170
|
+
with self._value_lock:
|
|
171
|
+
return self._func_cache_filled_num == len(self._func_cache)
|
|
172
|
+
|
|
173
|
+
def __call__(self, **args: Any) -> 'Execute':
|
|
174
|
+
"Node() 覆盖原本调用,用于参数填充"
|
|
175
|
+
return Execute(self, args)
|
|
176
|
+
|
|
177
|
+
def _report_status(self, flow: 'Flow', state: NodeStatus,
|
|
178
|
+
args: dict[str, Any], *, cached: bool = False) -> None:
|
|
179
|
+
"_report_status() 推送一次节点状态事件(未启用上报时为空操作)"
|
|
180
|
+
if flow._reporter is None:
|
|
181
|
+
return
|
|
182
|
+
flow._reporter.report_node(flow._workflow_id, self._node_id,
|
|
183
|
+
state.value, args, cached=cached)
|
|
184
|
+
|
|
185
|
+
def _run(self, flow: 'Flow') -> None:
|
|
186
|
+
"Node.run() 执行节点,并把异常延迟到工作流结束统一抛出。"
|
|
187
|
+
try:
|
|
188
|
+
self._run_impl(flow)
|
|
189
|
+
except (NodeTerminated, WorkflowAborted):
|
|
190
|
+
# 控制命令导致的终止不是节点执行错误。
|
|
191
|
+
self.status = NodeStatus.ERROR
|
|
192
|
+
except Exception as e:
|
|
193
|
+
with self._value_lock:
|
|
194
|
+
args = self._func_cache | (
|
|
195
|
+
self._func_cache_var_keyword
|
|
196
|
+
if self._func_cache_var_keyword is not None else {}
|
|
197
|
+
)
|
|
198
|
+
e.add_note("In node: " + str(self._node_id))
|
|
199
|
+
setattr(e, "node_id", self._node_id)
|
|
200
|
+
self.status = NodeStatus.ERROR
|
|
201
|
+
flow._result[self._node_id] = ""
|
|
202
|
+
flow._error[self._node_id] = e
|
|
203
|
+
# 节点线程内的异常立即上报,但不打断其它节点。
|
|
204
|
+
self._report_status(flow, NodeStatus.ERROR, args)
|
|
205
|
+
|
|
206
|
+
def _run_impl(self, flow: 'Flow') -> None:
|
|
207
|
+
"Node._run_impl() 执行节点主体。"
|
|
208
|
+
self.status = NodeStatus.RUNNING
|
|
209
|
+
with self._value_lock:
|
|
210
|
+
dic = self._func_cache | (
|
|
211
|
+
# 合并参数,填充可变关键字参数(若有)
|
|
212
|
+
self._func_cache_var_keyword if
|
|
213
|
+
self._func_cache_var_keyword is not None else {}
|
|
214
|
+
)
|
|
215
|
+
# === 节点级缓存上下文(携带代码体哈希,函数体变更即失效) ===
|
|
216
|
+
cache_ctx = CacheContext(
|
|
217
|
+
cache=flow.cache, workflow=flow._workflow_id,
|
|
218
|
+
node_id=self._node_id, session_scope=flow._session_scope,
|
|
219
|
+
code_hash=self._code_hash)
|
|
220
|
+
node_info = cache_ctx.node_key(dic)
|
|
221
|
+
hit: dict[str, Any] | None = None
|
|
222
|
+
if node_info is not None and flow.cache is not None:
|
|
223
|
+
hit = flow.cache.get_node(node_info)
|
|
224
|
+
# 推送:节点开始运行(含全部已填充参数与缓存命中标记)
|
|
225
|
+
self._report_status(flow, NodeStatus.RUNNING, dic,
|
|
226
|
+
cached=hit is not None)
|
|
227
|
+
# 推送:广播本节点的源码
|
|
228
|
+
if flow._reporter is not None:
|
|
229
|
+
flow._reporter.report_node_code(flow._workflow_id, self._node_id,
|
|
230
|
+
self._node_name, self._source)
|
|
231
|
+
if hit is not None:
|
|
232
|
+
cached = hit
|
|
233
|
+
# 缓存命中:跳过节点函数执行,按描述符重放结果/下游调度
|
|
234
|
+
try:
|
|
235
|
+
self._replay_cached(flow, cached)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
e.add_note("In node: " + str(self._node_id))
|
|
238
|
+
setattr(e, "node_id", self._node_id)
|
|
239
|
+
self.status = NodeStatus.ERROR
|
|
240
|
+
flow._result[self._node_id] = ""
|
|
241
|
+
flow._error[self._node_id] = e
|
|
242
|
+
# 推送:缓存命中节点重放失败
|
|
243
|
+
self._report_status(flow, NodeStatus.ERROR, dic,
|
|
244
|
+
cached=True)
|
|
245
|
+
return
|
|
246
|
+
self.status = NodeStatus.DONE
|
|
247
|
+
# 推送:缓存命中的节点同样推送完成状态与参数
|
|
248
|
+
self._report_status(flow, NodeStatus.DONE, dic, cached=True)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
ret: NodeResult = None # 返回值
|
|
252
|
+
try:
|
|
253
|
+
if inspect.isgeneratorfunction(self._func):
|
|
254
|
+
it: TaskGenerator = self._func(**dic) # 调用生成器
|
|
255
|
+
result: Any | None = None # Agent 返回
|
|
256
|
+
call_number: int = 0 # 本节点内第几次 Agent/MultiAgent 调用
|
|
257
|
+
while True:
|
|
258
|
+
try: # 循环调用生成器
|
|
259
|
+
agents_call = it.send(result)
|
|
260
|
+
except StopIteration as e:
|
|
261
|
+
# 生成器结束
|
|
262
|
+
ret = e.value
|
|
263
|
+
break
|
|
264
|
+
# 执行子 Agent(透传缓存上下文与调用序号)
|
|
265
|
+
call_number += 1
|
|
266
|
+
call_info: AgentStatus = []
|
|
267
|
+
self.agents.append(call_info) # 利用引用
|
|
268
|
+
# 推送:节点即将启动本组 agents
|
|
269
|
+
if flow._reporter is not None:
|
|
270
|
+
flow._reporter.report_agents_start(
|
|
271
|
+
flow._workflow_id, self._node_id, call_number,
|
|
272
|
+
agents_call)
|
|
273
|
+
result = agents_call.run(call_info, flow._agent,
|
|
274
|
+
cache_ctx=cache_ctx,
|
|
275
|
+
report=flow._report,
|
|
276
|
+
node_id=self._node_id,
|
|
277
|
+
call_index=call_number)
|
|
278
|
+
else:
|
|
279
|
+
val = self._func(**dic) # 调用函数
|
|
280
|
+
assert not isinstance(val, Generator)
|
|
281
|
+
ret = val
|
|
282
|
+
except NodeTerminated:
|
|
283
|
+
self.status = NodeStatus.ERROR
|
|
284
|
+
self._report_status(flow, NodeStatus.ERROR, dic)
|
|
285
|
+
if flow._reporter is not None:
|
|
286
|
+
flow._reporter.report_node(flow._workflow_id, self._node_id,
|
|
287
|
+
"terminated", dic)
|
|
288
|
+
return
|
|
289
|
+
except WorkflowAborted:
|
|
290
|
+
self.status = NodeStatus.ERROR
|
|
291
|
+
self._report_status(flow, NodeStatus.ERROR, dic)
|
|
292
|
+
return
|
|
293
|
+
except Exception as e:
|
|
294
|
+
e.add_note("In node: " + str(self._node_id))
|
|
295
|
+
setattr(e, "node_id", self._node_id)
|
|
296
|
+
self.status = NodeStatus.ERROR
|
|
297
|
+
flow._result[self._node_id] = ""
|
|
298
|
+
flow._error[self._node_id] = e
|
|
299
|
+
# 推送:节点执行失败
|
|
300
|
+
self._report_status(flow, NodeStatus.ERROR, dic)
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
self.status = NodeStatus.DONE
|
|
304
|
+
# 推送:节点执行完成
|
|
305
|
+
self._report_status(flow, NodeStatus.DONE, dic)
|
|
306
|
+
|
|
307
|
+
# 写入节点缓存(描述符含不可序列化成分时由 put_node 自行忽略)
|
|
308
|
+
if flow.cache is not None and node_info is not None:
|
|
309
|
+
descriptor = _describe_node_result(ret)
|
|
310
|
+
if descriptor is not None:
|
|
311
|
+
flow.cache.put_node(node_info, descriptor)
|
|
312
|
+
|
|
313
|
+
# 无返回值
|
|
314
|
+
if ret is None:
|
|
315
|
+
return
|
|
316
|
+
# 返回值处理
|
|
317
|
+
if isinstance(ret, Result):
|
|
318
|
+
flow._result[self._node_id] = ret.result()
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
# 执行流程下一步
|
|
322
|
+
if isinstance(ret, Execute):
|
|
323
|
+
ret = [ret]
|
|
324
|
+
for i in ret:
|
|
325
|
+
i.run(flow)
|
|
326
|
+
|
|
327
|
+
def _replay_cached(self, flow: 'Flow', cached: dict[str, Any]) -> None:
|
|
328
|
+
"Node._replay_cached() 重放缓存的节点结果(终值或下游调度)"
|
|
329
|
+
kind = cached.get("kind")
|
|
330
|
+
if kind == "none":
|
|
331
|
+
return
|
|
332
|
+
if kind == "result":
|
|
333
|
+
flow._result[self._node_id] = cached.get("value")
|
|
334
|
+
return
|
|
335
|
+
if kind == "executes":
|
|
336
|
+
known = {node._node_id: node for node in flow._nodes}
|
|
337
|
+
items = cached.get("items")
|
|
338
|
+
if not isinstance(items, list):
|
|
339
|
+
items = []
|
|
340
|
+
items = cast(list[dict[str, Any]], items)
|
|
341
|
+
for item in items:
|
|
342
|
+
target_id = str(item.get("node_id"))
|
|
343
|
+
target = known.get(target_id)
|
|
344
|
+
if target is None:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"Cached target node not found: {target_id}")
|
|
347
|
+
# 重建调用点参数并走正常填充/调度路径,
|
|
348
|
+
# 完整保留跨节点部分参数累积语义。
|
|
349
|
+
args: Any = item.get("args")
|
|
350
|
+
args_value: dict[str, Any] = dict(cast(dict[str, Any], args)) if isinstance(
|
|
351
|
+
args, dict) else {}
|
|
352
|
+
Execute(target, args_value).run(flow)
|
|
353
|
+
return
|
|
354
|
+
raise ValueError(f"Invalid cached node result kind: {kind!r}")
|
|
355
|
+
|
|
356
|
+
def _create_thread_task(self, flow: 'Flow') -> threading.Thread:
|
|
357
|
+
"Node.thread_tast() 启动线程"
|
|
358
|
+
if (self._thread is not None and self._thread.is_alive()):
|
|
359
|
+
# 线程已启动
|
|
360
|
+
raise RuntimeError("Node thread already started")
|
|
361
|
+
|
|
362
|
+
def _target() -> None:
|
|
363
|
+
if flow._reporter is not None:
|
|
364
|
+
# 子线程内替换内置 print:节点日志以 node_log 事件推送
|
|
365
|
+
bind_node_print(flow._reporter, flow._workflow_id,
|
|
366
|
+
self._node_id)
|
|
367
|
+
try:
|
|
368
|
+
self._run(flow)
|
|
369
|
+
finally:
|
|
370
|
+
unbind_node_print()
|
|
371
|
+
|
|
372
|
+
self._thread = threading.Thread(target=_target, daemon=True)
|
|
373
|
+
self._thread.start()
|
|
374
|
+
_NODE_RUNTIMES[self._node_id] = (self, self._thread, flow)
|
|
375
|
+
return self._thread
|
|
376
|
+
|
|
377
|
+
def clean_cache(self, *val: str) -> None:
|
|
378
|
+
"Node.clean_cache() 清理缓存"
|
|
379
|
+
|
|
380
|
+
with self._value_lock:
|
|
381
|
+
if (len(val) == 0): # 清空所有缓存
|
|
382
|
+
for i in self._func_cache.keys():
|
|
383
|
+
self._func_cache[i] = _empty
|
|
384
|
+
self._func_cache_var_keyword = {}
|
|
385
|
+
self._func_cache_filled_num = 0
|
|
386
|
+
|
|
387
|
+
# 填充默认值
|
|
388
|
+
for i in self._func_default_args.keys():
|
|
389
|
+
self._func_cache[i] = self._func_default_args[i]
|
|
390
|
+
self._func_cache_filled_num += 1
|
|
391
|
+
else:
|
|
392
|
+
for i in val:
|
|
393
|
+
if i in self._func_cache:
|
|
394
|
+
# 标准参数
|
|
395
|
+
if isinstance(self._func_cache[i], _Empty):
|
|
396
|
+
continue
|
|
397
|
+
# 参数已填充
|
|
398
|
+
self._func_cache[i] = _empty
|
|
399
|
+
self._func_cache_filled_num -= 1
|
|
400
|
+
if i in self._func_default_args: # 填充默认值
|
|
401
|
+
self._func_cache[i] = self._func_default_args[i]
|
|
402
|
+
self._func_cache_filled_num += 1
|
|
403
|
+
elif self._func_cache_var_keyword is not None and i in self._func_cache_var_keyword:
|
|
404
|
+
# 可变关键字参数
|
|
405
|
+
del self._func_cache_var_keyword[i]
|
|
406
|
+
else: # 参数未定义
|
|
407
|
+
raise ValueError(f"Unknown argument: {i}")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class Execute:
|
|
411
|
+
"Execute 执行对象"
|
|
412
|
+
|
|
413
|
+
def __init__(self, node: Node, args: dict[str, Any]) -> None:
|
|
414
|
+
self._node = node
|
|
415
|
+
self._args = args
|
|
416
|
+
|
|
417
|
+
def run(self, flow: 'Flow') -> None:
|
|
418
|
+
"Execute.run() 执行节点"
|
|
419
|
+
|
|
420
|
+
self._node._fill_args(**self._args)
|
|
421
|
+
if self._node._check_all_filled():
|
|
422
|
+
# 所有参数已填充,执行节点
|
|
423
|
+
flow._taskque.put(self._node)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
type Agents = (
|
|
427
|
+
MultiAgent |
|
|
428
|
+
Agent
|
|
429
|
+
) # Agents 子 Agent 对象
|
|
430
|
+
type NodeResult = (
|
|
431
|
+
Execute |
|
|
432
|
+
list[Execute] |
|
|
433
|
+
tuple[Execute, ...] |
|
|
434
|
+
Result |
|
|
435
|
+
None
|
|
436
|
+
) # Nodes 节点返回对象
|
|
437
|
+
type TaskGenerator = (Generator[
|
|
438
|
+
Agents,
|
|
439
|
+
StructureOutput | str,
|
|
440
|
+
NodeResult
|
|
441
|
+
]) # TaskGenerator 任务生成器
|
|
442
|
+
type TaskGen = (Callable[
|
|
443
|
+
...,
|
|
444
|
+
TaskGenerator
|
|
445
|
+
] | Callable[
|
|
446
|
+
...,
|
|
447
|
+
NodeResult
|
|
448
|
+
]) # TaskGen 任务生成函数
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class NodeTerminated(BaseException):
|
|
452
|
+
"""stdin 命令终止正在运行的节点。"""
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class WorkflowAborted(BaseException):
|
|
456
|
+
"""stdin 命令终止整个工作流。"""
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
_NODE_RUNTIMES: dict[str, tuple[Node, threading.Thread, "Flow"]] = {}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class Flow:
|
|
463
|
+
"Flow 工作流对象"
|
|
464
|
+
|
|
465
|
+
def __init__(self, workflow_id: str, /, *, agent: AgentClientInterface | None = None,
|
|
466
|
+
cache: bool = True, report: bool | None = None) -> None:
|
|
467
|
+
if agent is None:
|
|
468
|
+
agent = gen_conn_info_from_env()
|
|
469
|
+
self._workflow_id = workflow_id
|
|
470
|
+
self._nodes: list[Node] = []
|
|
471
|
+
self._taskque: queue.Queue[Node] = queue.Queue()
|
|
472
|
+
self._result: dict[str, Any] = {}
|
|
473
|
+
self._error: dict[str, Exception | None] = {}
|
|
474
|
+
self._agent = agent
|
|
475
|
+
self._freeze = False
|
|
476
|
+
self._graph: Graph | None = None
|
|
477
|
+
self._entries: list[Node] = []
|
|
478
|
+
# === 工作流缓存(.alkaid0/workflow.db,cache=False 可关闭) ===
|
|
479
|
+
self.cache: WorkflowCache | None = (
|
|
480
|
+
WorkflowCache(default_db_path()) if cache else None)
|
|
481
|
+
self._session_scope: str = "" # ALKAID0_WORKFLOW_SESSION_ID 作用域
|
|
482
|
+
# === stdio JSONL 状态上报(report=True 或 ALKAID0_WORKFLOW_REPORT=1 开启) ===
|
|
483
|
+
if report is None:
|
|
484
|
+
report = os.environ.get("ALKAID0_WORKFLOW_REPORT") == "1"
|
|
485
|
+
self._report: bool = report
|
|
486
|
+
self._reporter = optional_reporter(report)
|
|
487
|
+
self._abort = False
|
|
488
|
+
|
|
489
|
+
def node(self, node_name: str, /) -> Callable[[TaskGen], Node]:
|
|
490
|
+
"@Flow.node() 新增节点"
|
|
491
|
+
|
|
492
|
+
if self._freeze:
|
|
493
|
+
raise RuntimeError("Flow is frozen")
|
|
494
|
+
|
|
495
|
+
def wrapper(func: TaskGen) -> Node:
|
|
496
|
+
"Flow.node().wrapper() 节点包装器"
|
|
497
|
+
|
|
498
|
+
if self._freeze:
|
|
499
|
+
raise RuntimeError("Flow is frozen")
|
|
500
|
+
|
|
501
|
+
# 包装函数
|
|
502
|
+
funcname: str = func.__name__
|
|
503
|
+
node = Node(node_name=node_name, func=func, node_id=funcname)
|
|
504
|
+
self._nodes.append(node)
|
|
505
|
+
return node
|
|
506
|
+
return wrapper
|
|
507
|
+
|
|
508
|
+
def _loop(self) -> None:
|
|
509
|
+
"Flow._loop() 循环执行任务队列"
|
|
510
|
+
|
|
511
|
+
threads: list[threading.Thread] = []
|
|
512
|
+
while True:
|
|
513
|
+
try:
|
|
514
|
+
node = self._taskque.get(False) # 取队列
|
|
515
|
+
except queue.Empty: # 队列为空
|
|
516
|
+
alive = any(t.is_alive() for t in threads)
|
|
517
|
+
if not alive or self._abort:
|
|
518
|
+
break
|
|
519
|
+
time.sleep(1)
|
|
520
|
+
continue
|
|
521
|
+
if not self._abort:
|
|
522
|
+
threads.append(node._create_thread_task(self))
|
|
523
|
+
|
|
524
|
+
def _generate_graph_edges(self) -> dict[str, set[str]]:
|
|
525
|
+
"Node._generate_graph_edges() 生成流程图图边"
|
|
526
|
+
|
|
527
|
+
edges: dict[str, set[str]] = {} # 边集
|
|
528
|
+
known_node_ids = {
|
|
529
|
+
node._node_id for node in self._nodes} # 已知节点 ID,用于查找
|
|
530
|
+
for nodex in self._nodes: # 遍历节点
|
|
531
|
+
func = nodex._func
|
|
532
|
+
try: # 获取函数源码
|
|
533
|
+
src = inspect.getsource(func)
|
|
534
|
+
except OSError:
|
|
535
|
+
raise ValueError("Function source not found")
|
|
536
|
+
try: # 解析源码
|
|
537
|
+
tree = ast.parse(textwrap.dedent(src))
|
|
538
|
+
except SyntaxError:
|
|
539
|
+
raise ValueError("Function source syntax error")
|
|
540
|
+
|
|
541
|
+
# 取函数体
|
|
542
|
+
func_def = None
|
|
543
|
+
for ast_node in ast.walk(tree):
|
|
544
|
+
if isinstance(ast_node, ast.FunctionDef) and ast_node.name == nodex._func.__name__:
|
|
545
|
+
func_def = ast_node
|
|
546
|
+
break
|
|
547
|
+
if func_def is None:
|
|
548
|
+
raise ValueError("Function definition not found")
|
|
549
|
+
|
|
550
|
+
# 查找函数调用
|
|
551
|
+
referenced: set[str] = set()
|
|
552
|
+
|
|
553
|
+
class CallCollector(ast.NodeVisitor):
|
|
554
|
+
"Node._generate_graph_edges.CallCollector 调用收集器"
|
|
555
|
+
|
|
556
|
+
def visit_Call(self, node: ast.Call) -> Any:
|
|
557
|
+
"Node._generate_graph_edges.CallCollector.visit_Call() 遍历调用者"
|
|
558
|
+
|
|
559
|
+
# 查找调用者
|
|
560
|
+
if isinstance(node.func, ast.Name):
|
|
561
|
+
if node.func.id in known_node_ids:
|
|
562
|
+
referenced.add(node.func.id)
|
|
563
|
+
|
|
564
|
+
# 递归遍历参数及内部表达式
|
|
565
|
+
self.generic_visit(node)
|
|
566
|
+
|
|
567
|
+
# 启动收集
|
|
568
|
+
CallCollector().visit(func_def)
|
|
569
|
+
|
|
570
|
+
# 添加边
|
|
571
|
+
edges[nodex._node_id] = referenced
|
|
572
|
+
|
|
573
|
+
return edges
|
|
574
|
+
|
|
575
|
+
def _generate_graph(self, tasks: tuple[Execute, ...]) -> Graph:
|
|
576
|
+
"Node._generate_graph() 生成流程图"
|
|
577
|
+
|
|
578
|
+
edges = self._generate_graph_edges()
|
|
579
|
+
return {
|
|
580
|
+
"nodes": {
|
|
581
|
+
node._node_id: { # 节点信息
|
|
582
|
+
"name": node._node_id,
|
|
583
|
+
} for node in self._nodes
|
|
584
|
+
},
|
|
585
|
+
"edges": edges,
|
|
586
|
+
"start": {
|
|
587
|
+
i._node._node_id
|
|
588
|
+
for i in tasks
|
|
589
|
+
if i._node._check_all_filled() # 检查所有参数是否填充,可以正常执行
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
def _raise_exceptions(self) -> None:
|
|
594
|
+
"Flow._raise_exceptions() 抛出异常"
|
|
595
|
+
exceptions: list[Exception] = [
|
|
596
|
+
i for i in self._error.values() if i is not None]
|
|
597
|
+
if len(exceptions) > 0:
|
|
598
|
+
raise ExceptionGroup(
|
|
599
|
+
f"{len(exceptions)} node(s) run failed", exceptions)
|
|
600
|
+
|
|
601
|
+
def execute(self, *tasks: Execute) -> None:
|
|
602
|
+
"Flow.execute() 执行工作流"
|
|
603
|
+
|
|
604
|
+
# 每次执行时读取会话作用域,作为缓存联合键的组成部分
|
|
605
|
+
self._session_scope = os.environ.get("ALKAID0_WORKFLOW_SESSION_ID", "")
|
|
606
|
+
self._freeze = True
|
|
607
|
+
for i in tasks: # 填充参数
|
|
608
|
+
i.run(self)
|
|
609
|
+
self._graph = self._generate_graph(tasks)
|
|
610
|
+
if self._reporter is not None:
|
|
611
|
+
# 上报本次生成的流程图(JSONL,包裹 bracketed paste 标记)
|
|
612
|
+
self._reporter.report_graph(self._workflow_id, self._graph)
|
|
613
|
+
self._loop()
|
|
614
|
+
self._raise_exceptions()
|
|
615
|
+
|
|
616
|
+
def run(self, *tasks: Execute) -> NoReturn:
|
|
617
|
+
"""Node.run() 在 fork 子进程执行工作流,主进程监听 stdin JSONL。"""
|
|
618
|
+
self._report = True
|
|
619
|
+
if self._reporter is None:
|
|
620
|
+
self._reporter = optional_reporter(True)
|
|
621
|
+
if self._agent is None:
|
|
622
|
+
raise ValueError("No availble agent found!")
|
|
623
|
+
control: Any = multiprocessing.get_context("fork").Queue()
|
|
624
|
+
process = multiprocessing.get_context("fork").Process(
|
|
625
|
+
target=self._execute_child, args=(tasks, control))
|
|
626
|
+
process.start()
|
|
627
|
+
stdin_done = threading.Event()
|
|
628
|
+
|
|
629
|
+
def read_stdin() -> None:
|
|
630
|
+
try:
|
|
631
|
+
for line in sys.stdin:
|
|
632
|
+
try:
|
|
633
|
+
command = json.loads(line)
|
|
634
|
+
except json.JSONDecodeError:
|
|
635
|
+
continue
|
|
636
|
+
if isinstance(command, dict):
|
|
637
|
+
command = cast(dict[str, str], command)
|
|
638
|
+
control.put(command)
|
|
639
|
+
if command.get("cmd") == "shutdown":
|
|
640
|
+
return
|
|
641
|
+
finally:
|
|
642
|
+
stdin_done.set()
|
|
643
|
+
|
|
644
|
+
# Reading stdin can block forever, so keep it off the main thread. The
|
|
645
|
+
# main thread can then observe a worker crash or normal exit and stop.
|
|
646
|
+
threading.Thread(target=read_stdin, daemon=True).start()
|
|
647
|
+
try:
|
|
648
|
+
while process.is_alive() and not stdin_done.is_set():
|
|
649
|
+
process.join(0.1)
|
|
650
|
+
finally:
|
|
651
|
+
if process.is_alive():
|
|
652
|
+
control.put({"cmd": "shutdown"})
|
|
653
|
+
process.join(10)
|
|
654
|
+
if process.is_alive():
|
|
655
|
+
process.terminate()
|
|
656
|
+
process.join(2)
|
|
657
|
+
# Preserve a non-zero exit status when the child failed unexpectedly.
|
|
658
|
+
sys.exit(process.exitcode if process.exitcode is not None else 0)
|
|
659
|
+
|
|
660
|
+
def _execute_child(self, tasks: tuple[Execute, ...], control: Any) -> None:
|
|
661
|
+
def listen() -> None:
|
|
662
|
+
while True:
|
|
663
|
+
command = control.get()
|
|
664
|
+
if isinstance(command, dict):
|
|
665
|
+
command = cast(dict[str, str], command)
|
|
666
|
+
self._handle_command(command)
|
|
667
|
+
if command.get("cmd") == "shutdown":
|
|
668
|
+
return
|
|
669
|
+
threading.Thread(target=listen, daemon=True).start()
|
|
670
|
+
self.execute(*tasks)
|
|
671
|
+
|
|
672
|
+
def _handle_command(self, command: dict[str, Any]) -> None:
|
|
673
|
+
cmd = command.get("cmd")
|
|
674
|
+
if cmd == "shutdown":
|
|
675
|
+
self._abort = True
|
|
676
|
+
for node, thread, _ in list(_NODE_RUNTIMES.values()):
|
|
677
|
+
if thread.is_alive() and thread.ident is not None:
|
|
678
|
+
raise_in_thread(thread.ident, WorkflowAborted)
|
|
679
|
+
return
|
|
680
|
+
if cmd == "agent":
|
|
681
|
+
group = find_agent_group(str(command.get("nodeId", "")),
|
|
682
|
+
int(command.get("callIndex", 0)))
|
|
683
|
+
if group is None:
|
|
684
|
+
return
|
|
685
|
+
index = int(command.get("agentIndex", 0)) - 1
|
|
686
|
+
if command.get("action") == "terminate":
|
|
687
|
+
group.terminate(index)
|
|
688
|
+
elif command.get("action") == "retry":
|
|
689
|
+
group.retry(index)
|
|
690
|
+
return
|
|
691
|
+
if cmd == "node":
|
|
692
|
+
runtime = _NODE_RUNTIMES.get(str(command.get("nodeId", "")))
|
|
693
|
+
if runtime is None:
|
|
694
|
+
return
|
|
695
|
+
node, thread, flow = runtime
|
|
696
|
+
if not thread.is_alive() or thread.ident is None:
|
|
697
|
+
return
|
|
698
|
+
raise_in_thread(thread.ident, NodeTerminated)
|
|
699
|
+
if command.get("action") == "restart":
|
|
700
|
+
thread.join(2)
|
|
701
|
+
if not thread.is_alive():
|
|
702
|
+
node._create_thread_task(flow)
|
dynworkflow/py.typed
ADDED
|
File without changes
|