diff --git a/app/agent/middleware/tool_selection.py b/app/agent/middleware/tool_selection.py index ff79bac7..840e2550 100644 --- a/app/agent/middleware/tool_selection.py +++ b/app/agent/middleware/tool_selection.py @@ -321,7 +321,7 @@ class ToolSelectorMiddleware(LLMToolSelectorMiddleware): 处理工具筛选响应,并保留空结果回退所有工具的 MoviePilot 策略。 """ if response.get("tools") == []: - logger.warning("工具筛选结果为空,将恢复使用所有工具。") + logger.info("工具筛选结果为空,将恢复使用所有工具。") always_included_tools: list[BaseTool] = [ tool @@ -343,12 +343,15 @@ class ToolSelectorMiddleware(LLMToolSelectorMiddleware): valid_tool_names=valid_tool_names, available_tools=available_tools, ) - return super()._process_selection_response( + modified_request = super()._process_selection_response( response, available_tools, valid_tool_names, request, ) + selected_tool_names = self._extract_selected_tool_names(modified_request) + logger.info(f"工具筛选结果: {', '.join(selected_tool_names) or '无有效工具'}") + return modified_request @staticmethod def _parse_json_object(text: str) -> dict[str, Any]: diff --git a/tests/test_agent_tool_selector_middleware.py b/tests/test_agent_tool_selector_middleware.py index 5e923196..47f82eb3 100644 --- a/tests/test_agent_tool_selector_middleware.py +++ b/tests/test_agent_tool_selector_middleware.py @@ -1,5 +1,6 @@ import asyncio from types import SimpleNamespace +from unittest.mock import patch from langchain_core.messages import AIMessage, HumanMessage, SystemMessage @@ -291,6 +292,64 @@ def test_tool_selection_failure_falls_back_to_all_tools(): assert state_update == {"selected_tool_names": ["search", "calendar"]} +def test_empty_tool_selection_logs_info_not_warning(): + """工具筛选返回空数组时应按信息日志记录降级。""" + tools = [ + SimpleNamespace(name="search", description="Search for information"), + SimpleNamespace(name="calendar", description="Manage events"), + ] + middleware = tool_selector_module.ToolSelectorMiddleware( + max_tools=2, + selection_tools=tools, + ) + request = _FakeRequest( + tools=tools, + messages=[HumanMessage(content="帮我安排明天的行程并查天气")], + model=_FakeModel(), + ) + + with patch.object(tool_selector_module.logger, "info") as logger_info, \ + patch.object(tool_selector_module.logger, "warning") as logger_warning: + result = middleware._process_selection_response( + {"tools": []}, + available_tools=tools, + valid_tool_names=[tool.name for tool in tools], + request=request, + ) + + assert [tool.name for tool in result.tools] == ["search", "calendar"] + logger_info.assert_called_once_with("工具筛选结果为空,将恢复使用所有工具。") + logger_warning.assert_not_called() + + +def test_process_selection_response_logs_selected_tools(): + """工具筛选返回有效工具时应记录最终生效的工具名。""" + tools = [ + SimpleNamespace(name="search", description="Search for information"), + SimpleNamespace(name="calendar", description="Manage events"), + ] + middleware = tool_selector_module.ToolSelectorMiddleware( + max_tools=2, + selection_tools=tools, + ) + request = _FakeRequest( + tools=tools, + messages=[HumanMessage(content="帮我安排明天的行程并查天气")], + model=_FakeModel(), + ) + + with patch.object(tool_selector_module.logger, "info") as logger_info: + result = middleware._process_selection_response( + {"tools": ["calendar"]}, + available_tools=tools, + valid_tool_names=[tool.name for tool in tools], + request=request, + ) + + assert [tool.name for tool in result.tools] == ["calendar"] + logger_info.assert_called_once_with("工具筛选结果: calendar") + + def test_normalize_selection_response_accepts_code_fence_json(): """工具筛选响应应兼容 Markdown 代码围栏包裹的 JSON。""" middleware = tool_selector_module.ToolSelectorMiddleware()