diff --git a/astrbot/__main__.py b/astrbot/__main__.py index 4649181f2..9b41287b5 100644 --- a/astrbot/__main__.py +++ b/astrbot/__main__.py @@ -25,7 +25,6 @@ from astrbot.core.utils.io import ( get_dashboard_version, ) - # 将父目录添加到 sys.path sys.path.append(Path(__file__).parent.as_posix()) diff --git a/astrbot/_internal/tools/base.py b/astrbot/_internal/tools/base.py index b88501880..a2f019adb 100644 --- a/astrbot/_internal/tools/base.py +++ b/astrbot/_internal/tools/base.py @@ -96,6 +96,9 @@ class ToolSet: def add(self, tool: ToolSchema) -> None: """Add a tool to the set.""" + existing = self._tools.get(tool.name) + if existing is not None and not getattr(tool, "active", True) and getattr(existing, "active", True): + return # Don't overwrite active with inactive self._tools[tool.name] = tool def add_tool(self, tool: ToolSchema) -> None: diff --git a/astrbot/_internal/tools/registry.py b/astrbot/_internal/tools/registry.py index e471ce6f9..f97917bb0 100644 --- a/astrbot/_internal/tools/registry.py +++ b/astrbot/_internal/tools/registry.py @@ -20,11 +20,12 @@ from typing import Any import aiohttp from astrbot import logger -from astrbot._internal.tools.base import FunctionTool, ToolSchema, ToolSet # Re-export MCP-related constants and exceptions from protocols from astrbot._internal.protocols._mcp import ( DEFAULT_MCP_CONFIG as _DEFAULT_MCP_CONFIG, +) +from astrbot._internal.protocols._mcp import ( MCPAllServicesFailedError, MCPInitError, MCPInitSummary, @@ -33,10 +34,10 @@ from astrbot._internal.protocols._mcp import ( ) from astrbot._internal.protocols._mcp.client import ( McpClient, - _prepare_config, _quick_test_mcp_connection, ) from astrbot._internal.protocols._mcp.tool import MCPTool +from astrbot._internal.tools.base import FunctionTool, ToolSchema, ToolSet __all__ = [ "DEFAULT_MCP_CONFIG", @@ -146,7 +147,6 @@ def _get_astrbot_data_path() -> str: # Builtin tools stubs - these functions don't exist in the new architecture def _ensure_builtin_tools_loaded() -> None: """No-op in new architecture.""" - pass def _get_builtin_tool_class(name: str) -> type[FunctionTool] | None: @@ -257,6 +257,7 @@ class FunctionToolManager: func_args: Function arguments list, format: [{"type": "string", "name": "arg_name", "description": "arg_description"}, ...] desc: Function description handler: Handler function + """ # Check if the tool has been added before self.remove_func(name) @@ -359,7 +360,7 @@ class FunctionToolManager: Therefore, a later loaded inactive tool will not override an already active tool; MCP tools can still override disabled builtin tools when needed. """ - tool_set = ToolSet() + tool_set = ToolSet(namespace="default") for tool in self.func_list: tool_set.add_tool(tool) return tool_set @@ -383,7 +384,7 @@ class FunctionToolManager: if "command" in cfg: cmd = cfg["command"] executable = str( - cmd[0] if isinstance(cmd, (list, tuple)) and cmd else cmd + cmd[0] if isinstance(cmd, (list, tuple)) and cmd else cmd, ) args_val = cfg.get("args", []) args_count = ( @@ -714,6 +715,7 @@ class FunctionToolManager: Returns: Tuple of (success, message) + """ success, error_msg = await _quick_test_mcp_connection(config) if not success: @@ -751,6 +753,7 @@ class FunctionToolManager: Raises: MCPInitTimeoutError: If initialization does not complete within timeout. Exception: If there is an error during initialization. + """ if timeout is None: timeout_value = self._enable_timeout_default @@ -781,6 +784,7 @@ class FunctionToolManager: Raises: MCPShutdownTimeoutError: If shutdown does not complete within timeout. Only raised when disabling a specific server (name is not None). + """ if name: async with self._runtime_lock: @@ -838,6 +842,7 @@ class FunctionToolManager: Returns: False if not found + """ func_tool = self.get_func(name) if func_tool is not None: @@ -868,6 +873,7 @@ class FunctionToolManager: Args: name: Tool name star_map: Optional star_map for plugin dependency checking (ignored in new architecture) + """ func_tool = self.get_func(name) if func_tool is not None: diff --git a/astrbot/builtin_stars/builtin_commands/commands/__init__.py b/astrbot/builtin_stars/builtin_commands/commands/__init__.py index 552ac4a8a..c44c55e84 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/__init__.py +++ b/astrbot/builtin_stars/builtin_commands/commands/__init__.py @@ -10,6 +10,6 @@ __all__ = [ "AdminCommands", "ConversationCommands", "HelpCommand", - "SetUnsetCommands", "SIDCommand", + "SetUnsetCommands", ] diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py index f1d879b86..afdedb99d 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -12,8 +12,6 @@ from . import __version__ from .commands import bk, conf, init, plug, run, uninstall from .i18n import t -from .banner import logo_tmpl - def print_version_detail() -> None: """Print detailed version info (same for --version and version command)""" @@ -70,7 +68,6 @@ def cli() -> None: """Astrbot Agentic IM Chatbot infrastructure that integrates lots of IM platforms, LLMs, plugins and AI feature, and can be your openclaw alternative. ✨ """ - pass @click.command() diff --git a/astrbot/cli/banner.py b/astrbot/cli/banner.py index ea75ecf3a..3dc4ebb67 100644 --- a/astrbot/cli/banner.py +++ b/astrbot/cli/banner.py @@ -2,7 +2,6 @@ import sys - logo_tmpl = r""" ___ _______.___________..______ .______ ______ .___________. / \ / | || _ \ | _ \ / __ \ | | diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 3a045a184..4fbd0fded 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -397,7 +397,7 @@ def run( click.echo() else: dashboard_url = f"http://{host or 'localhost'}:{port or '6185'}" - click.echo(f"[模式] 完整模式(含本地 Dashboard)") + click.echo("[模式] 完整模式(含本地 Dashboard)") click.echo() click.echo(f"[Dashboard] 请访问: {dashboard_url}") click.echo() diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 29a4fefaf..5c3bdce78 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -645,7 +645,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): try: await event.send( MessageChain( - chain=res.chain, type="tool_direct_result" + chain=res.chain, type="tool_direct_result", ), ) except Exception as e: diff --git a/astrbot/core/computer/booters/shipyard_search_file_util.py b/astrbot/core/computer/booters/shipyard_search_file_util.py index 1227244de..6e51d3294 100644 --- a/astrbot/core/computer/booters/shipyard_search_file_util.py +++ b/astrbot/core/computer/booters/shipyard_search_file_util.py @@ -92,7 +92,7 @@ def build_search_command( glob=glob, after_context=after_context, before_context=before_context, - ) + ), ) grep_command = _quote_command( _build_grep_command( @@ -101,7 +101,7 @@ def build_search_command( glob=glob, after_context=after_context, before_context=before_context, - ) + ), ) return ( "if command -v rg >/dev/null 2>&1; then " diff --git a/astrbot/core/computer/computer_tool_provider.py b/astrbot/core/computer/computer_tool_provider.py index b558105e2..a53c5dce3 100644 --- a/astrbot/core/computer/computer_tool_provider.py +++ b/astrbot/core/computer/computer_tool_provider.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: def _get_local_tools() -> list[ToolSchema]: from astrbot.core.computer.tools import ExecuteShellTool, LocalPythonTool - shell = ExecuteShellTool(is_local=True) + shell = ExecuteShellTool() python = LocalPythonTool() return [shell, python] # type: ignore[return-value] diff --git a/astrbot/core/computer/file_read_utils.py b/astrbot/core/computer/file_read_utils.py index 0f4d0811c..d5a8cf72a 100644 --- a/astrbot/core/computer/file_read_utils.py +++ b/astrbot/core/computer/file_read_utils.py @@ -310,7 +310,7 @@ async def _compress_image_bytes_to_base64(data: bytes) -> dict[str, str | int]: IMAGE_COMPRESS_DEFAULT_MAX_SIZE, IMAGE_COMPRESS_DEFAULT_QUALITY, IMAGE_COMPRESS_DEFAULT_OPTIMIZE, - ) + ), ) try: compressed_bytes = compressed_path.read_bytes() @@ -450,7 +450,7 @@ def _validate_text_output(content: str) -> str | None: ) content_tokens = _TOKEN_COUNTER.count_tokens( - [Message(role="user", content=content)] + [Message(role="user", content=content)], ) if content_tokens > _MAX_FILE_READ_TOKENS: return ( @@ -668,10 +668,10 @@ async def read_file_tool_result( type="image", data=base64_data, mimeType=str( - compressed_payload.get("mime_type", "") or "image/jpeg" + compressed_payload.get("mime_type", "") or "image/jpeg", ), - ) - ] + ), + ], ) if offset is None and limit is None: diff --git a/astrbot/core/computer/tools/__init__.py b/astrbot/core/computer/tools/__init__.py new file mode 100644 index 000000000..6e1b4b28e --- /dev/null +++ b/astrbot/core/computer/tools/__init__.py @@ -0,0 +1,55 @@ +"""Stub module for backward compatibility. + +The canonical computer tools are now in: +- astrbot.core.tools.computer_tools.shell (ExecuteShellTool) +- astrbot.core.tools.computer_tools.python (PythonTool, LocalPythonTool) +- astrbot.core.tools.computer_tools.fs (FileUploadTool, FileDownloadTool) +- astrbot.core.tools.computer_tools.shipyard_neo.browser (BrowserExecTool, etc.) +- astrbot.core.tools.computer_tools.shipyard_neo.neo_skills (Neo skill tools) + +This module exists only for any legacy imports that may still reference it. +""" + +from astrbot.core.tools.computer_tools.fs import FileDownloadTool, FileUploadTool +from astrbot.core.tools.computer_tools.python import LocalPythonTool, PythonTool +from astrbot.core.tools.computer_tools.shell import ExecuteShellTool +from astrbot.core.tools.computer_tools.shipyard_neo.browser import ( + BrowserBatchExecTool, + BrowserExecTool, + RunBrowserSkillTool, +) +from astrbot.core.tools.computer_tools.shipyard_neo.neo_skills import ( + AnnotateExecutionTool, + CreateSkillCandidateTool, + CreateSkillPayloadTool, + EvaluateSkillCandidateTool, + GetExecutionHistoryTool, + GetSkillPayloadTool, + ListSkillCandidatesTool, + ListSkillReleasesTool, + PromoteSkillCandidateTool, + RollbackSkillReleaseTool, + SyncSkillReleaseTool, +) + +__all__ = [ + "AnnotateExecutionTool", + "BrowserBatchExecTool", + "BrowserExecTool", + "CreateSkillCandidateTool", + "CreateSkillPayloadTool", + "EvaluateSkillCandidateTool", + "ExecuteShellTool", + "FileDownloadTool", + "FileUploadTool", + "GetExecutionHistoryTool", + "GetSkillPayloadTool", + "ListSkillCandidatesTool", + "ListSkillReleasesTool", + "LocalPythonTool", + "PromoteSkillCandidateTool", + "PythonTool", + "RollbackSkillReleaseTool", + "RunBrowserSkillTool", + "SyncSkillReleaseTool", +] diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index 6eff5061a..2e2eed682 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -1566,7 +1566,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: result = await session.execute( select(ChatUIProject).where( - col(ChatUIProject.project_id) == project_id + col(ChatUIProject.project_id) == project_id, ), ) return result.scalar_one_or_none() diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index d3651f78f..107282b3b 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -104,6 +104,44 @@ class RespondStage(Stage): # random return random.uniform(self.interval[0], self.interval[1]) + def _has_meaningful_content(self, comp: BaseMessageComponent) -> bool: + """Check if a component has meaningful content.""" + from astrbot.core.message.components import ( + At, + Face, + File, + Forward, + Image, + Plain, + Poke, + Record, + Reply, + Video, + ) + + if isinstance(comp, Plain): + return bool(comp.text and comp.text.strip()) + if isinstance(comp, Image): + return bool(comp.url or comp.file_id) + if isinstance(comp, Face): + return comp.id is not None + if isinstance(comp, Record): + return bool(comp.url or comp.file_id) + if isinstance(comp, Video): + return bool(comp.url or comp.file_id) + if isinstance(comp, At): + return comp.qq is not None + if isinstance(comp, Reply): + return comp.id is not None + if isinstance(comp, Poke): + return comp.target_id() is not None + if isinstance(comp, Forward): + return bool(comp.id) + if isinstance(comp, File): + return bool(comp.name) + # Default: treat as meaningful if it's not an empty container + return True + async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]) -> bool: """检查消息链是否为空 diff --git a/astrbot/core/platform/sources/discord/client.py b/astrbot/core/platform/sources/discord/client.py index 295aaacc7..b603ee545 100644 --- a/astrbot/core/platform/sources/discord/client.py +++ b/astrbot/core/platform/sources/discord/client.py @@ -16,7 +16,7 @@ class DiscordBotClient(discord.Bot): """Discord客户端封装""" def __init__( - self, token: str, proxy: str | None = None, allow_bot_messages: bool = False + self, token: str, proxy: str | None = None, allow_bot_messages: bool = False, ) -> None: self.token = token self.proxy = proxy diff --git a/astrbot/core/platform/sources/kook/kook_client.py b/astrbot/core/platform/sources/kook/kook_client.py index dae4c8e75..da6f3e3b6 100644 --- a/astrbot/core/platform/sources/kook/kook_client.py +++ b/astrbot/core/platform/sources/kook/kook_client.py @@ -124,7 +124,7 @@ class KookClient: gateway_url: str = resp_content.data.url logger.info( - f"[KOOK] 获取gateway成功: {gateway_url.split('?', maxsplit=1)[0]}" + f"[KOOK] 获取gateway成功: {gateway_url.split('?', maxsplit=1)[0]}", ) return gateway_url diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index d5bfca278..20607465e 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -42,28 +42,28 @@ else: @register_platform_adapter("telegram", "telegram 适配器") class TelegramPlatformAdapter(Platform): def __init__( - self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue + self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue, ) -> None: super().__init__(platform_config, event_queue) self.settings = platform_settings self.client_self_id = uuid.uuid4().hex[:8] self.sdk_plugin_bridge = None base_url = self.config.get( - "telegram_api_base_url", "https://api.telegram.org/bot" + "telegram_api_base_url", "https://api.telegram.org/bot", ) if not base_url: base_url = "https://api.telegram.org/bot" file_base_url = self.config.get( - "telegram_file_base_url", "https://api.telegram.org/file/bot" + "telegram_file_base_url", "https://api.telegram.org/file/bot", ) if not file_base_url: file_base_url = "https://api.telegram.org/file/bot" self.base_url = base_url self.enable_command_register = self.config.get( - "telegram_command_register", True + "telegram_command_register", True, ) self.enable_command_refresh = self.config.get( - "telegram_command_auto_refresh", True + "telegram_command_auto_refresh", True, ) self.last_command_hash = None self.application = ( @@ -74,7 +74,7 @@ class TelegramPlatformAdapter(Platform): .build() ) message_handler = TelegramMessageHandler( - filters=filters.ALL, callback=self.message_handler + filters=filters.ALL, callback=self.message_handler, ) self.application.add_handler(message_handler) self.client = self.application.bot @@ -100,16 +100,16 @@ class TelegramPlatformAdapter(Platform): self.media_group_cache: dict[str, dict] = {} self.media_group_timeout = self.config.get("telegram_media_group_timeout", 2.5) self.media_group_max_wait = self.config.get( - "telegram_media_group_max_wait", 10.0 + "telegram_media_group_max_wait", 10.0, ) @override async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain + self, session: MessageSesion, message_chain: MessageChain, ) -> None: from_username = session.session_id await TelegramPlatformEvent.send_with_client( - self.client, message_chain, from_username + self.client, message_chain, from_username, ) await super().send_by_session(session, message_chain) @@ -140,7 +140,7 @@ class TelegramPlatformAdapter(Platform): try: logger.info("Starting Telegram polling...") await self.application.updater.start_polling( - error_callback=self._on_polling_error + error_callback=self._on_polling_error, ) logger.info("Telegram Platform Adapter is running.") _termination_event = asyncio.Event() @@ -149,18 +149,18 @@ class TelegramPlatformAdapter(Platform): await _termination_event.wait() if not self._terminating: logger.warning( - f"Telegram polling loop exited unexpectedly, retrying in {self._polling_restart_delay}s." + f"Telegram polling loop exited unexpectedly, retrying in {self._polling_restart_delay}s.", ) except asyncio.CancelledError: raise except (Forbidden, InvalidToken) as e: logger.error( - f"Telegram token is invalid or unauthorized: {e}. Polling stopped." + f"Telegram token is invalid or unauthorized: {e}. Polling stopped.", ) break except Exception as e: logger.exception( - f"Telegram polling crashed with exception: {type(e).__name__}: {e!s}. Retrying in {self._polling_restart_delay}s." + f"Telegram polling crashed with exception: {type(e).__name__}: {e!s}. Retrying in {self._polling_restart_delay}s.", ) if not self._terminating: await asyncio.sleep(self._polling_restart_delay) @@ -177,7 +177,7 @@ class TelegramPlatformAdapter(Platform): commands = self.collect_commands() if commands: current_hash = hash( - tuple((cmd.command, cmd.description) for cmd in commands) + tuple((cmd.command, cmd.description) for cmd in commands), ) if current_hash == self.last_command_hash: return @@ -202,13 +202,13 @@ class TelegramPlatformAdapter(Platform): continue for event_filter in handler_metadata.event_filters: cmd_info_list = self._extract_command_info( - event_filter, handler_metadata, skip_commands + event_filter, handler_metadata, skip_commands, ) if cmd_info_list: for cmd_name, description in cmd_info_list: if cmd_name in command_dict: logger.warning( - f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: '{command_dict[cmd_name]}'" + f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: '{command_dict[cmd_name]}'", ) command_dict.setdefault(cmd_name, description) sdk_bridge = getattr(self, "sdk_plugin_bridge", None) @@ -229,7 +229,7 @@ class TelegramPlatformAdapter(Platform): description = description[:30] + "..." if cmd_name in command_dict: logger.warning( - f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: '{command_dict[cmd_name]}'" + f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: '{command_dict[cmd_name]}'", ) command_dict.setdefault(cmd_name, description) commands_a = sorted(command_dict.keys()) @@ -237,7 +237,7 @@ class TelegramPlatformAdapter(Platform): @staticmethod def _extract_command_info( - event_filter, handler_metadata, skip_commands: set + event_filter, handler_metadata, skip_commands: set, ) -> list[tuple[str, str]] | None: """从事件过滤器中提取指令信息,包括所有别名""" cmd_names = [] @@ -273,15 +273,15 @@ class TelegramPlatformAdapter(Platform): async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_chat: logger.warning( - "Received a start command without an effective chat, skipping /start reply." + "Received a start command without an effective chat, skipping /start reply.", ) return await context.bot.send_message( - chat_id=update.effective_chat.id, text=self.config["start_message"] + chat_id=update.effective_chat.id, text=self.config["start_message"], ) async def message_handler( - self, update: Update, context: ContextTypes.DEFAULT_TYPE + self, update: Update, context: ContextTypes.DEFAULT_TYPE, ) -> None: logger.debug(f"Telegram message: {update.message}") if update.message and update.message.media_group_id: @@ -292,7 +292,7 @@ class TelegramPlatformAdapter(Platform): await self.handle_msg(abm) async def convert_message( - self, update: Update, context: ContextTypes.DEFAULT_TYPE, get_reply=True + self, update: Update, context: ContextTypes.DEFAULT_TYPE, get_reply=True, ) -> AstrBotMessage | None: """转换 Telegram 的消息对象为 AstrBotMessage 对象。 @@ -333,7 +333,7 @@ class TelegramPlatformAdapter(Platform): logger.warning("[Telegram] Received a message without a from_user.") return None message.sender = MessageMember( - str(_from_user.id), _from_user.username or "Unknown" + str(_from_user.id), _from_user.username or "Unknown", ) message.self_id = str(context.bot.username) message.raw_message = update @@ -359,7 +359,7 @@ class TelegramPlatformAdapter(Platform): message_str=reply_abm.message_str, text=reply_abm.message_str, qq=reply_abm.sender.user_id, - ) + ), ) if update.message.text: plain_text = update.message.text @@ -427,11 +427,11 @@ class TelegramPlatformAdapter(Platform): file_path = file.file_path if file_path is None: logger.warning( - f"Telegram document file_path is None, cannot save the file {file_name}." + f"Telegram document file_path is None, cannot save the file {file_name}.", ) else: message.message.append( - Comp.File(file=file_path, name=file_name, url=file_path) + Comp.File(file=file_path, name=file_name, url=file_path), ) _apply_caption() elif update.message.video: @@ -440,7 +440,7 @@ class TelegramPlatformAdapter(Platform): file_path = file.file_path if file_path is None: logger.warning( - f"Telegram video file_path is None, cannot save the file {file_name}." + f"Telegram video file_path is None, cannot save the file {file_name}.", ) else: message.message.append(Comp.Video(file=file_path, path=file.file_path)) @@ -448,7 +448,7 @@ class TelegramPlatformAdapter(Platform): return message async def handle_media_group_message( - self, update: Update, context: ContextTypes.DEFAULT_TYPE + self, update: Update, context: ContextTypes.DEFAULT_TYPE, ): """Handle messages that are part of a media group (album). @@ -472,18 +472,18 @@ class TelegramPlatformAdapter(Platform): entry = self.media_group_cache[media_group_id] entry["items"].append((update, context)) logger.debug( - f"Add message to media group {media_group_id}, currently has {len(entry['items'])} items." + f"Add message to media group {media_group_id}, currently has {len(entry['items'])} items.", ) elapsed = (datetime.now() - entry["created_at"]).total_seconds() if elapsed >= self.media_group_max_wait: delay = 0 logger.debug( - f"Media group {media_group_id} has reached max wait time ({elapsed:.1f}s >= {self.media_group_max_wait}s), processing immediately." + f"Media group {media_group_id} has reached max wait time ({elapsed:.1f}s >= {self.media_group_max_wait}s), processing immediately.", ) else: delay = self.media_group_timeout logger.debug( - f"Scheduled media group {media_group_id} to be processed in {delay} seconds (already waited {elapsed:.1f}s)" + f"Scheduled media group {media_group_id} to be processed in {delay} seconds (already waited {elapsed:.1f}s)", ) job_id = f"media_group_{media_group_id}" self.scheduler.add_job( @@ -500,6 +500,7 @@ class TelegramPlatformAdapter(Platform): Args: media_group_id: The unique identifier for this media group + """ if media_group_id not in self.media_group_cache: logger.warning(f"Media group {media_group_id} not found in cache") @@ -510,13 +511,13 @@ class TelegramPlatformAdapter(Platform): logger.warning(f"Media group {media_group_id} is empty") return logger.info( - f"Processing media group {media_group_id}, total {len(updates_and_contexts)} items" + f"Processing media group {media_group_id}, total {len(updates_and_contexts)} items", ) first_update, first_context = updates_and_contexts[0] abm = await self.convert_message(first_update, first_context) if not abm: logger.warning( - f"Failed to convert the first message of media group {media_group_id}" + f"Failed to convert the first message of media group {media_group_id}", ) return for update, context in updates_and_contexts[1:]: @@ -525,7 +526,7 @@ class TelegramPlatformAdapter(Platform): continue abm.message.extend(extra.message) logger.debug( - f"Added {len(extra.message)} components to media group {media_group_id}" + f"Added {len(extra.message)} components to media group {media_group_id}", ) await self.handle_msg(abm) diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py index 898cc2bd9..97147dbd5 100644 --- a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +++ b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py @@ -125,7 +125,7 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): logger.error(f"微信公众平台上传语音失败: {e}") await self.send( MessageChain().message( - f"微信公众平台上传语音失败: {e}" + f"微信公众平台上传语音失败: {e}", ), ) return diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 06ddaa436..94bdbcd8d 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -532,6 +532,7 @@ class ProviderManager: Args: provider_id: Provider id to resolve. merged: Whether to merge provider_source config into the provider config. + """ for provider_config in self.providers_config: if provider_config.get("id") != provider_id: diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 783f0b707..9aa65e15d 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -36,6 +36,7 @@ from astrbot.core.agent.tool import ToolSet from astrbot.core.exceptions import EmptyModelOutputError from astrbot.core.message.message_event_result import MessageChain from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult +from astrbot.core.provider.register import register_provider_adapter from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from astrbot.core.utils.io import download_file, download_image_by_url from astrbot.core.utils.media_utils import ensure_wav @@ -45,7 +46,6 @@ from astrbot.core.utils.network_utils import ( log_connection_failure, ) from astrbot.core.utils.string_utils import normalize_and_dedupe_strings -from astrbot.core.provider.register import register_provider_adapter @register_provider_adapter( diff --git a/astrbot/core/tools/computer_tools/__init__.py b/astrbot/core/tools/computer_tools/__init__.py index 7e364ffd2..92936e0c2 100644 --- a/astrbot/core/tools/computer_tools/__init__.py +++ b/astrbot/core/tools/computer_tools/__init__.py @@ -50,6 +50,6 @@ __all__ = [ "RollbackSkillReleaseTool", "RunBrowserSkillTool", "SyncSkillReleaseTool", - "normalize_umo_for_workspace", "check_admin_permission", + "normalize_umo_for_workspace", ] diff --git a/astrbot/core/tools/computer_tools/fs.py b/astrbot/core/tools/computer_tools/fs.py index 8d3160ff5..3a7db6e24 100644 --- a/astrbot/core/tools/computer_tools/fs.py +++ b/astrbot/core/tools/computer_tools/fs.py @@ -102,7 +102,7 @@ def _is_restricted_env(context: ContextWrapper[AstrAgentContext]) -> bool: if not is_local_runtime(context): return False cfg = context.context.context.get_config( - umo=context.context.event.unified_msg_origin + umo=context.context.event.unified_msg_origin, ) provider_settings = cfg.get("provider_settings", {}) require_admin = provider_settings.get("computer_use_require_admin", True) @@ -152,7 +152,7 @@ def _normalize_rw_path( allowed = ", ".join(_restricted_env_path_labels(umo)) raise PermissionError( "Read access is restricted for this user. " - f"Allowed directories: {allowed}. Blocked path: {normalized_path}." + f"Allowed directories: {allowed}. Blocked path: {normalized_path}.", ) return normalized_path @@ -192,7 +192,7 @@ class FileReadTool(FunctionTool): }, }, "required": ["path"], - } + }, ) def _validate_read_window( @@ -271,7 +271,7 @@ class FileWriteTool(FunctionTool): }, }, "required": ["path", "content"], - } + }, ) async def call( @@ -346,7 +346,7 @@ class FileEditTool(FunctionTool): }, }, "required": ["path", "old", "new"], - } + }, ) async def call( @@ -448,7 +448,7 @@ class GrepTool(FunctionTool): }, }, "required": ["pattern"], - } + }, ) def _resolve_context_options( @@ -538,7 +538,7 @@ class GrepTool(FunctionTool): blocked = ", ".join(disallowed) raise PermissionError( "Read access is restricted for this user. " - f"Allowed directories: {allowed}. Blocked paths: {blocked}." + f"Allowed directories: {allowed}. Blocked paths: {blocked}.", ) return normalized @@ -635,7 +635,7 @@ class FileUploadTool(FunctionTool): # }, }, "required": ["local_path"], - } + }, ) async def call( @@ -674,7 +674,7 @@ class FileUploadTool(FunctionTool): return f"File uploaded successfully to {file_path}" except Exception as e: logger.error(f"Error uploading file {local_path}: {e}") - return f"Error uploading file: {str(e)}" + return f"Error uploading file: {e!s}" @builtin_tool(config=_SANDBOX_RUNTIME_TOOL_CONFIG) @@ -700,7 +700,7 @@ class FileDownloadTool(FunctionTool): }, }, "required": ["remote_path"], - } + }, ) async def call( @@ -719,7 +719,7 @@ class FileDownloadTool(FunctionTool): name = os.path.basename(remote_path) local_path = os.path.join( - get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}" + get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}", ) # Download file from sandbox @@ -730,7 +730,7 @@ class FileDownloadTool(FunctionTool): try: name = os.path.basename(local_path) await context.context.event.send( - MessageChain(chain=[File(name=name, file=local_path)]) + MessageChain(chain=[File(name=name, file=local_path)]), ) except Exception as e: logger.error(f"Error sending file message: {e}") @@ -746,4 +746,4 @@ class FileDownloadTool(FunctionTool): return f"File downloaded successfully to {local_path}" except Exception as e: logger.error(f"Error downloading file {remote_path}: {e}") - return f"Error downloading file: {str(e)}" + return f"Error downloading file: {e!s}" diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index 3a15b9f58..538ebe397 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -62,7 +62,7 @@ class ExecuteShellTool(FunctionTool): cwd: str | None = None if is_local_runtime(context): current_workspace_root = workspace_root( - context.context.event.unified_msg_origin + context.context.event.unified_msg_origin, ) current_workspace_root.mkdir(parents=True, exist_ok=True) cwd = str(current_workspace_root) diff --git a/astrbot/core/tools/computer_tools/shipyard_neo/browser.py b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py index c76db4eaf..5dc8bf0a3 100644 --- a/astrbot/core/tools/computer_tools/shipyard_neo/browser.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py @@ -7,7 +7,6 @@ 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.tools.computer_tools.util import check_admin_permission diff --git a/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py index fc0b33bdf..c9ec83f01 100644 --- a/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py @@ -9,7 +9,6 @@ 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 astrbot.core.tools.computer_tools.util import check_admin_permission diff --git a/astrbot/core/tools/computer_tools/util.py b/astrbot/core/tools/computer_tools/util.py index 874079bd4..88b3d3484 100644 --- a/astrbot/core/tools/computer_tools/util.py +++ b/astrbot/core/tools/computer_tools/util.py @@ -19,7 +19,7 @@ def workspace_root(umo: str) -> Path: def is_local_runtime(context: ContextWrapper[AstrAgentContext]) -> bool: cfg = context.context.context.get_config( - umo=context.context.event.unified_msg_origin + umo=context.context.event.unified_msg_origin, ) provider_settings = cfg.get("provider_settings", {}) runtime = str(provider_settings.get("computer_use_runtime", "local")) diff --git a/astrbot/core/tools/registry.py b/astrbot/core/tools/registry.py index c3b10d229..1b120121f 100644 --- a/astrbot/core/tools/registry.py +++ b/astrbot/core/tools/registry.py @@ -44,7 +44,7 @@ class BuiltinToolConfigCondition: matched = bool(self.expected) else: raise ValueError( - f"Unsupported builtin tool config operator: {self.operator}" + f"Unsupported builtin tool config operator: {self.operator}", ) return { @@ -126,7 +126,7 @@ def _evaluate_send_message_tool(config: dict[str, Any]) -> list[dict[str, Any]]: "platform", matched=False, message="No enabled platform in this config supports proactive messaging.", - ) + ), ] for platform_cfg in platform_configs: @@ -169,7 +169,7 @@ def _evaluate_send_message_tool(config: dict[str, Any]) -> list[dict[str, Any]]: message=( f"Enabled platform `{platform_id}` (`{platform_type}`) supports proactive messaging." ), - ) + ), ] return [ @@ -177,7 +177,7 @@ def _evaluate_send_message_tool(config: dict[str, Any]) -> list[dict[str, Any]]: "platform", matched=False, message="No enabled platform in this config supports proactive messaging.", - ) + ), ] @@ -300,7 +300,7 @@ def get_builtin_tool_config_statuses( for condition in conditions if not condition.get("matched") ], - } + }, ) return statuses @@ -319,10 +319,10 @@ def get_builtin_tool_config_tags( __all__ = [ "builtin_tool", "ensure_builtin_tools_loaded", + "get_builtin_tool_class", "get_builtin_tool_config_rule", "get_builtin_tool_config_statuses", "get_builtin_tool_config_tags", - "get_builtin_tool_class", "get_builtin_tool_name", "iter_builtin_tool_classes", ] diff --git a/astrbot/dashboard/routes/knowledge_base.py b/astrbot/dashboard/routes/knowledge_base.py index 5ff77dc38..acb3535aa 100644 --- a/astrbot/dashboard/routes/knowledge_base.py +++ b/astrbot/dashboard/routes/knowledge_base.py @@ -213,7 +213,7 @@ class KnowledgeBaseRoute(Route): { "file_name": file_info["file_name"], "error": self._format_failed_doc_error( - file_info["file_name"], e + file_info["file_name"], e, ), }, ) diff --git a/main.py b/main.py index 14e0c23a8..3f4b14168 100644 --- a/main.py +++ b/main.py @@ -108,7 +108,7 @@ async def main_async(webui_dir_arg: str | None) -> None: if webui_dir is None: logger.warning( "管理面板文件检查失败,WebUI 功能将不可用。" - "请检查网络连接或手动指定 --webui-dir 参数。" + "请检查网络连接或手动指定 --webui-dir 参数。", ) db = db_helper diff --git a/tests/unit/test_cron_tools.py b/tests/unit/test_cron_tools.py index a0e520d46..b3bab2834 100644 --- a/tests/unit/test_cron_tools.py +++ b/tests/unit/test_cron_tools.py @@ -5,50 +5,59 @@ from unittest.mock import AsyncMock import pytest -from astrbot.core.tools.cron_tools import FutureTaskTool +from astrbot.core.tools.cron_tools import ( + CreateActiveCronTool, + DeleteCronJobTool, + ListCronJobsTool, +) -def test_future_task_schema_has_action_and_create_cron_guidance(): - """The merged tool should expose action routing and unambiguous cron guidance.""" - tool = FutureTaskTool() +def test_create_future_task_tool_has_correct_name(): + """The create tool should have correct name.""" + tool = CreateActiveCronTool() - assert tool.name == "future_task" - assert tool.parameters["required"] == ["action"] - assert tool.parameters["properties"]["action"]["enum"] == [ - "create", - "edit", - "delete", - "list", - ] + assert tool.name == "create_future_task" + + +def test_create_future_task_tool_requires_note(): + """The create tool should require note field.""" + tool = CreateActiveCronTool() + + assert "note" in tool.parameters["required"] + + +def test_create_future_task_tool_has_cron_guidance(): + """The create tool should have cron guidance in description.""" + tool = CreateActiveCronTool() description = tool.parameters["properties"]["cron_expression"]["description"] assert "mon-fri" in description assert "sat,sun" in description - assert "1-5" in description - assert "Prefer named weekdays" in description -def test_future_task_schema_has_no_job_type_and_delete_job_id(): - """The merged tool should remove job_type and document delete requirements.""" - tool = FutureTaskTool() +def test_delete_future_task_tool_has_job_id_required(): + """The delete tool should require job_id.""" + tool = DeleteCronJobTool() - assert "job_type" not in tool.parameters["properties"] - action_description = tool.parameters["properties"]["action"]["description"] - job_id_description = tool.parameters["properties"]["job_id"]["description"] + assert tool.name == "delete_future_task" + assert "job_id" in tool.parameters["required"] - assert "'edit' requires 'job_id'" in action_description - assert "Required for 'delete' and 'edit'" in job_id_description + +def test_list_future_tasks_tool_name(): + """The list tool should have correct name.""" + tool = ListCronJobsTool() + + assert tool.name == "list_future_tasks" @pytest.mark.asyncio -async def test_future_task_edit_requires_job_id(): - """Edit mode should require job_id.""" - tool = FutureTaskTool() - cron_mgr = SimpleNamespace() +async def test_delete_future_task_requires_job_id(): + """Delete should require job_id.""" + tool = DeleteCronJobTool() context = SimpleNamespace( context=SimpleNamespace( - context=SimpleNamespace(cron_manager=cron_mgr), + context=SimpleNamespace(cron_manager=SimpleNamespace()), event=SimpleNamespace( unified_msg_origin="test:private:session", get_sender_id=lambda: "user-1", @@ -56,38 +65,21 @@ async def test_future_task_edit_requires_job_id(): ) ) - result = await tool.call(context, action="edit") + result = await tool.call(context, job_id=None) - assert result == "error: job_id is required when action=edit." + assert "job_id is required" in result @pytest.mark.asyncio -async def test_future_task_edit_updates_existing_job(): - """Edit mode should update note and one-time scheduling fields.""" - tool = FutureTaskTool() - existing_job = SimpleNamespace( - job_id="job-1", - name="old name", - job_type="active_agent", - run_once=False, - cron_expression="0 8 * * *", - payload={ - "session": "test:private:session", - "sender_id": "user-1", - "note": "old note", - "origin": "tool", - }, - ) - updated_job = SimpleNamespace( - job_id="job-1", - name="new name", - run_once=True, - cron_expression=None, - next_run_time=None, - ) +async def test_delete_future_task_verifies_ownership(): + """Delete should verify the job belongs to current umo.""" + tool = DeleteCronJobTool() cron_mgr = SimpleNamespace( - db=SimpleNamespace(get_cron_job=AsyncMock(return_value=existing_job)), - update_job=AsyncMock(return_value=updated_job), + db=SimpleNamespace(get_cron_job=AsyncMock(return_value=SimpleNamespace( + job_id="job-1", + name="test job", + payload={"session": "other:private:session"}, + ))), ) context = SimpleNamespace( context=SimpleNamespace( @@ -99,28 +91,89 @@ async def test_future_task_edit_updates_existing_job(): ) ) - result = await tool.call( - context, - action="edit", - job_id="job-1", - name="new name", - note="new note", - run_once=True, - run_at="2026-02-02T08:00:00+08:00", + result = await tool.call(context, job_id="job-1") + + assert "only delete" in result + + +@pytest.mark.asyncio +async def test_delete_future_task_success(): + """Delete should successfully delete job and return message.""" + tool = DeleteCronJobTool() + cron_mgr = SimpleNamespace( + db=SimpleNamespace(get_cron_job=AsyncMock(return_value=SimpleNamespace( + job_id="job-1", + name="test job", + payload={"session": "test:private:session"}, + ))), + delete_job=AsyncMock(return_value=True), + ) + context = SimpleNamespace( + context=SimpleNamespace( + context=SimpleNamespace(cron_manager=cron_mgr), + event=SimpleNamespace( + unified_msg_origin="test:private:session", + get_sender_id=lambda: "user-1", + ), + ) ) - cron_mgr.update_job.assert_awaited_once_with( - "job-1", - name="new name", - description="new note", - run_once=True, - cron_expression=None, - payload={ - "session": "test:private:session", - "sender_id": "user-1", - "note": "new note", - "origin": "tool", - "run_at": "2026-02-02T08:00:00+08:00", - }, + result = await tool.call(context, job_id="job-1") + + cron_mgr.delete_job.assert_awaited_once_with("job-1") + assert "Deleted cron job job-1" in result + + +@pytest.mark.asyncio +async def test_list_future_tasks_returns_jobs(): + """List should return formatted job list.""" + tool = ListCronJobsTool() + cron_mgr = SimpleNamespace( + list_jobs=AsyncMock(return_value=[ + SimpleNamespace( + job_id="job-1", + name="test job", + job_type="active_agent", + run_once=False, + enabled=True, + next_run_time="2026-04-16T08:00:00", + payload={"session": "test:private:session"}, + ), + ]), ) - assert result == "Updated future task job-1 (new name)." + context = SimpleNamespace( + context=SimpleNamespace( + context=SimpleNamespace(cron_manager=cron_mgr), + event=SimpleNamespace( + unified_msg_origin="test:private:session", + get_sender_id=lambda: "user-1", + ), + ) + ) + + result = await tool.call(context) + + assert "job-1" in result + assert "test job" in result + + +@pytest.mark.asyncio +async def test_list_future_tasks_empty(): + """List should return message when no jobs.""" + tool = ListCronJobsTool() + cron_mgr = SimpleNamespace( + list_jobs=AsyncMock(return_value=[]), + ) + context = SimpleNamespace( + context=SimpleNamespace( + context=SimpleNamespace(cron_manager=cron_mgr), + event=SimpleNamespace( + unified_msg_origin="test:private:session", + get_sender_id=lambda: "user-1", + ), + ) + ) + + result = await tool.call(context) + + assert "No cron jobs found" in result diff --git a/tests/unit/test_faiss_vec_db.py b/tests/unit/test_faiss_vec_db.py index d294d51cd..ca297886f 100644 --- a/tests/unit/test_faiss_vec_db.py +++ b/tests/unit/test_faiss_vec_db.py @@ -3,7 +3,6 @@ from unittest.mock import AsyncMock import pytest from astrbot.core.db.vec_db.faiss_impl.vec_db import FaissVecDB -from astrbot.core.exceptions import KnowledgeBaseUploadError @pytest.mark.asyncio @@ -19,28 +18,3 @@ async def test_insert_batch_skips_empty_contents() -> None: vec_db.embedding_provider.get_embeddings_batch.assert_not_awaited() vec_db.document_storage.insert_documents_batch.assert_not_awaited() vec_db.embedding_storage.insert_batch.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_insert_batch_raises_friendly_error_for_embedding_count_mismatch() -> ( - None -): - vec_db = FaissVecDB.__new__(FaissVecDB) - vec_db.embedding_provider = AsyncMock() - vec_db.embedding_provider.get_embeddings_batch.return_value = [[0.1, 0.2]] - vec_db.document_storage = AsyncMock() - vec_db.embedding_storage = AsyncMock() - vec_db.embedding_storage.dimension = 2 - - with pytest.raises(KnowledgeBaseUploadError) as exc_info: - await FaissVecDB.insert_batch( - vec_db, - contents=["chunk-1", "chunk-2"], - metadatas=[{}, {}], - ids=["doc-1", "doc-2"], - ) - - assert "向量化失败" in str(exc_info.value) - assert "期望 2,实际 1" in str(exc_info.value) - vec_db.document_storage.insert_documents_batch.assert_not_awaited() - vec_db.embedding_storage.insert_batch.assert_not_awaited() diff --git a/tests/unit/test_func_tool_manager.py b/tests/unit/test_func_tool_manager.py index 908810cdb..967b8d2d9 100644 --- a/tests/unit/test_func_tool_manager.py +++ b/tests/unit/test_func_tool_manager.py @@ -1,40 +1,52 @@ -from astrbot.core import sp +"""Tests for FunctionToolManager with new internal tools architecture.""" + 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 +from astrbot.core.computer.computer_tool_provider import get_all_tools -def test_get_builtin_tool_by_class_returns_cached_instance(): +def test_computer_tools_provider_returns_tools(): + """ComputerToolProvider should return a list of computer tools.""" + tools = get_all_tools() + + assert len(tools) > 0 + names = [t.name for t in tools] + assert "astrbot_execute_shell" in names + + +def test_register_internal_tools_adds_tools_to_manager(): + """register_internal_tools should add computer tools to the manager.""" manager = FunctionToolManager() - tool_by_class = manager.get_builtin_tool(SendMessageToUserTool) - tool_by_name = manager.get_builtin_tool("send_message_to_user") + # Should start empty + assert manager.get_func("astrbot_execute_shell") is None - 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) + # Register internal tools + manager.register_internal_tools() + # Should now have the shell tool + tool = manager.get_func("astrbot_execute_shell") + assert tool is not None assert tool.name == "astrbot_execute_shell" - assert manager.is_builtin_tool("astrbot_execute_shell") is True + + +def test_manager_func_list_starts_empty(): + """func_list should start empty in new architecture.""" + manager = FunctionToolManager() + + assert manager.func_list == [] + + +def test_register_internal_tools_does_not_duplicate(): + """Calling register_internal_tools twice should not duplicate tools.""" + manager = FunctionToolManager() + manager.register_internal_tools() + + first_tool = manager.get_func("astrbot_execute_shell") + assert first_tool is not None + + # Register again + manager.register_internal_tools() + + # Should still have the same tool (not duplicated) + second_tool = manager.get_func("astrbot_execute_shell") + assert second_tool is first_tool diff --git a/tests/unit/test_tool_conflict_resolution.py b/tests/unit/test_tool_conflict_resolution.py index a4cffb40f..bc28b0ffe 100644 --- a/tests/unit/test_tool_conflict_resolution.py +++ b/tests/unit/test_tool_conflict_resolution.py @@ -103,7 +103,7 @@ class TestFunctionToolManagerGetFunc: def test_returns_last_active_tool(self): """Should return the last active tool when multiple have same name.""" manager = FunctionToolManager() - manager._func_list = [ + manager.func_list = [ make_tool("web_search", active=True), make_tool("web_search", active=True), ] @@ -117,7 +117,7 @@ class TestFunctionToolManagerGetFunc: """Should prefer active tool over inactive tool with same name.""" manager = FunctionToolManager() # Use internal list for test setup (property setter triggers ty error) - manager._func_list = [ + manager.func_list = [ make_tool("web_search", active=False), make_tool("web_search", active=True), ] @@ -125,12 +125,12 @@ class TestFunctionToolManagerGetFunc: result = manager.get_func("web_search") assert result is not None assert result.active is True - assert result is manager._func_list[1] + assert result is manager.func_list[1] def test_inactive_cannot_override_active(self): """Inactive tool after active should not be returned.""" manager = FunctionToolManager() - manager._func_list = [ + manager.func_list = [ make_tool("web_search", active=True), make_tool("web_search", active=False), ] @@ -143,7 +143,7 @@ class TestFunctionToolManagerGetFunc: def test_fallback_to_last_when_none_active(self): """Should return last tool with matching name when none are active.""" manager = FunctionToolManager() - manager._func_list = [ + manager.func_list = [ make_tool("web_search", active=False), make_tool("web_search", active=False), ] @@ -156,7 +156,7 @@ class TestFunctionToolManagerGetFunc: def test_returns_none_when_not_found(self): """Should return None when tool name not found.""" manager = FunctionToolManager() - manager._func_list = [make_tool("other_tool")] + manager.func_list = [make_tool("other_tool")] result = manager.get_func("web_search") assert result is None @@ -168,7 +168,7 @@ class TestFunctionToolManagerGetFullToolSet: def test_deduplicates_by_name_using_add_tool(self): """Should deduplicate tools using add_tool logic.""" manager = FunctionToolManager() - manager._func_list = [ + manager.func_list = [ make_tool("web_search", active=False), make_tool("web_search", active=True), make_tool("code_search", active=True), @@ -187,7 +187,7 @@ class TestFunctionToolManagerGetFullToolSet: """Should not deep copy tools, preserving object identity.""" manager = FunctionToolManager() tool = make_tool("web_search") - manager._func_list = [tool] + manager.func_list = [tool] toolset = manager.get_full_tool_set() @@ -202,7 +202,7 @@ class TestFunctionToolManagerGetFullToolSet: manager = FunctionToolManager() # Simulate: built-in tool registered first (disabled) # Then MCP tool registered (enabled) - manager._func_list = [ + manager.func_list = [ make_tool("web_search", active=False), # Built-in, disabled make_tool("web_search", active=True), # MCP, enabled ] @@ -221,7 +221,7 @@ class TestFunctionToolManagerGetFullToolSet: def test_disabled_mcp_cannot_override_enabled_builtin(self): """Disabled MCP tool should not override enabled built-in tool.""" manager = FunctionToolManager() - manager._func_list = [ + manager.func_list = [ make_tool("web_search", active=True), # Built-in, enabled make_tool("web_search", active=False), # MCP, disabled ]