agstack 1.17.0__tar.gz → 1.18.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.17.0 → agstack-1.18.0}/PKG-INFO +1 -1
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/agent.py +10 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/context.py +6 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/base.py +5 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/tool.py +26 -4
- {agstack-1.17.0 → agstack-1.18.0}/agstack/messagebus/redis.py +12 -5
- {agstack-1.17.0 → agstack-1.18.0}/agstack.egg-info/PKG-INFO +1 -1
- {agstack-1.17.0 → agstack-1.18.0}/pyproject.toml +1 -1
- {agstack-1.17.0 → agstack-1.18.0}/tests/test_messagebus_redis.py +16 -12
- {agstack-1.17.0 → agstack-1.18.0}/LICENSE +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/README.md +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/cache/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/cache/base.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/cache/memory.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/cache/redis.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/config/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/config/logger.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/config/manager.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/config/types.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/contexts.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/decorators.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/events.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/exceptions.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/fastapi/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/fastapi/exception.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/fastapi/middleware.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/fastapi/offline.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/fastapi/sse.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/infra/db/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/infra/es/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/infra/kg/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/infra/mq/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/client.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/event.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/exceptions.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/factory.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/flow.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/loader.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/agent_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/detect_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/echo_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/llm_chat_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/llm_embed_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/llm_rerank_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/python_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/subflow_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/switch_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/nodes/tool_node.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/records.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/registry.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/sandbox.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/flow/state.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/prompts.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/llm/token.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/messagebus/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/messagebus/base.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/messagebus/memory.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/schema.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/security/__init__.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/security/casbin.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/security/crypt.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack/status.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack.egg-info/SOURCES.txt +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack.egg-info/dependency_links.txt +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack.egg-info/requires.txt +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/agstack.egg-info/top_level.txt +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/setup.cfg +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/tests/test_cache_memory.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/tests/test_cache_redis.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/tests/test_flow_io.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/tests/test_flow_switch_subflow.py +0 -0
- {agstack-1.17.0 → agstack-1.18.0}/tests/test_messagebus_memory.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.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>
|
|
@@ -88,6 +88,10 @@ class Agent:
|
|
|
88
88
|
) -> AsyncIterator[dict[str, Any]]:
|
|
89
89
|
"""流式执行 Agent,输出 AG-UI 标准事件"""
|
|
90
90
|
|
|
91
|
+
# 注入 agent_call_id 供 tool 审计关联
|
|
92
|
+
agent_call_id = str(uuid4())
|
|
93
|
+
context.set_variable("_agent_call_id", agent_call_id)
|
|
94
|
+
|
|
91
95
|
# 输入来源:优先 inputs 参数,回退到 context.variables
|
|
92
96
|
user_input = ""
|
|
93
97
|
if inputs:
|
|
@@ -230,6 +234,7 @@ class Agent:
|
|
|
230
234
|
context.set_output(self.name, {"result": assistant_content})
|
|
231
235
|
# AG-UI: TEXT_MESSAGE_END
|
|
232
236
|
yield event.text_message_end(message_id=msg_id)
|
|
237
|
+
context.set_variable("_agent_call_id", None)
|
|
233
238
|
return
|
|
234
239
|
|
|
235
240
|
# 执行工具调用
|
|
@@ -266,5 +271,10 @@ class Agent:
|
|
|
266
271
|
# AG-UI: TOOL_CALL_RESULT
|
|
267
272
|
yield event.tool_call_result(tool_call_id=tool_call["id"], content=result_content)
|
|
268
273
|
|
|
274
|
+
# 可观测性:yield tool 执行记录
|
|
275
|
+
for record in context.pop_execution_records():
|
|
276
|
+
record["tool_call_id"] = tool_call["id"]
|
|
277
|
+
yield event.custom(name="tool_execution", value=record)
|
|
278
|
+
|
|
269
279
|
# 更新消息列表,继续下一轮
|
|
270
280
|
messages = [self.get_system_message()] + context.history + context.get_messages(self.name)
|
|
@@ -151,3 +151,9 @@ class FlowContext:
|
|
|
151
151
|
if task_id is None:
|
|
152
152
|
return self.execution_records
|
|
153
153
|
return [r for r in self.execution_records if r.get("task_id") == task_id]
|
|
154
|
+
|
|
155
|
+
def pop_execution_records(self) -> list[dict[str, Any]]:
|
|
156
|
+
"""取出并清空待发射的执行记录"""
|
|
157
|
+
records = self.execution_records
|
|
158
|
+
self.execution_records = []
|
|
159
|
+
return records
|
|
@@ -48,4 +48,9 @@ class NodeHandler:
|
|
|
48
48
|
yield event.step_started(step_name=step_name)
|
|
49
49
|
result = await self.execute(node, context)
|
|
50
50
|
context.set_output(node_id, result)
|
|
51
|
+
|
|
52
|
+
# 可观测性:drain tool 执行记录
|
|
53
|
+
for record in context.pop_execution_records():
|
|
54
|
+
yield event.custom(name="tool_execution", value=record)
|
|
55
|
+
|
|
51
56
|
yield event.step_finished(step_name=step_name)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
"""工具定义和执行"""
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
7
|
+
import time
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from typing import TYPE_CHECKING, Any, Callable
|
|
8
10
|
|
|
@@ -54,18 +56,38 @@ class Tool:
|
|
|
54
56
|
self.echo = echo
|
|
55
57
|
|
|
56
58
|
async def execute_async(self, context: "FlowContext", inputs: dict[str, Any] | None = None) -> ToolResult:
|
|
57
|
-
"""
|
|
59
|
+
"""异步执行工具(包含计时和可观测性记录)"""
|
|
60
|
+
args = inputs or {}
|
|
61
|
+
_t0 = time.perf_counter()
|
|
62
|
+
result = await self._execute(context, args)
|
|
63
|
+
_duration_ms = int((time.perf_counter() - _t0) * 1000)
|
|
64
|
+
|
|
65
|
+
result_content = json.dumps(result.result) if result.success else json.dumps({"error": result.error})
|
|
66
|
+
context.execution_records.append({
|
|
67
|
+
"agent_call_id": context.get_variable("_agent_call_id"),
|
|
68
|
+
"tool_name": self.name,
|
|
69
|
+
"tool_args": args,
|
|
70
|
+
"success": result.success,
|
|
71
|
+
"result": result_content,
|
|
72
|
+
"error": result.error,
|
|
73
|
+
"duration_ms": _duration_ms,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
async def _execute(self, context: "FlowContext", inputs: dict[str, Any]) -> ToolResult:
|
|
79
|
+
"""实际执行逻辑,子类应覆写此方法"""
|
|
58
80
|
try:
|
|
59
|
-
result = self.function(context, inputs
|
|
81
|
+
result = self.function(context, inputs)
|
|
60
82
|
if hasattr(result, "__await__"):
|
|
61
83
|
result = await result
|
|
62
84
|
|
|
63
|
-
return ToolResult(name=self.name, arguments=inputs
|
|
85
|
+
return ToolResult(name=self.name, arguments=inputs, result=result, success=True)
|
|
64
86
|
except Exception as e:
|
|
65
87
|
logger.warning("Tool %s failed: %s", self.name, e, exc_info=True)
|
|
66
88
|
return ToolResult(
|
|
67
89
|
name=self.name,
|
|
68
|
-
arguments=inputs
|
|
90
|
+
arguments=inputs,
|
|
69
91
|
result={},
|
|
70
92
|
success=False,
|
|
71
93
|
error=str(e),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import AsyncIterator
|
|
5
|
+
from typing import Self
|
|
5
6
|
|
|
6
7
|
import redis.asyncio as aioredis
|
|
7
8
|
from redis.asyncio.client import PubSub
|
|
@@ -17,15 +18,21 @@ class RedisSubscription(Subscription):
|
|
|
17
18
|
self._channels = channels
|
|
18
19
|
self._task: asyncio.Task[None] | None = None
|
|
19
20
|
self._queue: asyncio.Queue[tuple[str, bytes]] = asyncio.Queue()
|
|
21
|
+
self._started = False
|
|
20
22
|
self._closed = False
|
|
21
23
|
|
|
22
24
|
async def _start(self) -> None:
|
|
23
|
-
|
|
25
|
+
if self._started:
|
|
26
|
+
return
|
|
27
|
+
self._started = True
|
|
24
28
|
await self._pubsub.subscribe(*self._channels)
|
|
25
29
|
self._task = asyncio.create_task(self._listen())
|
|
26
30
|
|
|
31
|
+
async def __aenter__(self) -> Self:
|
|
32
|
+
await self._start()
|
|
33
|
+
return self
|
|
34
|
+
|
|
27
35
|
async def _listen(self) -> None:
|
|
28
|
-
"""后台监听 Redis Pub/Sub 消息"""
|
|
29
36
|
try:
|
|
30
37
|
async for msg in self._pubsub.listen():
|
|
31
38
|
if self._closed:
|
|
@@ -47,6 +54,8 @@ class RedisSubscription(Subscription):
|
|
|
47
54
|
return self
|
|
48
55
|
|
|
49
56
|
async def __anext__(self) -> tuple[str, bytes]:
|
|
57
|
+
if not self._started:
|
|
58
|
+
await self._start()
|
|
50
59
|
if self._closed and self._queue.empty():
|
|
51
60
|
raise StopAsyncIteration
|
|
52
61
|
try:
|
|
@@ -79,9 +88,7 @@ class RedisMessageBus(MessageBus):
|
|
|
79
88
|
|
|
80
89
|
def subscribe(self, *channels: str) -> RedisSubscription:
|
|
81
90
|
pubsub = self._client.pubsub()
|
|
82
|
-
|
|
83
|
-
# 调用方需要在 async with 或手动 await _start() 后使用
|
|
84
|
-
return sub
|
|
91
|
+
return RedisSubscription(pubsub, channels)
|
|
85
92
|
|
|
86
93
|
async def close(self) -> None:
|
|
87
94
|
await self._client.aclose()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.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>
|
|
@@ -26,40 +26,44 @@ async def bus():
|
|
|
26
26
|
|
|
27
27
|
class TestPublishSubscribe:
|
|
28
28
|
async def test_single_channel(self, bus: RedisMessageBus) -> None:
|
|
29
|
-
|
|
30
|
-
await sub._start()
|
|
31
|
-
async with sub:
|
|
29
|
+
async with bus.subscribe("test:ch1") as sub:
|
|
32
30
|
await asyncio.sleep(0.1)
|
|
33
31
|
await bus.publish("test:ch1", b"hello")
|
|
34
|
-
channel, message = await asyncio.wait_for(sub
|
|
32
|
+
channel, message = await asyncio.wait_for(anext(sub), timeout=3)
|
|
35
33
|
assert channel == "test:ch1"
|
|
36
34
|
assert message == b"hello"
|
|
37
35
|
|
|
38
36
|
async def test_multi_channel(self, bus: RedisMessageBus) -> None:
|
|
39
|
-
|
|
40
|
-
await sub._start()
|
|
41
|
-
async with sub:
|
|
37
|
+
async with bus.subscribe("test:ch1", "test:ch2") as sub:
|
|
42
38
|
await asyncio.sleep(0.1)
|
|
43
39
|
await bus.publish("test:ch1", b"msg1")
|
|
44
40
|
await bus.publish("test:ch2", b"msg2")
|
|
45
41
|
results = []
|
|
46
42
|
for _ in range(2):
|
|
47
|
-
ch, msg = await asyncio.wait_for(sub
|
|
43
|
+
ch, msg = await asyncio.wait_for(anext(sub), timeout=3)
|
|
48
44
|
results.append((ch, msg))
|
|
49
45
|
channels = [r[0] for r in results]
|
|
50
46
|
assert "test:ch1" in channels
|
|
51
47
|
assert "test:ch2" in channels
|
|
52
48
|
|
|
49
|
+
async def test_lazy_start_without_context_manager(self, bus: RedisMessageBus) -> None:
|
|
50
|
+
sub = bus.subscribe("test:ch1")
|
|
51
|
+
await asyncio.sleep(0.1)
|
|
52
|
+
await bus.publish("test:ch1", b"world")
|
|
53
|
+
channel, message = await asyncio.wait_for(anext(sub), timeout=3)
|
|
54
|
+
assert channel == "test:ch1"
|
|
55
|
+
assert message == b"world"
|
|
56
|
+
await sub.close()
|
|
57
|
+
|
|
53
58
|
async def test_publish_no_subscribers(self, bus: RedisMessageBus) -> None:
|
|
54
59
|
await bus.publish("test:nobody", b"lost message")
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
class TestSubscriptionClose:
|
|
58
63
|
async def test_close(self, bus: RedisMessageBus) -> None:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await sub.close()
|
|
64
|
+
async with bus.subscribe("test:ch1") as sub:
|
|
65
|
+
await asyncio.sleep(0.1)
|
|
66
|
+
assert sub._closed
|
|
63
67
|
|
|
64
68
|
|
|
65
69
|
class TestBusClose:
|
|
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
|
|
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
|