feat(agent-runner): support host tool lookup

This commit is contained in:
huanghuoguoguo
2026-06-14 11:04:52 +08:00
parent a97d2040bb
commit ee24398d80
7 changed files with 47 additions and 5 deletions

View File

@@ -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}')

View File

@@ -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:
"""检查工具是否存在"""

View File

@@ -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():

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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