From e9fe2f2d432d1566bf3403ca64c7aa16712f8706 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <1051233107@qq.com> Date: Sun, 14 Jun 2026 11:29:57 +0800 Subject: [PATCH] feat(agent-runner): support host tool lookup (#2244) --- src/langbot/pkg/provider/tools/errors.py | 6 ++++++ src/langbot/pkg/provider/tools/loader.py | 10 ++++++++++ src/langbot/pkg/provider/tools/loaders/mcp.py | 7 +++++++ .../pkg/provider/tools/loaders/native.py | 3 ++- .../pkg/provider/tools/loaders/plugin.py | 3 ++- src/langbot/pkg/provider/tools/toolmgr.py | 19 ++++++++++++++++++- .../unit_tests/provider/test_tool_manager.py | 4 ++-- 7 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 src/langbot/pkg/provider/tools/errors.py diff --git a/src/langbot/pkg/provider/tools/errors.py b/src/langbot/pkg/provider/tools/errors.py new file mode 100644 index 00000000..d44b39ba --- /dev/null +++ b/src/langbot/pkg/provider/tools/errors.py @@ -0,0 +1,6 @@ +class ToolNotFoundError(ValueError): + """Raised when a requested tool cannot be found in any active loader.""" + + def __init__(self, name: str): + self.name = name + super().__init__(f'Tool not found: {name}') diff --git a/src/langbot/pkg/provider/tools/loader.py b/src/langbot/pkg/provider/tools/loader.py index e90f07b3..f97e8216 100644 --- a/src/langbot/pkg/provider/tools/loader.py +++ b/src/langbot/pkg/provider/tools/loader.py @@ -4,12 +4,15 @@ import abc import typing from typing import TYPE_CHECKING +from langbot_plugin.api.definition.components.manifest import ComponentManifest from langbot_plugin.api.entities.events import pipeline_query import langbot_plugin.api.entities.builtin.resource.tool as resource_tool if TYPE_CHECKING: from ...core import app +ToolLookupResult = resource_tool.LLMTool | ComponentManifest + preregistered_loaders: list[typing.Type[ToolLoader]] = [] @@ -43,6 +46,13 @@ class ToolLoader(abc.ABC): """获取所有工具""" pass + async def get_tool(self, name: str) -> ToolLookupResult | None: + """Get one tool by name.""" + for tool in await self.get_tools(): + if tool.name == name: + return tool + return None + @abc.abstractmethod async def has_tool(self, name: str) -> bool: """检查工具是否存在""" diff --git a/src/langbot/pkg/provider/tools/loaders/mcp.py b/src/langbot/pkg/provider/tools/loaders/mcp.py index 117f29cd..8049b185 100644 --- a/src/langbot/pkg/provider/tools/loaders/mcp.py +++ b/src/langbot/pkg/provider/tools/loaders/mcp.py @@ -567,6 +567,13 @@ class MCPLoader(loader.ToolLoader): return True return False + async def get_tool(self, name: str) -> resource_tool.LLMTool | None: + for session in self.sessions.values(): + for function in session.get_tools(): + if function.name == name: + return function + return None + async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any: """执行工具调用""" for session in self.sessions.values(): diff --git a/src/langbot/pkg/provider/tools/loaders/native.py b/src/langbot/pkg/provider/tools/loaders/native.py index d6ef11d1..83390049 100644 --- a/src/langbot/pkg/provider/tools/loaders/native.py +++ b/src/langbot/pkg/provider/tools/loaders/native.py @@ -7,6 +7,7 @@ import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from langbot_plugin.api.entities.events import pipeline_query from .. import loader +from ..errors import ToolNotFoundError from . import skill as skill_loader EXEC_TOOL_NAME = 'exec' @@ -90,7 +91,7 @@ class NativeToolLoader(loader.ToolLoader): return await self._invoke_glob(parameters, query) if name == GREP_TOOL_NAME: return await self._invoke_grep(parameters, query) - raise ValueError(f'未找到工具: {name}') + raise ToolNotFoundError(name) async def shutdown(self): pass diff --git a/src/langbot/pkg/provider/tools/loaders/plugin.py b/src/langbot/pkg/provider/tools/loaders/plugin.py index 5b741848..544882d3 100644 --- a/src/langbot/pkg/provider/tools/loaders/plugin.py +++ b/src/langbot/pkg/provider/tools/loaders/plugin.py @@ -3,6 +3,7 @@ from __future__ import annotations import typing import traceback +from langbot_plugin.api.definition.components.manifest import ComponentManifest from langbot_plugin.api.entities.events import pipeline_query from .. import loader @@ -39,7 +40,7 @@ class PluginToolLoader(loader.ToolLoader): return True return False - async def _get_tool(self, name: str) -> resource_tool.LLMTool: + async def get_tool(self, name: str) -> ComponentManifest | None: for tool in await self.ap.plugin_connector.list_tools(): if tool.metadata.name == name: return tool diff --git a/src/langbot/pkg/provider/tools/toolmgr.py b/src/langbot/pkg/provider/tools/toolmgr.py index fd03b303..38b08aa1 100644 --- a/src/langbot/pkg/provider/tools/toolmgr.py +++ b/src/langbot/pkg/provider/tools/toolmgr.py @@ -6,6 +6,9 @@ from typing import TYPE_CHECKING import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from langbot_plugin.api.entities.events import pipeline_query +from . import loader as tool_loader +from .errors import ToolNotFoundError + if TYPE_CHECKING: from ...core import app from langbot.pkg.provider.tools.loaders import ( @@ -67,6 +70,20 @@ class ToolManager: return all_functions + async def get_tool_by_name(self, name: str) -> tool_loader.ToolLookupResult | None: + """Get tool by name from any active loader.""" + for active_loader in ( + self.native_tool_loader, + self.plugin_tool_loader, + self.mcp_tool_loader, + self.skill_tool_loader, + ): + tool = await active_loader.get_tool(name) + if tool: + return tool + + return None + async def generate_tools_for_openai(self, use_funcs: list[resource_tool.LLMTool]) -> list: tools = [] @@ -98,7 +115,7 @@ class ToolManager: if await self.skill_tool_loader.has_tool(name): telemetry_features.increment(query, 'tool_calls', 'skill') return await self.skill_tool_loader.invoke_tool(name, parameters, query) - raise ValueError(f'未找到工具: {name}') + raise ToolNotFoundError(name) async def shutdown(self): await self.native_tool_loader.shutdown() diff --git a/tests/unit_tests/provider/test_tool_manager.py b/tests/unit_tests/provider/test_tool_manager.py index fbfcb13f..2fcf25fb 100644 --- a/tests/unit_tests/provider/test_tool_manager.py +++ b/tests/unit_tests/provider/test_tool_manager.py @@ -226,7 +226,7 @@ class TestToolManagerExecuteFuncCall: @pytest.mark.asyncio async def test_execute_raises_when_tool_not_found(self, mock_app_with_loaders, sample_query): - """Test that execute_func_call raises ValueError when tool not found.""" + """Test that execute_func_call raises ToolNotFoundError when tool not found.""" toolmgr = get_toolmgr_module() mock_app, mock_plugin_loader, mock_mcp_loader = mock_app_with_loaders @@ -236,7 +236,7 @@ class TestToolManagerExecuteFuncCall: manager = toolmgr.ToolManager(mock_app) self._wire_loaders(manager, mock_app, mock_plugin_loader, mock_mcp_loader) - with pytest.raises(ValueError, match='未找到工具'): + with pytest.raises(toolmgr.ToolNotFoundError, match='Tool not found: unknown_tool'): await manager.execute_func_call('unknown_tool', {}, sample_query) @pytest.mark.asyncio