agstack 1.2.2__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.
Files changed (51) hide show
  1. {agstack-1.2.2 → agstack-1.3.0}/PKG-INFO +2 -2
  2. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/client.py +35 -0
  3. agstack-1.3.0/agstack/llm/flow/flow.py +272 -0
  4. {agstack-1.2.2 → agstack-1.3.0}/agstack/security/casbin.py +1 -1
  5. {agstack-1.2.2 → agstack-1.3.0}/agstack.egg-info/PKG-INFO +2 -2
  6. {agstack-1.2.2 → agstack-1.3.0}/agstack.egg-info/requires.txt +1 -1
  7. {agstack-1.2.2 → agstack-1.3.0}/pyproject.toml +2 -2
  8. agstack-1.2.2/agstack/llm/flow/flow.py +0 -142
  9. {agstack-1.2.2 → agstack-1.3.0}/LICENSE +0 -0
  10. {agstack-1.2.2 → agstack-1.3.0}/README.md +0 -0
  11. {agstack-1.2.2 → agstack-1.3.0}/agstack/__init__.py +0 -0
  12. {agstack-1.2.2 → agstack-1.3.0}/agstack/config/__init__.py +0 -0
  13. {agstack-1.2.2 → agstack-1.3.0}/agstack/config/logger.py +0 -0
  14. {agstack-1.2.2 → agstack-1.3.0}/agstack/config/manager.py +0 -0
  15. {agstack-1.2.2 → agstack-1.3.0}/agstack/config/types.py +0 -0
  16. {agstack-1.2.2 → agstack-1.3.0}/agstack/contexts.py +0 -0
  17. {agstack-1.2.2 → agstack-1.3.0}/agstack/decorators.py +0 -0
  18. {agstack-1.2.2 → agstack-1.3.0}/agstack/events.py +0 -0
  19. {agstack-1.2.2 → agstack-1.3.0}/agstack/exceptions.py +0 -0
  20. {agstack-1.2.2 → agstack-1.3.0}/agstack/fastapi/__init__.py +0 -0
  21. {agstack-1.2.2 → agstack-1.3.0}/agstack/fastapi/exception.py +0 -0
  22. {agstack-1.2.2 → agstack-1.3.0}/agstack/fastapi/middleware.py +0 -0
  23. {agstack-1.2.2 → agstack-1.3.0}/agstack/fastapi/offline.py +0 -0
  24. {agstack-1.2.2 → agstack-1.3.0}/agstack/fastapi/sse.py +0 -0
  25. {agstack-1.2.2 → agstack-1.3.0}/agstack/infra/db/__init__.py +0 -0
  26. {agstack-1.2.2 → agstack-1.3.0}/agstack/infra/es/__init__.py +0 -0
  27. {agstack-1.2.2 → agstack-1.3.0}/agstack/infra/kg/__init__.py +0 -0
  28. {agstack-1.2.2 → agstack-1.3.0}/agstack/infra/mq/__init__.py +0 -0
  29. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/__init__.py +0 -0
  30. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/__init__.py +0 -0
  31. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/agent.py +0 -0
  32. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/context.py +0 -0
  33. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/event.py +0 -0
  34. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/exceptions.py +0 -0
  35. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/factory.py +0 -0
  36. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/loader.py +0 -0
  37. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/records.py +0 -0
  38. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/registry.py +0 -0
  39. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/state.py +0 -0
  40. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/flow/tool.py +0 -0
  41. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/prompts.py +0 -0
  42. {agstack-1.2.2 → agstack-1.3.0}/agstack/llm/token.py +0 -0
  43. {agstack-1.2.2 → agstack-1.3.0}/agstack/registry.py +0 -0
  44. {agstack-1.2.2 → agstack-1.3.0}/agstack/schema.py +0 -0
  45. {agstack-1.2.2 → agstack-1.3.0}/agstack/security/__init__.py +0 -0
  46. {agstack-1.2.2 → agstack-1.3.0}/agstack/security/crypt.py +0 -0
  47. {agstack-1.2.2 → agstack-1.3.0}/agstack/status.py +0 -0
  48. {agstack-1.2.2 → agstack-1.3.0}/agstack.egg-info/SOURCES.txt +0 -0
  49. {agstack-1.2.2 → agstack-1.3.0}/agstack.egg-info/dependency_links.txt +0 -0
  50. {agstack-1.2.2 → agstack-1.3.0}/agstack.egg-info/top_level.txt +0 -0
  51. {agstack-1.2.2 → 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.2.2
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.2.5
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
@@ -4,6 +4,7 @@ import logging
4
4
  import time
5
5
  from typing import TYPE_CHECKING, Any, AsyncIterator, Literal, overload
6
6
 
7
+ import httpx
7
8
  from httpx import AsyncClient
8
9
  from httpx import Timeout as HttpxTimeout
9
10
  from openai import APIError, APITimeoutError, AsyncOpenAI, OpenAI, RateLimitError
@@ -447,7 +448,15 @@ class LLMClient:
447
448
  :param model: 模型名称
448
449
  :return: [(index, score, text), ...] 按相关性降序排列
449
450
  """
451
+ from httpx import ConnectTimeout, TimeoutException
450
452
 
453
+ @autoretry(
454
+ logger,
455
+ retries=3,
456
+ delay=2.0,
457
+ backoff=2.0,
458
+ exceptions=(ConnectTimeout, ConnectionError),
459
+ )
451
460
  async def _call():
452
461
  return await self._async_http_client.post(
453
462
  f"{self._base_url}/reranks",
@@ -463,6 +472,7 @@ class LLMClient:
463
472
 
464
473
  try:
465
474
  response = await _call()
475
+ response.raise_for_status()
466
476
  data = response.json()
467
477
 
468
478
  # 解析响应
@@ -476,6 +486,17 @@ class LLMClient:
476
486
 
477
487
  return results
478
488
 
489
+ except TimeoutException as e:
490
+ logger.error(f"Rerank timeout: {e}")
491
+ raise LLMTimeoutError("LLM_RERANK_TIMEOUT") from e
492
+
493
+ except httpx.HTTPStatusError as e:
494
+ if e.response.status_code == 429:
495
+ logger.error(f"Rerank rate limit: {e}")
496
+ raise LLMRateLimitError("LLM_RERANK_RATE_LIMIT") from e
497
+ logger.error(f"Rerank HTTP error {e.response.status_code}: {e}")
498
+ raise LLMError("LLM_RERANK_ERROR", {"error": str(e)}, http_status=e.response.status_code) from e
499
+
479
500
  except Exception as e:
480
501
  logger.error(f"Rerank error: {e}")
481
502
  raise LLMError("LLM_RERANK_ERROR", {"error": str(e)}) from e
@@ -495,6 +516,8 @@ class LLMClient:
495
516
  :param model: 模型名称
496
517
  :return: [(index, score, text), ...] 按相关性降序排列
497
518
  """
519
+ import requests
520
+
498
521
  session = self._get_sync_http_session()
499
522
 
500
523
  try:
@@ -522,6 +545,18 @@ class LLMClient:
522
545
 
523
546
  return results
524
547
 
548
+ except requests.Timeout as e:
549
+ logger.error(f"Rerank timeout (sync): {e}")
550
+ raise LLMTimeoutError("LLM_RERANK_TIMEOUT") from e
551
+
552
+ except requests.HTTPError as e:
553
+ status = e.response.status_code if e.response is not None else 500
554
+ if status == 429:
555
+ logger.error(f"Rerank rate limit (sync): {e}")
556
+ raise LLMRateLimitError("LLM_RERANK_RATE_LIMIT") from e
557
+ logger.error(f"Rerank HTTP error {status} (sync): {e}")
558
+ raise LLMError("LLM_RERANK_ERROR", {"error": str(e)}, http_status=status) from e
559
+
525
560
  except Exception as e:
526
561
  logger.error(f"Rerank error (sync): {e}")
527
562
  raise LLMError("LLM_RERANK_ERROR", {"error": str(e)}) from e
@@ -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
@@ -40,7 +40,7 @@ class CasbinRules(ObjectModel):
40
40
  created_at: Column[datetime] = column(type="datetime", default_factory=datetime.now)
41
41
 
42
42
  class Config:
43
- table_name = "system_casbin_rule"
43
+ table_name = "system_casbin_rules"
44
44
 
45
45
 
46
46
  class SqlObjectsAdapter(AsyncAdapter, AsyncFilteredAdapter):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agstack
3
- Version: 1.2.2
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.2.5
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
@@ -12,6 +12,6 @@ pydantic>=2.12.4
12
12
  python-multipart>=0.0.20
13
13
  requests>=2.32.5
14
14
  sqlalchemy[asyncio]>=2.0.48
15
- sqlobjects>=1.2.5
15
+ sqlobjects>=1.3.0
16
16
  tiktoken>=0.12.0
17
17
  uvicorn>=0.41.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agstack"
3
- version = "1.2.2"
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.2.5",
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