AstrBot 4.12.3__py3-none-any.whl → 4.13.0__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 (72) hide show
  1. astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
  2. astrbot/builtin_stars/builtin_commands/commands/__init__.py +0 -2
  3. astrbot/builtin_stars/builtin_commands/commands/persona.py +68 -6
  4. astrbot/builtin_stars/builtin_commands/main.py +0 -26
  5. astrbot/cli/__init__.py +1 -1
  6. astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
  7. astrbot/core/agent/tool.py +61 -20
  8. astrbot/core/astr_agent_hooks.py +3 -1
  9. astrbot/core/astr_agent_run_util.py +243 -1
  10. astrbot/core/astr_agent_tool_exec.py +2 -2
  11. astrbot/core/{sandbox → computer}/booters/base.py +4 -4
  12. astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
  13. astrbot/core/computer/booters/local.py +234 -0
  14. astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
  15. astrbot/core/computer/computer_client.py +102 -0
  16. astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
  17. astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
  18. astrbot/core/computer/tools/python.py +94 -0
  19. astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
  20. astrbot/core/config/default.py +90 -9
  21. astrbot/core/db/__init__.py +94 -1
  22. astrbot/core/db/po.py +46 -0
  23. astrbot/core/db/sqlite.py +248 -0
  24. astrbot/core/message/components.py +2 -2
  25. astrbot/core/persona_mgr.py +162 -2
  26. astrbot/core/pipeline/context_utils.py +2 -2
  27. astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
  28. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +73 -6
  29. astrbot/core/pipeline/process_stage/utils.py +31 -4
  30. astrbot/core/pipeline/scheduler.py +1 -1
  31. astrbot/core/pipeline/waking_check/stage.py +0 -1
  32. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
  33. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +32 -14
  34. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +61 -2
  35. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +57 -11
  36. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
  37. astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
  38. astrbot/core/platform/sources/webchat/webchat_event.py +24 -0
  39. astrbot/core/provider/manager.py +38 -0
  40. astrbot/core/provider/provider.py +54 -0
  41. astrbot/core/provider/sources/gemini_embedding_source.py +1 -1
  42. astrbot/core/provider/sources/gemini_source.py +12 -9
  43. astrbot/core/provider/sources/genie_tts.py +128 -0
  44. astrbot/core/provider/sources/openai_embedding_source.py +1 -1
  45. astrbot/core/skills/__init__.py +3 -0
  46. astrbot/core/skills/skill_manager.py +237 -0
  47. astrbot/core/star/command_management.py +1 -1
  48. astrbot/core/star/config.py +1 -1
  49. astrbot/core/star/context.py +9 -8
  50. astrbot/core/star/filter/command.py +1 -1
  51. astrbot/core/star/filter/custom_filter.py +2 -2
  52. astrbot/core/star/register/star_handler.py +2 -4
  53. astrbot/core/utils/astrbot_path.py +6 -0
  54. astrbot/dashboard/routes/__init__.py +2 -0
  55. astrbot/dashboard/routes/config.py +236 -2
  56. astrbot/dashboard/routes/live_chat.py +423 -0
  57. astrbot/dashboard/routes/persona.py +265 -1
  58. astrbot/dashboard/routes/skills.py +148 -0
  59. astrbot/dashboard/routes/util.py +102 -0
  60. astrbot/dashboard/server.py +21 -5
  61. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/METADATA +1 -1
  62. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/RECORD +69 -63
  63. astrbot/builtin_stars/builtin_commands/commands/tool.py +0 -31
  64. astrbot/core/sandbox/sandbox_client.py +0 -52
  65. astrbot/core/sandbox/tools/python.py +0 -74
  66. /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
  67. /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
  68. /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
  69. /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
  70. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/WHEEL +0 -0
  71. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/entry_points.txt +0 -0
  72. {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -10,8 +10,11 @@ from astrbot.api.provider import Provider, ProviderRequest
10
10
  from astrbot.core.agent.message import TextPart
11
11
  from astrbot.core.pipeline.process_stage.utils import (
12
12
  CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
13
+ LOCAL_EXECUTE_SHELL_TOOL,
14
+ LOCAL_PYTHON_TOOL,
13
15
  )
14
16
  from astrbot.core.provider.func_tool_manager import ToolSet
17
+ from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt
15
18
 
16
19
 
17
20
  class ProcessLLMRequest:
@@ -25,6 +28,15 @@ class ProcessLLMRequest:
25
28
  else:
26
29
  logger.info(f"Timezone set to: {self.timezone}")
27
30
 
31
+ self.skill_manager = SkillManager()
32
+
33
+ def _apply_local_env_tools(self, req: ProviderRequest) -> None:
34
+ """Add local environment tools to the provider request."""
35
+ if req.func_tool is None:
36
+ req.func_tool = ToolSet()
37
+ req.func_tool.add_tool(LOCAL_EXECUTE_SHELL_TOOL)
38
+ req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
39
+
28
40
  async def _ensure_persona(
29
41
  self, req: ProviderRequest, cfg: dict, umo: str, platform_type: str
30
42
  ):
@@ -66,6 +78,30 @@ class ProcessLLMRequest:
66
78
  if begin_dialogs := copy.deepcopy(persona["_begin_dialogs_processed"]):
67
79
  req.contexts[:0] = begin_dialogs
68
80
 
81
+ # skills select and prompt
82
+ runtime = self.skills_cfg.get("runtime", "local")
83
+ skills = self.skill_manager.list_skills(active_only=True, runtime=runtime)
84
+ if runtime == "sandbox" and not self.sandbox_cfg.get("enable", False):
85
+ logger.warning(
86
+ "Skills runtime is set to sandbox, but sandbox mode is disabled, will skip skills prompt injection.",
87
+ )
88
+ req.system_prompt += "\n[Background: User added some skills, and skills runtime is set to sandbox, but sandbox mode is disabled. So skills will be unavailable.]\n"
89
+ elif skills:
90
+ # persona.skills == None means all skills are allowed
91
+ if persona and persona.get("skills") is not None:
92
+ if not persona["skills"]:
93
+ return
94
+ allowed = set(persona["skills"])
95
+ skills = [skill for skill in skills if skill.name in allowed]
96
+ if skills:
97
+ req.system_prompt += f"\n{build_skills_prompt(skills)}\n"
98
+
99
+ # if user wants to use skills in non-sandbox mode, apply local env tools
100
+ runtime = self.skills_cfg.get("runtime", "local")
101
+ sandbox_enabled = self.sandbox_cfg.get("enable", False)
102
+ if runtime == "local" and not sandbox_enabled:
103
+ self._apply_local_env_tools(req)
104
+
69
105
  # tools select
70
106
  tmgr = self.ctx.get_llm_tool_manager()
71
107
  if (persona and persona.get("tools") is None) or not persona:
@@ -81,7 +117,10 @@ class ProcessLLMRequest:
81
117
  tool = tmgr.get_func(tool_name)
82
118
  if tool and tool.active:
83
119
  toolset.add_tool(tool)
84
- req.func_tool = toolset
120
+ if not req.func_tool:
121
+ req.func_tool = toolset
122
+ else:
123
+ req.func_tool.merge(toolset)
85
124
  logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
86
125
 
87
126
  async def _ensure_img_caption(
@@ -134,6 +173,8 @@ class ProcessLLMRequest:
134
173
  cfg: dict = self.ctx.get_config(umo=event.unified_msg_origin)[
135
174
  "provider_settings"
136
175
  ]
176
+ self.skills_cfg = cfg.get("skills", {})
177
+ self.sandbox_cfg = cfg.get("sandbox", {})
137
178
 
138
179
  # prompt prefix
139
180
  if prefix := cfg.get("prompt_prefix"):
@@ -11,7 +11,6 @@ from .provider import ProviderCommands
11
11
  from .setunset import SetUnsetCommands
12
12
  from .sid import SIDCommand
13
13
  from .t2i import T2ICommand
14
- from .tool import ToolCommands
15
14
  from .tts import TTSCommand
16
15
 
17
16
  __all__ = [
@@ -27,5 +26,4 @@ __all__ = [
27
26
  "SetUnsetCommands",
28
27
  "T2ICommand",
29
28
  "TTSCommand",
30
- "ToolCommands",
31
29
  ]
@@ -1,13 +1,55 @@
1
1
  import builtins
2
+ from typing import TYPE_CHECKING
2
3
 
3
4
  from astrbot.api import sp, star
4
5
  from astrbot.api.event import AstrMessageEvent, MessageEventResult
5
6
 
7
+ if TYPE_CHECKING:
8
+ from astrbot.core.db.po import Persona
9
+
6
10
 
7
11
  class PersonaCommands:
8
12
  def __init__(self, context: star.Context):
9
13
  self.context = context
10
14
 
15
+ def _build_tree_output(
16
+ self,
17
+ folder_tree: list[dict],
18
+ all_personas: list["Persona"],
19
+ depth: int = 0,
20
+ ) -> list[str]:
21
+ """递归构建树状输出,使用短线条表示层级"""
22
+ lines: list[str] = []
23
+ # 使用短线条作为缩进前缀,每层只用 "│" 加一个空格
24
+ prefix = "│ " * depth
25
+
26
+ for folder in folder_tree:
27
+ # 输出文件夹
28
+ lines.append(f"{prefix}├ 📁 {folder['name']}/")
29
+
30
+ # 获取该文件夹下的人格
31
+ folder_personas = [
32
+ p for p in all_personas if p.folder_id == folder["folder_id"]
33
+ ]
34
+ child_prefix = "│ " * (depth + 1)
35
+
36
+ # 输出该文件夹下的人格
37
+ for persona in folder_personas:
38
+ lines.append(f"{child_prefix}├ 👤 {persona.persona_id}")
39
+
40
+ # 递归处理子文件夹
41
+ children = folder.get("children", [])
42
+ if children:
43
+ lines.extend(
44
+ self._build_tree_output(
45
+ children,
46
+ all_personas,
47
+ depth + 1,
48
+ )
49
+ )
50
+
51
+ return lines
52
+
11
53
  async def persona(self, message: AstrMessageEvent):
12
54
  l = message.message_str.split(" ") # noqa: E741
13
55
  umo = message.unified_msg_origin
@@ -69,12 +111,32 @@ class PersonaCommands:
69
111
  .use_t2i(False),
70
112
  )
71
113
  elif l[1] == "list":
72
- parts = ["人格列表:\n"]
73
- for persona in self.context.provider_manager.personas:
74
- parts.append(f"- {persona['name']}\n")
75
- parts.append("\n\n*输入 `/persona view 人格名` 查看人格详细信息")
76
- msg = "".join(parts)
77
- message.set_result(MessageEventResult().message(msg))
114
+ # 获取文件夹树和所有人格
115
+ folder_tree = await self.context.persona_manager.get_folder_tree()
116
+ all_personas = self.context.persona_manager.personas
117
+
118
+ lines = ["📂 人格列表:\n"]
119
+
120
+ # 构建树状输出
121
+ tree_lines = self._build_tree_output(folder_tree, all_personas)
122
+ lines.extend(tree_lines)
123
+
124
+ # 输出根目录下的人格(没有文件夹的)
125
+ root_personas = [p for p in all_personas if p.folder_id is None]
126
+ if root_personas:
127
+ if tree_lines: # 如果有文件夹内容,加个空行
128
+ lines.append("")
129
+ for persona in root_personas:
130
+ lines.append(f"👤 {persona.persona_id}")
131
+
132
+ # 统计信息
133
+ total_count = len(all_personas)
134
+ lines.append(f"\n共 {total_count} 个人格")
135
+ lines.append("\n*使用 `/persona <人格名>` 设置人格")
136
+ lines.append("*使用 `/persona view <人格名>` 查看详细信息")
137
+
138
+ msg = "\n".join(lines)
139
+ message.set_result(MessageEventResult().message(msg).use_t2i(False))
78
140
  elif l[1] == "view":
79
141
  if len(l) == 2:
80
142
  message.set_result(MessageEventResult().message("请输入人格情景名"))
@@ -13,7 +13,6 @@ from .commands import (
13
13
  SetUnsetCommands,
14
14
  SIDCommand,
15
15
  T2ICommand,
16
- ToolCommands,
17
16
  TTSCommand,
18
17
  )
19
18
 
@@ -24,7 +23,6 @@ class Main(star.Star):
24
23
 
25
24
  self.help_c = HelpCommand(self.context)
26
25
  self.llm_c = LLMCommands(self.context)
27
- self.tool_c = ToolCommands(self.context)
28
26
  self.plugin_c = PluginCommands(self.context)
29
27
  self.admin_c = AdminCommands(self.context)
30
28
  self.conversation_c = ConversationCommands(self.context)
@@ -47,30 +45,6 @@ class Main(star.Star):
47
45
  """开启/关闭 LLM"""
48
46
  await self.llm_c.llm(event)
49
47
 
50
- @filter.command_group("tool")
51
- def tool(self):
52
- """函数工具管理"""
53
-
54
- @tool.command("ls")
55
- async def tool_ls(self, event: AstrMessageEvent):
56
- """查看函数工具列表"""
57
- await self.tool_c.tool_ls(event)
58
-
59
- @tool.command("on")
60
- async def tool_on(self, event: AstrMessageEvent, tool_name: str):
61
- """启用一个函数工具"""
62
- await self.tool_c.tool_on(event, tool_name)
63
-
64
- @tool.command("off")
65
- async def tool_off(self, event: AstrMessageEvent, tool_name: str):
66
- """停用一个函数工具"""
67
- await self.tool_c.tool_off(event, tool_name)
68
-
69
- @tool.command("off_all")
70
- async def tool_all_off(self, event: AstrMessageEvent):
71
- """停用所有函数工具"""
72
- await self.tool_c.tool_all_off(event)
73
-
74
48
  @filter.command_group("plugin")
75
49
  def plugin(self):
76
50
  """插件管理"""
astrbot/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.12.3"
1
+ __version__ = "4.13.0"
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import sys
2
3
  import time
3
4
  import traceback
@@ -14,6 +15,7 @@ from mcp.types import (
14
15
 
15
16
  from astrbot import logger
16
17
  from astrbot.core.agent.message import TextPart, ThinkPart
18
+ from astrbot.core.agent.tool import ToolSet
17
19
  from astrbot.core.message.components import Json
18
20
  from astrbot.core.message.message_event_result import (
19
21
  MessageChain,
@@ -64,6 +66,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
64
66
  # customize
65
67
  custom_token_counter: TokenCounter | None = None,
66
68
  custom_compressor: ContextCompressor | None = None,
69
+ tool_schema_mode: str | None = "full",
67
70
  **kwargs: T.Any,
68
71
  ) -> None:
69
72
  self.req = request
@@ -99,6 +102,24 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
99
102
  self.agent_hooks = agent_hooks
100
103
  self.run_context = run_context
101
104
 
105
+ # These two are used for tool schema mode handling
106
+ # We now have two modes:
107
+ # - "full": use full tool schema for LLM calls, default.
108
+ # - "skills_like": use light tool schema for LLM calls, and re-query with param-only schema when needed.
109
+ # Light tool schema does not include tool parameters.
110
+ # This can reduce token usage when tools have large descriptions.
111
+ # See #4681
112
+ self.tool_schema_mode = tool_schema_mode
113
+ self._tool_schema_param_set = None
114
+ if tool_schema_mode == "skills_like":
115
+ tool_set = self.req.func_tool
116
+ if not tool_set:
117
+ return
118
+ light_set = tool_set.get_light_tool_set()
119
+ self._tool_schema_param_set = tool_set.get_param_only_tool_set()
120
+ # MODIFIE the req.func_tool to use light tool schemas
121
+ self.req.func_tool = light_set
122
+
102
123
  messages = []
103
124
  # append existing messages in the run context
104
125
  for msg in request.contexts:
@@ -253,6 +274,9 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
253
274
 
254
275
  # 如果有工具调用,还需处理工具调用
255
276
  if llm_resp.tools_call_name:
277
+ if self.tool_schema_mode == "skills_like":
278
+ llm_resp, _ = await self._resolve_tool_exec(llm_resp)
279
+
256
280
  tool_call_result_blocks = []
257
281
  async for result in self._handle_function_tools(self.req, llm_resp):
258
282
  if isinstance(result, list):
@@ -269,6 +293,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
269
293
  type=ar_type,
270
294
  data=AgentResponseData(chain=result),
271
295
  )
296
+
272
297
  # 将结果添加到上下文中
273
298
  parts = []
274
299
  if llm_resp.reasoning_content or llm_resp.reasoning_signature:
@@ -354,7 +379,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
354
379
  try:
355
380
  if not req.func_tool:
356
381
  return
357
- func_tool = req.func_tool.get_func(func_tool_name)
382
+ func_tool = req.func_tool.get_tool(func_tool_name)
358
383
  logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
359
384
 
360
385
  if not func_tool:
@@ -537,6 +562,71 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
537
562
  if tool_call_result_blocks:
538
563
  yield tool_call_result_blocks
539
564
 
565
+ def _build_tool_requery_context(
566
+ self, tool_names: list[str]
567
+ ) -> list[dict[str, T.Any]]:
568
+ """Build contexts for re-querying LLM with param-only tool schemas."""
569
+ contexts: list[dict[str, T.Any]] = []
570
+ for msg in self.run_context.messages:
571
+ if hasattr(msg, "model_dump"):
572
+ contexts.append(msg.model_dump()) # type: ignore[call-arg]
573
+ elif isinstance(msg, dict):
574
+ contexts.append(copy.deepcopy(msg))
575
+ instruction = (
576
+ "You have decided to call tool(s): "
577
+ + ", ".join(tool_names)
578
+ + ". Now call the tool(s) with required arguments using the tool schema, "
579
+ "and follow the existing tool-use rules."
580
+ )
581
+ if contexts and contexts[0].get("role") == "system":
582
+ content = contexts[0].get("content") or ""
583
+ contexts[0]["content"] = f"{content}\n{instruction}"
584
+ else:
585
+ contexts.insert(0, {"role": "system", "content": instruction})
586
+ return contexts
587
+
588
+ def _build_tool_subset(self, tool_set: ToolSet, tool_names: list[str]) -> ToolSet:
589
+ """Build a subset of tools from the given tool set based on tool names."""
590
+ subset = ToolSet()
591
+ for name in tool_names:
592
+ tool = tool_set.get_tool(name)
593
+ if tool:
594
+ subset.add_tool(tool)
595
+ return subset
596
+
597
+ async def _resolve_tool_exec(
598
+ self,
599
+ llm_resp: LLMResponse,
600
+ ) -> tuple[LLMResponse, ToolSet | None]:
601
+ """Used in 'skills_like' tool schema mode to re-query LLM with param-only tool schemas."""
602
+ tool_names = llm_resp.tools_call_name
603
+ if not tool_names:
604
+ return llm_resp, self.req.func_tool
605
+ full_tool_set = self.req.func_tool
606
+ if not isinstance(full_tool_set, ToolSet):
607
+ return llm_resp, self.req.func_tool
608
+
609
+ subset = self._build_tool_subset(full_tool_set, tool_names)
610
+ if not subset.tools:
611
+ return llm_resp, full_tool_set
612
+
613
+ if isinstance(self._tool_schema_param_set, ToolSet):
614
+ param_subset = self._build_tool_subset(
615
+ self._tool_schema_param_set, tool_names
616
+ )
617
+ if param_subset.tools and tool_names:
618
+ contexts = self._build_tool_requery_context(tool_names)
619
+ requery_resp = await self.provider.text_chat(
620
+ contexts=contexts,
621
+ func_tool=param_subset,
622
+ model=self.req.model,
623
+ session_id=self.req.session_id,
624
+ )
625
+ if requery_resp:
626
+ llm_resp = requery_resp
627
+
628
+ return llm_resp, subset
629
+
540
630
  def done(self) -> bool:
541
631
  """检查 Agent 是否已完成工作"""
542
632
  return self._state in (AgentState.DONE, AgentState.ERROR)
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  from collections.abc import AsyncGenerator, Awaitable, Callable
2
3
  from typing import Any, Generic
3
4
 
@@ -102,6 +103,47 @@ class ToolSet:
102
103
  return tool
103
104
  return None
104
105
 
106
+ def get_light_tool_set(self) -> "ToolSet":
107
+ """Return a light tool set with only name/description."""
108
+ light_tools = []
109
+ for tool in self.tools:
110
+ if hasattr(tool, "active") and not tool.active:
111
+ continue
112
+ light_params = {
113
+ "type": "object",
114
+ "properties": {},
115
+ }
116
+ light_tools.append(
117
+ FunctionTool(
118
+ name=tool.name,
119
+ parameters=light_params,
120
+ description=tool.description,
121
+ handler=None,
122
+ )
123
+ )
124
+ return ToolSet(light_tools)
125
+
126
+ def get_param_only_tool_set(self) -> "ToolSet":
127
+ """Return a tool set with name/parameters only (no description)."""
128
+ param_tools = []
129
+ for tool in self.tools:
130
+ if hasattr(tool, "active") and not tool.active:
131
+ continue
132
+ params = (
133
+ copy.deepcopy(tool.parameters)
134
+ if tool.parameters
135
+ else {"type": "object", "properties": {}}
136
+ )
137
+ param_tools.append(
138
+ FunctionTool(
139
+ name=tool.name,
140
+ parameters=params,
141
+ description="",
142
+ handler=None,
143
+ )
144
+ )
145
+ return ToolSet(param_tools)
146
+
105
147
  @deprecated(reason="Use add_tool() instead", version="4.0.0")
106
148
  def add_func(
107
149
  self,
@@ -147,18 +189,15 @@ class ToolSet:
147
189
  """Convert tools to OpenAI API function calling schema format."""
148
190
  result = []
149
191
  for tool in self.tools:
150
- func_def = {
151
- "type": "function",
152
- "function": {
153
- "name": tool.name,
154
- "description": tool.description,
155
- },
156
- }
192
+ func_def = {"type": "function", "function": {"name": tool.name}}
193
+ if tool.description:
194
+ func_def["function"]["description"] = tool.description
157
195
 
158
- if (
159
- tool.parameters and tool.parameters.get("properties")
160
- ) or not omit_empty_parameter_field:
161
- func_def["function"]["parameters"] = tool.parameters
196
+ if tool.parameters is not None:
197
+ if (
198
+ tool.parameters and tool.parameters.get("properties")
199
+ ) or not omit_empty_parameter_field:
200
+ func_def["function"]["parameters"] = tool.parameters
162
201
 
163
202
  result.append(func_def)
164
203
  return result
@@ -171,11 +210,9 @@ class ToolSet:
171
210
  if tool.parameters:
172
211
  input_schema["properties"] = tool.parameters.get("properties", {})
173
212
  input_schema["required"] = tool.parameters.get("required", [])
174
- tool_def = {
175
- "name": tool.name,
176
- "description": tool.description,
177
- "input_schema": input_schema,
178
- }
213
+ tool_def = {"name": tool.name, "input_schema": input_schema}
214
+ if tool.description:
215
+ tool_def["description"] = tool.description
179
216
  result.append(tool_def)
180
217
  return result
181
218
 
@@ -245,10 +282,9 @@ class ToolSet:
245
282
 
246
283
  tools = []
247
284
  for tool in self.tools:
248
- d: dict[str, Any] = {
249
- "name": tool.name,
250
- "description": tool.description,
251
- }
285
+ d: dict[str, Any] = {"name": tool.name}
286
+ if tool.description:
287
+ d["description"] = tool.description
252
288
  if tool.parameters:
253
289
  d["parameters"] = convert_schema(tool.parameters)
254
290
  tools.append(d)
@@ -274,6 +310,11 @@ class ToolSet:
274
310
  """获取所有工具的名称列表"""
275
311
  return [tool.name for tool in self.tools]
276
312
 
313
+ def merge(self, other: "ToolSet"):
314
+ """Merge another ToolSet into this one."""
315
+ for tool in other.tools:
316
+ self.add_tool(tool)
317
+
277
318
  def __len__(self):
278
319
  return len(self.tools)
279
320
 
@@ -56,8 +56,10 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
56
56
  )
57
57
 
58
58
  # special handle web_search_tavily
59
+ platform_name = run_context.context.event.get_platform_name()
59
60
  if (
60
- tool.name == "web_search_tavily"
61
+ platform_name == "webchat"
62
+ and tool.name == "web_search_tavily"
61
63
  and len(run_context.messages) > 0
62
64
  and tool_result
63
65
  and len(tool_result.content)