diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index c2c5ae741..d668dfeec 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -19,16 +19,6 @@ from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.astr_main_agent_resources import ( BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT, - EXECUTE_SHELL_TOOL, - FILE_DOWNLOAD_TOOL, - FILE_EDIT_TOOL, - FILE_UPLOAD_TOOL, - FILE_WRITE_TOOL, - GREP_TOOL, - LOCAL_EXECUTE_SHELL_TOOL, - LOCAL_PYTHON_TOOL, - PYTHON_TOOL, - READ_FILE_TOOL, ) from astrbot.core.cron.events import CronMessageEvent from astrbot.core.message.components import Image @@ -40,6 +30,17 @@ from astrbot.core.message.message_event_result import ( from astrbot.core.platform.message_session import MessageSession from astrbot.core.provider.entites import ProviderRequest from astrbot.core.provider.register import llm_tools +from astrbot.core.tools.computer_tools import ( + ExecuteShellTool, + FileDownloadTool, + FileEditTool, + FileReadTool, + FileUploadTool, + FileWriteTool, + GrepTool, + LocalPythonTool, + PythonTool, +) from astrbot.core.tools.message_tools import SendMessageToUserTool from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from astrbot.core.utils.history_saver import persist_agent_history @@ -181,26 +182,44 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): return @classmethod - def _get_runtime_computer_tools(cls, runtime: str) -> dict[str, FunctionTool]: + def _get_runtime_computer_tools( + cls, + runtime: str, + tool_mgr, + ) -> dict[str, FunctionTool]: if runtime == "sandbox": + shell_tool = tool_mgr.get_builtin_tool(ExecuteShellTool) + python_tool = tool_mgr.get_builtin_tool(PythonTool) + upload_tool = tool_mgr.get_builtin_tool(FileUploadTool) + download_tool = tool_mgr.get_builtin_tool(FileDownloadTool) + read_tool = tool_mgr.get_builtin_tool(FileReadTool) + write_tool = tool_mgr.get_builtin_tool(FileWriteTool) + edit_tool = tool_mgr.get_builtin_tool(FileEditTool) + grep_tool = tool_mgr.get_builtin_tool(GrepTool) return { - EXECUTE_SHELL_TOOL.name: EXECUTE_SHELL_TOOL, - PYTHON_TOOL.name: PYTHON_TOOL, - FILE_UPLOAD_TOOL.name: FILE_UPLOAD_TOOL, - FILE_DOWNLOAD_TOOL.name: FILE_DOWNLOAD_TOOL, - READ_FILE_TOOL.name: READ_FILE_TOOL, - FILE_WRITE_TOOL.name: FILE_WRITE_TOOL, - FILE_EDIT_TOOL.name: FILE_EDIT_TOOL, - GREP_TOOL.name: GREP_TOOL, + shell_tool.name: shell_tool, + python_tool.name: python_tool, + upload_tool.name: upload_tool, + download_tool.name: download_tool, + read_tool.name: read_tool, + write_tool.name: write_tool, + edit_tool.name: edit_tool, + grep_tool.name: grep_tool, } if runtime == "local": + shell_tool = tool_mgr.get_builtin_tool(ExecuteShellTool) + python_tool = tool_mgr.get_builtin_tool(LocalPythonTool) + read_tool = tool_mgr.get_builtin_tool(FileReadTool) + write_tool = tool_mgr.get_builtin_tool(FileWriteTool) + edit_tool = tool_mgr.get_builtin_tool(FileEditTool) + grep_tool = tool_mgr.get_builtin_tool(GrepTool) return { - LOCAL_EXECUTE_SHELL_TOOL.name: LOCAL_EXECUTE_SHELL_TOOL, - LOCAL_PYTHON_TOOL.name: LOCAL_PYTHON_TOOL, - READ_FILE_TOOL.name: READ_FILE_TOOL, - FILE_WRITE_TOOL.name: FILE_WRITE_TOOL, - FILE_EDIT_TOOL.name: FILE_EDIT_TOOL, - GREP_TOOL.name: GREP_TOOL, + shell_tool.name: shell_tool, + python_tool.name: python_tool, + read_tool.name: read_tool, + write_tool.name: write_tool, + edit_tool.name: edit_tool, + grep_tool.name: grep_tool, } return {} @@ -215,7 +234,15 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): cfg = ctx.get_config(umo=event.unified_msg_origin) provider_settings = cfg.get("provider_settings", {}) runtime = str(provider_settings.get("computer_use_runtime", "local")) - runtime_computer_tools = cls._get_runtime_computer_tools(runtime) + tool_mgr = ( + ctx.get_llm_tool_manager() + if hasattr(ctx, "get_llm_tool_manager") + else llm_tools + ) + runtime_computer_tools = cls._get_runtime_computer_tools( + runtime, + tool_mgr, + ) # Keep persona semantics aligned with the main agent: tools=None means # "all tools", including runtime computer-use tools. diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 8de545b1f..55bd4fe8c 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -20,34 +20,10 @@ from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS from astrbot.core.astr_agent_run_util import AgentRunner from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor from astrbot.core.astr_main_agent_resources import ( - ANNOTATE_EXECUTION_TOOL, - BROWSER_BATCH_EXEC_TOOL, - BROWSER_EXEC_TOOL, CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT, - CREATE_SKILL_CANDIDATE_TOOL, - CREATE_SKILL_PAYLOAD_TOOL, - EVALUATE_SKILL_CANDIDATE_TOOL, - EXECUTE_SHELL_TOOL, - FILE_DOWNLOAD_TOOL, - FILE_EDIT_TOOL, - FILE_UPLOAD_TOOL, - FILE_WRITE_TOOL, - GET_EXECUTION_HISTORY_TOOL, - GET_SKILL_PAYLOAD_TOOL, - GREP_TOOL, - LIST_SKILL_CANDIDATES_TOOL, - LIST_SKILL_RELEASES_TOOL, LIVE_MODE_SYSTEM_PROMPT, LLM_SAFETY_MODE_SYSTEM_PROMPT, - LOCAL_EXECUTE_SHELL_TOOL, - LOCAL_PYTHON_TOOL, - PROMOTE_SKILL_CANDIDATE_TOOL, - PYTHON_TOOL, - READ_FILE_TOOL, - ROLLBACK_SKILL_RELEASE_TOOL, - RUN_BROWSER_SKILL_TOOL, SANDBOX_MODE_PROMPT, - SYNC_SKILL_RELEASE_TOOL, TOOL_CALL_PROMPT, TOOL_CALL_PROMPT_SKILLS_LIKE_MODE, ) @@ -63,6 +39,31 @@ from astrbot.core.provider.entities import ProviderRequest from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt from astrbot.core.star.context import Context from astrbot.core.star.star_handler import star_map +from astrbot.core.tools.computer_tools import ( + AnnotateExecutionTool, + BrowserBatchExecTool, + BrowserExecTool, + CreateSkillCandidateTool, + CreateSkillPayloadTool, + EvaluateSkillCandidateTool, + ExecuteShellTool, + FileDownloadTool, + FileEditTool, + FileReadTool, + FileUploadTool, + FileWriteTool, + GetExecutionHistoryTool, + GetSkillPayloadTool, + GrepTool, + ListSkillCandidatesTool, + ListSkillReleasesTool, + LocalPythonTool, + PromoteSkillCandidateTool, + PythonTool, + RollbackSkillReleaseTool, + RunBrowserSkillTool, + SyncSkillReleaseTool, +) from astrbot.core.tools.cron_tools import ( CreateActiveCronTool, DeleteCronJobTool, @@ -299,15 +300,16 @@ def _apply_prompt_prefix(req: ProviderRequest, cfg: dict) -> None: req.prompt = f"{prefix}{req.prompt}" -def _apply_local_env_tools(req: ProviderRequest) -> None: +def _apply_local_env_tools(req: ProviderRequest, plugin_context: Context) -> None: if req.func_tool is None: req.func_tool = ToolSet() - req.func_tool.add_tool(LOCAL_EXECUTE_SHELL_TOOL) - req.func_tool.add_tool(LOCAL_PYTHON_TOOL) - req.func_tool.add_tool(READ_FILE_TOOL) - req.func_tool.add_tool(FILE_WRITE_TOOL) - req.func_tool.add_tool(FILE_EDIT_TOOL) - req.func_tool.add_tool(GREP_TOOL) + tool_mgr = plugin_context.get_llm_tool_manager() + req.func_tool.add_tool(tool_mgr.get_builtin_tool(ExecuteShellTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(LocalPythonTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileReadTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileWriteTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileEditTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(GrepTool)) req.system_prompt = f"{req.system_prompt or ''}\n{_build_local_mode_prompt()}\n" @@ -994,7 +996,10 @@ def _apply_llm_safety_mode(config: MainAgentBuildConfig, req: ProviderRequest) - def _apply_sandbox_tools( - config: MainAgentBuildConfig, req: ProviderRequest, session_id: str + config: MainAgentBuildConfig, + req: ProviderRequest, + session_id: str, + plugin_context: Context, ) -> None: if req.func_tool is None: req.func_tool = ToolSet() @@ -1010,14 +1015,15 @@ def _apply_sandbox_tools( os.environ["SHIPYARD_ENDPOINT"] = ep os.environ["SHIPYARD_ACCESS_TOKEN"] = at - req.func_tool.add_tool(EXECUTE_SHELL_TOOL) - req.func_tool.add_tool(PYTHON_TOOL) - req.func_tool.add_tool(FILE_UPLOAD_TOOL) - req.func_tool.add_tool(FILE_DOWNLOAD_TOOL) - req.func_tool.add_tool(READ_FILE_TOOL) - req.func_tool.add_tool(FILE_WRITE_TOOL) - req.func_tool.add_tool(FILE_EDIT_TOOL) - req.func_tool.add_tool(GREP_TOOL) + tool_mgr = plugin_context.get_llm_tool_manager() + req.func_tool.add_tool(tool_mgr.get_builtin_tool(ExecuteShellTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(PythonTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileUploadTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileDownloadTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileReadTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileWriteTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileEditTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(GrepTool)) if booter == "shipyard_neo": # Neo-specific path rule: filesystem tools operate relative to sandbox # workspace root. Do not prepend "/workspace". @@ -1053,22 +1059,22 @@ def _apply_sandbox_tools( # Browser tools: only register if profile supports browser # (or if capabilities are unknown because sandbox hasn't booted yet) if sandbox_capabilities is None or "browser" in sandbox_capabilities: - req.func_tool.add_tool(BROWSER_EXEC_TOOL) - req.func_tool.add_tool(BROWSER_BATCH_EXEC_TOOL) - req.func_tool.add_tool(RUN_BROWSER_SKILL_TOOL) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(BrowserExecTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(BrowserBatchExecTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(RunBrowserSkillTool)) # Neo-specific tools (always available for shipyard_neo) - req.func_tool.add_tool(GET_EXECUTION_HISTORY_TOOL) - req.func_tool.add_tool(ANNOTATE_EXECUTION_TOOL) - req.func_tool.add_tool(CREATE_SKILL_PAYLOAD_TOOL) - req.func_tool.add_tool(GET_SKILL_PAYLOAD_TOOL) - req.func_tool.add_tool(CREATE_SKILL_CANDIDATE_TOOL) - req.func_tool.add_tool(LIST_SKILL_CANDIDATES_TOOL) - req.func_tool.add_tool(EVALUATE_SKILL_CANDIDATE_TOOL) - req.func_tool.add_tool(PROMOTE_SKILL_CANDIDATE_TOOL) - req.func_tool.add_tool(LIST_SKILL_RELEASES_TOOL) - req.func_tool.add_tool(ROLLBACK_SKILL_RELEASE_TOOL) - req.func_tool.add_tool(SYNC_SKILL_RELEASE_TOOL) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(GetExecutionHistoryTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(AnnotateExecutionTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(CreateSkillPayloadTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(GetSkillPayloadTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(CreateSkillCandidateTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(ListSkillCandidatesTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(EvaluateSkillCandidateTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(PromoteSkillCandidateTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(ListSkillReleasesTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(RollbackSkillReleaseTool)) + req.func_tool.add_tool(tool_mgr.get_builtin_tool(SyncSkillReleaseTool)) req.system_prompt = f"{req.system_prompt or ''}\n{SANDBOX_MODE_PROMPT}\n" @@ -1358,9 +1364,9 @@ async def build_main_agent( _apply_llm_safety_mode(config, req) if config.computer_use_runtime == "sandbox": - _apply_sandbox_tools(config, req, req.session_id) + _apply_sandbox_tools(config, req, req.session_id, plugin_context) elif config.computer_use_runtime == "local": - _apply_local_env_tools(req) + _apply_local_env_tools(req, plugin_context) agent_runner = AgentRunner() astr_agent_ctx = AstrAgentContext( @@ -1398,7 +1404,9 @@ async def build_main_agent( ) if config.computer_use_runtime == "local": - from astrbot.core.computer.tools.fs import _normalize_umo_for_workspace + from astrbot.core.tools.computer_tools.fs import ( + _normalize_umo_for_workspace, + ) normalized_umo = _normalize_umo_for_workspace(event.unified_msg_origin) tool_prompt += ( diff --git a/astrbot/core/astr_main_agent_resources.py b/astrbot/core/astr_main_agent_resources.py index 442b91e16..821ece702 100644 --- a/astrbot/core/astr_main_agent_resources.py +++ b/astrbot/core/astr_main_agent_resources.py @@ -1,31 +1,5 @@ import base64 -from astrbot.core.computer.tools import ( - AnnotateExecutionTool, - BrowserBatchExecTool, - BrowserExecTool, - CreateSkillCandidateTool, - CreateSkillPayloadTool, - EvaluateSkillCandidateTool, - ExecuteShellTool, - FileDownloadTool, - FileEditTool, - FileReadTool, - FileUploadTool, - FileWriteTool, - GetExecutionHistoryTool, - GetSkillPayloadTool, - GrepTool, - ListSkillCandidatesTool, - ListSkillReleasesTool, - LocalPythonTool, - PromoteSkillCandidateTool, - PythonTool, - RollbackSkillReleaseTool, - RunBrowserSkillTool, - SyncSkillReleaseTool, -) - LLM_SAFETY_MODE_SYSTEM_PROMPT = """You are running in Safe Mode. Rules: @@ -134,32 +108,6 @@ BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT = ( "{background_task_result}" ) - -EXECUTE_SHELL_TOOL = ExecuteShellTool() -LOCAL_EXECUTE_SHELL_TOOL = ExecuteShellTool(is_local=True) -PYTHON_TOOL = PythonTool() -LOCAL_PYTHON_TOOL = LocalPythonTool() -FILE_UPLOAD_TOOL = FileUploadTool() -FILE_DOWNLOAD_TOOL = FileDownloadTool() -READ_FILE_TOOL = FileReadTool() -FILE_WRITE_TOOL = FileWriteTool() -FILE_EDIT_TOOL = FileEditTool() -GREP_TOOL = GrepTool() -BROWSER_EXEC_TOOL = BrowserExecTool() -BROWSER_BATCH_EXEC_TOOL = BrowserBatchExecTool() -RUN_BROWSER_SKILL_TOOL = RunBrowserSkillTool() -GET_EXECUTION_HISTORY_TOOL = GetExecutionHistoryTool() -ANNOTATE_EXECUTION_TOOL = AnnotateExecutionTool() -CREATE_SKILL_PAYLOAD_TOOL = CreateSkillPayloadTool() -GET_SKILL_PAYLOAD_TOOL = GetSkillPayloadTool() -CREATE_SKILL_CANDIDATE_TOOL = CreateSkillCandidateTool() -LIST_SKILL_CANDIDATES_TOOL = ListSkillCandidatesTool() -EVALUATE_SKILL_CANDIDATE_TOOL = EvaluateSkillCandidateTool() -PROMOTE_SKILL_CANDIDATE_TOOL = PromoteSkillCandidateTool() -LIST_SKILL_RELEASES_TOOL = ListSkillReleasesTool() -ROLLBACK_SKILL_RELEASE_TOOL = RollbackSkillReleaseTool() -SYNC_SKILL_RELEASE_TOOL = SyncSkillReleaseTool() - # we prevent astrbot from connecting to known malicious hosts # these hosts are base64 encoded BLOCKED = {"dGZid2h2d3IuY2xvdWQuc2VhbG9zLmlv", "a291cmljaGF0"} diff --git a/astrbot/core/computer/tools/__init__.py b/astrbot/core/tools/computer_tools/__init__.py similarity index 82% rename from astrbot/core/computer/tools/__init__.py rename to astrbot/core/tools/computer_tools/__init__.py index ec34388e1..a147ccbcd 100644 --- a/astrbot/core/computer/tools/__init__.py +++ b/astrbot/core/tools/computer_tools/__init__.py @@ -1,4 +1,3 @@ -from .browser import BrowserBatchExecTool, BrowserExecTool, RunBrowserSkillTool from .fs import ( FileDownloadTool, FileEditTool, @@ -6,9 +5,15 @@ from .fs import ( FileUploadTool, FileWriteTool, GrepTool, + _normalize_umo_for_workspace, ) -from .neo_skills import ( +from .permissions import check_admin_permission +from .python import LocalPythonTool, PythonTool +from .shell import ExecuteShellTool +from .shipyard_neo import ( AnnotateExecutionTool, + BrowserBatchExecTool, + BrowserExecTool, CreateSkillCandidateTool, CreateSkillPayloadTool, EvaluateSkillCandidateTool, @@ -18,33 +23,34 @@ from .neo_skills import ( ListSkillReleasesTool, PromoteSkillCandidateTool, RollbackSkillReleaseTool, + RunBrowserSkillTool, SyncSkillReleaseTool, ) -from .python import LocalPythonTool, PythonTool -from .shell import ExecuteShellTool __all__ = [ - "BrowserExecTool", - "BrowserBatchExecTool", - "RunBrowserSkillTool", - "GetExecutionHistoryTool", "AnnotateExecutionTool", - "CreateSkillPayloadTool", - "GetSkillPayloadTool", + "BrowserBatchExecTool", + "BrowserExecTool", "CreateSkillCandidateTool", - "ListSkillCandidatesTool", + "CreateSkillPayloadTool", "EvaluateSkillCandidateTool", - "PromoteSkillCandidateTool", - "ListSkillReleasesTool", - "RollbackSkillReleaseTool", - "SyncSkillReleaseTool", - "FileUploadTool", - "FileWriteTool", - "FileEditTool", - "GrepTool", - "FileReadTool", - "PythonTool", - "LocalPythonTool", "ExecuteShellTool", "FileDownloadTool", + "FileEditTool", + "FileReadTool", + "FileUploadTool", + "FileWriteTool", + "GetExecutionHistoryTool", + "GetSkillPayloadTool", + "GrepTool", + "ListSkillCandidatesTool", + "ListSkillReleasesTool", + "LocalPythonTool", + "PromoteSkillCandidateTool", + "PythonTool", + "RollbackSkillReleaseTool", + "RunBrowserSkillTool", + "SyncSkillReleaseTool", + "_normalize_umo_for_workspace", + "check_admin_permission", ] diff --git a/astrbot/core/computer/tools/fs.py b/astrbot/core/tools/computer_tools/fs.py similarity index 99% rename from astrbot/core/computer/tools/fs.py rename to astrbot/core/tools/computer_tools/fs.py index 7c9c68e40..b23296bdb 100644 --- a/astrbot/core/computer/tools/fs.py +++ b/astrbot/core/tools/computer_tools/fs.py @@ -42,6 +42,7 @@ from astrbot.api.event import MessageChain from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext +from astrbot.core.computer.computer_client import get_booter from astrbot.core.computer.file_read_utils import read_file_tool_result from astrbot.core.message.components import File from astrbot.core.utils.astrbot_path import ( @@ -50,7 +51,7 @@ from astrbot.core.utils.astrbot_path import ( get_astrbot_workspaces_path, ) -from ..computer_client import get_booter +from ..registry import builtin_tool from .permissions import check_admin_permission @@ -162,6 +163,7 @@ def _decode_escaped_text(value: str) -> str: ) +@builtin_tool @dataclass class FileReadTool(FunctionTool): name: str = "astrbot_file_read_tool" @@ -241,6 +243,7 @@ class FileReadTool(FunctionTool): return f"Error reading file: {exc}" +@builtin_tool @dataclass class FileWriteTool(FunctionTool): name: str = "astrbot_file_write_tool" @@ -307,6 +310,7 @@ class FileWriteTool(FunctionTool): return f"Error writing file: {exc}" +@builtin_tool @dataclass class FileEditTool(FunctionTool): name: str = "astrbot_file_edit_tool" @@ -392,6 +396,7 @@ class FileEditTool(FunctionTool): return f"Error editing file: {exc}" +@builtin_tool @dataclass class GrepTool(FunctionTool): name: str = "astrbot_grep_tool" @@ -597,6 +602,7 @@ class GrepTool(FunctionTool): return f"Error searching files: {exc}" +@builtin_tool @dataclass class FileUploadTool(FunctionTool): name: str = "astrbot_upload_file" @@ -662,6 +668,7 @@ class FileUploadTool(FunctionTool): return f"Error uploading file: {str(e)}" +@builtin_tool @dataclass class FileDownloadTool(FunctionTool): name: str = "astrbot_download_file" diff --git a/astrbot/core/computer/tools/permissions.py b/astrbot/core/tools/computer_tools/permissions.py similarity index 100% rename from astrbot/core/computer/tools/permissions.py rename to astrbot/core/tools/computer_tools/permissions.py diff --git a/astrbot/core/computer/tools/python.py b/astrbot/core/tools/computer_tools/python.py similarity index 96% rename from astrbot/core/computer/tools/python.py rename to astrbot/core/tools/computer_tools/python.py index bf9aaa14e..148beb7c5 100644 --- a/astrbot/core/computer/tools/python.py +++ b/astrbot/core/tools/computer_tools/python.py @@ -8,9 +8,11 @@ from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent from astrbot.core.computer.computer_client import get_booter, get_local_booter -from astrbot.core.computer.tools.permissions import check_admin_permission from astrbot.core.message.message_event_result import MessageChain +from ..registry import builtin_tool +from .permissions import check_admin_permission + _OS_NAME = platform.system() param_schema = { @@ -61,6 +63,7 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult return resp +@builtin_tool @dataclass class PythonTool(FunctionTool): name: str = "astrbot_execute_ipython" @@ -83,6 +86,7 @@ class PythonTool(FunctionTool): return f"Error executing code: {str(e)}" +@builtin_tool @dataclass class LocalPythonTool(FunctionTool): name: str = "astrbot_execute_python" diff --git a/astrbot/core/computer/tools/shell.py b/astrbot/core/tools/computer_tools/shell.py similarity index 86% rename from astrbot/core/computer/tools/shell.py rename to astrbot/core/tools/computer_tools/shell.py index b5009d30f..0c23dae43 100644 --- a/astrbot/core/computer/tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -5,11 +5,13 @@ from astrbot.api import FunctionTool from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext +from astrbot.core.computer.computer_client import get_booter -from ..computer_client import get_booter, get_local_booter +from ..registry import builtin_tool from .permissions import check_admin_permission +@builtin_tool @dataclass class ExecuteShellTool(FunctionTool): name: str = "astrbot_execute_shell" @@ -38,8 +40,6 @@ class ExecuteShellTool(FunctionTool): } ) - is_local: bool = False - async def call( self, context: ContextWrapper[AstrAgentContext], @@ -50,13 +50,10 @@ class ExecuteShellTool(FunctionTool): if permission_error := check_admin_permission(context, "Shell execution"): return permission_error - if self.is_local: - sb = get_local_booter() - else: - sb = await get_booter( - context.context.context, - context.context.event.unified_msg_origin, - ) + sb = await get_booter( + context.context.context, + context.context.event.unified_msg_origin, + ) try: result = await sb.shell.exec(command, background=background, env=env) return json.dumps(result) diff --git a/astrbot/core/tools/computer_tools/shipyard_neo/__init__.py b/astrbot/core/tools/computer_tools/shipyard_neo/__init__.py new file mode 100644 index 000000000..9228c8635 --- /dev/null +++ b/astrbot/core/tools/computer_tools/shipyard_neo/__init__.py @@ -0,0 +1,31 @@ +from .browser import BrowserBatchExecTool, BrowserExecTool, RunBrowserSkillTool +from .neo_skills import ( + AnnotateExecutionTool, + CreateSkillCandidateTool, + CreateSkillPayloadTool, + EvaluateSkillCandidateTool, + GetExecutionHistoryTool, + GetSkillPayloadTool, + ListSkillCandidatesTool, + ListSkillReleasesTool, + PromoteSkillCandidateTool, + RollbackSkillReleaseTool, + SyncSkillReleaseTool, +) + +__all__ = [ + "AnnotateExecutionTool", + "BrowserBatchExecTool", + "BrowserExecTool", + "CreateSkillCandidateTool", + "CreateSkillPayloadTool", + "EvaluateSkillCandidateTool", + "GetExecutionHistoryTool", + "GetSkillPayloadTool", + "ListSkillCandidatesTool", + "ListSkillReleasesTool", + "PromoteSkillCandidateTool", + "RollbackSkillReleaseTool", + "RunBrowserSkillTool", + "SyncSkillReleaseTool", +] diff --git a/astrbot/core/computer/tools/browser.py b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py similarity index 96% rename from astrbot/core/computer/tools/browser.py rename to astrbot/core/tools/computer_tools/shipyard_neo/browser.py index cd8484acb..f3cfb3449 100644 --- a/astrbot/core/computer/tools/browser.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py @@ -6,9 +6,9 @@ from astrbot.api import FunctionTool from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext - -from ..computer_client import get_booter -from .permissions import check_admin_permission +from astrbot.core.computer.computer_client import get_booter +from astrbot.core.tools.computer_tools.permissions import check_admin_permission +from astrbot.core.tools.registry import builtin_tool def _to_json(data: Any) -> str: @@ -29,6 +29,7 @@ async def _get_browser_component(context: ContextWrapper[AstrAgentContext]) -> A return browser +@builtin_tool @dataclass class BrowserExecTool(FunctionTool): name: str = "astrbot_execute_browser" @@ -86,6 +87,7 @@ class BrowserExecTool(FunctionTool): return f"Error executing browser command: {str(e)}" +@builtin_tool @dataclass class BrowserBatchExecTool(FunctionTool): name: str = "astrbot_execute_browser_batch" @@ -150,6 +152,7 @@ class BrowserBatchExecTool(FunctionTool): return f"Error executing browser batch command: {str(e)}" +@builtin_tool @dataclass class RunBrowserSkillTool(FunctionTool): name: str = "astrbot_run_browser_skill" diff --git a/astrbot/core/computer/tools/neo_skills.py b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py similarity index 98% rename from astrbot/core/computer/tools/neo_skills.py rename to astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py index 327f14472..03ba1c4a4 100644 --- a/astrbot/core/computer/tools/neo_skills.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py @@ -7,10 +7,10 @@ from astrbot.api import FunctionTool from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext +from astrbot.core.computer.computer_client import get_booter from astrbot.core.skills.neo_skill_sync import NeoSkillSyncManager - -from ..computer_client import get_booter -from .permissions import check_admin_permission +from astrbot.core.tools.computer_tools.permissions import check_admin_permission +from astrbot.core.tools.registry import builtin_tool def _to_jsonable(model_like: Any) -> Any: @@ -64,6 +64,7 @@ class NeoSkillToolBase(FunctionTool): return f"{self.error_prefix} {error_action}: {str(e)}" +@builtin_tool @dataclass class GetExecutionHistoryTool(NeoSkillToolBase): name: str = "astrbot_get_execution_history" @@ -110,6 +111,7 @@ class GetExecutionHistoryTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class AnnotateExecutionTool(NeoSkillToolBase): name: str = "astrbot_annotate_execution" @@ -147,6 +149,7 @@ class AnnotateExecutionTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class CreateSkillPayloadTool(NeoSkillToolBase): name: str = "astrbot_create_skill_payload" @@ -194,6 +197,7 @@ class CreateSkillPayloadTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class GetSkillPayloadTool(NeoSkillToolBase): name: str = "astrbot_get_skill_payload" @@ -220,6 +224,7 @@ class GetSkillPayloadTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class CreateSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_create_skill_candidate" @@ -273,6 +278,7 @@ class CreateSkillCandidateTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class ListSkillCandidatesTool(NeoSkillToolBase): name: str = "astrbot_list_skill_candidates" @@ -310,6 +316,7 @@ class ListSkillCandidatesTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class EvaluateSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_evaluate_skill_candidate" @@ -350,6 +357,7 @@ class EvaluateSkillCandidateTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class PromoteSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_promote_skill_candidate" @@ -420,6 +428,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase): return f"Error promoting skill candidate: {str(e)}" +@builtin_tool @dataclass class ListSkillReleasesTool(NeoSkillToolBase): name: str = "astrbot_list_skill_releases" @@ -460,6 +469,7 @@ class ListSkillReleasesTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class RollbackSkillReleaseTool(NeoSkillToolBase): name: str = "astrbot_rollback_skill_release" @@ -486,6 +496,7 @@ class RollbackSkillReleaseTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class SyncSkillReleaseTool(NeoSkillToolBase): name: str = "astrbot_sync_skill_release" diff --git a/astrbot/core/tools/registry.py b/astrbot/core/tools/registry.py index eaca4af14..4a1fa8d66 100644 --- a/astrbot/core/tools/registry.py +++ b/astrbot/core/tools/registry.py @@ -8,6 +8,7 @@ from astrbot.core.agent.tool import FunctionTool TFunctionTool = TypeVar("TFunctionTool", bound=type[FunctionTool]) _BUILTIN_TOOL_MODULES = ( + "astrbot.core.tools.computer_tools", "astrbot.core.tools.cron_tools", "astrbot.core.tools.knowledge_base_tools", "astrbot.core.tools.message_tools", diff --git a/tests/test_computer_fs_tools.py b/tests/test_computer_fs_tools.py index a75bfdaad..6e718dfbd 100644 --- a/tests/test_computer_fs_tools.py +++ b/tests/test_computer_fs_tools.py @@ -11,7 +11,7 @@ from PIL import Image from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.computer import file_read_utils from astrbot.core.computer.booters.local import LocalBooter -from astrbot.core.computer.tools import fs as fs_tools +from astrbot.core.tools.computer_tools import fs as fs_tools def _make_context( diff --git a/tests/test_computer_tool_permissions.py b/tests/test_computer_tool_permissions.py index 669cd6b8a..07f7983da 100644 --- a/tests/test_computer_tool_permissions.py +++ b/tests/test_computer_tool_permissions.py @@ -4,8 +4,10 @@ from types import SimpleNamespace import pytest from astrbot.core.agent.run_context import ContextWrapper -from astrbot.core.computer.tools.browser import BrowserExecTool -from astrbot.core.computer.tools.neo_skills import GetExecutionHistoryTool +from astrbot.core.tools.computer_tools.shipyard_neo.browser import BrowserExecTool +from astrbot.core.tools.computer_tools.shipyard_neo.neo_skills import ( + GetExecutionHistoryTool, +) class _FakeBrowser: @@ -49,7 +51,7 @@ async def test_browser_tool_allows_non_admin_when_admin_requirement_disabled( return SimpleNamespace(browser=_FakeBrowser()) monkeypatch.setattr( - "astrbot.core.computer.tools.browser.get_booter", + "astrbot.core.tools.computer_tools.shipyard_neo.browser.get_booter", _fake_get_booter, ) @@ -72,7 +74,7 @@ async def test_neo_skill_tool_allows_non_admin_when_admin_requirement_disabled( ) monkeypatch.setattr( - "astrbot.core.computer.tools.neo_skills.get_booter", + "astrbot.core.tools.computer_tools.shipyard_neo.neo_skills.get_booter", _fake_get_booter, ) diff --git a/tests/test_neo_skill_tools.py b/tests/test_neo_skill_tools.py index 218ada57b..076da0094 100644 --- a/tests/test_neo_skill_tools.py +++ b/tests/test_neo_skill_tools.py @@ -4,7 +4,9 @@ import asyncio from types import SimpleNamespace from astrbot.core.agent.run_context import ContextWrapper -from astrbot.core.computer.tools.neo_skills import PromoteSkillCandidateTool +from astrbot.core.tools.computer_tools.shipyard_neo.neo_skills import ( + PromoteSkillCandidateTool, +) class _FakeSkills: @@ -46,11 +48,11 @@ def test_promote_stable_sync_failure_auto_rolls_back(monkeypatch): raise ValueError("sync failed") monkeypatch.setattr( - "astrbot.core.computer.tools.neo_skills.get_booter", + "astrbot.core.tools.computer_tools.shipyard_neo.neo_skills.get_booter", _fake_get_booter, ) monkeypatch.setattr( - "astrbot.core.computer.tools.neo_skills.NeoSkillSyncManager.sync_release", + "astrbot.core.tools.computer_tools.shipyard_neo.neo_skills.NeoSkillSyncManager.sync_release", _fake_sync_release, ) diff --git a/tests/unit/test_astr_main_agent.py b/tests/unit/test_astr_main_agent.py index 4619bb065..631cc2832 100644 --- a/tests/unit/test_astr_main_agent.py +++ b/tests/unit/test_astr_main_agent.py @@ -40,7 +40,9 @@ def mock_context(): return_value=(None, None, None, False) ) ctx.persona_manager.get_persona_v3_by_id = MagicMock(return_value=None) - ctx.get_llm_tool_manager.return_value = MagicMock() + tool_mgr = MagicMock() + tool_mgr.get_builtin_tool.side_effect = lambda cls, **kwargs: cls(**kwargs) + ctx.get_llm_tool_manager.return_value = tool_mgr ctx.subagent_orchestrator = None return ctx @@ -621,9 +623,10 @@ class TestEnsurePersonaAndSkills: tmgr = mock_context.get_llm_tool_manager.return_value tmgr.func_list = [tool_a, tool_b] tmgr.get_full_tool_set.return_value = ToolSet([tool_a, tool_b]) - tmgr.get_func.side_effect = lambda name: {"tool_a": tool_a, "tool_b": tool_b}.get( - name - ) + tmgr.get_func.side_effect = lambda name: { + "tool_a": tool_a, + "tool_b": tool_b, + }.get(name) handoff = MagicMock() handoff.name = "transfer_to_planner" @@ -1488,7 +1491,7 @@ class TestApplyLlmSafetyMode: class TestApplySandboxTools: """Tests for _apply_sandbox_tools function.""" - def test_apply_sandbox_tools_creates_toolset_if_none(self): + def test_apply_sandbox_tools_creates_toolset_if_none(self, mock_context): """Test that ToolSet is created when func_tool is None.""" module = ama config = module.MainAgentBuildConfig( @@ -1498,12 +1501,12 @@ class TestApplySandboxTools: ) req = ProviderRequest(prompt="Test", func_tool=None) - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) assert req.func_tool is not None assert isinstance(req.func_tool, ToolSet) - def test_apply_sandbox_tools_adds_required_tools(self): + def test_apply_sandbox_tools_adds_required_tools(self, mock_context): """Test that all required sandbox tools are added.""" module = ama config = module.MainAgentBuildConfig( @@ -1513,7 +1516,7 @@ class TestApplySandboxTools: ) req = ProviderRequest(prompt="Test", func_tool=None) - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) tool_names = req.func_tool.names() assert "astrbot_execute_shell" in tool_names @@ -1521,7 +1524,7 @@ class TestApplySandboxTools: assert "astrbot_upload_file" in tool_names assert "astrbot_download_file" in tool_names - def test_apply_sandbox_tools_adds_sandbox_prompt(self): + def test_apply_sandbox_tools_adds_sandbox_prompt(self, mock_context): """Test that sandbox mode prompt is added to system_prompt.""" module = ama config = module.MainAgentBuildConfig( @@ -1531,11 +1534,11 @@ class TestApplySandboxTools: ) req = ProviderRequest(prompt="Test", system_prompt="Original prompt") - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) assert "sandboxed environment" in req.system_prompt - def test_apply_sandbox_tools_with_shipyard_booter(self, monkeypatch): + def test_apply_sandbox_tools_with_shipyard_booter(self, monkeypatch, mock_context): """Test sandbox tools with shipyard booter configuration.""" module = ama config = module.MainAgentBuildConfig( @@ -1552,12 +1555,12 @@ class TestApplySandboxTools: monkeypatch.delenv("SHIPYARD_ENDPOINT", raising=False) monkeypatch.delenv("SHIPYARD_ACCESS_TOKEN", raising=False) - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) assert os.environ.get("SHIPYARD_ENDPOINT") == "https://shipyard.example.com" assert os.environ.get("SHIPYARD_ACCESS_TOKEN") == "test-token" - def test_apply_sandbox_tools_shipyard_missing_endpoint(self): + def test_apply_sandbox_tools_shipyard_missing_endpoint(self, mock_context): """Test that shipyard config is skipped when endpoint is missing.""" module = ama config = module.MainAgentBuildConfig( @@ -1572,7 +1575,7 @@ class TestApplySandboxTools: req = ProviderRequest(prompt="Test", func_tool=None) with patch("astrbot.core.astr_main_agent.logger") as mock_logger: - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) mock_logger.error.assert_called_once() assert ( @@ -1580,7 +1583,7 @@ class TestApplySandboxTools: in mock_logger.error.call_args[0][0] ) - def test_apply_sandbox_tools_shipyard_missing_access_token(self): + def test_apply_sandbox_tools_shipyard_missing_access_token(self, mock_context): """Test that shipyard config is skipped when access token is missing.""" module = ama config = module.MainAgentBuildConfig( @@ -1595,11 +1598,11 @@ class TestApplySandboxTools: req = ProviderRequest(prompt="Test", func_tool=None) with patch("astrbot.core.astr_main_agent.logger") as mock_logger: - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) mock_logger.error.assert_called_once() - def test_apply_sandbox_tools_preserves_existing_toolset(self): + def test_apply_sandbox_tools_preserves_existing_toolset(self, mock_context): """Test that existing tools are preserved when adding sandbox tools.""" module = ama config = module.MainAgentBuildConfig( @@ -1613,12 +1616,12 @@ class TestApplySandboxTools: existing_toolset.add_tool(existing_tool) req = ProviderRequest(prompt="Test", func_tool=existing_toolset) - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) assert "existing_tool" in req.func_tool.names() assert "astrbot_execute_shell" in req.func_tool.names() - def test_apply_sandbox_tools_appends_to_existing_system_prompt(self): + def test_apply_sandbox_tools_appends_to_existing_system_prompt(self, mock_context): """Test that sandbox prompt is appended to existing system prompt.""" module = ama config = module.MainAgentBuildConfig( @@ -1628,12 +1631,12 @@ class TestApplySandboxTools: ) req = ProviderRequest(prompt="Test", system_prompt="Base prompt") - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) assert req.system_prompt.startswith("Base prompt") assert "sandboxed environment" in req.system_prompt - def test_apply_sandbox_tools_with_none_system_prompt(self): + def test_apply_sandbox_tools_with_none_system_prompt(self, mock_context): """Test that sandbox prompt is applied when system_prompt is None.""" module = ama config = module.MainAgentBuildConfig( @@ -1643,7 +1646,7 @@ class TestApplySandboxTools: ) req = ProviderRequest(prompt="Test", system_prompt=None) - module._apply_sandbox_tools(config, req, "session-123") + module._apply_sandbox_tools(config, req, "session-123", mock_context) assert isinstance(req.system_prompt, str) assert "sandboxed environment" in req.system_prompt diff --git a/tests/unit/test_func_tool_manager.py b/tests/unit/test_func_tool_manager.py index 3c03e618f..908810cdb 100644 --- a/tests/unit/test_func_tool_manager.py +++ b/tests/unit/test_func_tool_manager.py @@ -1,5 +1,6 @@ from astrbot.core import sp from astrbot.core.provider.func_tool_manager import FunctionToolManager +from astrbot.core.tools.computer_tools.shell import ExecuteShellTool from astrbot.core.tools.message_tools import SendMessageToUserTool @@ -12,3 +13,28 @@ def test_get_builtin_tool_by_class_returns_cached_instance(): assert tool_by_class is tool_by_name assert manager.get_func("send_message_to_user") is tool_by_class assert tool_by_class.name == "send_message_to_user" + + +def test_builtin_tool_ignores_inactivated_llm_tools(): + manager = FunctionToolManager() + sp.put( + "inactivated_llm_tools", + ["send_message_to_user"], + scope="global", + scope_id="global", + ) + + try: + tool = manager.get_builtin_tool(SendMessageToUserTool) + assert tool.active is True + finally: + sp.put("inactivated_llm_tools", [], scope="global", scope_id="global") + + +def test_computer_tools_are_registered_as_builtin_tools(): + manager = FunctionToolManager() + + tool = manager.get_builtin_tool(ExecuteShellTool) + + assert tool.name == "astrbot_execute_shell" + assert manager.is_builtin_tool("astrbot_execute_shell") is True diff --git a/tests/unit/test_python_tools.py b/tests/unit/test_python_tools.py index 2aae65d2a..deabc0db3 100644 --- a/tests/unit/test_python_tools.py +++ b/tests/unit/test_python_tools.py @@ -1,5 +1,7 @@ import platform -from astrbot.core.computer.tools.python import PythonTool, LocalPythonTool + +from astrbot.core.tools.computer_tools.python import LocalPythonTool, PythonTool + def test_python_tool_description_contains_os(): """测试 PythonTool 的描述中是否包含当前操作系统信息""" @@ -8,6 +10,7 @@ def test_python_tool_description_contains_os(): assert current_os in tool.description assert "IPython" in tool.description + def test_local_python_tool_description_contains_os(): """测试 LocalPythonTool 的描述中是否包含当前操作系统信息和兼容性提示""" tool = LocalPythonTool()