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.
- astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
- astrbot/builtin_stars/builtin_commands/commands/__init__.py +0 -2
- astrbot/builtin_stars/builtin_commands/commands/persona.py +68 -6
- astrbot/builtin_stars/builtin_commands/main.py +0 -26
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
- astrbot/core/agent/tool.py +61 -20
- astrbot/core/astr_agent_hooks.py +3 -1
- astrbot/core/astr_agent_run_util.py +243 -1
- astrbot/core/astr_agent_tool_exec.py +2 -2
- astrbot/core/{sandbox → computer}/booters/base.py +4 -4
- astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
- astrbot/core/computer/booters/local.py +234 -0
- astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
- astrbot/core/computer/computer_client.py +102 -0
- astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
- astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
- astrbot/core/computer/tools/python.py +94 -0
- astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
- astrbot/core/config/default.py +90 -9
- astrbot/core/db/__init__.py +94 -1
- astrbot/core/db/po.py +46 -0
- astrbot/core/db/sqlite.py +248 -0
- astrbot/core/message/components.py +2 -2
- astrbot/core/persona_mgr.py +162 -2
- astrbot/core/pipeline/context_utils.py +2 -2
- astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +73 -6
- astrbot/core/pipeline/process_stage/utils.py +31 -4
- astrbot/core/pipeline/scheduler.py +1 -1
- astrbot/core/pipeline/waking_check/stage.py +0 -1
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +32 -14
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +61 -2
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +57 -11
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
- astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
- astrbot/core/platform/sources/webchat/webchat_event.py +24 -0
- astrbot/core/provider/manager.py +38 -0
- astrbot/core/provider/provider.py +54 -0
- astrbot/core/provider/sources/gemini_embedding_source.py +1 -1
- astrbot/core/provider/sources/gemini_source.py +12 -9
- astrbot/core/provider/sources/genie_tts.py +128 -0
- astrbot/core/provider/sources/openai_embedding_source.py +1 -1
- astrbot/core/skills/__init__.py +3 -0
- astrbot/core/skills/skill_manager.py +237 -0
- astrbot/core/star/command_management.py +1 -1
- astrbot/core/star/config.py +1 -1
- astrbot/core/star/context.py +9 -8
- astrbot/core/star/filter/command.py +1 -1
- astrbot/core/star/filter/custom_filter.py +2 -2
- astrbot/core/star/register/star_handler.py +2 -4
- astrbot/core/utils/astrbot_path.py +6 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/config.py +236 -2
- astrbot/dashboard/routes/live_chat.py +423 -0
- astrbot/dashboard/routes/persona.py +265 -1
- astrbot/dashboard/routes/skills.py +148 -0
- astrbot/dashboard/routes/util.py +102 -0
- astrbot/dashboard/server.py +21 -5
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/METADATA +1 -1
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/RECORD +69 -63
- astrbot/builtin_stars/builtin_commands/commands/tool.py +0 -31
- astrbot/core/sandbox/sandbox_client.py +0 -52
- astrbot/core/sandbox/tools/python.py +0 -74
- /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/WHEEL +0 -0
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
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.
|
|
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)
|
astrbot/core/agent/tool.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
176
|
-
"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
|
-
|
|
250
|
-
"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
|
|
astrbot/core/astr_agent_hooks.py
CHANGED
|
@@ -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
|
-
|
|
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)
|