agstack 1.4.0__tar.gz → 1.5.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.5.0}/PKG-INFO +2 -2
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/flow.py +128 -63
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/sandbox.py +13 -4
- {agstack-1.4.0 → agstack-1.5.0}/agstack.egg-info/PKG-INFO +2 -2
- {agstack-1.4.0 → agstack-1.5.0}/agstack.egg-info/requires.txt +1 -1
- {agstack-1.4.0 → agstack-1.5.0}/pyproject.toml +2 -2
- {agstack-1.4.0 → agstack-1.5.0}/LICENSE +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/README.md +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/config/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/config/logger.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/config/manager.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/config/types.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/contexts.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/decorators.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/events.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/exceptions.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/fastapi/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/fastapi/exception.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/fastapi/middleware.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/fastapi/offline.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/fastapi/sse.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/infra/db/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/infra/es/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/infra/kg/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/infra/mq/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/client.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/agent.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/context.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/event.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/exceptions.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/factory.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/loader.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/records.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/registry.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/state.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/flow/tool.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/prompts.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/llm/token.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/registry.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/schema.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/security/__init__.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/security/casbin.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/security/crypt.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack/status.py +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack.egg-info/SOURCES.txt +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack.egg-info/dependency_links.txt +0 -0
- {agstack-1.4.0 → agstack-1.5.0}/agstack.egg-info/top_level.txt +0 -0
- {agstack-1.4.0 → agstack-1.5.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.5.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
|
|
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
11
|
from . import event
|
|
12
|
-
from .exceptions import FlowError
|
|
12
|
+
from .exceptions import FlowError, NodeExecutionError
|
|
13
13
|
from .registry import registry
|
|
14
14
|
|
|
15
15
|
|
|
@@ -17,6 +17,15 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from .context import FlowContext
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
@dataclass
|
|
21
|
+
class RetryPolicy:
|
|
22
|
+
"""节点重试策略"""
|
|
23
|
+
|
|
24
|
+
max_retries: int = 0 # 0 = 不重试
|
|
25
|
+
delay: float = 1.0 # 初始延迟(秒)
|
|
26
|
+
backoff: float = 2.0 # 退避倍数
|
|
27
|
+
|
|
28
|
+
|
|
20
29
|
class _SafeFormatDict(dict):
|
|
21
30
|
"""安全的模板变量替换,缺失 key 时保留原始占位符"""
|
|
22
31
|
|
|
@@ -35,6 +44,21 @@ 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
|
+
# ── 重试策略 ──
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _get_retry_policy(node: dict) -> RetryPolicy:
|
|
51
|
+
"""从节点 config 解析重试策略"""
|
|
52
|
+
config = node.get("config", {})
|
|
53
|
+
retry_cfg = config.get("retry", {})
|
|
54
|
+
if not retry_cfg:
|
|
55
|
+
return RetryPolicy()
|
|
56
|
+
return RetryPolicy(
|
|
57
|
+
max_retries=retry_cfg.get("max_retries", 0),
|
|
58
|
+
delay=retry_cfg.get("delay", 1.0),
|
|
59
|
+
backoff=retry_cfg.get("backoff", 2.0),
|
|
60
|
+
)
|
|
61
|
+
|
|
38
62
|
# ── 边驱动路由 ──
|
|
39
63
|
|
|
40
64
|
def _resolve_next_node(self, current_id: str, result: str | None = None) -> str | None:
|
|
@@ -46,33 +70,22 @@ class Flow:
|
|
|
46
70
|
return edge.get("target")
|
|
47
71
|
return None
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _extract_route_key(result: Any) -> str:
|
|
75
|
+
"""从节点执行结果中提取路由键。
|
|
50
76
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
client = get_llm_client()
|
|
66
|
-
response = await client.chat(
|
|
67
|
-
messages=[{"role": "user", "content": prompt}],
|
|
68
|
-
model=config.get("model", "gpt-4o-mini"),
|
|
69
|
-
temperature=0,
|
|
70
|
-
)
|
|
71
|
-
text = response.choices[0].message.content or ""
|
|
77
|
+
支持 ``{"result": "qa"}`` 形式的 JSON 字符串,
|
|
78
|
+
以及纯字符串结果。
|
|
79
|
+
"""
|
|
80
|
+
if not isinstance(result, str):
|
|
81
|
+
return "done"
|
|
72
82
|
try:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
parsed = _json.loads(result)
|
|
84
|
+
if isinstance(parsed, dict) and "result" in parsed:
|
|
85
|
+
return str(parsed["result"])
|
|
86
|
+
except (ValueError, TypeError):
|
|
87
|
+
pass
|
|
88
|
+
return result or "done"
|
|
76
89
|
|
|
77
90
|
# ── message 节点 ──
|
|
78
91
|
|
|
@@ -86,6 +99,69 @@ class Flow:
|
|
|
86
99
|
yield event.text_message_content(message_id=msg_id, delta=text)
|
|
87
100
|
yield event.text_message_end(message_id=msg_id)
|
|
88
101
|
|
|
102
|
+
# ── 带重试的节点执行 ──
|
|
103
|
+
|
|
104
|
+
async def _execute_node_with_retry(
|
|
105
|
+
self,
|
|
106
|
+
node: dict,
|
|
107
|
+
context: "FlowContext",
|
|
108
|
+
node_id: str,
|
|
109
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
110
|
+
"""执行节点,带重试策略,产出 AG-UI 事件"""
|
|
111
|
+
policy = self._get_retry_policy(node)
|
|
112
|
+
node_type = node.get("type")
|
|
113
|
+
config = node.get("config", {})
|
|
114
|
+
label = config.get("agent_name") or config.get("tool_name") or node_id
|
|
115
|
+
last_error: Exception | None = None
|
|
116
|
+
|
|
117
|
+
for attempt in range(policy.max_retries + 1):
|
|
118
|
+
try:
|
|
119
|
+
if attempt > 0:
|
|
120
|
+
wait = policy.delay * (policy.backoff ** (attempt - 1))
|
|
121
|
+
await asyncio.sleep(wait)
|
|
122
|
+
yield event.custom(
|
|
123
|
+
name="node_retry",
|
|
124
|
+
value={
|
|
125
|
+
"nodeId": node_id,
|
|
126
|
+
"nodeType": node_type,
|
|
127
|
+
"label": label,
|
|
128
|
+
"attempt": attempt + 1,
|
|
129
|
+
"maxAttempts": policy.max_retries + 1,
|
|
130
|
+
"error": str(last_error),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if node_type == "agent":
|
|
135
|
+
yield event.step_started(step_name=f"agent:{label}")
|
|
136
|
+
self._set_parameters(config, context)
|
|
137
|
+
ag = self._create_agent(config)
|
|
138
|
+
async for evt in ag.stream(context):
|
|
139
|
+
yield evt
|
|
140
|
+
result = context.get_last_output(ag.name) or ""
|
|
141
|
+
context.set_node_result(node_id, result)
|
|
142
|
+
yield event.step_finished(step_name=f"agent:{label}")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
elif node_type == "tool":
|
|
146
|
+
yield event.step_started(step_name=f"tool:{label}")
|
|
147
|
+
result = await self._execute_node(node, context)
|
|
148
|
+
context.set_node_result(node_id, result)
|
|
149
|
+
yield event.step_finished(step_name=f"tool:{label}")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
last_error = e
|
|
154
|
+
if attempt < policy.max_retries:
|
|
155
|
+
continue
|
|
156
|
+
yield event.run_error(
|
|
157
|
+
message=str(e),
|
|
158
|
+
code=type(e).__name__,
|
|
159
|
+
)
|
|
160
|
+
raise NodeExecutionError(
|
|
161
|
+
"NODE_EXECUTION_FAILED",
|
|
162
|
+
args={"node_id": node_id, "error": str(e)},
|
|
163
|
+
) from e
|
|
164
|
+
|
|
89
165
|
# ── 执行入口 ──
|
|
90
166
|
|
|
91
167
|
async def run(self, context: "FlowContext") -> dict[str, Any]:
|
|
@@ -109,11 +185,7 @@ class Flow:
|
|
|
109
185
|
context.current_node = current_node_id
|
|
110
186
|
node_type = node.get("type")
|
|
111
187
|
|
|
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":
|
|
188
|
+
if node_type == "message":
|
|
117
189
|
config = node.get("config", {})
|
|
118
190
|
template = config.get("content", "")
|
|
119
191
|
text = template.format_map(_SafeFormatDict(context.variables))
|
|
@@ -122,7 +194,10 @@ class Flow:
|
|
|
122
194
|
elif node_type in ("agent", "tool"):
|
|
123
195
|
result = await self._execute_node(node, context)
|
|
124
196
|
context.set_node_result(current_node_id, result)
|
|
125
|
-
|
|
197
|
+
route_key = self._extract_route_key(result)
|
|
198
|
+
current_node_id = self._resolve_next_node(current_node_id, route_key) or self._resolve_next_node(
|
|
199
|
+
current_node_id, "done"
|
|
200
|
+
)
|
|
126
201
|
|
|
127
202
|
elif node_type == "parallel":
|
|
128
203
|
config = node.get("config", {})
|
|
@@ -253,16 +328,9 @@ class Flow:
|
|
|
253
328
|
context.current_node = node_id
|
|
254
329
|
yield event.step_started(step_name=f"node:{node_id}")
|
|
255
330
|
|
|
256
|
-
if node.get("type")
|
|
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):
|
|
331
|
+
if node.get("type") in ("agent", "tool"):
|
|
332
|
+
async for evt in self._execute_node_with_retry(node, context, node_id):
|
|
262
333
|
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
334
|
else:
|
|
267
335
|
tool_name = node.get("config", {}).get("tool_name", "")
|
|
268
336
|
yield event.step_started(step_name=f"tool:{tool_name}")
|
|
@@ -277,40 +345,37 @@ class Flow:
|
|
|
277
345
|
while current_node_id:
|
|
278
346
|
node = self.get_node_config(current_node_id)
|
|
279
347
|
if not node:
|
|
280
|
-
|
|
348
|
+
yield event.run_error(
|
|
349
|
+
message=f"Node not found: {current_node_id}",
|
|
350
|
+
code="NODE_NOT_FOUND",
|
|
351
|
+
)
|
|
352
|
+
raise NodeExecutionError("NODE_NOT_FOUND", args={"node_id": current_node_id})
|
|
281
353
|
|
|
282
354
|
context.current_node = current_node_id
|
|
283
355
|
node_type = node.get("type")
|
|
284
356
|
|
|
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":
|
|
357
|
+
if node_type == "message":
|
|
291
358
|
async for evt in self._emit_message(node, context):
|
|
292
359
|
yield evt
|
|
293
360
|
current_node_id = self._resolve_next_node(current_node_id, "done")
|
|
294
361
|
|
|
295
362
|
elif node_type == "agent":
|
|
296
|
-
|
|
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):
|
|
363
|
+
async for evt in self._execute_node_with_retry(node, context, current_node_id):
|
|
301
364
|
yield evt
|
|
302
|
-
result = context.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
365
|
+
result = context.node_results.get(current_node_id, "")
|
|
366
|
+
route_key = self._extract_route_key(result)
|
|
367
|
+
current_node_id = self._resolve_next_node(current_node_id, route_key) or self._resolve_next_node(
|
|
368
|
+
current_node_id, "done"
|
|
369
|
+
)
|
|
306
370
|
|
|
307
371
|
elif node_type == "tool":
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
result =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
372
|
+
async for evt in self._execute_node_with_retry(node, context, current_node_id):
|
|
373
|
+
yield evt
|
|
374
|
+
result = context.node_results.get(current_node_id, "")
|
|
375
|
+
route_key = self._extract_route_key(result)
|
|
376
|
+
current_node_id = self._resolve_next_node(current_node_id, route_key) or self._resolve_next_node(
|
|
377
|
+
current_node_id, "done"
|
|
378
|
+
)
|
|
314
379
|
|
|
315
380
|
elif node_type == "parallel":
|
|
316
381
|
config = node.get("config", {})
|
|
@@ -11,10 +11,19 @@ from RestrictedPython.Guards import guarded_unpack_sequence, safer_getattr
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# 白名单内置模块
|
|
14
|
-
_ALLOWED_MODULES = frozenset(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
_ALLOWED_MODULES = frozenset(
|
|
15
|
+
{
|
|
16
|
+
"json",
|
|
17
|
+
"re",
|
|
18
|
+
"math",
|
|
19
|
+
"datetime",
|
|
20
|
+
"collections",
|
|
21
|
+
"itertools",
|
|
22
|
+
"functools",
|
|
23
|
+
"operator",
|
|
24
|
+
"string",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
18
27
|
|
|
19
28
|
_builtins_import = builtins.__import__
|
|
20
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agstack"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
4
4
|
description = "Production-ready toolkit for building FastAPI and LLM applications"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -54,7 +54,7 @@ dependencies = [
|
|
|
54
54
|
"requests>=2.32.5",
|
|
55
55
|
"RestrictedPython>=7.0",
|
|
56
56
|
"sqlalchemy[asyncio]>=2.0.48",
|
|
57
|
-
"sqlobjects>=1.
|
|
57
|
+
"sqlobjects>=1.4.0",
|
|
58
58
|
"tiktoken>=0.12.0",
|
|
59
59
|
"uvicorn>=0.41.0",
|
|
60
60
|
]
|
|
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
|