agstack 1.6.0__tar.gz → 1.8.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.6.0 → agstack-1.8.0}/PKG-INFO +3 -3
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/__init__.py +3 -1
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/agent.py +16 -11
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/context.py +23 -25
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/flow.py +37 -66
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/__init__.py +0 -10
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/agent_node.py +6 -13
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/base.py +2 -10
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/detect_node.py +12 -12
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/llm_chat_node.py +16 -16
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/llm_embed_node.py +0 -1
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/llm_rerank_node.py +0 -1
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/python_node.py +15 -6
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/nodes/tool_node.py +3 -9
- agstack-1.8.0/agstack/llm/flow/registry.py +139 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/tool.py +9 -10
- {agstack-1.6.0 → agstack-1.8.0}/agstack.egg-info/PKG-INFO +3 -3
- {agstack-1.6.0 → agstack-1.8.0}/agstack.egg-info/SOURCES.txt +2 -2
- {agstack-1.6.0 → agstack-1.8.0}/agstack.egg-info/requires.txt +2 -2
- {agstack-1.6.0 → agstack-1.8.0}/pyproject.toml +4 -3
- agstack-1.8.0/tests/test_flow_io.py +435 -0
- agstack-1.6.0/agstack/llm/flow/registry.py +0 -81
- agstack-1.6.0/agstack/registry.py +0 -187
- {agstack-1.6.0 → agstack-1.8.0}/LICENSE +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/README.md +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/config/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/config/logger.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/config/manager.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/config/types.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/contexts.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/decorators.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/events.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/exceptions.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/fastapi/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/fastapi/exception.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/fastapi/middleware.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/fastapi/offline.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/fastapi/sse.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/infra/db/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/infra/es/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/infra/kg/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/infra/mq/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/client.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/event.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/exceptions.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/factory.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/loader.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/records.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/sandbox.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/flow/state.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/prompts.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/llm/token.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/schema.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/security/__init__.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/security/casbin.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/security/crypt.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack/status.py +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack.egg-info/dependency_links.txt +0 -0
- {agstack-1.6.0 → agstack-1.8.0}/agstack.egg-info/top_level.txt +0 -0
- {agstack-1.6.0 → agstack-1.8.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.8.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>
|
|
@@ -27,7 +27,7 @@ Requires-Dist: fastapi>=0.133.1
|
|
|
27
27
|
Requires-Dist: jwcrypto>=1.5.6
|
|
28
28
|
Requires-Dist: loguru>=0.7.3
|
|
29
29
|
Requires-Dist: nebula3-python>=3.8.3
|
|
30
|
-
Requires-Dist: openai>=2.
|
|
30
|
+
Requires-Dist: openai>=2.28.0
|
|
31
31
|
Requires-Dist: passlib[bcrypt]>=1.7.4
|
|
32
32
|
Requires-Dist: pycasbin>=2.8.0
|
|
33
33
|
Requires-Dist: pydantic>=2.12.4
|
|
@@ -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.5.0
|
|
39
39
|
Requires-Dist: tiktoken>=0.12.0
|
|
40
40
|
Requires-Dist: uvicorn>=0.41.0
|
|
41
41
|
Dynamic: license-file
|
|
@@ -18,13 +18,15 @@ 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
|
|
21
|
+
from .nodes import NodeHandler
|
|
22
22
|
from .records import Record, Status
|
|
23
23
|
from .registry import registry
|
|
24
24
|
from .state import FlowState
|
|
25
25
|
from .tool import Tool, ToolResult
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
register_node_handler = registry.register_node_handler
|
|
29
|
+
|
|
28
30
|
__all__ = [
|
|
29
31
|
# 核心抽象
|
|
30
32
|
"Tool",
|
|
@@ -64,23 +64,29 @@ class Agent:
|
|
|
64
64
|
return tool
|
|
65
65
|
return None
|
|
66
66
|
|
|
67
|
-
async def run(self, context: "FlowContext") -> str:
|
|
67
|
+
async def run(self, context: "FlowContext", inputs: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
68
68
|
"""执行 Agent 逻辑"""
|
|
69
69
|
content_parts = []
|
|
70
|
-
async for evt in self.stream(context):
|
|
70
|
+
async for evt in self.stream(context, inputs):
|
|
71
71
|
# AG-UI 事件格式
|
|
72
72
|
if isinstance(evt, dict):
|
|
73
73
|
if evt.get("type") == EventType.TEXT_MESSAGE_CONTENT:
|
|
74
74
|
content_parts.append(evt.get("delta", ""))
|
|
75
75
|
elif evt.get("type") == EventType.RUN_ERROR:
|
|
76
76
|
raise FlowError("AGENT_EXECUTION_FAILED", 500, {"error": evt.get("message")})
|
|
77
|
-
return "".join(content_parts)
|
|
77
|
+
return {"result": "".join(content_parts)}
|
|
78
78
|
|
|
79
|
-
async def stream(
|
|
79
|
+
async def stream(
|
|
80
|
+
self, context: "FlowContext", inputs: dict[str, Any] | None = None
|
|
81
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
80
82
|
"""流式执行 Agent,输出 AG-UI 标准事件"""
|
|
81
83
|
|
|
82
|
-
# 输入来源:优先
|
|
83
|
-
user_input =
|
|
84
|
+
# 输入来源:优先 inputs 参数,回退到 context.variables
|
|
85
|
+
user_input = ""
|
|
86
|
+
if inputs:
|
|
87
|
+
user_input = inputs.get("input", "")
|
|
88
|
+
if not user_input:
|
|
89
|
+
user_input = context.get_variable("input") or context.get_variable("query", "")
|
|
84
90
|
msg_id = context.message_id or str(uuid4())
|
|
85
91
|
|
|
86
92
|
# 添加用户消息(scoped by agent name)
|
|
@@ -214,7 +220,7 @@ class Agent:
|
|
|
214
220
|
# 如果没有工具调用,结束循环
|
|
215
221
|
if not tool_calls:
|
|
216
222
|
# 存储结果供 Flow/A2A 使用
|
|
217
|
-
context.
|
|
223
|
+
context.set_output(self.name, {"result": assistant_content})
|
|
218
224
|
# AG-UI: TEXT_MESSAGE_END
|
|
219
225
|
yield event.text_message_end(message_id=msg_id)
|
|
220
226
|
return
|
|
@@ -237,15 +243,14 @@ class Agent:
|
|
|
237
243
|
)
|
|
238
244
|
continue
|
|
239
245
|
|
|
240
|
-
# 解析 LLM
|
|
246
|
+
# 解析 LLM 返回的工具参数
|
|
241
247
|
try:
|
|
242
248
|
tool_args = json.loads(tool_call["arguments"]) if tool_call["arguments"] else {}
|
|
243
249
|
except json.JSONDecodeError:
|
|
244
250
|
tool_args = {}
|
|
245
|
-
context.update_variables(tool_args)
|
|
246
251
|
|
|
247
|
-
#
|
|
248
|
-
result = await tool.execute_async(context)
|
|
252
|
+
# 执行工具(传入 LLM 解析的参数作为 inputs)
|
|
253
|
+
result = await tool.execute_async(context, tool_args)
|
|
249
254
|
|
|
250
255
|
# 保存工具结果
|
|
251
256
|
result_content = json.dumps(result.result) if result.success else json.dumps({"error": result.error})
|
|
@@ -50,7 +50,7 @@ class FlowContext:
|
|
|
50
50
|
message_id: str | None = None
|
|
51
51
|
|
|
52
52
|
# 图执行状态
|
|
53
|
-
|
|
53
|
+
outputs: dict[str, Any] = field(default_factory=dict)
|
|
54
54
|
current_node: str | None = None
|
|
55
55
|
|
|
56
56
|
# 执行记录(可选)
|
|
@@ -115,33 +115,31 @@ class FlowContext:
|
|
|
115
115
|
self.messages.clear()
|
|
116
116
|
self.turn_count = 0
|
|
117
117
|
|
|
118
|
-
def resolve_reference(self, ref:
|
|
119
|
-
"""解析变量引用
|
|
120
|
-
if not isinstance(ref, str) or not ref.startswith("{"):
|
|
121
|
-
return ref
|
|
118
|
+
def resolve_reference(self, ref: Any) -> Any:
|
|
119
|
+
"""解析变量引用
|
|
122
120
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
$o.node_id.field.subfield → context.outputs["node_id"]["field"]["subfield"]
|
|
122
|
+
$v.key → context.variables["key"]
|
|
123
|
+
其他字符串 → 原样返回(字面值)
|
|
124
|
+
"""
|
|
125
|
+
if not isinstance(ref, str):
|
|
126
|
+
return ref
|
|
127
|
+
if ref.startswith("$o."):
|
|
128
|
+
parts = ref[3:].split(".")
|
|
129
|
+
result = self.outputs.get(parts[0])
|
|
130
|
+
for part in parts[1:]:
|
|
131
|
+
if isinstance(result, dict):
|
|
132
|
+
result = result.get(part)
|
|
133
|
+
else:
|
|
134
|
+
result = getattr(result, part, None)
|
|
129
135
|
return result
|
|
136
|
+
if ref.startswith("$v."):
|
|
137
|
+
return self.variables.get(ref[3:])
|
|
138
|
+
return ref
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# 支持嵌套字段访问 variable.field.subfield
|
|
135
|
-
for field_name in var_path.split("."):
|
|
136
|
-
if isinstance(result, dict):
|
|
137
|
-
result = result.get(field_name)
|
|
138
|
-
else:
|
|
139
|
-
result = getattr(result, field_name, None)
|
|
140
|
-
return result
|
|
141
|
-
|
|
142
|
-
def set_node_result(self, node_id: str, result: Any):
|
|
143
|
-
"""设置节点执行结果"""
|
|
144
|
-
self.node_results[node_id] = result
|
|
140
|
+
def set_output(self, node_id: str, result: Any):
|
|
141
|
+
"""设置节点输出"""
|
|
142
|
+
self.outputs[node_id] = result
|
|
145
143
|
|
|
146
144
|
def add_execution_record(self, task_id: str, status: str, **kwargs) -> None:
|
|
147
145
|
"""添加执行记录"""
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
"""Flow 定义和执行"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import json as _json
|
|
7
6
|
from dataclasses import dataclass, field
|
|
8
7
|
from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
9
8
|
from uuid import uuid4
|
|
@@ -47,12 +46,9 @@ class Flow:
|
|
|
47
46
|
_node_handlers: dict[str, "NodeHandler"] = field(default_factory=dict, init=False, repr=False)
|
|
48
47
|
|
|
49
48
|
def __post_init__(self) -> None:
|
|
50
|
-
from .
|
|
49
|
+
from .registry import registry
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
self._node_handlers[handler.node_type] = handler
|
|
54
|
-
# 全局注册的自定义 handler 可覆盖内置
|
|
55
|
-
self._node_handlers.update(_global_node_handlers)
|
|
51
|
+
self._node_handlers = dict(registry.get_all_node_handlers())
|
|
56
52
|
|
|
57
53
|
# ── 重试策略 ──
|
|
58
54
|
|
|
@@ -82,27 +78,22 @@ class Flow:
|
|
|
82
78
|
|
|
83
79
|
@staticmethod
|
|
84
80
|
def _extract_route_key(result: Any) -> str:
|
|
85
|
-
"""
|
|
81
|
+
"""从节点输出 dict 中提取路由键。
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
节点输出 dict 中若包含 ``choice`` 字段,即为路由键。
|
|
84
|
+
没有 ``choice`` 则默认 ``"done"``。
|
|
89
85
|
"""
|
|
90
|
-
if
|
|
91
|
-
return "done"
|
|
92
|
-
|
|
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"
|
|
86
|
+
if isinstance(result, dict):
|
|
87
|
+
return str(result.get("choice", "done"))
|
|
88
|
+
return "done"
|
|
99
89
|
|
|
100
90
|
# ── message 节点 ──
|
|
101
91
|
|
|
102
92
|
async def _emit_message(self, node: dict, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
103
|
-
"""
|
|
93
|
+
"""输出模板文本,支持 $v. 引用"""
|
|
104
94
|
config = node.get("config", {})
|
|
105
95
|
template = config.get("content", "")
|
|
96
|
+
# 用 variables 做 format_map 替换 {var} 占位符
|
|
106
97
|
text = template.format_map(_SafeFormatDict(context.variables))
|
|
107
98
|
msg_id = context.message_id or str(uuid4())
|
|
108
99
|
yield event.text_message_start(message_id=msg_id, role="assistant")
|
|
@@ -180,7 +171,7 @@ class Flow:
|
|
|
180
171
|
handler = self._node_handlers.get(node_type)
|
|
181
172
|
if handler:
|
|
182
173
|
result = await handler.execute(node, context)
|
|
183
|
-
context.
|
|
174
|
+
context.set_output(node_id, result)
|
|
184
175
|
else:
|
|
185
176
|
raise NodeExecutionError("UNKNOWN_NODE_TYPE", args={"node_type": node_type})
|
|
186
177
|
else:
|
|
@@ -197,7 +188,7 @@ class Flow:
|
|
|
197
188
|
config = node.get("config", {})
|
|
198
189
|
template = config.get("content", "")
|
|
199
190
|
text = template.format_map(_SafeFormatDict(context.variables))
|
|
200
|
-
context.
|
|
191
|
+
context.set_output(current_node_id, {"result": text})
|
|
201
192
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
202
193
|
|
|
203
194
|
elif node_type == "parallel":
|
|
@@ -213,25 +204,22 @@ class Flow:
|
|
|
213
204
|
branch_handler = self._node_handlers.get(branch_type)
|
|
214
205
|
if branch_handler:
|
|
215
206
|
result = await branch_handler.execute(branch_node, context)
|
|
216
|
-
context.
|
|
207
|
+
context.set_output(branch_id, result)
|
|
217
208
|
|
|
218
209
|
await asyncio.gather(*[_run_branch(bid) for bid in branches])
|
|
219
|
-
context.
|
|
210
|
+
context.set_output(current_node_id, {"choice": "done"})
|
|
220
211
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
221
212
|
|
|
222
213
|
elif node_type == "iteration":
|
|
223
214
|
config = node.get("config", {})
|
|
224
215
|
items_ref = config.get("items", "")
|
|
225
216
|
items = context.resolve_reference(items_ref) if isinstance(items_ref, str) else items_ref
|
|
226
|
-
if isinstance(items, str):
|
|
227
|
-
items = _json.loads(items)
|
|
228
217
|
if not isinstance(items, list):
|
|
229
218
|
items = [items]
|
|
230
219
|
|
|
231
220
|
item_var = config.get("item_variable", "item")
|
|
232
221
|
index_var = config.get("index_variable", "index")
|
|
233
222
|
body_nodes: list[str] = config.get("body", [])
|
|
234
|
-
output_var = config.get("output_variable", "iteration_results")
|
|
235
223
|
results: list[Any] = []
|
|
236
224
|
|
|
237
225
|
for idx, item in enumerate(items):
|
|
@@ -245,12 +233,11 @@ class Flow:
|
|
|
245
233
|
body_handler = self._node_handlers.get(body_type)
|
|
246
234
|
if body_handler:
|
|
247
235
|
body_result = await body_handler.execute(body_node, context)
|
|
248
|
-
context.
|
|
236
|
+
context.set_output(body_node_id, body_result)
|
|
249
237
|
if body_nodes:
|
|
250
|
-
results.append(context.
|
|
238
|
+
results.append(context.outputs.get(body_nodes[-1]))
|
|
251
239
|
|
|
252
|
-
context.
|
|
253
|
-
context.set_node_result(current_node_id, _json.dumps(results, ensure_ascii=False))
|
|
240
|
+
context.set_output(current_node_id, {"results": results})
|
|
254
241
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
255
242
|
|
|
256
243
|
elif node_type == "loop":
|
|
@@ -271,26 +258,20 @@ class Flow:
|
|
|
271
258
|
body_handler = self._node_handlers.get(body_type)
|
|
272
259
|
if body_handler:
|
|
273
260
|
body_result = await body_handler.execute(body_node, context)
|
|
274
|
-
context.
|
|
261
|
+
context.set_output(body_node_id, body_result)
|
|
275
262
|
if condition_node_id:
|
|
276
|
-
cond_result = context.
|
|
277
|
-
if isinstance(cond_result,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
break
|
|
282
|
-
except (ValueError, TypeError):
|
|
283
|
-
if cond_result == break_cond:
|
|
284
|
-
break
|
|
285
|
-
|
|
286
|
-
context.set_node_result(current_node_id, "done")
|
|
263
|
+
cond_result = context.outputs.get(condition_node_id, {})
|
|
264
|
+
if isinstance(cond_result, dict) and cond_result.get("choice") == break_cond:
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
context.set_output(current_node_id, {"choice": "done"})
|
|
287
268
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
288
269
|
|
|
289
270
|
elif node_type in self._node_handlers:
|
|
290
271
|
# 所有执行类节点统一分发
|
|
291
272
|
handler = self._node_handlers[node_type]
|
|
292
273
|
result = await handler.execute(node, context)
|
|
293
|
-
context.
|
|
274
|
+
context.set_output(current_node_id, result)
|
|
294
275
|
route_key = self._extract_route_key(result)
|
|
295
276
|
current_node_id = self._resolve_next_node(current_node_id, route_key) or self._resolve_next_node(
|
|
296
277
|
current_node_id, "done"
|
|
@@ -299,7 +280,7 @@ class Flow:
|
|
|
299
280
|
else:
|
|
300
281
|
raise NodeExecutionError("UNKNOWN_NODE_TYPE", args={"node_type": node_type})
|
|
301
282
|
|
|
302
|
-
return context.
|
|
283
|
+
return context.outputs
|
|
303
284
|
|
|
304
285
|
async def stream(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
305
286
|
"""流式执行 Flow(输出 AG-UI 标准事件)"""
|
|
@@ -371,10 +352,10 @@ class Flow:
|
|
|
371
352
|
branch_handler = self._node_handlers.get(branch_type)
|
|
372
353
|
if branch_handler:
|
|
373
354
|
result = await branch_handler.execute(branch_node, context)
|
|
374
|
-
context.
|
|
355
|
+
context.set_output(branch_id, result)
|
|
375
356
|
|
|
376
357
|
await asyncio.gather(*[_exec_branch(bid) for bid in branches])
|
|
377
|
-
context.
|
|
358
|
+
context.set_output(current_node_id, {"choice": "done"})
|
|
378
359
|
yield event.step_finished(step_name=f"parallel:{current_node_id}")
|
|
379
360
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
380
361
|
|
|
@@ -382,15 +363,12 @@ class Flow:
|
|
|
382
363
|
config = node.get("config", {})
|
|
383
364
|
items_ref = config.get("items", "")
|
|
384
365
|
items = context.resolve_reference(items_ref) if isinstance(items_ref, str) else items_ref
|
|
385
|
-
if isinstance(items, str):
|
|
386
|
-
items = _json.loads(items)
|
|
387
366
|
if not isinstance(items, list):
|
|
388
367
|
items = [items]
|
|
389
368
|
|
|
390
369
|
item_var = config.get("item_variable", "item")
|
|
391
370
|
index_var = config.get("index_variable", "index")
|
|
392
371
|
body_nodes: list[str] = config.get("body", [])
|
|
393
|
-
output_var = config.get("output_variable", "iteration_results")
|
|
394
372
|
results: list[Any] = []
|
|
395
373
|
|
|
396
374
|
yield event.step_started(step_name=f"iteration:{current_node_id}")
|
|
@@ -405,12 +383,11 @@ class Flow:
|
|
|
405
383
|
body_handler = self._node_handlers.get(body_type)
|
|
406
384
|
if body_handler:
|
|
407
385
|
body_result = await body_handler.execute(body_node, context)
|
|
408
|
-
context.
|
|
386
|
+
context.set_output(body_node_id, body_result)
|
|
409
387
|
if body_nodes:
|
|
410
|
-
results.append(context.
|
|
388
|
+
results.append(context.outputs.get(body_nodes[-1]))
|
|
411
389
|
|
|
412
|
-
context.
|
|
413
|
-
context.set_node_result(current_node_id, _json.dumps(results, ensure_ascii=False))
|
|
390
|
+
context.set_output(current_node_id, {"results": results})
|
|
414
391
|
yield event.step_finished(step_name=f"iteration:{current_node_id}")
|
|
415
392
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
416
393
|
|
|
@@ -433,20 +410,14 @@ class Flow:
|
|
|
433
410
|
body_handler = self._node_handlers.get(body_type)
|
|
434
411
|
if body_handler:
|
|
435
412
|
body_result = await body_handler.execute(body_node, context)
|
|
436
|
-
context.
|
|
413
|
+
context.set_output(body_node_id, body_result)
|
|
437
414
|
# 检查终止条件
|
|
438
415
|
if condition_node_id:
|
|
439
|
-
cond_result = context.
|
|
440
|
-
if isinstance(cond_result,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
break
|
|
445
|
-
except (ValueError, TypeError):
|
|
446
|
-
if cond_result == break_cond:
|
|
447
|
-
break
|
|
448
|
-
|
|
449
|
-
context.set_node_result(current_node_id, "done")
|
|
416
|
+
cond_result = context.outputs.get(condition_node_id, {})
|
|
417
|
+
if isinstance(cond_result, dict) and cond_result.get("choice") == break_cond:
|
|
418
|
+
break
|
|
419
|
+
|
|
420
|
+
context.set_output(current_node_id, {"choice": "done"})
|
|
450
421
|
yield event.step_finished(step_name=f"loop:{current_node_id}")
|
|
451
422
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
452
423
|
|
|
@@ -454,7 +425,7 @@ class Flow:
|
|
|
454
425
|
# 所有执行类节点统一分发
|
|
455
426
|
async for evt in self._execute_node_with_retry(node, context, current_node_id):
|
|
456
427
|
yield evt
|
|
457
|
-
result = context.
|
|
428
|
+
result = context.outputs.get(current_node_id, {})
|
|
458
429
|
route_key = self._extract_route_key(result)
|
|
459
430
|
current_node_id = self._resolve_next_node(current_node_id, route_key) or self._resolve_next_node(
|
|
460
431
|
current_node_id, "done"
|
|
@@ -23,17 +23,7 @@ builtin_handlers: list[NodeHandler] = [
|
|
|
23
23
|
DetectNodeHandler(),
|
|
24
24
|
]
|
|
25
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
26
|
__all__ = [
|
|
36
27
|
"NodeHandler",
|
|
37
28
|
"builtin_handlers",
|
|
38
|
-
"register_node_handler",
|
|
39
29
|
]
|
|
@@ -15,16 +15,10 @@ if TYPE_CHECKING:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class AgentNodeHandler(NodeHandler):
|
|
18
|
-
"""Agent 节点:通过 registry 查找 agent → ag.stream(context)"""
|
|
18
|
+
"""Agent 节点:通过 registry 查找 agent → ag.stream(context, inputs)"""
|
|
19
19
|
|
|
20
20
|
node_type = "agent"
|
|
21
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
22
|
def _create_agent(self, config: dict):
|
|
29
23
|
agent_name = config.get("agent_name")
|
|
30
24
|
if not agent_name:
|
|
@@ -36,20 +30,19 @@ class AgentNodeHandler(NodeHandler):
|
|
|
36
30
|
|
|
37
31
|
async def execute(self, node: dict, context: "FlowContext") -> Any:
|
|
38
32
|
config = node.get("config", {})
|
|
39
|
-
self.
|
|
33
|
+
resolved = self.resolve_inputs(config, context)
|
|
40
34
|
ag = self._create_agent(config)
|
|
41
|
-
return await ag.run(context)
|
|
35
|
+
return await ag.run(context, inputs=resolved)
|
|
42
36
|
|
|
43
37
|
async def stream(self, node: dict, context: "FlowContext", node_id: str) -> AsyncIterator[dict[str, Any]]:
|
|
44
38
|
config = node.get("config", {})
|
|
45
39
|
step_name = self.get_step_name(node, node_id)
|
|
46
40
|
|
|
47
41
|
yield event.step_started(step_name=step_name)
|
|
48
|
-
self.
|
|
42
|
+
resolved = self.resolve_inputs(config, context)
|
|
49
43
|
ag = self._create_agent(config)
|
|
50
|
-
async for evt in ag.stream(context):
|
|
44
|
+
async for evt in ag.stream(context, inputs=resolved):
|
|
51
45
|
yield evt
|
|
52
46
|
result = context.get_last_output(ag.name) or ""
|
|
53
|
-
context.
|
|
54
|
-
self.map_outputs(config, context, {"result": result})
|
|
47
|
+
context.set_output(node_id, {"result": result})
|
|
55
48
|
yield event.step_finished(step_name=step_name)
|
|
@@ -31,14 +31,8 @@ class NodeHandler:
|
|
|
31
31
|
inputs_spec = config.get("inputs", {})
|
|
32
32
|
return {k: context.resolve_reference(v) if isinstance(v, str) else v for k, v in inputs_spec.items()}
|
|
33
33
|
|
|
34
|
-
def map_outputs(self, config: dict, context: "FlowContext", result: dict) -> None:
|
|
35
|
-
"""将结果映射到 context.variables"""
|
|
36
|
-
for key in config.get("outputs", {}):
|
|
37
|
-
if isinstance(result, dict) and key in result:
|
|
38
|
-
context.set_variable(key, result[key])
|
|
39
|
-
|
|
40
34
|
async def execute(self, node: dict, context: "FlowContext") -> Any:
|
|
41
|
-
"""执行节点,返回结果(将存入
|
|
35
|
+
"""执行节点,返回结果(将存入 context.outputs)
|
|
42
36
|
|
|
43
37
|
子类必须实现此方法。
|
|
44
38
|
"""
|
|
@@ -53,7 +47,5 @@ class NodeHandler:
|
|
|
53
47
|
step_name = self.get_step_name(node, node_id)
|
|
54
48
|
yield event.step_started(step_name=step_name)
|
|
55
49
|
result = await self.execute(node, context)
|
|
56
|
-
context.
|
|
57
|
-
config = node.get("config", {})
|
|
58
|
-
self.map_outputs(config, context, result)
|
|
50
|
+
context.set_output(node_id, result)
|
|
59
51
|
yield event.step_finished(step_name=step_name)
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
"""Detect 节点 — 分类/检测,输出路由键"""
|
|
4
4
|
|
|
5
|
-
import json as _json
|
|
6
5
|
from typing import TYPE_CHECKING, Any
|
|
7
6
|
|
|
8
7
|
from openai.types.chat import ChatCompletionMessageParam
|
|
@@ -19,10 +18,10 @@ if TYPE_CHECKING:
|
|
|
19
18
|
class DetectNodeHandler(NodeHandler):
|
|
20
19
|
"""分类/检测节点
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
对输入文本进行分类,输出路由键。结果 dict 中的 ``choice`` 字段用于边路由。
|
|
23
22
|
|
|
24
23
|
输入:query(待检测文本)+ instruction + options
|
|
25
|
-
输出:{"
|
|
24
|
+
输出:{"choice": "<option>"}
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
27
|
node_type = "detect"
|
|
@@ -76,18 +75,19 @@ class DetectNodeHandler(NodeHandler):
|
|
|
76
75
|
)
|
|
77
76
|
)
|
|
78
77
|
|
|
79
|
-
#
|
|
78
|
+
# 从 LLM 响应中提取选项
|
|
79
|
+
import json as _json
|
|
80
|
+
|
|
81
|
+
option = result_text.strip()
|
|
80
82
|
try:
|
|
81
|
-
parsed = _json.loads(
|
|
83
|
+
parsed = _json.loads(option)
|
|
82
84
|
if isinstance(parsed, dict) and "result" in parsed:
|
|
83
|
-
|
|
85
|
+
option = str(parsed["result"])
|
|
84
86
|
except (ValueError, TypeError):
|
|
85
87
|
pass
|
|
86
88
|
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return _json.dumps({"result": stripped}, ensure_ascii=False)
|
|
89
|
+
# 如果提取的选项在合法列表中,使用它;否则用原始文本
|
|
90
|
+
if option not in options:
|
|
91
|
+
option = result_text.strip()
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
return _json.dumps({"result": stripped}, ensure_ascii=False)
|
|
93
|
+
return {"choice": option}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
"""LLM Chat 节点 — 单轮 LLM 调用(支持流式/非流式)"""
|
|
4
4
|
|
|
5
|
+
import json as _json
|
|
5
6
|
from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
6
7
|
from uuid import uuid4
|
|
7
8
|
|
|
@@ -34,16 +35,17 @@ class LLMChatNodeHandler(NodeHandler):
|
|
|
34
35
|
|
|
35
36
|
node_type = "llm_chat"
|
|
36
37
|
|
|
37
|
-
def _build_prompt(self,
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
def _build_prompt(self, template: str, resolved_inputs: dict[str, Any]) -> str:
|
|
39
|
+
"""将模板中的 {var} 占位符替换为 resolved 的输入值"""
|
|
40
|
+
format_dict = _SafeFormatDict(
|
|
41
|
+
{k: v if isinstance(v, str) else _json.dumps(v, ensure_ascii=False) for k, v in resolved_inputs.items()}
|
|
42
|
+
)
|
|
41
43
|
return template.format_map(format_dict)
|
|
42
44
|
|
|
43
45
|
async def execute(self, node: dict, context: "FlowContext") -> Any:
|
|
44
46
|
config = node.get("config", {})
|
|
45
47
|
resolved_inputs = self.resolve_inputs(config, context)
|
|
46
|
-
prompt_text = self._build_prompt(config, resolved_inputs)
|
|
48
|
+
prompt_text = self._build_prompt(config.get("prompt", ""), resolved_inputs)
|
|
47
49
|
|
|
48
50
|
model = config.get("model", "gpt-4o")
|
|
49
51
|
temperature = config.get("temperature", 0.7)
|
|
@@ -52,10 +54,11 @@ class LLMChatNodeHandler(NodeHandler):
|
|
|
52
54
|
client = get_llm_client()
|
|
53
55
|
messages: list[ChatCompletionMessageParam] = [{"role": "user", "content": prompt_text}]
|
|
54
56
|
|
|
55
|
-
# 如果有 system_prompt
|
|
57
|
+
# 如果有 system_prompt,支持变量替换并放在前面
|
|
56
58
|
system_prompt = config.get("system_prompt")
|
|
57
59
|
if system_prompt:
|
|
58
|
-
|
|
60
|
+
system_text = self._build_prompt(system_prompt, resolved_inputs)
|
|
61
|
+
messages.insert(0, {"role": "system", "content": system_text})
|
|
59
62
|
|
|
60
63
|
response = await client.chat(
|
|
61
64
|
messages=messages,
|
|
@@ -78,9 +81,7 @@ class LLMChatNodeHandler(NodeHandler):
|
|
|
78
81
|
)
|
|
79
82
|
)
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
self.map_outputs(config, context, result)
|
|
83
|
-
return result_text
|
|
84
|
+
return {"result": result_text}
|
|
84
85
|
|
|
85
86
|
async def stream(self, node: dict, context: "FlowContext", node_id: str) -> AsyncIterator[dict[str, Any]]:
|
|
86
87
|
config = node.get("config", {})
|
|
@@ -91,7 +92,7 @@ class LLMChatNodeHandler(NodeHandler):
|
|
|
91
92
|
step_name = self.get_step_name(node, node_id)
|
|
92
93
|
yield event.step_started(step_name=step_name)
|
|
93
94
|
result = await self.execute(node, context)
|
|
94
|
-
context.
|
|
95
|
+
context.set_output(node_id, result)
|
|
95
96
|
yield event.step_finished(step_name=step_name)
|
|
96
97
|
return
|
|
97
98
|
|
|
@@ -100,7 +101,7 @@ class LLMChatNodeHandler(NodeHandler):
|
|
|
100
101
|
yield event.step_started(step_name=step_name)
|
|
101
102
|
|
|
102
103
|
resolved_inputs = self.resolve_inputs(config, context)
|
|
103
|
-
prompt_text = self._build_prompt(config, resolved_inputs)
|
|
104
|
+
prompt_text = self._build_prompt(config.get("prompt", ""), resolved_inputs)
|
|
104
105
|
|
|
105
106
|
model = config.get("model", "gpt-4o")
|
|
106
107
|
temperature = config.get("temperature", 0.7)
|
|
@@ -111,7 +112,8 @@ class LLMChatNodeHandler(NodeHandler):
|
|
|
111
112
|
|
|
112
113
|
system_prompt = config.get("system_prompt")
|
|
113
114
|
if system_prompt:
|
|
114
|
-
|
|
115
|
+
system_text = self._build_prompt(system_prompt, resolved_inputs)
|
|
116
|
+
messages.insert(0, {"role": "system", "content": system_text})
|
|
115
117
|
|
|
116
118
|
msg_id = context.message_id or str(uuid4())
|
|
117
119
|
yield event.text_message_start(message_id=msg_id, role="assistant")
|
|
@@ -145,8 +147,6 @@ class LLMChatNodeHandler(NodeHandler):
|
|
|
145
147
|
yield event.text_message_end(message_id=msg_id)
|
|
146
148
|
|
|
147
149
|
result_text = "".join(content_parts)
|
|
148
|
-
|
|
149
|
-
self.map_outputs(config, context, result)
|
|
150
|
-
context.set_node_result(node_id, result_text)
|
|
150
|
+
context.set_output(node_id, {"result": result_text})
|
|
151
151
|
|
|
152
152
|
yield event.step_finished(step_name=step_name)
|