Compare commits

...

1 Commits

Author SHA1 Message Date
huanghuoguoguo
ee24398d80 feat(agent-runner): support host tool lookup 2026-06-14 11:04:52 +08:00
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 import typing
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from langbot_plugin.api.definition.components.manifest import ComponentManifest
from langbot_plugin.api.entities.events import pipeline_query from langbot_plugin.api.entities.events import pipeline_query
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
if TYPE_CHECKING: if TYPE_CHECKING:
from ...core import app from ...core import app
ToolLookupResult = resource_tool.LLMTool | ComponentManifest
preregistered_loaders: list[typing.Type[ToolLoader]] = [] preregistered_loaders: list[typing.Type[ToolLoader]] = []
@@ -43,6 +46,13 @@ class ToolLoader(abc.ABC):
"""获取所有工具""" """获取所有工具"""
pass 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 @abc.abstractmethod
async def has_tool(self, name: str) -> bool: async def has_tool(self, name: str) -> bool:
"""检查工具是否存在""" """检查工具是否存在"""

View File

@@ -567,6 +567,13 @@ class MCPLoader(loader.ToolLoader):
return True return True
return False 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: async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
"""执行工具调用""" """执行工具调用"""
for session in self.sessions.values(): 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 langbot_plugin.api.entities.events import pipeline_query
from .. import loader from .. import loader
from ..errors import ToolNotFoundError
from . import skill as skill_loader from . import skill as skill_loader
EXEC_TOOL_NAME = 'exec' EXEC_TOOL_NAME = 'exec'
@@ -90,7 +91,7 @@ class NativeToolLoader(loader.ToolLoader):
return await self._invoke_glob(parameters, query) return await self._invoke_glob(parameters, query)
if name == GREP_TOOL_NAME: if name == GREP_TOOL_NAME:
return await self._invoke_grep(parameters, query) return await self._invoke_grep(parameters, query)
raise ValueError(f'未找到工具: {name}') raise ToolNotFoundError(name)
async def shutdown(self): async def shutdown(self):
pass pass

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import typing import typing
import traceback import traceback
from langbot_plugin.api.definition.components.manifest import ComponentManifest
from langbot_plugin.api.entities.events import pipeline_query from langbot_plugin.api.entities.events import pipeline_query
from .. import loader from .. import loader
@@ -39,7 +40,7 @@ class PluginToolLoader(loader.ToolLoader):
return True return True
return False 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(): for tool in await self.ap.plugin_connector.list_tools():
if tool.metadata.name == name: if tool.metadata.name == name:
return tool return tool

View File

@@ -6,6 +6,9 @@ from typing import TYPE_CHECKING
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
from langbot_plugin.api.entities.events import pipeline_query from langbot_plugin.api.entities.events import pipeline_query
from . import loader as tool_loader
from .errors import ToolNotFoundError
if TYPE_CHECKING: if TYPE_CHECKING:
from ...core import app from ...core import app
from langbot.pkg.provider.tools.loaders import ( from langbot.pkg.provider.tools.loaders import (
@@ -67,6 +70,20 @@ class ToolManager:
return all_functions 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: async def generate_tools_for_openai(self, use_funcs: list[resource_tool.LLMTool]) -> list:
tools = [] tools = []
@@ -98,7 +115,7 @@ class ToolManager:
if await self.skill_tool_loader.has_tool(name): if await self.skill_tool_loader.has_tool(name):
telemetry_features.increment(query, 'tool_calls', 'skill') telemetry_features.increment(query, 'tool_calls', 'skill')
return await self.skill_tool_loader.invoke_tool(name, parameters, query) return await self.skill_tool_loader.invoke_tool(name, parameters, query)
raise ValueError(f'未找到工具: {name}') raise ToolNotFoundError(name)
async def shutdown(self): async def shutdown(self):
await self.native_tool_loader.shutdown() await self.native_tool_loader.shutdown()

View File

@@ -226,7 +226,7 @@ class TestToolManagerExecuteFuncCall:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_execute_raises_when_tool_not_found(self, mock_app_with_loaders, sample_query): 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() toolmgr = get_toolmgr_module()
mock_app, mock_plugin_loader, mock_mcp_loader = mock_app_with_loaders mock_app, mock_plugin_loader, mock_mcp_loader = mock_app_with_loaders
@@ -236,7 +236,7 @@ class TestToolManagerExecuteFuncCall:
manager = toolmgr.ToolManager(mock_app) manager = toolmgr.ToolManager(mock_app)
self._wire_loaders(manager, mock_app, mock_plugin_loader, mock_mcp_loader) 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) await manager.execute_func_call('unknown_tool', {}, sample_query)
@pytest.mark.asyncio @pytest.mark.asyncio