AstrBot 4.13.2__py3-none-any.whl → 4.14.1__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 (61) hide show
  1. astrbot/builtin_stars/astrbot/main.py +0 -6
  2. astrbot/builtin_stars/session_controller/main.py +1 -2
  3. astrbot/cli/__init__.py +1 -1
  4. astrbot/core/agent/agent.py +2 -1
  5. astrbot/core/agent/handoff.py +14 -1
  6. astrbot/core/agent/runners/tool_loop_agent_runner.py +14 -1
  7. astrbot/core/agent/tool.py +5 -0
  8. astrbot/core/astr_agent_run_util.py +21 -3
  9. astrbot/core/astr_agent_tool_exec.py +178 -3
  10. astrbot/core/astr_main_agent.py +980 -0
  11. astrbot/core/astr_main_agent_resources.py +453 -0
  12. astrbot/core/computer/computer_client.py +10 -1
  13. astrbot/core/computer/tools/fs.py +22 -14
  14. astrbot/core/config/default.py +84 -58
  15. astrbot/core/core_lifecycle.py +43 -1
  16. astrbot/core/cron/__init__.py +3 -0
  17. astrbot/core/cron/events.py +67 -0
  18. astrbot/core/cron/manager.py +376 -0
  19. astrbot/core/db/__init__.py +60 -0
  20. astrbot/core/db/po.py +31 -0
  21. astrbot/core/db/sqlite.py +120 -0
  22. astrbot/core/event_bus.py +0 -1
  23. astrbot/core/message/message_event_result.py +21 -3
  24. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +111 -580
  25. astrbot/core/pipeline/scheduler.py +0 -2
  26. astrbot/core/platform/astr_message_event.py +5 -5
  27. astrbot/core/platform/platform.py +9 -0
  28. astrbot/core/platform/platform_metadata.py +2 -0
  29. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -0
  30. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -0
  31. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +1 -0
  32. astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
  33. astrbot/core/platform/sources/wecom/wecom_adapter.py +1 -0
  34. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +1 -0
  35. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +1 -0
  36. astrbot/core/provider/entities.py +1 -1
  37. astrbot/core/skills/skill_manager.py +9 -8
  38. astrbot/core/star/context.py +8 -0
  39. astrbot/core/star/filter/custom_filter.py +3 -3
  40. astrbot/core/star/register/star_handler.py +1 -1
  41. astrbot/core/subagent_orchestrator.py +96 -0
  42. astrbot/core/tools/cron_tools.py +174 -0
  43. astrbot/core/utils/history_saver.py +31 -0
  44. astrbot/core/utils/trace.py +4 -0
  45. astrbot/dashboard/routes/__init__.py +4 -0
  46. astrbot/dashboard/routes/cron.py +174 -0
  47. astrbot/dashboard/routes/log.py +36 -0
  48. astrbot/dashboard/routes/plugin.py +11 -0
  49. astrbot/dashboard/routes/skills.py +12 -37
  50. astrbot/dashboard/routes/subagent.py +117 -0
  51. astrbot/dashboard/routes/tools.py +41 -14
  52. astrbot/dashboard/server.py +3 -0
  53. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/METADATA +21 -2
  54. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/RECORD +57 -51
  55. astrbot/builtin_stars/astrbot/process_llm_request.py +0 -308
  56. astrbot/builtin_stars/reminder/main.py +0 -266
  57. astrbot/builtin_stars/reminder/metadata.yaml +0 -4
  58. astrbot/core/pipeline/process_stage/utils.py +0 -219
  59. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/WHEEL +0 -0
  60. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/entry_points.txt +0 -0
  61. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,6 @@ from astrbot.api.provider import LLMResponse, ProviderRequest
7
7
  from astrbot.core import logger
8
8
 
9
9
  from .long_term_memory import LongTermMemory
10
- from .process_llm_request import ProcessLLMRequest
11
10
 
12
11
 
13
12
  class Main(star.Star):
@@ -19,8 +18,6 @@ class Main(star.Star):
19
18
  except BaseException as e:
20
19
  logger.error(f"聊天增强 err: {e}")
21
20
 
22
- self.proc_llm_req = ProcessLLMRequest(self.context)
23
-
24
21
  def ltm_enabled(self, event: AstrMessageEvent):
25
22
  ltmse = self.context.get_config(umo=event.unified_msg_origin)[
26
23
  "provider_ltm_settings"
@@ -80,7 +77,6 @@ class Main(star.Star):
80
77
 
81
78
  yield event.request_llm(
82
79
  prompt=prompt,
83
- func_tool_manager=self.context.get_llm_tool_manager(),
84
80
  session_id=event.session_id,
85
81
  conversation=conv,
86
82
  )
@@ -91,8 +87,6 @@ class Main(star.Star):
91
87
  @filter.on_llm_request()
92
88
  async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
93
89
  """在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
94
- await self.proc_llm_req.process_llm_request(event, req)
95
-
96
90
  if self.ltm and self.ltm_enabled(event):
97
91
  try:
98
92
  await self.ltm.on_req_llm(event, req)
@@ -49,7 +49,7 @@ class Main(Star):
49
49
  if p_settings.get("empty_mention_waiting_need_reply", True):
50
50
  try:
51
51
  # 尝试使用 LLM 生成更生动的回复
52
- func_tools_mgr = self.context.get_llm_tool_manager()
52
+ # func_tools_mgr = self.context.get_llm_tool_manager()
53
53
 
54
54
  # 获取用户当前的对话信息
55
55
  curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
@@ -76,7 +76,6 @@ class Main(Star):
76
76
  "你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
77
77
  "请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
78
78
  ),
79
- func_tool_manager=func_tools_mgr,
80
79
  session_id=curr_cid,
81
80
  contexts=[],
82
81
  system_prompt="",
astrbot/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.13.2"
1
+ __version__ = "4.14.1"
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from typing import Generic
2
+ from typing import Any, Generic
3
3
 
4
4
  from .hooks import BaseAgentRunHooks
5
5
  from .run_context import TContext
@@ -12,3 +12,4 @@ class Agent(Generic[TContext]):
12
12
  instructions: str | None = None
13
13
  tools: list[str | FunctionTool] | None = None
14
14
  run_hooks: BaseAgentRunHooks[TContext] | None = None
15
+ begin_dialogs: list[Any] | None = None
@@ -12,16 +12,29 @@ class HandoffTool(FunctionTool, Generic[TContext]):
12
12
  self,
13
13
  agent: Agent[TContext],
14
14
  parameters: dict | None = None,
15
+ tool_description: str | None = None,
15
16
  **kwargs,
16
17
  ):
17
18
  self.agent = agent
19
+
20
+ # Avoid passing duplicate `description` to the FunctionTool dataclass.
21
+ # Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs
22
+ # to override what the main agent sees, while we also compute a default
23
+ # description here.
24
+ # `tool_description` is the public description shown to the main LLM.
25
+ # Keep a separate kwarg to avoid conflicting with FunctionTool's `description`.
26
+ description = tool_description or self.default_description(agent.name)
18
27
  super().__init__(
19
28
  name=f"transfer_to_{agent.name}",
20
29
  parameters=parameters or self.default_parameters(),
21
- description=agent.instructions or self.default_description(agent.name),
30
+ description=description,
22
31
  **kwargs,
23
32
  )
24
33
 
34
+ # Optional provider override for this subagent. When set, the handoff
35
+ # execution will use this chat provider id instead of the global/default.
36
+ self.provider_id: str | None = None
37
+
25
38
  def default_parameters(self) -> dict:
26
39
  return {
27
40
  "type": "object",
@@ -111,10 +111,12 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
111
111
  # See #4681
112
112
  self.tool_schema_mode = tool_schema_mode
113
113
  self._tool_schema_param_set = None
114
+ self._skill_like_raw_tool_set = None
114
115
  if tool_schema_mode == "skills_like":
115
116
  tool_set = self.req.func_tool
116
117
  if not tool_set:
117
118
  return
119
+ self._skill_like_raw_tool_set = tool_set
118
120
  light_set = tool_set.get_light_tool_set()
119
121
  self._tool_schema_param_set = tool_set.get_param_only_tool_set()
120
122
  # MODIFIE the req.func_tool to use light tool schemas
@@ -379,7 +381,17 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
379
381
  try:
380
382
  if not req.func_tool:
381
383
  return
382
- func_tool = req.func_tool.get_tool(func_tool_name)
384
+
385
+ if (
386
+ self.tool_schema_mode == "skills_like"
387
+ and self._skill_like_raw_tool_set
388
+ ):
389
+ # in 'skills_like' mode, raw.func_tool is light schema, does not have handler
390
+ # so we need to get the tool from the raw tool set
391
+ func_tool = self._skill_like_raw_tool_set.get_tool(func_tool_name)
392
+ else:
393
+ func_tool = req.func_tool.get_tool(func_tool_name)
394
+
383
395
  logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
384
396
 
385
397
  if not func_tool:
@@ -557,6 +569,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
557
569
  )
558
570
  ],
559
571
  )
572
+ logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}")
560
573
 
561
574
  # 处理函数调用响应
562
575
  if tool_call_result_blocks:
@@ -58,6 +58,11 @@ class FunctionTool(ToolSchema, Generic[TContext]):
58
58
  Whether the tool is active. This field is a special field for AstrBot.
59
59
  You can ignore it when integrating with other frameworks.
60
60
  """
61
+ is_background_task: bool = False
62
+ """
63
+ Declare this tool as a background task. Background tasks return immediately
64
+ with a task identifier while the real work continues asynchronously.
65
+ """
61
66
 
62
67
  def __repr__(self):
63
68
  return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
@@ -54,6 +54,14 @@ async def run_agent(
54
54
  return
55
55
  if resp.type == "tool_call_result":
56
56
  msg_chain = resp.data["chain"]
57
+
58
+ astr_event.trace.record(
59
+ "agent_tool_result",
60
+ tool_result=msg_chain.get_plain_text(
61
+ with_other_comps_mark=True
62
+ ),
63
+ )
64
+
57
65
  if msg_chain.type == "tool_direct_result":
58
66
  # tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
59
67
  await astr_event.send(msg_chain)
@@ -67,12 +75,22 @@ async def run_agent(
67
75
  # 用来标记流式响应需要分节
68
76
  yield MessageChain(chain=[], type="break")
69
77
 
78
+ tool_info = None
79
+
80
+ if resp.data["chain"].chain:
81
+ json_comp = resp.data["chain"].chain[0]
82
+ if isinstance(json_comp, Json):
83
+ tool_info = json_comp.data
84
+ astr_event.trace.record(
85
+ "agent_tool_call",
86
+ tool_name=tool_info if tool_info else "unknown",
87
+ )
88
+
70
89
  if astr_event.get_platform_name() == "webchat":
71
90
  await astr_event.send(resp.data["chain"])
72
91
  elif show_tool_use:
73
- json_comp = resp.data["chain"].chain[0]
74
- if isinstance(json_comp, Json):
75
- m = f"🔨 调用工具: {json_comp.data.get('name')}"
92
+ if tool_info:
93
+ m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
76
94
  else:
77
95
  m = "🔨 调用工具..."
78
96
  chain = MessageChain(type="tool_call").message(m)
@@ -1,23 +1,34 @@
1
1
  import asyncio
2
2
  import inspect
3
+ import json
3
4
  import traceback
4
5
  import typing as T
6
+ import uuid
5
7
 
6
8
  import mcp
7
9
 
8
10
  from astrbot import logger
9
11
  from astrbot.core.agent.handoff import HandoffTool
10
12
  from astrbot.core.agent.mcp_client import MCPTool
13
+ from astrbot.core.agent.message import Message
11
14
  from astrbot.core.agent.run_context import ContextWrapper
12
15
  from astrbot.core.agent.tool import FunctionTool, ToolSet
13
16
  from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
14
17
  from astrbot.core.astr_agent_context import AstrAgentContext
18
+ from astrbot.core.astr_main_agent_resources import (
19
+ BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
20
+ SEND_MESSAGE_TO_USER_TOOL,
21
+ )
22
+ from astrbot.core.cron.events import CronMessageEvent
15
23
  from astrbot.core.message.message_event_result import (
16
24
  CommandResult,
17
25
  MessageChain,
18
26
  MessageEventResult,
19
27
  )
28
+ from astrbot.core.platform.message_session import MessageSession
29
+ from astrbot.core.provider.entites import ProviderRequest
20
30
  from astrbot.core.provider.register import llm_tools
31
+ from astrbot.core.utils.history_saver import persist_agent_history
21
32
 
22
33
 
23
34
  class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
@@ -43,6 +54,31 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
43
54
  yield r
44
55
  return
45
56
 
57
+ elif tool.is_background_task:
58
+ task_id = uuid.uuid4().hex
59
+
60
+ async def _run_in_background():
61
+ try:
62
+ await cls._execute_background(
63
+ tool=tool,
64
+ run_context=run_context,
65
+ task_id=task_id,
66
+ **tool_args,
67
+ )
68
+ except Exception as e: # noqa: BLE001
69
+ logger.error(
70
+ f"Background task {task_id} failed: {e!s}",
71
+ exc_info=True,
72
+ )
73
+
74
+ asyncio.create_task(_run_in_background())
75
+ text_content = mcp.types.TextContent(
76
+ type="text",
77
+ text=f"Background task submitted. task_id={task_id}",
78
+ )
79
+ yield mcp.types.CallToolResult(content=[text_content])
80
+
81
+ return
46
82
  else:
47
83
  async for r in cls._execute_local(tool, run_context, **tool_args):
48
84
  yield r
@@ -74,13 +110,35 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
74
110
  ctx = run_context.context.context
75
111
  event = run_context.context.event
76
112
  umo = event.unified_msg_origin
77
- prov_id = await ctx.get_current_chat_provider_id(umo)
113
+
114
+ # Use per-subagent provider override if configured; otherwise fall back
115
+ # to the current/default provider resolution.
116
+ prov_id = getattr(
117
+ tool, "provider_id", None
118
+ ) or await ctx.get_current_chat_provider_id(umo)
119
+
120
+ # prepare begin dialogs
121
+ contexts = None
122
+ dialogs = tool.agent.begin_dialogs
123
+ if dialogs:
124
+ contexts = []
125
+ for dialog in dialogs:
126
+ try:
127
+ contexts.append(
128
+ dialog
129
+ if isinstance(dialog, Message)
130
+ else Message.model_validate(dialog)
131
+ )
132
+ except Exception:
133
+ continue
134
+
78
135
  llm_resp = await ctx.tool_loop_agent(
79
136
  event=event,
80
137
  chat_provider_id=prov_id,
81
138
  prompt=input_,
82
139
  system_prompt=tool.agent.instructions,
83
140
  tools=toolset,
141
+ contexts=contexts,
84
142
  max_steps=30,
85
143
  run_hooks=tool.agent.run_hooks,
86
144
  )
@@ -88,11 +146,128 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
88
146
  content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)]
89
147
  )
90
148
 
149
+ @classmethod
150
+ async def _execute_background(
151
+ cls,
152
+ tool: FunctionTool,
153
+ run_context: ContextWrapper[AstrAgentContext],
154
+ task_id: str,
155
+ **tool_args,
156
+ ):
157
+ from astrbot.core.astr_main_agent import (
158
+ MainAgentBuildConfig,
159
+ _get_session_conv,
160
+ build_main_agent,
161
+ )
162
+
163
+ # run the tool
164
+ result_text = ""
165
+ try:
166
+ async for r in cls._execute_local(
167
+ tool, run_context, tool_call_timeout=3600, **tool_args
168
+ ):
169
+ # collect results, currently we just collect the text results
170
+ if isinstance(r, mcp.types.CallToolResult):
171
+ result_text = ""
172
+ for content in r.content:
173
+ if isinstance(content, mcp.types.TextContent):
174
+ result_text += content.text + "\n"
175
+ except Exception as e:
176
+ result_text = (
177
+ f"error: Background task execution failed, internal error: {e!s}"
178
+ )
179
+
180
+ event = run_context.context.event
181
+ ctx = run_context.context.context
182
+
183
+ note = (
184
+ event.get_extra("background_note")
185
+ or f"Background task {tool.name} finished."
186
+ )
187
+ extras = {
188
+ "background_task_result": {
189
+ "task_id": task_id,
190
+ "tool_name": tool.name,
191
+ "result": result_text or "",
192
+ "tool_args": tool_args,
193
+ }
194
+ }
195
+ session = MessageSession.from_str(event.unified_msg_origin)
196
+ cron_event = CronMessageEvent(
197
+ context=ctx,
198
+ session=session,
199
+ message=note,
200
+ extras=extras,
201
+ message_type=session.message_type,
202
+ )
203
+ cron_event.role = event.role
204
+ config = MainAgentBuildConfig(tool_call_timeout=3600)
205
+
206
+ req = ProviderRequest()
207
+ conv = await _get_session_conv(event=cron_event, plugin_context=ctx)
208
+ req.conversation = conv
209
+ context = json.loads(conv.history)
210
+ if context:
211
+ req.contexts = context
212
+ context_dump = req._print_friendly_context()
213
+ req.contexts = []
214
+ req.system_prompt += (
215
+ "\n\nBellow is you and user previous conversation history:\n"
216
+ f"{context_dump}"
217
+ )
218
+
219
+ bg = json.dumps(extras["background_task_result"], ensure_ascii=False)
220
+ req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format(
221
+ background_task_result=bg
222
+ )
223
+ req.prompt = (
224
+ "Proceed according to your system instructions. "
225
+ "Output using same language as previous conversation."
226
+ " After completing your task, summarize and output your actions and results."
227
+ )
228
+ if not req.func_tool:
229
+ req.func_tool = ToolSet()
230
+ req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)
231
+
232
+ result = await build_main_agent(
233
+ event=cron_event, plugin_context=ctx, config=config, req=req
234
+ )
235
+ if not result:
236
+ logger.error("Failed to build main agent for background task job.")
237
+ return
238
+
239
+ runner = result.agent_runner
240
+ async for _ in runner.step_until_done(30):
241
+ # agent will send message to user via using tools
242
+ pass
243
+ llm_resp = runner.get_final_llm_resp()
244
+ task_meta = extras.get("background_task_result", {})
245
+ summary_note = (
246
+ f"[BackgroundTask] {task_meta.get('tool_name', tool.name)} "
247
+ f"(task_id={task_meta.get('task_id', task_id)}) finished. "
248
+ f"Result: {task_meta.get('result') or result_text or 'no content'}"
249
+ )
250
+ if llm_resp and llm_resp.completion_text:
251
+ summary_note += (
252
+ f"I finished the task, here is the result: {llm_resp.completion_text}"
253
+ )
254
+ await persist_agent_history(
255
+ ctx.conversation_manager,
256
+ event=cron_event,
257
+ req=req,
258
+ summary_note=summary_note,
259
+ )
260
+ if not llm_resp:
261
+ logger.warning("background task agent got no response")
262
+ return
263
+
91
264
  @classmethod
92
265
  async def _execute_local(
93
266
  cls,
94
267
  tool: FunctionTool,
95
268
  run_context: ContextWrapper[AstrAgentContext],
269
+ *,
270
+ tool_call_timeout: int | None = None,
96
271
  **tool_args,
97
272
  ):
98
273
  event = run_context.context.event
@@ -133,7 +308,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
133
308
  try:
134
309
  resp = await asyncio.wait_for(
135
310
  anext(wrapper),
136
- timeout=run_context.tool_call_timeout,
311
+ timeout=tool_call_timeout or run_context.tool_call_timeout,
137
312
  )
138
313
  if resp is not None:
139
314
  if isinstance(resp, mcp.types.CallToolResult):
@@ -165,7 +340,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
165
340
  yield None
166
341
  except asyncio.TimeoutError:
167
342
  raise Exception(
168
- f"tool {tool.name} execution timeout after {run_context.tool_call_timeout} seconds.",
343
+ f"tool {tool.name} execution timeout after {tool_call_timeout or run_context.tool_call_timeout} seconds.",
169
344
  )
170
345
  except StopAsyncIteration:
171
346
  break