agstack 1.2.0__tar.gz → 1.2.2__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.0 → agstack-1.2.2}/PKG-INFO +1 -2
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/__init__.py +3 -3
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/agent.py +60 -43
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/context.py +45 -11
- agstack-1.2.2/agstack/llm/flow/event.py +123 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/flow.py +12 -25
- {agstack-1.2.0 → agstack-1.2.2}/agstack/registry.py +4 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack.egg-info/PKG-INFO +1 -2
- {agstack-1.2.0 → agstack-1.2.2}/agstack.egg-info/SOURCES.txt +1 -1
- {agstack-1.2.0 → agstack-1.2.2}/agstack.egg-info/requires.txt +0 -1
- {agstack-1.2.0 → agstack-1.2.2}/pyproject.toml +1 -2
- agstack-1.2.0/agstack/llm/flow/events.py +0 -45
- {agstack-1.2.0 → agstack-1.2.2}/LICENSE +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/README.md +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/config/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/config/logger.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/config/manager.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/config/types.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/contexts.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/decorators.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/events.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/exceptions.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/fastapi/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/fastapi/exception.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/fastapi/middleware.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/fastapi/offline.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/fastapi/sse.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/infra/db/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/infra/es/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/infra/kg/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/infra/mq/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/client.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/exceptions.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/factory.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/loader.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/records.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/registry.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/state.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/flow/tool.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/prompts.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/llm/token.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/schema.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/security/__init__.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/security/casbin.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/security/crypt.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack/status.py +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack.egg-info/dependency_links.txt +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/agstack.egg-info/top_level.txt +0 -0
- {agstack-1.2.0 → agstack-1.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
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>
|
|
@@ -20,7 +20,6 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: ag-ui-protocol>=0.1.13
|
|
24
23
|
Requires-Dist: aio-pika>=9.6.1
|
|
25
24
|
Requires-Dist: asyncpg>=0.30.0
|
|
26
25
|
Requires-Dist: elasticsearch[async]>=9.3.0
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
"""统一的执行框架"""
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
from . import event
|
|
6
6
|
from .agent import Agent
|
|
7
7
|
from .context import FlowContext, Usage
|
|
8
|
-
from .
|
|
8
|
+
from .event import EventType
|
|
9
9
|
from .exceptions import (
|
|
10
10
|
AgentError,
|
|
11
11
|
FlowConfigError,
|
|
@@ -33,8 +33,8 @@ __all__ = [
|
|
|
33
33
|
"FlowContext",
|
|
34
34
|
"Usage",
|
|
35
35
|
# AG-UI 协议
|
|
36
|
-
"Event",
|
|
37
36
|
"EventType",
|
|
37
|
+
"event",
|
|
38
38
|
# 注册和工厂(registry 返回 None 失败,factory 函数抛出异常)
|
|
39
39
|
"registry",
|
|
40
40
|
"create_tool",
|
|
@@ -7,8 +7,9 @@ from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
|
7
7
|
from uuid import uuid4
|
|
8
8
|
|
|
9
9
|
from ..client import get_llm_client
|
|
10
|
+
from . import event
|
|
10
11
|
from .context import Usage
|
|
11
|
-
from .
|
|
12
|
+
from .event import EventType
|
|
12
13
|
from .exceptions import FlowError
|
|
13
14
|
|
|
14
15
|
|
|
@@ -66,30 +67,31 @@ class Agent:
|
|
|
66
67
|
async def run(self, context: "FlowContext") -> str:
|
|
67
68
|
"""执行 Agent 逻辑"""
|
|
68
69
|
content_parts = []
|
|
69
|
-
async for
|
|
70
|
+
async for evt in self.stream(context):
|
|
70
71
|
# AG-UI 事件格式
|
|
71
|
-
if isinstance(
|
|
72
|
-
if
|
|
73
|
-
content_parts.append(
|
|
74
|
-
elif
|
|
75
|
-
raise FlowError("AGENT_EXECUTION_FAILED", 500, {"error":
|
|
72
|
+
if isinstance(evt, dict):
|
|
73
|
+
if evt.get("type") == EventType.TEXT_MESSAGE_CONTENT:
|
|
74
|
+
content_parts.append(evt.get("delta", ""))
|
|
75
|
+
elif evt.get("type") == EventType.RUN_ERROR:
|
|
76
|
+
raise FlowError("AGENT_EXECUTION_FAILED", 500, {"error": evt.get("message")})
|
|
76
77
|
return "".join(content_parts)
|
|
77
78
|
|
|
78
79
|
async def stream(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
79
80
|
"""流式执行 Agent,输出 AG-UI 标准事件"""
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
# 输入来源:优先 input(A2A 传入),回退到 query
|
|
83
|
+
user_input = context.get_variable("input") or context.get_variable("query", "")
|
|
84
|
+
msg_id = context.message_id or str(uuid4())
|
|
83
85
|
|
|
84
|
-
#
|
|
85
|
-
context.add_message("user",
|
|
86
|
+
# 添加用户消息(scoped by agent name)
|
|
87
|
+
context.add_message(self.name, "user", user_input)
|
|
86
88
|
context.last_agent = self.name
|
|
87
89
|
|
|
88
90
|
# AG-UI: TEXT_MESSAGE_START
|
|
89
|
-
yield
|
|
91
|
+
yield event.text_message_start(message_id=msg_id, role="assistant")
|
|
90
92
|
|
|
91
|
-
#
|
|
92
|
-
messages = [self.get_system_message()] + context.
|
|
93
|
+
# 构建消息列表:system + 共享历史 + 当前 agent 的隔离消息
|
|
94
|
+
messages = [self.get_system_message()] + context.history + context.get_messages(self.name)
|
|
93
95
|
tools_schema = self.get_tools_schema() if self.tools else None
|
|
94
96
|
|
|
95
97
|
# 获取 LLM 客户端
|
|
@@ -130,11 +132,10 @@ class Agent:
|
|
|
130
132
|
# 内容增量 - AG-UI: TEXT_MESSAGE_CONTENT
|
|
131
133
|
if delta.content:
|
|
132
134
|
assistant_content += delta.content
|
|
133
|
-
yield
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
135
|
+
yield event.text_message_content(
|
|
136
|
+
message_id=msg_id,
|
|
137
|
+
delta=delta.content,
|
|
138
|
+
)
|
|
138
139
|
|
|
139
140
|
# 工具调用
|
|
140
141
|
if delta.tool_calls:
|
|
@@ -161,21 +162,19 @@ class Agent:
|
|
|
161
162
|
tool_calls.append(tool_call_data)
|
|
162
163
|
|
|
163
164
|
# TOOL_CALL_START
|
|
164
|
-
yield
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
165
|
+
yield event.tool_call_start(
|
|
166
|
+
tool_call_id=tool_call_data["id"],
|
|
167
|
+
tool_call_name=tool_call_data["name"],
|
|
168
|
+
)
|
|
169
169
|
|
|
170
170
|
# TOOL_CALL_ARGS
|
|
171
|
-
yield
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
171
|
+
yield event.tool_call_args(
|
|
172
|
+
tool_call_id=tool_call_data["id"],
|
|
173
|
+
delta=tool_call_data["arguments"],
|
|
174
|
+
)
|
|
176
175
|
|
|
177
176
|
# TOOL_CALL_END
|
|
178
|
-
yield
|
|
177
|
+
yield event.tool_call_end(tool_call_id=tool_call_data["id"])
|
|
179
178
|
|
|
180
179
|
# 更新 usage
|
|
181
180
|
if hasattr(chunk, "usage") and chunk.usage:
|
|
@@ -190,23 +189,34 @@ class Agent:
|
|
|
190
189
|
except Exception as e:
|
|
191
190
|
error_msg = str(e)
|
|
192
191
|
# AG-UI: RUN_ERROR
|
|
193
|
-
yield
|
|
192
|
+
yield event.run_error(message=error_msg)
|
|
194
193
|
raise FlowError("AGENT_EXECUTION_FAILED", 500, {"error": error_msg}) from e
|
|
195
194
|
|
|
196
|
-
# 保存 assistant
|
|
195
|
+
# 保存 assistant 消息(tool_calls 转为 OpenAI 标准格式)
|
|
197
196
|
if tool_calls:
|
|
197
|
+
openai_tool_calls = [
|
|
198
|
+
{
|
|
199
|
+
"id": tc["id"],
|
|
200
|
+
"type": "function",
|
|
201
|
+
"function": {"name": tc["name"], "arguments": tc["arguments"]},
|
|
202
|
+
}
|
|
203
|
+
for tc in tool_calls
|
|
204
|
+
]
|
|
198
205
|
context.add_message(
|
|
206
|
+
self.name,
|
|
199
207
|
"assistant",
|
|
200
208
|
content=assistant_content or None,
|
|
201
|
-
tool_calls=
|
|
209
|
+
tool_calls=openai_tool_calls,
|
|
202
210
|
)
|
|
203
211
|
else:
|
|
204
|
-
context.add_message("assistant", assistant_content)
|
|
212
|
+
context.add_message(self.name, "assistant", assistant_content)
|
|
205
213
|
|
|
206
214
|
# 如果没有工具调用,结束循环
|
|
207
215
|
if not tool_calls:
|
|
216
|
+
# 存储结果供 Flow/A2A 使用
|
|
217
|
+
context.set_node_result(self.name, assistant_content)
|
|
208
218
|
# AG-UI: TEXT_MESSAGE_END
|
|
209
|
-
yield
|
|
219
|
+
yield event.text_message_end(message_id=msg_id)
|
|
210
220
|
return
|
|
211
221
|
|
|
212
222
|
# 执行工具调用
|
|
@@ -215,27 +225,34 @@ class Agent:
|
|
|
215
225
|
if not tool:
|
|
216
226
|
error_msg = f"Tool not found: {tool_call['name']}"
|
|
217
227
|
context.add_message(
|
|
228
|
+
self.name,
|
|
218
229
|
"tool",
|
|
219
230
|
content=json.dumps({"error": error_msg}),
|
|
220
231
|
tool_call_id=tool_call["id"],
|
|
221
232
|
)
|
|
222
233
|
# AG-UI: TOOL_CALL_RESULT (错误)
|
|
223
|
-
yield
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
}
|
|
234
|
+
yield event.tool_call_result(
|
|
235
|
+
tool_call_id=tool_call["id"],
|
|
236
|
+
content=json.dumps({"error": error_msg}),
|
|
237
|
+
)
|
|
228
238
|
continue
|
|
229
239
|
|
|
240
|
+
# 解析 LLM 返回的工具参数并注入 context
|
|
241
|
+
try:
|
|
242
|
+
tool_args = json.loads(tool_call["arguments"]) if tool_call["arguments"] else {}
|
|
243
|
+
except json.JSONDecodeError:
|
|
244
|
+
tool_args = {}
|
|
245
|
+
context.update_variables(tool_args)
|
|
246
|
+
|
|
230
247
|
# 执行工具
|
|
231
248
|
result = await tool.execute_async(context)
|
|
232
249
|
|
|
233
250
|
# 保存工具结果
|
|
234
251
|
result_content = json.dumps(result.result) if result.success else json.dumps({"error": result.error})
|
|
235
|
-
context.add_message("tool", content=result_content, tool_call_id=tool_call["id"])
|
|
252
|
+
context.add_message(self.name, "tool", content=result_content, tool_call_id=tool_call["id"])
|
|
236
253
|
|
|
237
254
|
# AG-UI: TOOL_CALL_RESULT
|
|
238
|
-
yield
|
|
255
|
+
yield event.tool_call_result(tool_call_id=tool_call["id"], content=result_content)
|
|
239
256
|
|
|
240
257
|
# 更新消息列表,继续下一轮
|
|
241
|
-
messages = [self.get_system_message()] + context.
|
|
258
|
+
messages = [self.get_system_message()] + context.history + context.get_messages(self.name)
|
|
@@ -34,14 +34,21 @@ class FlowContext:
|
|
|
34
34
|
kb_ids: list[UUID] = field(default_factory=list)
|
|
35
35
|
variables: dict[str, Any] = field(default_factory=dict)
|
|
36
36
|
|
|
37
|
-
# Agent 层面
|
|
38
|
-
messages: list[dict[str, Any]] = field(default_factory=
|
|
37
|
+
# Agent 层面 — 按 agent name 隔离的消息
|
|
38
|
+
messages: dict[str, list[dict[str, Any]]] = field(default_factory=dict)
|
|
39
|
+
# 预加载的对话历史(只读,所有 agent 共享)
|
|
40
|
+
history: list[dict[str, Any]] = field(default_factory=list)
|
|
39
41
|
usage: Usage = field(default_factory=Usage)
|
|
40
42
|
context_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
41
43
|
created_at: datetime = field(default_factory=datetime.now)
|
|
42
44
|
last_agent: str | None = None
|
|
43
45
|
turn_count: int = 0
|
|
44
46
|
|
|
47
|
+
# 事件 ID(应用层可注入)
|
|
48
|
+
thread_id: str | None = None
|
|
49
|
+
run_id: str | None = None
|
|
50
|
+
message_id: str | None = None
|
|
51
|
+
|
|
45
52
|
# 图执行状态
|
|
46
53
|
node_results: dict[str, Any] = field(default_factory=dict)
|
|
47
54
|
current_node: str | None = None
|
|
@@ -65,12 +72,32 @@ class FlowContext:
|
|
|
65
72
|
"""获取特定作用域的变量"""
|
|
66
73
|
return {k: v for k, v in self.variables.items() if k.startswith(f"{scope}.")}
|
|
67
74
|
|
|
68
|
-
def add_message(self, role: str, content: str | None = None, **kwargs) -> None:
|
|
69
|
-
"""
|
|
75
|
+
def add_message(self, agent_name: str, role: str, content: str | None = None, **kwargs) -> None:
|
|
76
|
+
"""添加消息到指定 agent 的消息列表"""
|
|
70
77
|
message = {"role": role, **kwargs}
|
|
71
78
|
if content is not None:
|
|
72
79
|
message["content"] = content
|
|
73
|
-
self.messages.append(message)
|
|
80
|
+
self.messages.setdefault(agent_name, []).append(message)
|
|
81
|
+
|
|
82
|
+
def get_messages(self, agent_name: str) -> list[dict[str, Any]]:
|
|
83
|
+
"""获取指定 agent 的消息列表"""
|
|
84
|
+
return self.messages.get(agent_name, [])
|
|
85
|
+
|
|
86
|
+
def get_last_output(self, agent_name: str) -> str | None:
|
|
87
|
+
"""获取指定 agent 最后一条 assistant 消息的内容"""
|
|
88
|
+
agent_messages = self.messages.get(agent_name, [])
|
|
89
|
+
for msg in reversed(agent_messages):
|
|
90
|
+
if msg.get("role") == "assistant" and msg.get("content"):
|
|
91
|
+
return msg["content"]
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def all_messages(self) -> list[dict[str, Any]]:
|
|
96
|
+
"""扁平视图,合并所有 agent 消息(调试/日志用)"""
|
|
97
|
+
result: list[dict[str, Any]] = []
|
|
98
|
+
for agent_msgs in self.messages.values():
|
|
99
|
+
result.extend(agent_msgs)
|
|
100
|
+
return result
|
|
74
101
|
|
|
75
102
|
def add_usage(self, usage: Usage) -> None:
|
|
76
103
|
"""累加 token 使用量"""
|
|
@@ -80,19 +107,26 @@ class FlowContext:
|
|
|
80
107
|
"""增加轮次计数"""
|
|
81
108
|
self.turn_count += 1
|
|
82
109
|
|
|
83
|
-
def clear_messages(self) -> None:
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
def clear_messages(self, agent_name: str | None = None) -> None:
|
|
111
|
+
"""清空指定 agent 或全部消息历史"""
|
|
112
|
+
if agent_name is not None:
|
|
113
|
+
self.messages.pop(agent_name, None)
|
|
114
|
+
else:
|
|
115
|
+
self.messages.clear()
|
|
116
|
+
self.turn_count = 0
|
|
87
117
|
|
|
88
118
|
def resolve_reference(self, ref: str) -> Any:
|
|
89
|
-
"""解析变量引用 {node@variable.field}"""
|
|
119
|
+
"""解析变量引用 {node@variable.field} 或 {node_id}"""
|
|
90
120
|
if not isinstance(ref, str) or not ref.startswith("{"):
|
|
91
121
|
return ref
|
|
92
122
|
|
|
93
123
|
ref_content = ref[1:-1] # 移除 {}
|
|
94
124
|
if "@" not in ref_content:
|
|
95
|
-
|
|
125
|
+
# 先从 variables 查找,回退到 node_results
|
|
126
|
+
result = self.variables.get(ref_content)
|
|
127
|
+
if result is None:
|
|
128
|
+
result = self.node_results.get(ref_content)
|
|
129
|
+
return result
|
|
96
130
|
|
|
97
131
|
node_id, var_path = ref_content.split("@", 1)
|
|
98
132
|
result = self.node_results.get(node_id)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Copyright (c) 2020-2026 XtraVisions, All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""AG-UI 事件构造 — 具名构造函数,snake_case 参数,camelCase 输出"""
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventType(StrEnum):
|
|
11
|
+
TEXT_MESSAGE_START = "TEXT_MESSAGE_START"
|
|
12
|
+
TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT"
|
|
13
|
+
TEXT_MESSAGE_END = "TEXT_MESSAGE_END"
|
|
14
|
+
TOOL_CALL_START = "TOOL_CALL_START"
|
|
15
|
+
TOOL_CALL_ARGS = "TOOL_CALL_ARGS"
|
|
16
|
+
TOOL_CALL_END = "TOOL_CALL_END"
|
|
17
|
+
TOOL_CALL_RESULT = "TOOL_CALL_RESULT"
|
|
18
|
+
RUN_STARTED = "RUN_STARTED"
|
|
19
|
+
RUN_FINISHED = "RUN_FINISHED"
|
|
20
|
+
RUN_ERROR = "RUN_ERROR"
|
|
21
|
+
STEP_STARTED = "STEP_STARTED"
|
|
22
|
+
STEP_FINISHED = "STEP_FINISHED"
|
|
23
|
+
STATE_SNAPSHOT = "STATE_SNAPSHOT"
|
|
24
|
+
STATE_DELTA = "STATE_DELTA"
|
|
25
|
+
CUSTOM = "CUSTOM"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── 内部工具 ──
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _to_camel(s: str) -> str:
|
|
32
|
+
parts = s.split("_")
|
|
33
|
+
return parts[0] + "".join(p.capitalize() for p in parts[1:])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ev(type: EventType, **kwargs: Any) -> dict[str, Any]:
|
|
37
|
+
return {"type": type, **{_to_camel(k): v for k, v in kwargs.items()}}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Text Message ──
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def text_message_start(*, message_id: str, role: str = "assistant") -> dict[str, Any]:
|
|
44
|
+
return _ev(EventType.TEXT_MESSAGE_START, message_id=message_id, role=role)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def text_message_content(*, message_id: str, delta: str) -> dict[str, Any]:
|
|
48
|
+
return _ev(EventType.TEXT_MESSAGE_CONTENT, message_id=message_id, delta=delta)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def text_message_end(*, message_id: str) -> dict[str, Any]:
|
|
52
|
+
return _ev(EventType.TEXT_MESSAGE_END, message_id=message_id)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── Tool Call ──
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def tool_call_start(*, tool_call_id: str, tool_call_name: str) -> dict[str, Any]:
|
|
59
|
+
return _ev(EventType.TOOL_CALL_START, tool_call_id=tool_call_id, tool_call_name=tool_call_name)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def tool_call_args(*, tool_call_id: str, delta: str) -> dict[str, Any]:
|
|
63
|
+
return _ev(EventType.TOOL_CALL_ARGS, tool_call_id=tool_call_id, delta=delta)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def tool_call_end(*, tool_call_id: str) -> dict[str, Any]:
|
|
67
|
+
return _ev(EventType.TOOL_CALL_END, tool_call_id=tool_call_id)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def tool_call_result(*, tool_call_id: str, content: str, message_id: str | None = None) -> dict[str, Any]:
|
|
71
|
+
return _ev(
|
|
72
|
+
EventType.TOOL_CALL_RESULT,
|
|
73
|
+
message_id=message_id or str(uuid4()),
|
|
74
|
+
tool_call_id=tool_call_id,
|
|
75
|
+
content=content,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Run lifecycle ──
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def run_started(*, thread_id: str, run_id: str) -> dict[str, Any]:
|
|
83
|
+
return _ev(EventType.RUN_STARTED, thread_id=thread_id, run_id=run_id)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_finished(*, thread_id: str, run_id: str) -> dict[str, Any]:
|
|
87
|
+
return _ev(EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_error(*, message: str, code: str | None = None) -> dict[str, Any]:
|
|
91
|
+
d = _ev(EventType.RUN_ERROR, message=message)
|
|
92
|
+
if code is not None:
|
|
93
|
+
d["code"] = code
|
|
94
|
+
return d
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Step ──
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def step_started(*, step_name: str) -> dict[str, Any]:
|
|
101
|
+
return _ev(EventType.STEP_STARTED, step_name=step_name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def step_finished(*, step_name: str) -> dict[str, Any]:
|
|
105
|
+
return _ev(EventType.STEP_FINISHED, step_name=step_name)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── State ──
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def state_snapshot(*, snapshot: dict[str, Any]) -> dict[str, Any]:
|
|
112
|
+
return _ev(EventType.STATE_SNAPSHOT, snapshot=snapshot)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def state_delta(*, delta: list[Any]) -> dict[str, Any]:
|
|
116
|
+
return _ev(EventType.STATE_DELTA, delta=delta)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── Custom ──
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def custom(*, name: str, value: Any) -> dict[str, Any]:
|
|
123
|
+
return _ev(EventType.CUSTOM, name=name, value=value)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
7
7
|
|
|
8
|
-
from .
|
|
8
|
+
from . import event
|
|
9
9
|
from .exceptions import FlowError
|
|
10
10
|
from .registry import registry
|
|
11
11
|
|
|
@@ -41,8 +41,7 @@ class Flow:
|
|
|
41
41
|
|
|
42
42
|
async def stream(self, context: "FlowContext") -> AsyncIterator[dict[str, Any]]:
|
|
43
43
|
"""流式执行 Flow(输出 AG-UI 标准事件)"""
|
|
44
|
-
|
|
45
|
-
yield {"type": EventType.TEXT_MESSAGE_CONTENT, "message_id": "", "delta": f"**开始执行流程**: {self.name}\n\n"}
|
|
44
|
+
yield event.step_started(step_name=f"flow:{self.name}")
|
|
46
45
|
|
|
47
46
|
# 按顺序执行节点
|
|
48
47
|
for node in self.nodes:
|
|
@@ -51,51 +50,39 @@ class Flow:
|
|
|
51
50
|
continue
|
|
52
51
|
|
|
53
52
|
context.current_node = node_id
|
|
54
|
-
|
|
55
|
-
# 发送节点开始事件
|
|
56
|
-
yield {"type": EventType.TEXT_MESSAGE_CONTENT, "message_id": "", "delta": f"**执行节点**: {node_id}\n"}
|
|
53
|
+
yield event.step_started(step_name=f"node:{node_id}")
|
|
57
54
|
|
|
58
55
|
# 执行节点
|
|
59
56
|
if node.get("type") == "agent":
|
|
60
57
|
# Agent 节点 - 流式执行
|
|
61
58
|
agent_name = node.get("config", {}).get("agent_name", "")
|
|
62
|
-
yield {
|
|
63
|
-
"type": EventType.TEXT_MESSAGE_CONTENT,
|
|
64
|
-
"message_id": "",
|
|
65
|
-
"delta": f"正在调用智能体 {agent_name}...\n\n",
|
|
66
|
-
}
|
|
59
|
+
yield event.step_started(step_name=f"agent:{agent_name}")
|
|
67
60
|
|
|
68
61
|
# 设置参数
|
|
69
62
|
self._set_parameters(node.get("config", {}), context)
|
|
70
63
|
|
|
71
64
|
# 创建并流式执行 Agent
|
|
72
|
-
|
|
73
|
-
async for
|
|
74
|
-
yield
|
|
65
|
+
ag = self._create_agent(node.get("config", {}))
|
|
66
|
+
async for evt in ag.stream(context):
|
|
67
|
+
yield evt
|
|
75
68
|
|
|
76
69
|
# 获取最终结果
|
|
77
|
-
result = context.
|
|
70
|
+
result = context.get_last_output(ag.name) or ""
|
|
78
71
|
context.set_node_result(node_id, result)
|
|
79
72
|
|
|
80
|
-
yield
|
|
73
|
+
yield event.step_finished(step_name=f"agent:{agent_name}")
|
|
81
74
|
|
|
82
75
|
else:
|
|
83
76
|
# Tool 节点 - 非流式执行
|
|
84
77
|
tool_name = node.get("config", {}).get("tool_name", "")
|
|
85
|
-
yield {
|
|
86
|
-
"type": EventType.TEXT_MESSAGE_CONTENT,
|
|
87
|
-
"message_id": "",
|
|
88
|
-
"delta": f"正在调用工具 {tool_name}...\n",
|
|
89
|
-
}
|
|
78
|
+
yield event.step_started(step_name=f"tool:{tool_name}")
|
|
90
79
|
|
|
91
80
|
result = await self._execute_node(node, context)
|
|
92
81
|
context.set_node_result(node_id, result)
|
|
93
82
|
|
|
94
|
-
yield
|
|
83
|
+
yield event.step_finished(step_name=f"tool:{tool_name}")
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
yield {"type": EventType.TEXT_MESSAGE_CONTENT, "message_id": "", "delta": f"**流程执行完成**: {self.name}"}
|
|
98
|
-
yield {"type": EventType.TEXT_MESSAGE_END, "message_id": ""}
|
|
85
|
+
yield event.step_finished(step_name=f"flow:{self.name}")
|
|
99
86
|
|
|
100
87
|
async def _execute_node(self, node_config: dict, context: "FlowContext") -> Any:
|
|
101
88
|
"""执行节点"""
|
|
@@ -144,6 +144,10 @@ class Registry:
|
|
|
144
144
|
return None
|
|
145
145
|
|
|
146
146
|
component = manifest.component
|
|
147
|
+
# 已实例化的 Tool 对象直接返回
|
|
148
|
+
if hasattr(component, "execute_async"):
|
|
149
|
+
return component
|
|
150
|
+
# 类或工厂函数:实例化
|
|
147
151
|
if callable(component):
|
|
148
152
|
return component(**kwargs) if kwargs else component()
|
|
149
153
|
return component
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agstack
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
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>
|
|
@@ -20,7 +20,6 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: ag-ui-protocol>=0.1.13
|
|
24
23
|
Requires-Dist: aio-pika>=9.6.1
|
|
25
24
|
Requires-Dist: asyncpg>=0.30.0
|
|
26
25
|
Requires-Dist: elasticsearch[async]>=9.3.0
|
|
@@ -34,7 +34,7 @@ agstack/llm/token.py
|
|
|
34
34
|
agstack/llm/flow/__init__.py
|
|
35
35
|
agstack/llm/flow/agent.py
|
|
36
36
|
agstack/llm/flow/context.py
|
|
37
|
-
agstack/llm/flow/
|
|
37
|
+
agstack/llm/flow/event.py
|
|
38
38
|
agstack/llm/flow/exceptions.py
|
|
39
39
|
agstack/llm/flow/factory.py
|
|
40
40
|
agstack/llm/flow/flow.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agstack"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.2"
|
|
4
4
|
description = "Production-ready toolkit for building FastAPI and LLM applications"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -39,7 +39,6 @@ classifiers = [
|
|
|
39
39
|
]
|
|
40
40
|
requires-python = ">=3.12"
|
|
41
41
|
dependencies = [
|
|
42
|
-
"ag-ui-protocol>=0.1.13",
|
|
43
42
|
"aio-pika>=9.6.1",
|
|
44
43
|
"asyncpg>=0.30.0",
|
|
45
44
|
"elasticsearch[async]>=9.3.0",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2020-2026 XtraVisions, All rights reserved.
|
|
2
|
-
|
|
3
|
-
"""事件协议定义"""
|
|
4
|
-
|
|
5
|
-
# 导出 AG-UI 标准事件
|
|
6
|
-
from ag_ui.core.events import (
|
|
7
|
-
Event,
|
|
8
|
-
EventType,
|
|
9
|
-
MessagesSnapshotEvent,
|
|
10
|
-
RunErrorEvent,
|
|
11
|
-
RunFinishedEvent,
|
|
12
|
-
RunStartedEvent,
|
|
13
|
-
StateDeltaEvent,
|
|
14
|
-
StateSnapshotEvent,
|
|
15
|
-
TextMessageContentEvent,
|
|
16
|
-
TextMessageEndEvent,
|
|
17
|
-
TextMessageStartEvent,
|
|
18
|
-
ThinkingEndEvent,
|
|
19
|
-
ThinkingStartEvent,
|
|
20
|
-
ToolCallArgsEvent,
|
|
21
|
-
ToolCallEndEvent,
|
|
22
|
-
ToolCallResultEvent,
|
|
23
|
-
ToolCallStartEvent,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
__all__ = [
|
|
28
|
-
"Event",
|
|
29
|
-
"EventType",
|
|
30
|
-
"TextMessageStartEvent",
|
|
31
|
-
"TextMessageContentEvent",
|
|
32
|
-
"TextMessageEndEvent",
|
|
33
|
-
"ToolCallStartEvent",
|
|
34
|
-
"ToolCallArgsEvent",
|
|
35
|
-
"ToolCallEndEvent",
|
|
36
|
-
"ToolCallResultEvent",
|
|
37
|
-
"RunStartedEvent",
|
|
38
|
-
"RunFinishedEvent",
|
|
39
|
-
"RunErrorEvent",
|
|
40
|
-
"ThinkingStartEvent",
|
|
41
|
-
"ThinkingEndEvent",
|
|
42
|
-
"StateSnapshotEvent",
|
|
43
|
-
"StateDeltaEvent",
|
|
44
|
-
"MessagesSnapshotEvent",
|
|
45
|
-
]
|
|
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
|