diff --git a/src/langbot/pkg/plugin/handler.py b/src/langbot/pkg/plugin/handler.py index b21cc95c..23a2a4cf 100644 --- a/src/langbot/pkg/plugin/handler.py +++ b/src/langbot/pkg/plugin/handler.py @@ -527,6 +527,47 @@ class RuntimeConnectionHandler(handler.Handler): message=f'Failed to execute tool {tool_name}: {e}', ) + @self.action(PluginToRuntimeAction.GET_TOOL_DETAIL) + async def get_tool_detail(data: dict[str, Any]) -> handler.ActionResponse: + """Get tool detail for LLM function calling. + + For AgentRunner calls: requires run_id and validates tool_name against session.resources.tools. + For regular plugin calls: no run_id, unrestricted access (backward compatibility). + + Returns tool manifest including name, description, and parameters schema. + """ + tool_name = data['tool_name'] + run_id = data.get('run_id') # Optional: present for AgentRunner calls + + # Permission validation for AgentRunner calls + if run_id: + session, error = await _validate_run_authorization( + run_id, 'tool', tool_name, self.ap + ) + if error: + return error + + try: + tool = self.ap.tool_mgr.get_tool_by_name(tool_name) + if tool is None: + return handler.ActionResponse.error( + message=f'Tool {tool_name} not found', + ) + + # Build tool detail for LLM function calling + tool_detail = { + 'name': tool.name, + 'description': tool.description or '', + 'parameters': tool.parameters or {}, + } + + return handler.ActionResponse.success(data=tool_detail) + except Exception as e: + traceback.print_exc() + return handler.ActionResponse.error( + message=f'Failed to get tool detail for {tool_name}: {e}', + ) + # ================= Binary Storage Handlers ================= # Permission validation: # - For AgentRunner calls (with run_id): validates storage permission via session_registry diff --git a/src/langbot/pkg/provider/tools/loaders/mcp.py b/src/langbot/pkg/provider/tools/loaders/mcp.py index 46d63b84..c81ce2be 100644 --- a/src/langbot/pkg/provider/tools/loaders/mcp.py +++ b/src/langbot/pkg/provider/tools/loaders/mcp.py @@ -384,6 +384,21 @@ class MCPLoader(loader.ToolLoader): return True return False + async def _get_tool(self, name: str) -> resource_tool.LLMTool | None: + """Get tool by name. + + Args: + name: Tool name to find + + Returns: + LLMTool if found, None otherwise + """ + 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/toolmgr.py b/src/langbot/pkg/provider/tools/toolmgr.py index f921c094..53507178 100644 --- a/src/langbot/pkg/provider/tools/toolmgr.py +++ b/src/langbot/pkg/provider/tools/toolmgr.py @@ -40,6 +40,27 @@ class ToolManager: return all_functions + async def get_tool_by_name(self, name: str) -> resource_tool.LLMTool | None: + """Get tool by name from plugin or MCP loaders. + + Args: + name: Tool name (format: plugin_author/plugin_name/tool_name or mcp_server/tool_name) + + Returns: + LLMTool if found, None otherwise + """ + # Try plugin loader first + tool = await self.plugin_tool_loader._get_tool(name) + if tool: + return tool + + # Try MCP loader + tool = await self.mcp_tool_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 = []