agstack 1.2.3__tar.gz → 1.3.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.2.3 → agstack-1.3.0}/PKG-INFO +2 -2
- agstack-1.3.0/agstack/llm/flow/flow.py +272 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack.egg-info/PKG-INFO +2 -2
- {agstack-1.2.3 → agstack-1.3.0}/agstack.egg-info/requires.txt +1 -1
- {agstack-1.2.3 → agstack-1.3.0}/pyproject.toml +2 -2
- agstack-1.2.3/agstack/llm/flow/flow.py +0 -142
- {agstack-1.2.3 → agstack-1.3.0}/LICENSE +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/README.md +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/config/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/config/logger.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/config/manager.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/config/types.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/contexts.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/decorators.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/events.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/exceptions.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/fastapi/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/fastapi/exception.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/fastapi/middleware.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/fastapi/offline.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/fastapi/sse.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/infra/db/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/infra/es/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/infra/kg/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/infra/mq/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/client.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/agent.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/context.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/event.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/exceptions.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/factory.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/loader.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/records.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/registry.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/state.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/flow/tool.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/prompts.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/llm/token.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/registry.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/schema.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/security/__init__.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/security/casbin.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/security/crypt.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack/status.py +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack.egg-info/SOURCES.txt +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack.egg-info/dependency_links.txt +0 -0
- {agstack-1.2.3 → agstack-1.3.0}/agstack.egg-info/top_level.txt +0 -0
- {agstack-1.2.3 → agstack-1.3.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.3.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>
|
|
@@ -34,7 +34,7 @@ Requires-Dist: pydantic>=2.12.4
|
|
|
34
34
|
Requires-Dist: python-multipart>=0.0.20
|
|
35
35
|
Requires-Dist: requests>=2.32.5
|
|
36
36
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.48
|
|
37
|
-
Requires-Dist: sqlobjects>=1.
|
|
37
|
+
Requires-Dist: sqlobjects>=1.3.0
|
|
38
38
|
Requires-Dist: tiktoken>=0.12.0
|
|
39
39
|
Requires-Dist: uvicorn>=0.41.0
|
|
40
40
|
Dynamic: license-file
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Copyright (c) 2020-2025 XtraVisions, All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Flow 定义和执行"""
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from . import event
|
|
11
|
+
from .exceptions import FlowError
|
|
12
|
+
from .registry import registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .context import FlowContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _SafeFormatDict(dict):
|
|
20
|
+
"""安全的模板变量替换,缺失 key 时保留原始占位符"""
|
|
21
|
+
|
|
22
|
+
def __missing__(self, key: str) -> str:
|
|
23
|
+
return f"{{{key}}}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Flow:
|
|
28
|
+
"""Flow 配置定义"""
|
|
29
|
+
|
|
30
|
+
flow_id: str
|
|
31
|
+
name: str
|
|
32
|
+
description: str = ""
|
|
33
|
+
nodes: list[dict[str, Any]] = field(default_factory=list)
|
|
34
|
+
edges: list[dict[str, Any]] = field(default_factory=list)
|
|
35
|
+
variables: dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
# ── 边驱动路由 ──
|
|
38
|
+
|
|
39
|
+
def _resolve_next_node(self, current_id: str, result: str | None = None) -> str | None:
|
|
40
|
+
"""根据当前节点和执行结果,通过 edges 查找下一节点"""
|
|
41
|
+
for edge in self.edges:
|
|
42
|
+
if edge.get("source") == current_id:
|
|
43
|
+
cond = edge.get("condition")
|
|
44
|
+
if cond is None or cond == result:
|
|
45
|
+
return edge.get("target")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
# ── condition 节点 ──
|
|
49
|
+
|
|
50
|
+
async def _evaluate_condition(self, node: dict, context: "FlowContext") -> str:
|
|
51
|
+
"""调用 LLM 判断条件是否匹配"""
|
|
52
|
+
config = node.get("config", {})
|
|
53
|
+
topic = config.get("topic", "")
|
|
54
|
+
query = context.get_variable("query", "")
|
|
55
|
+
|
|
56
|
+
prompt = (
|
|
57
|
+
f"判断以下问题是否属于「{topic}」相关问题。\n"
|
|
58
|
+
f"问题:{query}\n"
|
|
59
|
+
f'仅回复 JSON:{{"result": "match"}} 或 {{"result": "reject"}}'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
from ..client import get_llm_client
|
|
63
|
+
|
|
64
|
+
client = get_llm_client()
|
|
65
|
+
response = await client.chat(
|
|
66
|
+
messages=[{"role": "user", "content": prompt}],
|
|
67
|
+
model=config.get("model", "gpt-4o-mini"),
|
|
68
|
+
temperature=0,
|
|
69
|
+
)
|
|
70
|
+
text = response.choices[0].message.content or ""
|
|
71
|
+
try:
|
|
72
|
+
return _json.loads(text).get("result", "reject")
|
|
73
|
+
except Exception:
|
|
74
|
+
return "match" if "match" in text.lower() else "reject"
|
|
75
|
+
|
|
76
|
+
# ── message 节点 ──
|
|
77
|
+
|
|
78
|
+
async def _emit_message(self, node: dict, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
79
|
+
"""输出模板文本"""
|
|
80
|
+
config = node.get("config", {})
|
|
81
|
+
template = config.get("content", "")
|
|
82
|
+
text = template.format_map(_SafeFormatDict(context.variables))
|
|
83
|
+
msg_id = context.message_id or str(uuid4())
|
|
84
|
+
yield event.text_message_start(message_id=msg_id, role="assistant")
|
|
85
|
+
yield event.text_message_content(message_id=msg_id, delta=text)
|
|
86
|
+
yield event.text_message_end(message_id=msg_id)
|
|
87
|
+
|
|
88
|
+
# ── 执行入口 ──
|
|
89
|
+
|
|
90
|
+
async def run(self, context: "FlowContext") -> dict[str, Any]:
|
|
91
|
+
"""执行 Flow"""
|
|
92
|
+
if not self.edges:
|
|
93
|
+
# 向后兼容:无 edges 时按 nodes 列表顺序执行
|
|
94
|
+
for node in self.nodes:
|
|
95
|
+
node_id = node.get("id")
|
|
96
|
+
if not node_id:
|
|
97
|
+
continue
|
|
98
|
+
context.current_node = node_id
|
|
99
|
+
result = await self._execute_node(node, context)
|
|
100
|
+
context.set_node_result(node_id, result)
|
|
101
|
+
else:
|
|
102
|
+
# edge 驱动执行
|
|
103
|
+
current_node_id: str | None = self.nodes[0]["id"] if self.nodes else None
|
|
104
|
+
while current_node_id:
|
|
105
|
+
node = self.get_node_config(current_node_id)
|
|
106
|
+
if not node:
|
|
107
|
+
break
|
|
108
|
+
context.current_node = current_node_id
|
|
109
|
+
node_type = node.get("type")
|
|
110
|
+
|
|
111
|
+
if node_type == "condition":
|
|
112
|
+
result = await self._evaluate_condition(node, context)
|
|
113
|
+
context.set_node_result(current_node_id, result)
|
|
114
|
+
current_node_id = self._resolve_next_node(current_node_id, result)
|
|
115
|
+
elif node_type == "message":
|
|
116
|
+
config = node.get("config", {})
|
|
117
|
+
template = config.get("content", "")
|
|
118
|
+
text = template.format_map(_SafeFormatDict(context.variables))
|
|
119
|
+
context.set_node_result(current_node_id, text)
|
|
120
|
+
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
121
|
+
elif node_type in ("agent", "tool"):
|
|
122
|
+
result = await self._execute_node(node, context)
|
|
123
|
+
context.set_node_result(current_node_id, result)
|
|
124
|
+
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
125
|
+
else:
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
return context.node_results
|
|
129
|
+
|
|
130
|
+
async def stream(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
131
|
+
"""流式执行 Flow(输出 AG-UI 标准事件)"""
|
|
132
|
+
yield event.step_started(step_name=f"flow:{self.name}")
|
|
133
|
+
|
|
134
|
+
if not self.edges:
|
|
135
|
+
# 向后兼容:无 edges 时按 nodes 列表顺序执行(原有逻辑)
|
|
136
|
+
async for evt in self._stream_sequential(context):
|
|
137
|
+
yield evt
|
|
138
|
+
else:
|
|
139
|
+
# edge 驱动执行
|
|
140
|
+
async for evt in self._stream_edge_driven(context):
|
|
141
|
+
yield evt
|
|
142
|
+
|
|
143
|
+
yield event.step_finished(step_name=f"flow:{self.name}")
|
|
144
|
+
|
|
145
|
+
async def _stream_sequential(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
146
|
+
"""顺序流式执行(原有逻辑)"""
|
|
147
|
+
for node in self.nodes:
|
|
148
|
+
node_id = node.get("id")
|
|
149
|
+
if not node_id:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
context.current_node = node_id
|
|
153
|
+
yield event.step_started(step_name=f"node:{node_id}")
|
|
154
|
+
|
|
155
|
+
if node.get("type") == "agent":
|
|
156
|
+
agent_name = node.get("config", {}).get("agent_name", "")
|
|
157
|
+
yield event.step_started(step_name=f"agent:{agent_name}")
|
|
158
|
+
self._set_parameters(node.get("config", {}), context)
|
|
159
|
+
ag = self._create_agent(node.get("config", {}))
|
|
160
|
+
async for evt in ag.stream(context):
|
|
161
|
+
yield evt
|
|
162
|
+
result = context.get_last_output(ag.name) or ""
|
|
163
|
+
context.set_node_result(node_id, result)
|
|
164
|
+
yield event.step_finished(step_name=f"agent:{agent_name}")
|
|
165
|
+
else:
|
|
166
|
+
tool_name = node.get("config", {}).get("tool_name", "")
|
|
167
|
+
yield event.step_started(step_name=f"tool:{tool_name}")
|
|
168
|
+
result = await self._execute_node(node, context)
|
|
169
|
+
context.set_node_result(node_id, result)
|
|
170
|
+
yield event.step_finished(step_name=f"tool:{tool_name}")
|
|
171
|
+
|
|
172
|
+
async def _stream_edge_driven(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
173
|
+
"""边驱动流式执行"""
|
|
174
|
+
current_node_id: str | None = self.nodes[0]["id"] if self.nodes else None
|
|
175
|
+
|
|
176
|
+
while current_node_id:
|
|
177
|
+
node = self.get_node_config(current_node_id)
|
|
178
|
+
if not node:
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
context.current_node = current_node_id
|
|
182
|
+
node_type = node.get("type")
|
|
183
|
+
|
|
184
|
+
if node_type == "condition":
|
|
185
|
+
result = await self._evaluate_condition(node, context)
|
|
186
|
+
context.set_node_result(current_node_id, result)
|
|
187
|
+
current_node_id = self._resolve_next_node(current_node_id, result)
|
|
188
|
+
|
|
189
|
+
elif node_type == "message":
|
|
190
|
+
async for evt in self._emit_message(node, context):
|
|
191
|
+
yield evt
|
|
192
|
+
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
193
|
+
|
|
194
|
+
elif node_type == "agent":
|
|
195
|
+
agent_name = node.get("config", {}).get("agent_name", "")
|
|
196
|
+
yield event.step_started(step_name=f"agent:{agent_name}")
|
|
197
|
+
self._set_parameters(node.get("config", {}), context)
|
|
198
|
+
ag = self._create_agent(node.get("config", {}))
|
|
199
|
+
async for evt in ag.stream(context):
|
|
200
|
+
yield evt
|
|
201
|
+
result = context.get_last_output(ag.name) or ""
|
|
202
|
+
context.set_node_result(current_node_id, result)
|
|
203
|
+
yield event.step_finished(step_name=f"agent:{agent_name}")
|
|
204
|
+
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
205
|
+
|
|
206
|
+
elif node_type == "tool":
|
|
207
|
+
tool_name = node.get("config", {}).get("tool_name", "")
|
|
208
|
+
yield event.step_started(step_name=f"tool:{tool_name}")
|
|
209
|
+
result = await self._execute_node(node, context)
|
|
210
|
+
context.set_node_result(current_node_id, result)
|
|
211
|
+
yield event.step_finished(step_name=f"tool:{tool_name}")
|
|
212
|
+
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
async def _execute_node(self, node_config: dict, context: "FlowContext") -> Any:
|
|
218
|
+
"""执行节点"""
|
|
219
|
+
node_type = node_config.get("type")
|
|
220
|
+
config = node_config.get("config", {})
|
|
221
|
+
|
|
222
|
+
# 设置参数到 context
|
|
223
|
+
self._set_parameters(config, context)
|
|
224
|
+
|
|
225
|
+
# 创建并执行 runnable
|
|
226
|
+
if node_type == "agent":
|
|
227
|
+
runnable = self._create_agent(config)
|
|
228
|
+
elif node_type == "tool":
|
|
229
|
+
runnable = self._create_tool(config)
|
|
230
|
+
else:
|
|
231
|
+
raise FlowError("UNKNOWN_NODE_TYPE", 400, {"type": node_type})
|
|
232
|
+
|
|
233
|
+
return await runnable.run(context)
|
|
234
|
+
|
|
235
|
+
def _set_parameters(self, config: dict, context: "FlowContext") -> None:
|
|
236
|
+
"""设置参数到 context"""
|
|
237
|
+
parameters = config.get("parameters", {})
|
|
238
|
+
|
|
239
|
+
for key, value in parameters.items():
|
|
240
|
+
resolved_value = context.resolve_reference(value) if isinstance(value, str) else value
|
|
241
|
+
context.set_variable(key, resolved_value)
|
|
242
|
+
|
|
243
|
+
def _create_agent(self, config: dict):
|
|
244
|
+
"""创建 Agent"""
|
|
245
|
+
agent_name = config.get("agent_name")
|
|
246
|
+
if not agent_name:
|
|
247
|
+
raise FlowError("MISSING_AGENT_NAME", 400)
|
|
248
|
+
|
|
249
|
+
agent = registry.create_agent(agent_name)
|
|
250
|
+
if not agent:
|
|
251
|
+
raise FlowError("AGENT_NOT_FOUND", 404, {"agent_name": agent_name})
|
|
252
|
+
|
|
253
|
+
return agent
|
|
254
|
+
|
|
255
|
+
def _create_tool(self, config: dict):
|
|
256
|
+
"""创建 Tool"""
|
|
257
|
+
tool_name = config.get("tool_name")
|
|
258
|
+
if not tool_name:
|
|
259
|
+
raise FlowError("MISSING_TOOL_NAME", 400)
|
|
260
|
+
|
|
261
|
+
tool = registry.create_tool(tool_name)
|
|
262
|
+
if not tool:
|
|
263
|
+
raise FlowError("TOOL_NOT_FOUND", 404, {"tool_name": tool_name})
|
|
264
|
+
|
|
265
|
+
return tool
|
|
266
|
+
|
|
267
|
+
def get_node_config(self, node_id: str) -> dict[str, Any] | None:
|
|
268
|
+
"""获取节点配置"""
|
|
269
|
+
for node in self.nodes:
|
|
270
|
+
if node.get("id") == node_id:
|
|
271
|
+
return node
|
|
272
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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>
|
|
@@ -34,7 +34,7 @@ Requires-Dist: pydantic>=2.12.4
|
|
|
34
34
|
Requires-Dist: python-multipart>=0.0.20
|
|
35
35
|
Requires-Dist: requests>=2.32.5
|
|
36
36
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.48
|
|
37
|
-
Requires-Dist: sqlobjects>=1.
|
|
37
|
+
Requires-Dist: sqlobjects>=1.3.0
|
|
38
38
|
Requires-Dist: tiktoken>=0.12.0
|
|
39
39
|
Requires-Dist: uvicorn>=0.41.0
|
|
40
40
|
Dynamic: license-file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agstack"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.0"
|
|
4
4
|
description = "Production-ready toolkit for building FastAPI and LLM applications"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -53,7 +53,7 @@ dependencies = [
|
|
|
53
53
|
"python-multipart>=0.0.20",
|
|
54
54
|
"requests>=2.32.5",
|
|
55
55
|
"sqlalchemy[asyncio]>=2.0.48",
|
|
56
|
-
"sqlobjects>=1.
|
|
56
|
+
"sqlobjects>=1.3.0",
|
|
57
57
|
"tiktoken>=0.12.0",
|
|
58
58
|
"uvicorn>=0.41.0",
|
|
59
59
|
]
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2020-2025 XtraVisions, All rights reserved.
|
|
2
|
-
|
|
3
|
-
"""Flow 定义和执行"""
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
7
|
-
|
|
8
|
-
from . import event
|
|
9
|
-
from .exceptions import FlowError
|
|
10
|
-
from .registry import registry
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from .context import FlowContext
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class Flow:
|
|
19
|
-
"""Flow 配置定义"""
|
|
20
|
-
|
|
21
|
-
flow_id: str
|
|
22
|
-
name: str
|
|
23
|
-
description: str = ""
|
|
24
|
-
nodes: list[dict[str, Any]] = field(default_factory=list)
|
|
25
|
-
edges: list[dict[str, Any]] = field(default_factory=list)
|
|
26
|
-
variables: dict[str, Any] = field(default_factory=dict)
|
|
27
|
-
|
|
28
|
-
async def run(self, context: "FlowContext") -> dict[str, Any]:
|
|
29
|
-
"""执行 Flow"""
|
|
30
|
-
# 简单的顺序执行(可扩展为拓扑排序)
|
|
31
|
-
for node in self.nodes:
|
|
32
|
-
node_id = node.get("id")
|
|
33
|
-
if not node_id:
|
|
34
|
-
continue
|
|
35
|
-
|
|
36
|
-
context.current_node = node_id
|
|
37
|
-
result = await self._execute_node(node, context)
|
|
38
|
-
context.set_node_result(node_id, result)
|
|
39
|
-
|
|
40
|
-
return context.node_results
|
|
41
|
-
|
|
42
|
-
async def stream(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
43
|
-
"""流式执行 Flow(输出 AG-UI 标准事件)"""
|
|
44
|
-
yield event.step_started(step_name=f"flow:{self.name}")
|
|
45
|
-
|
|
46
|
-
# 按顺序执行节点
|
|
47
|
-
for node in self.nodes:
|
|
48
|
-
node_id = node.get("id")
|
|
49
|
-
if not node_id:
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
context.current_node = node_id
|
|
53
|
-
yield event.step_started(step_name=f"node:{node_id}")
|
|
54
|
-
|
|
55
|
-
# 执行节点
|
|
56
|
-
if node.get("type") == "agent":
|
|
57
|
-
# Agent 节点 - 流式执行
|
|
58
|
-
agent_name = node.get("config", {}).get("agent_name", "")
|
|
59
|
-
yield event.step_started(step_name=f"agent:{agent_name}")
|
|
60
|
-
|
|
61
|
-
# 设置参数
|
|
62
|
-
self._set_parameters(node.get("config", {}), context)
|
|
63
|
-
|
|
64
|
-
# 创建并流式执行 Agent
|
|
65
|
-
ag = self._create_agent(node.get("config", {}))
|
|
66
|
-
async for evt in ag.stream(context):
|
|
67
|
-
yield evt
|
|
68
|
-
|
|
69
|
-
# 获取最终结果
|
|
70
|
-
result = context.get_last_output(ag.name) or ""
|
|
71
|
-
context.set_node_result(node_id, result)
|
|
72
|
-
|
|
73
|
-
yield event.step_finished(step_name=f"agent:{agent_name}")
|
|
74
|
-
|
|
75
|
-
else:
|
|
76
|
-
# Tool 节点 - 非流式执行
|
|
77
|
-
tool_name = node.get("config", {}).get("tool_name", "")
|
|
78
|
-
yield event.step_started(step_name=f"tool:{tool_name}")
|
|
79
|
-
|
|
80
|
-
result = await self._execute_node(node, context)
|
|
81
|
-
context.set_node_result(node_id, result)
|
|
82
|
-
|
|
83
|
-
yield event.step_finished(step_name=f"tool:{tool_name}")
|
|
84
|
-
|
|
85
|
-
yield event.step_finished(step_name=f"flow:{self.name}")
|
|
86
|
-
|
|
87
|
-
async def _execute_node(self, node_config: dict, context: "FlowContext") -> Any:
|
|
88
|
-
"""执行节点"""
|
|
89
|
-
node_type = node_config.get("type")
|
|
90
|
-
config = node_config.get("config", {})
|
|
91
|
-
|
|
92
|
-
# 设置参数到 context
|
|
93
|
-
self._set_parameters(config, context)
|
|
94
|
-
|
|
95
|
-
# 创建并执行 runnable
|
|
96
|
-
if node_type == "agent":
|
|
97
|
-
runnable = self._create_agent(config)
|
|
98
|
-
elif node_type == "tool":
|
|
99
|
-
runnable = self._create_tool(config)
|
|
100
|
-
else:
|
|
101
|
-
raise FlowError("UNKNOWN_NODE_TYPE", 400, {"type": node_type})
|
|
102
|
-
|
|
103
|
-
return await runnable.run(context)
|
|
104
|
-
|
|
105
|
-
def _set_parameters(self, config: dict, context: "FlowContext") -> None:
|
|
106
|
-
"""设置参数到 context"""
|
|
107
|
-
parameters = config.get("parameters", {})
|
|
108
|
-
|
|
109
|
-
for key, value in parameters.items():
|
|
110
|
-
resolved_value = context.resolve_reference(value) if isinstance(value, str) else value
|
|
111
|
-
context.set_variable(key, resolved_value)
|
|
112
|
-
|
|
113
|
-
def _create_agent(self, config: dict):
|
|
114
|
-
"""创建 Agent"""
|
|
115
|
-
agent_name = config.get("agent_name")
|
|
116
|
-
if not agent_name:
|
|
117
|
-
raise FlowError("MISSING_AGENT_NAME", 400)
|
|
118
|
-
|
|
119
|
-
agent = registry.create_agent(agent_name)
|
|
120
|
-
if not agent:
|
|
121
|
-
raise FlowError("AGENT_NOT_FOUND", 404, {"agent_name": agent_name})
|
|
122
|
-
|
|
123
|
-
return agent
|
|
124
|
-
|
|
125
|
-
def _create_tool(self, config: dict):
|
|
126
|
-
"""创建 Tool"""
|
|
127
|
-
tool_name = config.get("tool_name")
|
|
128
|
-
if not tool_name:
|
|
129
|
-
raise FlowError("MISSING_TOOL_NAME", 400)
|
|
130
|
-
|
|
131
|
-
tool = registry.create_tool(tool_name)
|
|
132
|
-
if not tool:
|
|
133
|
-
raise FlowError("TOOL_NOT_FOUND", 404, {"tool_name": tool_name})
|
|
134
|
-
|
|
135
|
-
return tool
|
|
136
|
-
|
|
137
|
-
def get_node_config(self, node_id: str) -> dict[str, Any] | None:
|
|
138
|
-
"""获取节点配置"""
|
|
139
|
-
for node in self.nodes:
|
|
140
|
-
if node.get("id") == node_id:
|
|
141
|
-
return node
|
|
142
|
-
return None
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|