agstack 1.4.0__tar.gz → 1.6.0__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.
- {agstack-1.4.0 → agstack-1.6.0}/PKG-INFO +2 -2
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/__init__.py +4 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/flow.py +186 -197
- agstack-1.6.0/agstack/llm/flow/nodes/__init__.py +39 -0
- agstack-1.6.0/agstack/llm/flow/nodes/agent_node.py +55 -0
- agstack-1.6.0/agstack/llm/flow/nodes/base.py +59 -0
- agstack-1.6.0/agstack/llm/flow/nodes/detect_node.py +93 -0
- agstack-1.6.0/agstack/llm/flow/nodes/llm_chat_node.py +152 -0
- agstack-1.6.0/agstack/llm/flow/nodes/llm_embed_node.py +39 -0
- agstack-1.6.0/agstack/llm/flow/nodes/llm_rerank_node.py +49 -0
- agstack-1.4.0/agstack/llm/flow/sandbox.py → agstack-1.6.0/agstack/llm/flow/nodes/python_node.py +44 -8
- agstack-1.6.0/agstack/llm/flow/nodes/tool_node.py +40 -0
- agstack-1.6.0/agstack/llm/flow/sandbox.py +8 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack.egg-info/PKG-INFO +2 -2
- {agstack-1.4.0 → agstack-1.6.0}/agstack.egg-info/SOURCES.txt +9 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack.egg-info/requires.txt +1 -1
- {agstack-1.4.0 → agstack-1.6.0}/pyproject.toml +2 -2
- {agstack-1.4.0 → agstack-1.6.0}/LICENSE +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/README.md +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/config/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/config/logger.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/config/manager.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/config/types.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/contexts.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/decorators.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/events.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/exceptions.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/fastapi/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/fastapi/exception.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/fastapi/middleware.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/fastapi/offline.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/fastapi/sse.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/infra/db/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/infra/es/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/infra/kg/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/infra/mq/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/client.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/agent.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/context.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/event.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/exceptions.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/factory.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/loader.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/records.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/registry.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/state.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/flow/tool.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/prompts.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/llm/token.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/registry.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/schema.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/security/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/security/casbin.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/security/crypt.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack/status.py +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack.egg-info/dependency_links.txt +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/agstack.egg-info/top_level.txt +0 -0
- {agstack-1.4.0 → agstack-1.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Production-ready toolkit for building FastAPI and LLM applications
|
|
5
5
|
Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
6
6
|
Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
@@ -35,7 +35,7 @@ Requires-Dist: python-multipart>=0.0.20
|
|
|
35
35
|
Requires-Dist: requests>=2.32.5
|
|
36
36
|
Requires-Dist: RestrictedPython>=7.0
|
|
37
37
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.48
|
|
38
|
-
Requires-Dist: sqlobjects>=1.
|
|
38
|
+
Requires-Dist: sqlobjects>=1.4.0
|
|
39
39
|
Requires-Dist: tiktoken>=0.12.0
|
|
40
40
|
Requires-Dist: uvicorn>=0.41.0
|
|
41
41
|
Dynamic: license-file
|
|
@@ -18,6 +18,7 @@ from .exceptions import (
|
|
|
18
18
|
from .factory import create_agent, create_tool
|
|
19
19
|
from .flow import Flow
|
|
20
20
|
from .loader import FlowLoader
|
|
21
|
+
from .nodes import NodeHandler, register_node_handler
|
|
21
22
|
from .records import Record, Status
|
|
22
23
|
from .registry import registry
|
|
23
24
|
from .state import FlowState
|
|
@@ -32,6 +33,9 @@ __all__ = [
|
|
|
32
33
|
"Flow",
|
|
33
34
|
"FlowContext",
|
|
34
35
|
"Usage",
|
|
36
|
+
# 节点处理器
|
|
37
|
+
"NodeHandler",
|
|
38
|
+
"register_node_handler",
|
|
35
39
|
# AG-UI 协议
|
|
36
40
|
"EventType",
|
|
37
41
|
"event",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2020-
|
|
1
|
+
# Copyright (c) 2020-2026 XtraVisions, All rights reserved.
|
|
2
2
|
|
|
3
3
|
"""Flow 定义和执行"""
|
|
4
4
|
|
|
@@ -9,12 +9,21 @@ from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
11
|
from . import event
|
|
12
|
-
from .exceptions import
|
|
13
|
-
from .registry import registry
|
|
12
|
+
from .exceptions import NodeExecutionError
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
if TYPE_CHECKING:
|
|
17
16
|
from .context import FlowContext
|
|
17
|
+
from .nodes.base import NodeHandler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class RetryPolicy:
|
|
22
|
+
"""节点重试策略"""
|
|
23
|
+
|
|
24
|
+
max_retries: int = 0 # 0 = 不重试
|
|
25
|
+
delay: float = 1.0 # 初始延迟(秒)
|
|
26
|
+
backoff: float = 2.0 # 退避倍数
|
|
18
27
|
|
|
19
28
|
|
|
20
29
|
class _SafeFormatDict(dict):
|
|
@@ -35,6 +44,31 @@ class Flow:
|
|
|
35
44
|
edges: list[dict[str, Any]] = field(default_factory=list)
|
|
36
45
|
variables: dict[str, Any] = field(default_factory=dict)
|
|
37
46
|
|
|
47
|
+
_node_handlers: dict[str, "NodeHandler"] = field(default_factory=dict, init=False, repr=False)
|
|
48
|
+
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
from .nodes import _global_node_handlers, builtin_handlers
|
|
51
|
+
|
|
52
|
+
for handler in builtin_handlers:
|
|
53
|
+
self._node_handlers[handler.node_type] = handler
|
|
54
|
+
# 全局注册的自定义 handler 可覆盖内置
|
|
55
|
+
self._node_handlers.update(_global_node_handlers)
|
|
56
|
+
|
|
57
|
+
# ── 重试策略 ──
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _get_retry_policy(node: dict) -> RetryPolicy:
|
|
61
|
+
"""从节点 config 解析重试策略"""
|
|
62
|
+
config = node.get("config", {})
|
|
63
|
+
retry_cfg = config.get("retry", {})
|
|
64
|
+
if not retry_cfg:
|
|
65
|
+
return RetryPolicy()
|
|
66
|
+
return RetryPolicy(
|
|
67
|
+
max_retries=retry_cfg.get("max_retries", 0),
|
|
68
|
+
delay=retry_cfg.get("delay", 1.0),
|
|
69
|
+
backoff=retry_cfg.get("backoff", 2.0),
|
|
70
|
+
)
|
|
71
|
+
|
|
38
72
|
# ── 边驱动路由 ──
|
|
39
73
|
|
|
40
74
|
def _resolve_next_node(self, current_id: str, result: str | None = None) -> str | None:
|
|
@@ -46,33 +80,22 @@ class Flow:
|
|
|
46
80
|
return edge.get("target")
|
|
47
81
|
return None
|
|
48
82
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"""调用 LLM 判断条件是否匹配"""
|
|
53
|
-
config = node.get("config", {})
|
|
54
|
-
topic = config.get("topic", "")
|
|
55
|
-
query = context.get_variable("query", "")
|
|
56
|
-
|
|
57
|
-
prompt = (
|
|
58
|
-
f"判断以下问题是否属于「{topic}」相关问题。\n"
|
|
59
|
-
f"问题:{query}\n"
|
|
60
|
-
f'仅回复 JSON:{{"result": "match"}} 或 {{"result": "reject"}}'
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
from ..client import get_llm_client
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _extract_route_key(result: Any) -> str:
|
|
85
|
+
"""从节点执行结果中提取路由键。
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
text = response.choices[0].message.content or ""
|
|
87
|
+
支持 ``{"result": "qa"}`` 形式的 JSON 字符串,
|
|
88
|
+
以及纯字符串结果。
|
|
89
|
+
"""
|
|
90
|
+
if not isinstance(result, str):
|
|
91
|
+
return "done"
|
|
72
92
|
try:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
parsed = _json.loads(result)
|
|
94
|
+
if isinstance(parsed, dict) and "result" in parsed:
|
|
95
|
+
return str(parsed["result"])
|
|
96
|
+
except (ValueError, TypeError):
|
|
97
|
+
pass
|
|
98
|
+
return result or "done"
|
|
76
99
|
|
|
77
100
|
# ── message 节点 ──
|
|
78
101
|
|
|
@@ -86,6 +109,62 @@ class Flow:
|
|
|
86
109
|
yield event.text_message_content(message_id=msg_id, delta=text)
|
|
87
110
|
yield event.text_message_end(message_id=msg_id)
|
|
88
111
|
|
|
112
|
+
# ── 带重试的节点执行(统一走 NodeHandler) ──
|
|
113
|
+
|
|
114
|
+
async def _execute_node_with_retry(
|
|
115
|
+
self,
|
|
116
|
+
node: dict,
|
|
117
|
+
context: "FlowContext",
|
|
118
|
+
node_id: str,
|
|
119
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
120
|
+
"""执行节点,带重试策略,产出 AG-UI 事件"""
|
|
121
|
+
node_type: str = node.get("type", "")
|
|
122
|
+
handler = self._node_handlers.get(node_type)
|
|
123
|
+
if not handler:
|
|
124
|
+
yield event.run_error(
|
|
125
|
+
message=f"Unknown node type: {node_type}",
|
|
126
|
+
code="UNKNOWN_NODE_TYPE",
|
|
127
|
+
)
|
|
128
|
+
raise NodeExecutionError("UNKNOWN_NODE_TYPE", args={"node_type": node_type})
|
|
129
|
+
|
|
130
|
+
policy = self._get_retry_policy(node)
|
|
131
|
+
label = handler.get_step_name(node, node_id)
|
|
132
|
+
last_error: Exception | None = None
|
|
133
|
+
|
|
134
|
+
for attempt in range(policy.max_retries + 1):
|
|
135
|
+
try:
|
|
136
|
+
if attempt > 0:
|
|
137
|
+
wait = policy.delay * (policy.backoff ** (attempt - 1))
|
|
138
|
+
await asyncio.sleep(wait)
|
|
139
|
+
yield event.custom(
|
|
140
|
+
name="node_retry",
|
|
141
|
+
value={
|
|
142
|
+
"nodeId": node_id,
|
|
143
|
+
"nodeType": node_type,
|
|
144
|
+
"label": label,
|
|
145
|
+
"attempt": attempt + 1,
|
|
146
|
+
"maxAttempts": policy.max_retries + 1,
|
|
147
|
+
"error": str(last_error),
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async for evt in handler.stream(node, context, node_id):
|
|
152
|
+
yield evt
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
last_error = e
|
|
157
|
+
if attempt < policy.max_retries:
|
|
158
|
+
continue
|
|
159
|
+
yield event.run_error(
|
|
160
|
+
message=str(e),
|
|
161
|
+
code=type(e).__name__,
|
|
162
|
+
)
|
|
163
|
+
raise NodeExecutionError(
|
|
164
|
+
"NODE_EXECUTION_FAILED",
|
|
165
|
+
args={"node_id": node_id, "error": str(e)},
|
|
166
|
+
) from e
|
|
167
|
+
|
|
89
168
|
# ── 执行入口 ──
|
|
90
169
|
|
|
91
170
|
async def run(self, context: "FlowContext") -> dict[str, Any]:
|
|
@@ -97,8 +176,13 @@ class Flow:
|
|
|
97
176
|
if not node_id:
|
|
98
177
|
continue
|
|
99
178
|
context.current_node = node_id
|
|
100
|
-
|
|
101
|
-
|
|
179
|
+
node_type: str = node.get("type", "")
|
|
180
|
+
handler = self._node_handlers.get(node_type)
|
|
181
|
+
if handler:
|
|
182
|
+
result = await handler.execute(node, context)
|
|
183
|
+
context.set_node_result(node_id, result)
|
|
184
|
+
else:
|
|
185
|
+
raise NodeExecutionError("UNKNOWN_NODE_TYPE", args={"node_type": node_type})
|
|
102
186
|
else:
|
|
103
187
|
# edge 驱动执行
|
|
104
188
|
current_node_id: str | None = self.nodes[0]["id"] if self.nodes else None
|
|
@@ -107,22 +191,14 @@ class Flow:
|
|
|
107
191
|
if not node:
|
|
108
192
|
break
|
|
109
193
|
context.current_node = current_node_id
|
|
110
|
-
node_type = node.get("type")
|
|
194
|
+
node_type: str = node.get("type", "")
|
|
111
195
|
|
|
112
|
-
if node_type == "
|
|
113
|
-
result = await self._evaluate_condition(node, context)
|
|
114
|
-
context.set_node_result(current_node_id, result)
|
|
115
|
-
current_node_id = self._resolve_next_node(current_node_id, result)
|
|
116
|
-
elif node_type == "message":
|
|
196
|
+
if node_type == "message":
|
|
117
197
|
config = node.get("config", {})
|
|
118
198
|
template = config.get("content", "")
|
|
119
199
|
text = template.format_map(_SafeFormatDict(context.variables))
|
|
120
200
|
context.set_node_result(current_node_id, text)
|
|
121
201
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
122
|
-
elif node_type in ("agent", "tool"):
|
|
123
|
-
result = await self._execute_node(node, context)
|
|
124
|
-
context.set_node_result(current_node_id, result)
|
|
125
|
-
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
126
202
|
|
|
127
203
|
elif node_type == "parallel":
|
|
128
204
|
config = node.get("config", {})
|
|
@@ -133,9 +209,11 @@ class Flow:
|
|
|
133
209
|
if not branch_node:
|
|
134
210
|
return
|
|
135
211
|
context.current_node = branch_id
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
212
|
+
branch_type: str = branch_node.get("type", "")
|
|
213
|
+
branch_handler = self._node_handlers.get(branch_type)
|
|
214
|
+
if branch_handler:
|
|
215
|
+
result = await branch_handler.execute(branch_node, context)
|
|
216
|
+
context.set_node_result(branch_id, result)
|
|
139
217
|
|
|
140
218
|
await asyncio.gather(*[_run_branch(bid) for bid in branches])
|
|
141
219
|
context.set_node_result(current_node_id, "done")
|
|
@@ -163,9 +241,11 @@ class Flow:
|
|
|
163
241
|
body_node = self.get_node_config(body_node_id)
|
|
164
242
|
if not body_node:
|
|
165
243
|
continue
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
244
|
+
body_type: str = body_node.get("type", "")
|
|
245
|
+
body_handler = self._node_handlers.get(body_type)
|
|
246
|
+
if body_handler:
|
|
247
|
+
body_result = await body_handler.execute(body_node, context)
|
|
248
|
+
context.set_node_result(body_node_id, body_result)
|
|
169
249
|
if body_nodes:
|
|
170
250
|
results.append(context.node_results.get(body_nodes[-1]))
|
|
171
251
|
|
|
@@ -187,9 +267,11 @@ class Flow:
|
|
|
187
267
|
body_node = self.get_node_config(body_node_id)
|
|
188
268
|
if not body_node:
|
|
189
269
|
continue
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
270
|
+
body_type: str = body_node.get("type", "")
|
|
271
|
+
body_handler = self._node_handlers.get(body_type)
|
|
272
|
+
if body_handler:
|
|
273
|
+
body_result = await body_handler.execute(body_node, context)
|
|
274
|
+
context.set_node_result(body_node_id, body_result)
|
|
193
275
|
if condition_node_id:
|
|
194
276
|
cond_result = context.node_results.get(condition_node_id, "")
|
|
195
277
|
if isinstance(cond_result, str):
|
|
@@ -204,27 +286,18 @@ class Flow:
|
|
|
204
286
|
context.set_node_result(current_node_id, "done")
|
|
205
287
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
206
288
|
|
|
207
|
-
elif node_type
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
code_str = config.get("code", "")
|
|
217
|
-
py_result = execute_python_node(code_str, resolved_inputs)
|
|
218
|
-
|
|
219
|
-
outputs_spec: dict[str, Any] = config.get("outputs", {})
|
|
220
|
-
for key in outputs_spec:
|
|
221
|
-
if key in py_result:
|
|
222
|
-
context.set_variable(key, py_result[key])
|
|
289
|
+
elif node_type in self._node_handlers:
|
|
290
|
+
# 所有执行类节点统一分发
|
|
291
|
+
handler = self._node_handlers[node_type]
|
|
292
|
+
result = await handler.execute(node, context)
|
|
293
|
+
context.set_node_result(current_node_id, result)
|
|
294
|
+
route_key = self._extract_route_key(result)
|
|
295
|
+
current_node_id = self._resolve_next_node(current_node_id, route_key) or self._resolve_next_node(
|
|
296
|
+
current_node_id, "done"
|
|
297
|
+
)
|
|
223
298
|
|
|
224
|
-
context.set_node_result(current_node_id, _json.dumps(py_result, ensure_ascii=False))
|
|
225
|
-
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
226
299
|
else:
|
|
227
|
-
|
|
300
|
+
raise NodeExecutionError("UNKNOWN_NODE_TYPE", args={"node_type": node_type})
|
|
228
301
|
|
|
229
302
|
return context.node_results
|
|
230
303
|
|
|
@@ -251,24 +324,17 @@ class Flow:
|
|
|
251
324
|
continue
|
|
252
325
|
|
|
253
326
|
context.current_node = node_id
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if
|
|
257
|
-
|
|
258
|
-
yield event.step_started(step_name=f"agent:{agent_name}")
|
|
259
|
-
self._set_parameters(node.get("config", {}), context)
|
|
260
|
-
ag = self._create_agent(node.get("config", {}))
|
|
261
|
-
async for evt in ag.stream(context):
|
|
327
|
+
node_type: str = node.get("type", "")
|
|
328
|
+
|
|
329
|
+
if node_type in self._node_handlers:
|
|
330
|
+
async for evt in self._execute_node_with_retry(node, context, node_id):
|
|
262
331
|
yield evt
|
|
263
|
-
result = context.get_last_output(ag.name) or ""
|
|
264
|
-
context.set_node_result(node_id, result)
|
|
265
|
-
yield event.step_finished(step_name=f"agent:{agent_name}")
|
|
266
332
|
else:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
333
|
+
yield event.run_error(
|
|
334
|
+
message=f"Unknown node type: {node_type}",
|
|
335
|
+
code="UNKNOWN_NODE_TYPE",
|
|
336
|
+
)
|
|
337
|
+
raise NodeExecutionError("UNKNOWN_NODE_TYPE", args={"node_type": node_type})
|
|
272
338
|
|
|
273
339
|
async def _stream_edge_driven(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
274
340
|
"""边驱动流式执行"""
|
|
@@ -277,41 +343,20 @@ class Flow:
|
|
|
277
343
|
while current_node_id:
|
|
278
344
|
node = self.get_node_config(current_node_id)
|
|
279
345
|
if not node:
|
|
280
|
-
|
|
346
|
+
yield event.run_error(
|
|
347
|
+
message=f"Node not found: {current_node_id}",
|
|
348
|
+
code="NODE_NOT_FOUND",
|
|
349
|
+
)
|
|
350
|
+
raise NodeExecutionError("NODE_NOT_FOUND", args={"node_id": current_node_id})
|
|
281
351
|
|
|
282
352
|
context.current_node = current_node_id
|
|
283
|
-
node_type = node.get("type")
|
|
353
|
+
node_type: str = node.get("type", "")
|
|
284
354
|
|
|
285
|
-
if node_type == "
|
|
286
|
-
result = await self._evaluate_condition(node, context)
|
|
287
|
-
context.set_node_result(current_node_id, result)
|
|
288
|
-
current_node_id = self._resolve_next_node(current_node_id, result)
|
|
289
|
-
|
|
290
|
-
elif node_type == "message":
|
|
355
|
+
if node_type == "message":
|
|
291
356
|
async for evt in self._emit_message(node, context):
|
|
292
357
|
yield evt
|
|
293
358
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
294
359
|
|
|
295
|
-
elif node_type == "agent":
|
|
296
|
-
agent_name = node.get("config", {}).get("agent_name", "")
|
|
297
|
-
yield event.step_started(step_name=f"agent:{agent_name}")
|
|
298
|
-
self._set_parameters(node.get("config", {}), context)
|
|
299
|
-
ag = self._create_agent(node.get("config", {}))
|
|
300
|
-
async for evt in ag.stream(context):
|
|
301
|
-
yield evt
|
|
302
|
-
result = context.get_last_output(ag.name) or ""
|
|
303
|
-
context.set_node_result(current_node_id, result)
|
|
304
|
-
yield event.step_finished(step_name=f"agent:{agent_name}")
|
|
305
|
-
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
306
|
-
|
|
307
|
-
elif node_type == "tool":
|
|
308
|
-
tool_name = node.get("config", {}).get("tool_name", "")
|
|
309
|
-
yield event.step_started(step_name=f"tool:{tool_name}")
|
|
310
|
-
result = await self._execute_node(node, context)
|
|
311
|
-
context.set_node_result(current_node_id, result)
|
|
312
|
-
yield event.step_finished(step_name=f"tool:{tool_name}")
|
|
313
|
-
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
314
|
-
|
|
315
360
|
elif node_type == "parallel":
|
|
316
361
|
config = node.get("config", {})
|
|
317
362
|
branches = config.get("branches", [])
|
|
@@ -322,9 +367,11 @@ class Flow:
|
|
|
322
367
|
if not branch_node:
|
|
323
368
|
return
|
|
324
369
|
context.current_node = branch_id
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
370
|
+
branch_type = branch_node.get("type", "")
|
|
371
|
+
branch_handler = self._node_handlers.get(branch_type)
|
|
372
|
+
if branch_handler:
|
|
373
|
+
result = await branch_handler.execute(branch_node, context)
|
|
374
|
+
context.set_node_result(branch_id, result)
|
|
328
375
|
|
|
329
376
|
await asyncio.gather(*[_exec_branch(bid) for bid in branches])
|
|
330
377
|
context.set_node_result(current_node_id, "done")
|
|
@@ -354,9 +401,11 @@ class Flow:
|
|
|
354
401
|
body_node = self.get_node_config(body_node_id)
|
|
355
402
|
if not body_node:
|
|
356
403
|
continue
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
404
|
+
body_type = body_node.get("type", "")
|
|
405
|
+
body_handler = self._node_handlers.get(body_type)
|
|
406
|
+
if body_handler:
|
|
407
|
+
body_result = await body_handler.execute(body_node, context)
|
|
408
|
+
context.set_node_result(body_node_id, body_result)
|
|
360
409
|
if body_nodes:
|
|
361
410
|
results.append(context.node_results.get(body_nodes[-1]))
|
|
362
411
|
|
|
@@ -380,9 +429,11 @@ class Flow:
|
|
|
380
429
|
body_node = self.get_node_config(body_node_id)
|
|
381
430
|
if not body_node:
|
|
382
431
|
continue
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
432
|
+
body_type = body_node.get("type", "")
|
|
433
|
+
body_handler = self._node_handlers.get(body_type)
|
|
434
|
+
if body_handler:
|
|
435
|
+
body_result = await body_handler.execute(body_node, context)
|
|
436
|
+
context.set_node_result(body_node_id, body_result)
|
|
386
437
|
# 检查终止条件
|
|
387
438
|
if condition_node_id:
|
|
388
439
|
cond_result = context.node_results.get(condition_node_id, "")
|
|
@@ -399,84 +450,22 @@ class Flow:
|
|
|
399
450
|
yield event.step_finished(step_name=f"loop:{current_node_id}")
|
|
400
451
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
401
452
|
|
|
402
|
-
elif node_type
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# 沙箱执行
|
|
413
|
-
from .sandbox import execute_python_node
|
|
414
|
-
|
|
415
|
-
code_str = config.get("code", "")
|
|
416
|
-
py_result = execute_python_node(code_str, resolved_inputs)
|
|
417
|
-
|
|
418
|
-
# 映射 outputs 到 context.variables
|
|
419
|
-
outputs_spec: dict[str, Any] = config.get("outputs", {})
|
|
420
|
-
for key in outputs_spec:
|
|
421
|
-
if key in py_result:
|
|
422
|
-
context.set_variable(key, py_result[key])
|
|
423
|
-
|
|
424
|
-
context.set_node_result(current_node_id, _json.dumps(py_result, ensure_ascii=False))
|
|
425
|
-
yield event.step_finished(step_name=f"python:{current_node_id}")
|
|
426
|
-
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
453
|
+
elif node_type in self._node_handlers:
|
|
454
|
+
# 所有执行类节点统一分发
|
|
455
|
+
async for evt in self._execute_node_with_retry(node, context, current_node_id):
|
|
456
|
+
yield evt
|
|
457
|
+
result = context.node_results.get(current_node_id, "")
|
|
458
|
+
route_key = self._extract_route_key(result)
|
|
459
|
+
current_node_id = self._resolve_next_node(current_node_id, route_key) or self._resolve_next_node(
|
|
460
|
+
current_node_id, "done"
|
|
461
|
+
)
|
|
427
462
|
|
|
428
463
|
else:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
config = node_config.get("config", {})
|
|
435
|
-
|
|
436
|
-
# 设置参数到 context
|
|
437
|
-
self._set_parameters(config, context)
|
|
438
|
-
|
|
439
|
-
# 创建并执行 runnable
|
|
440
|
-
if node_type == "agent":
|
|
441
|
-
runnable = self._create_agent(config)
|
|
442
|
-
elif node_type == "tool":
|
|
443
|
-
runnable = self._create_tool(config)
|
|
444
|
-
else:
|
|
445
|
-
raise FlowError("UNKNOWN_NODE_TYPE", 400, {"type": node_type})
|
|
446
|
-
|
|
447
|
-
return await runnable.run(context)
|
|
448
|
-
|
|
449
|
-
def _set_parameters(self, config: dict, context: "FlowContext") -> None:
|
|
450
|
-
"""设置参数到 context"""
|
|
451
|
-
parameters = config.get("parameters", {})
|
|
452
|
-
|
|
453
|
-
for key, value in parameters.items():
|
|
454
|
-
resolved_value = context.resolve_reference(value) if isinstance(value, str) else value
|
|
455
|
-
context.set_variable(key, resolved_value)
|
|
456
|
-
|
|
457
|
-
def _create_agent(self, config: dict):
|
|
458
|
-
"""创建 Agent"""
|
|
459
|
-
agent_name = config.get("agent_name")
|
|
460
|
-
if not agent_name:
|
|
461
|
-
raise FlowError("MISSING_AGENT_NAME", 400)
|
|
462
|
-
|
|
463
|
-
agent = registry.create_agent(agent_name)
|
|
464
|
-
if not agent:
|
|
465
|
-
raise FlowError("AGENT_NOT_FOUND", 404, {"agent_name": agent_name})
|
|
466
|
-
|
|
467
|
-
return agent
|
|
468
|
-
|
|
469
|
-
def _create_tool(self, config: dict):
|
|
470
|
-
"""创建 Tool"""
|
|
471
|
-
tool_name = config.get("tool_name")
|
|
472
|
-
if not tool_name:
|
|
473
|
-
raise FlowError("MISSING_TOOL_NAME", 400)
|
|
474
|
-
|
|
475
|
-
tool = registry.create_tool(tool_name)
|
|
476
|
-
if not tool:
|
|
477
|
-
raise FlowError("TOOL_NOT_FOUND", 404, {"tool_name": tool_name})
|
|
478
|
-
|
|
479
|
-
return tool
|
|
464
|
+
yield event.run_error(
|
|
465
|
+
message=f"Unknown node type: {node_type}",
|
|
466
|
+
code="UNKNOWN_NODE_TYPE",
|
|
467
|
+
)
|
|
468
|
+
raise NodeExecutionError("UNKNOWN_NODE_TYPE", args={"node_type": node_type})
|
|
480
469
|
|
|
481
470
|
def get_node_config(self, node_id: str) -> dict[str, Any] | None:
|
|
482
471
|
"""获取节点配置"""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright (c) 2020-2026 XtraVisions, All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""内置节点处理器注册"""
|
|
4
|
+
|
|
5
|
+
from .agent_node import AgentNodeHandler
|
|
6
|
+
from .base import NodeHandler
|
|
7
|
+
from .detect_node import DetectNodeHandler
|
|
8
|
+
from .llm_chat_node import LLMChatNodeHandler
|
|
9
|
+
from .llm_embed_node import LLMEmbedNodeHandler
|
|
10
|
+
from .llm_rerank_node import LLMRerankNodeHandler
|
|
11
|
+
from .python_node import PythonNodeHandler
|
|
12
|
+
from .tool_node import ToolNodeHandler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# 所有内置 handler 实例
|
|
16
|
+
builtin_handlers: list[NodeHandler] = [
|
|
17
|
+
AgentNodeHandler(),
|
|
18
|
+
ToolNodeHandler(),
|
|
19
|
+
PythonNodeHandler(),
|
|
20
|
+
LLMChatNodeHandler(),
|
|
21
|
+
LLMEmbedNodeHandler(),
|
|
22
|
+
LLMRerankNodeHandler(),
|
|
23
|
+
DetectNodeHandler(),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# 全局自定义节点注册
|
|
27
|
+
_global_node_handlers: dict[str, NodeHandler] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_node_handler(node_type: str, handler: NodeHandler) -> None:
|
|
31
|
+
"""注册自定义节点处理器(全局,所有 Flow 实例共享)"""
|
|
32
|
+
_global_node_handlers[node_type] = handler
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"NodeHandler",
|
|
37
|
+
"builtin_handlers",
|
|
38
|
+
"register_node_handler",
|
|
39
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright (c) 2020-2026 XtraVisions, All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Agent 节点处理器 — 从 flow.py 提取"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
6
|
+
|
|
7
|
+
from .. import event
|
|
8
|
+
from ..exceptions import FlowError
|
|
9
|
+
from ..registry import registry
|
|
10
|
+
from .base import NodeHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..context import FlowContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentNodeHandler(NodeHandler):
|
|
18
|
+
"""Agent 节点:通过 registry 查找 agent → ag.stream(context)"""
|
|
19
|
+
|
|
20
|
+
node_type = "agent"
|
|
21
|
+
|
|
22
|
+
def _set_parameters(self, config: dict, context: "FlowContext") -> None:
|
|
23
|
+
parameters = config.get("parameters", {})
|
|
24
|
+
for key, value in parameters.items():
|
|
25
|
+
resolved = context.resolve_reference(value) if isinstance(value, str) else value
|
|
26
|
+
context.set_variable(key, resolved)
|
|
27
|
+
|
|
28
|
+
def _create_agent(self, config: dict):
|
|
29
|
+
agent_name = config.get("agent_name")
|
|
30
|
+
if not agent_name:
|
|
31
|
+
raise FlowError("MISSING_AGENT_NAME", 400)
|
|
32
|
+
agent = registry.create_agent(agent_name)
|
|
33
|
+
if not agent:
|
|
34
|
+
raise FlowError("AGENT_NOT_FOUND", 404, {"agent_name": agent_name})
|
|
35
|
+
return agent
|
|
36
|
+
|
|
37
|
+
async def execute(self, node: dict, context: "FlowContext") -> Any:
|
|
38
|
+
config = node.get("config", {})
|
|
39
|
+
self._set_parameters(config, context)
|
|
40
|
+
ag = self._create_agent(config)
|
|
41
|
+
return await ag.run(context)
|
|
42
|
+
|
|
43
|
+
async def stream(self, node: dict, context: "FlowContext", node_id: str) -> AsyncIterator[dict[str, Any]]:
|
|
44
|
+
config = node.get("config", {})
|
|
45
|
+
step_name = self.get_step_name(node, node_id)
|
|
46
|
+
|
|
47
|
+
yield event.step_started(step_name=step_name)
|
|
48
|
+
self._set_parameters(config, context)
|
|
49
|
+
ag = self._create_agent(config)
|
|
50
|
+
async for evt in ag.stream(context):
|
|
51
|
+
yield evt
|
|
52
|
+
result = context.get_last_output(ag.name) or ""
|
|
53
|
+
context.set_node_result(node_id, result)
|
|
54
|
+
self.map_outputs(config, context, {"result": result})
|
|
55
|
+
yield event.step_finished(step_name=step_name)
|