AstrBot 4.9.2__py3-none-any.whl → 4.10.0a1__py3-none-any.whl

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 (41) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/message.py +6 -4
  3. astrbot/core/agent/response.py +22 -1
  4. astrbot/core/agent/run_context.py +1 -1
  5. astrbot/core/agent/runners/tool_loop_agent_runner.py +54 -15
  6. astrbot/core/astr_agent_context.py +3 -1
  7. astrbot/core/astr_agent_run_util.py +23 -2
  8. astrbot/core/config/default.py +127 -184
  9. astrbot/core/core_lifecycle.py +3 -0
  10. astrbot/core/db/__init__.py +72 -0
  11. astrbot/core/db/po.py +59 -0
  12. astrbot/core/db/sqlite.py +240 -0
  13. astrbot/core/message/components.py +4 -5
  14. astrbot/core/pipeline/respond/stage.py +1 -1
  15. astrbot/core/platform/sources/telegram/tg_event.py +9 -0
  16. astrbot/core/platform/sources/webchat/webchat_event.py +22 -18
  17. astrbot/core/provider/entities.py +41 -0
  18. astrbot/core/provider/manager.py +203 -93
  19. astrbot/core/provider/sources/anthropic_source.py +55 -11
  20. astrbot/core/provider/sources/gemini_source.py +68 -33
  21. astrbot/core/provider/sources/openai_source.py +21 -6
  22. astrbot/core/star/command_management.py +449 -0
  23. astrbot/core/star/context.py +4 -0
  24. astrbot/core/star/filter/command.py +1 -0
  25. astrbot/core/star/filter/command_group.py +1 -0
  26. astrbot/core/star/star_handler.py +4 -0
  27. astrbot/core/star/star_manager.py +2 -0
  28. astrbot/core/utils/llm_metadata.py +63 -0
  29. astrbot/core/utils/migra_helper.py +93 -0
  30. astrbot/dashboard/routes/__init__.py +2 -0
  31. astrbot/dashboard/routes/chat.py +56 -13
  32. astrbot/dashboard/routes/command.py +82 -0
  33. astrbot/dashboard/routes/config.py +291 -33
  34. astrbot/dashboard/routes/stat.py +96 -0
  35. astrbot/dashboard/routes/tools.py +20 -4
  36. astrbot/dashboard/server.py +1 -0
  37. {astrbot-4.9.2.dist-info → astrbot-4.10.0a1.dist-info}/METADATA +2 -2
  38. {astrbot-4.9.2.dist-info → astrbot-4.10.0a1.dist-info}/RECORD +41 -38
  39. {astrbot-4.9.2.dist-info → astrbot-4.10.0a1.dist-info}/WHEEL +0 -0
  40. {astrbot-4.9.2.dist-info → astrbot-4.10.0a1.dist-info}/entry_points.txt +0 -0
  41. {astrbot-4.9.2.dist-info → astrbot-4.10.0a1.dist-info}/licenses/LICENSE +0 -0
astrbot/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.9.2"
1
+ __version__ = "4.10.0-alpha.1"
@@ -3,7 +3,7 @@
3
3
 
4
4
  from typing import Any, ClassVar, Literal, cast
5
5
 
6
- from pydantic import BaseModel, GetCoreSchemaHandler, model_validator
6
+ from pydantic import BaseModel, GetCoreSchemaHandler, model_serializer, model_validator
7
7
  from pydantic_core import core_schema
8
8
 
9
9
 
@@ -122,10 +122,12 @@ class ToolCall(BaseModel):
122
122
  extra_content: dict[str, Any] | None = None
123
123
  """Extra metadata for the tool call."""
124
124
 
125
- def model_dump(self, **kwargs: Any) -> dict[str, Any]:
125
+ @model_serializer(mode="wrap")
126
+ def serialize(self, handler):
127
+ data = handler(self)
126
128
  if self.extra_content is None:
127
- kwargs.setdefault("exclude", set()).add("extra_content")
128
- return super().model_dump(**kwargs)
129
+ data.pop("extra_content", None)
130
+ return data
129
131
 
130
132
 
131
133
  class ToolCallPart(BaseModel):
@@ -1,7 +1,8 @@
1
1
  import typing as T
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
3
 
4
4
  from astrbot.core.message.message_event_result import MessageChain
5
+ from astrbot.core.provider.entities import TokenUsage
5
6
 
6
7
 
7
8
  class AgentResponseData(T.TypedDict):
@@ -12,3 +13,23 @@ class AgentResponseData(T.TypedDict):
12
13
  class AgentResponse:
13
14
  type: str
14
15
  data: AgentResponseData
16
+
17
+
18
+ @dataclass
19
+ class AgentStats:
20
+ token_usage: TokenUsage = field(default_factory=TokenUsage)
21
+ start_time: float = 0.0
22
+ end_time: float = 0.0
23
+ time_to_first_token: float = 0.0
24
+
25
+ @property
26
+ def duration(self) -> float:
27
+ return self.end_time - self.start_time
28
+
29
+ def to_dict(self) -> dict:
30
+ return {
31
+ "token_usage": self.token_usage.__dict__,
32
+ "start_time": self.start_time,
33
+ "end_time": self.end_time,
34
+ "time_to_first_token": self.time_to_first_token,
35
+ }
@@ -9,7 +9,7 @@ from .message import Message
9
9
  TContext = TypeVar("TContext", default=Any)
10
10
 
11
11
 
12
- @dataclass(config={"arbitrary_types_allowed": True})
12
+ @dataclass
13
13
  class ContextWrapper(Generic[TContext]):
14
14
  """A context for running an agent, which can be used to pass additional data or state."""
15
15
 
@@ -1,4 +1,5 @@
1
1
  import sys
2
+ import time
2
3
  import traceback
3
4
  import typing as T
4
5
 
@@ -12,6 +13,7 @@ from mcp.types import (
12
13
  )
13
14
 
14
15
  from astrbot import logger
16
+ from astrbot.core.message.components import Json
15
17
  from astrbot.core.message.message_event_result import (
16
18
  MessageChain,
17
19
  )
@@ -24,7 +26,7 @@ from astrbot.core.provider.provider import Provider
24
26
 
25
27
  from ..hooks import BaseAgentRunHooks
26
28
  from ..message import AssistantMessageSegment, Message, ToolCallMessageSegment
27
- from ..response import AgentResponseData
29
+ from ..response import AgentResponseData, AgentStats
28
30
  from ..run_context import ContextWrapper, TContext
29
31
  from ..tool_executor import BaseFunctionToolExecutor
30
32
  from .base import AgentResponse, AgentState, BaseAgentRunner
@@ -69,6 +71,9 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
69
71
  )
70
72
  self.run_context.messages = messages
71
73
 
74
+ self.stats = AgentStats()
75
+ self.stats.start_time = time.time()
76
+
72
77
  async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
73
78
  """Yields chunks *and* a final LLMResponse."""
74
79
  if self.streaming:
@@ -98,6 +103,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
98
103
 
99
104
  async for llm_response in self._iter_llm_responses():
100
105
  if llm_response.is_chunk:
106
+ # update ttft
107
+ if self.stats.time_to_first_token == 0:
108
+ self.stats.time_to_first_token = time.time() - self.stats.start_time
109
+
101
110
  if llm_response.result_chain:
102
111
  yield AgentResponse(
103
112
  type="streaming_delta",
@@ -121,6 +130,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
121
130
  )
122
131
  continue
123
132
  llm_resp_result = llm_response
133
+
134
+ if not llm_response.is_chunk and llm_response.usage:
135
+ # only count the token usage of the final response for computation purpose
136
+ self.stats.token_usage += llm_response.usage
124
137
  break # got final response
125
138
 
126
139
  if not llm_resp_result:
@@ -132,6 +145,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
132
145
  if llm_resp.role == "err":
133
146
  # 如果 LLM 响应错误,转换到错误状态
134
147
  self.final_llm_resp = llm_resp
148
+ self.stats.end_time = time.time()
135
149
  self._transition_state(AgentState.ERROR)
136
150
  yield AgentResponse(
137
151
  type="err",
@@ -146,6 +160,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
146
160
  # 如果没有工具调用,转换到完成状态
147
161
  self.final_llm_resp = llm_resp
148
162
  self._transition_state(AgentState.DONE)
163
+ self.stats.end_time = time.time()
149
164
  # record the final assistant message
150
165
  self.run_context.messages.append(
151
166
  Message(
@@ -175,22 +190,19 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
175
190
  # 如果有工具调用,还需处理工具调用
176
191
  if llm_resp.tools_call_name:
177
192
  tool_call_result_blocks = []
178
- for tool_call_name in llm_resp.tools_call_name:
179
- yield AgentResponse(
180
- type="tool_call",
181
- data=AgentResponseData(
182
- chain=MessageChain(type="tool_call").message(
183
- f"🔨 调用工具: {tool_call_name}"
184
- ),
185
- ),
186
- )
187
193
  async for result in self._handle_function_tools(self.req, llm_resp):
188
194
  if isinstance(result, list):
189
195
  tool_call_result_blocks = result
190
196
  elif isinstance(result, MessageChain):
191
- result.type = "tool_call_result"
197
+ if result.type is None:
198
+ # should not happen
199
+ continue
200
+ if result.type == "tool_direct_result":
201
+ ar_type = "tool_call_result"
202
+ else:
203
+ ar_type = result.type
192
204
  yield AgentResponse(
193
- type="tool_call_result",
205
+ type=ar_type,
194
206
  data=AgentResponseData(chain=result),
195
207
  )
196
208
  # 将结果添加到上下文中
@@ -233,6 +245,19 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
233
245
  llm_response.tools_call_args,
234
246
  llm_response.tools_call_ids,
235
247
  ):
248
+ yield MessageChain(
249
+ type="tool_call",
250
+ chain=[
251
+ Json(
252
+ data={
253
+ "id": func_tool_id,
254
+ "name": func_tool_name,
255
+ "args": func_tool_args,
256
+ "ts": time.time(),
257
+ }
258
+ )
259
+ ],
260
+ )
236
261
  try:
237
262
  if not req.func_tool:
238
263
  return
@@ -306,7 +331,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
306
331
  content=res.content[0].text,
307
332
  ),
308
333
  )
309
- yield MessageChain().message(res.content[0].text)
310
334
  elif isinstance(res.content[0], ImageContent):
311
335
  tool_call_result_blocks.append(
312
336
  ToolCallMessageSegment(
@@ -328,7 +352,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
328
352
  content=resource.text,
329
353
  ),
330
354
  )
331
- yield MessageChain().message(resource.text)
332
355
  elif (
333
356
  isinstance(resource, BlobResourceContents)
334
357
  and resource.mimeType
@@ -352,7 +375,22 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
352
375
  content="返回的数据类型不受支持",
353
376
  ),
354
377
  )
355
- yield MessageChain().message("返回的数据类型不受支持。")
378
+
379
+ # yield the last tool call result
380
+ if tool_call_result_blocks:
381
+ last_tcr_content = str(tool_call_result_blocks[-1].content)
382
+ yield MessageChain(
383
+ type="tool_call_result",
384
+ chain=[
385
+ Json(
386
+ data={
387
+ "id": func_tool_id,
388
+ "ts": time.time(),
389
+ "result": last_tcr_content,
390
+ }
391
+ )
392
+ ],
393
+ )
356
394
 
357
395
  elif resp is None:
358
396
  # Tool 直接请求发送消息给用户
@@ -362,6 +400,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
362
400
  f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户,此工具调用不会被记录到历史中。"
363
401
  )
364
402
  self._transition_state(AgentState.DONE)
403
+ self.stats.end_time = time.time()
365
404
  else:
366
405
  # 不应该出现其他类型
367
406
  logger.warning(
@@ -6,8 +6,10 @@ from astrbot.core.platform.astr_message_event import AstrMessageEvent
6
6
  from astrbot.core.star.context import Context
7
7
 
8
8
 
9
- @dataclass(config={"arbitrary_types_allowed": True})
9
+ @dataclass
10
10
  class AstrAgentContext:
11
+ __pydantic_config__ = {"arbitrary_types_allowed": True}
12
+
11
13
  context: Context
12
14
  """The star context instance"""
13
15
  event: AstrMessageEvent
@@ -4,6 +4,7 @@ from collections.abc import AsyncGenerator
4
4
  from astrbot.core import logger
5
5
  from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
6
6
  from astrbot.core.astr_agent_context import AstrAgentContext
7
+ from astrbot.core.message.components import Json
7
8
  from astrbot.core.message.message_event_result import (
8
9
  MessageChain,
9
10
  MessageEventResult,
@@ -33,16 +34,27 @@ async def run_agent(
33
34
  msg_chain = resp.data["chain"]
34
35
  if msg_chain.type == "tool_direct_result":
35
36
  # tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
36
- await astr_event.send(resp.data["chain"])
37
+ await astr_event.send(msg_chain)
37
38
  continue
39
+ if astr_event.get_platform_id() == "webchat":
40
+ await astr_event.send(msg_chain)
38
41
  # 对于其他情况,暂时先不处理
39
42
  continue
40
43
  elif resp.type == "tool_call":
41
44
  if agent_runner.streaming:
42
45
  # 用来标记流式响应需要分节
43
46
  yield MessageChain(chain=[], type="break")
44
- if show_tool_use:
47
+
48
+ if astr_event.get_platform_name() == "webchat":
45
49
  await astr_event.send(resp.data["chain"])
50
+ elif show_tool_use:
51
+ json_comp = resp.data["chain"].chain[0]
52
+ if isinstance(json_comp, Json):
53
+ m = f"🔨 调用工具: {json_comp.data.get('name')}"
54
+ else:
55
+ m = "🔨 调用工具..."
56
+ chain = MessageChain(type="tool_call").message(m)
57
+ await astr_event.send(chain)
46
58
  continue
47
59
 
48
60
  if stream_to_general and resp.type == "streaming_delta":
@@ -69,6 +81,15 @@ async def run_agent(
69
81
  continue
70
82
  yield resp.data["chain"] # MessageChain
71
83
  if agent_runner.done():
84
+ # send agent stats to webchat
85
+ if astr_event.get_platform_name() == "webchat":
86
+ await astr_event.send(
87
+ MessageChain(
88
+ type="agent_stats",
89
+ chain=[Json(data=agent_runner.stats.to_dict())],
90
+ )
91
+ )
92
+
72
93
  break
73
94
 
74
95
  except Exception as e: