agstack 1.2.1__tar.gz → 1.2.3__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.1 → agstack-1.2.3}/PKG-INFO +1 -2
  2. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/client.py +35 -0
  3. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/__init__.py +3 -3
  4. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/agent.py +53 -43
  5. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/context.py +45 -11
  6. agstack-1.2.3/agstack/llm/flow/event.py +123 -0
  7. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/flow.py +12 -25
  8. {agstack-1.2.1 → agstack-1.2.3}/agstack/security/casbin.py +1 -1
  9. {agstack-1.2.1 → agstack-1.2.3}/agstack.egg-info/PKG-INFO +1 -2
  10. {agstack-1.2.1 → agstack-1.2.3}/agstack.egg-info/SOURCES.txt +1 -1
  11. {agstack-1.2.1 → agstack-1.2.3}/agstack.egg-info/requires.txt +0 -1
  12. {agstack-1.2.1 → agstack-1.2.3}/pyproject.toml +1 -2
  13. agstack-1.2.1/agstack/llm/flow/events.py +0 -45
  14. {agstack-1.2.1 → agstack-1.2.3}/LICENSE +0 -0
  15. {agstack-1.2.1 → agstack-1.2.3}/README.md +0 -0
  16. {agstack-1.2.1 → agstack-1.2.3}/agstack/__init__.py +0 -0
  17. {agstack-1.2.1 → agstack-1.2.3}/agstack/config/__init__.py +0 -0
  18. {agstack-1.2.1 → agstack-1.2.3}/agstack/config/logger.py +0 -0
  19. {agstack-1.2.1 → agstack-1.2.3}/agstack/config/manager.py +0 -0
  20. {agstack-1.2.1 → agstack-1.2.3}/agstack/config/types.py +0 -0
  21. {agstack-1.2.1 → agstack-1.2.3}/agstack/contexts.py +0 -0
  22. {agstack-1.2.1 → agstack-1.2.3}/agstack/decorators.py +0 -0
  23. {agstack-1.2.1 → agstack-1.2.3}/agstack/events.py +0 -0
  24. {agstack-1.2.1 → agstack-1.2.3}/agstack/exceptions.py +0 -0
  25. {agstack-1.2.1 → agstack-1.2.3}/agstack/fastapi/__init__.py +0 -0
  26. {agstack-1.2.1 → agstack-1.2.3}/agstack/fastapi/exception.py +0 -0
  27. {agstack-1.2.1 → agstack-1.2.3}/agstack/fastapi/middleware.py +0 -0
  28. {agstack-1.2.1 → agstack-1.2.3}/agstack/fastapi/offline.py +0 -0
  29. {agstack-1.2.1 → agstack-1.2.3}/agstack/fastapi/sse.py +0 -0
  30. {agstack-1.2.1 → agstack-1.2.3}/agstack/infra/db/__init__.py +0 -0
  31. {agstack-1.2.1 → agstack-1.2.3}/agstack/infra/es/__init__.py +0 -0
  32. {agstack-1.2.1 → agstack-1.2.3}/agstack/infra/kg/__init__.py +0 -0
  33. {agstack-1.2.1 → agstack-1.2.3}/agstack/infra/mq/__init__.py +0 -0
  34. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/__init__.py +0 -0
  35. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/exceptions.py +0 -0
  36. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/factory.py +0 -0
  37. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/loader.py +0 -0
  38. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/records.py +0 -0
  39. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/registry.py +0 -0
  40. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/state.py +0 -0
  41. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/flow/tool.py +0 -0
  42. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/prompts.py +0 -0
  43. {agstack-1.2.1 → agstack-1.2.3}/agstack/llm/token.py +0 -0
  44. {agstack-1.2.1 → agstack-1.2.3}/agstack/registry.py +0 -0
  45. {agstack-1.2.1 → agstack-1.2.3}/agstack/schema.py +0 -0
  46. {agstack-1.2.1 → agstack-1.2.3}/agstack/security/__init__.py +0 -0
  47. {agstack-1.2.1 → agstack-1.2.3}/agstack/security/crypt.py +0 -0
  48. {agstack-1.2.1 → agstack-1.2.3}/agstack/status.py +0 -0
  49. {agstack-1.2.1 → agstack-1.2.3}/agstack.egg-info/dependency_links.txt +0 -0
  50. {agstack-1.2.1 → agstack-1.2.3}/agstack.egg-info/top_level.txt +0 -0
  51. {agstack-1.2.1 → agstack-1.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agstack
3
- Version: 1.2.1
3
+ Version: 1.2.3
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
@@ -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
@@ -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 .events import Event, EventType
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 .events import EventType
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 event in self.stream(context):
70
+ async for evt in self.stream(context):
70
71
  # AG-UI 事件格式
71
- if isinstance(event, dict):
72
- if event.get("type") == EventType.TEXT_MESSAGE_CONTENT:
73
- content_parts.append(event.get("delta", ""))
74
- elif event.get("type") == EventType.RUN_ERROR:
75
- raise FlowError("AGENT_EXECUTION_FAILED", 500, {"error": event.get("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
- query = context.get_variable("query", "")
82
- message_id = str(uuid4())
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", query)
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 {"type": EventType.TEXT_MESSAGE_START, "messageId": message_id, "role": "assistant"}
91
+ yield event.text_message_start(message_id=msg_id, role="assistant")
90
92
 
91
- # 构建消息列表
92
- messages = [self.get_system_message()] + context.messages
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
- "type": EventType.TEXT_MESSAGE_CONTENT,
135
- "messageId": message_id,
136
- "delta": delta.content,
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
- "type": EventType.TOOL_CALL_START,
166
- "toolCallId": tool_call_data["id"],
167
- "toolCallName": tool_call_data["name"],
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
- "type": EventType.TOOL_CALL_ARGS,
173
- "toolCallId": tool_call_data["id"],
174
- "delta": tool_call_data["arguments"],
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 {"type": EventType.TOOL_CALL_END, "toolCallId": tool_call_data["id"]}
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 {"type": EventType.RUN_ERROR, "error": error_msg}
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=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 {"type": EventType.TEXT_MESSAGE_END, "messageId": message_id}
219
+ yield event.text_message_end(message_id=msg_id)
210
220
  return
211
221
 
212
222
  # 执行工具调用
@@ -215,16 +225,16 @@ 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
- "type": EventType.TOOL_CALL_RESULT,
225
- "toolCallId": tool_call["id"],
226
- "content": json.dumps({"error": error_msg}),
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
 
230
240
  # 解析 LLM 返回的工具参数并注入 context
@@ -239,10 +249,10 @@ class Agent:
239
249
 
240
250
  # 保存工具结果
241
251
  result_content = json.dumps(result.result) if result.success else json.dumps({"error": result.error})
242
- 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"])
243
253
 
244
254
  # AG-UI: TOOL_CALL_RESULT
245
- yield {"type": EventType.TOOL_CALL_RESULT, "toolCallId": tool_call["id"], "content": result_content}
255
+ yield event.tool_call_result(tool_call_id=tool_call["id"], content=result_content)
246
256
 
247
257
  # 更新消息列表,继续下一轮
248
- messages = [self.get_system_message()] + context.messages
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=list)
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
- self.messages.clear()
86
- self.turn_count = 0
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
- return self.variables.get(ref_content)
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 .events import EventType
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
- agent = self._create_agent(node.get("config", {}))
73
- async for event in agent.stream(context):
74
- yield event
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.messages[-1]["content"] if context.messages else ""
70
+ result = context.get_last_output(ag.name) or ""
78
71
  context.set_node_result(node_id, result)
79
72
 
80
- yield {"type": EventType.TEXT_MESSAGE_CONTENT, "message_id": "", "delta": "\n\n智能体执行完成\n\n"}
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 {"type": EventType.TEXT_MESSAGE_CONTENT, "message_id": "", "delta": "工具执行完成\n\n"}
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
  """执行节点"""
@@ -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.1
3
+ Version: 1.2.3
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/events.py
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,4 +1,3 @@
1
- ag-ui-protocol>=0.1.13
2
1
  aio-pika>=9.6.1
3
2
  asyncpg>=0.30.0
4
3
  elasticsearch[async]>=9.3.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agstack"
3
- version = "1.2.1"
3
+ version = "1.2.3"
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