fix(agent-runner): clean plugin review issues

This commit is contained in:
huanghuoguoguo
2026-06-06 14:34:40 +08:00
parent 754f7197c5
commit cb33bf3260
18 changed files with 71 additions and 73 deletions
@@ -12,7 +12,7 @@ class MCPRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('/servers', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN) @self.route('/servers', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
"""获取MCP服务器列表""" """List MCP servers or create a new MCP server."""
if quart.request.method == 'GET': if quart.request.method == 'GET':
servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True) servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True)
@@ -30,7 +30,7 @@ class MCPRouterGroup(group.RouterGroup):
@self.route('/servers/<server_name>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN) @self.route('/servers/<server_name>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN)
async def _(server_name: str) -> str: async def _(server_name: str) -> str:
"""获取、更新或删除MCP服务器配置""" """Get, update, or delete an MCP server configuration."""
from urllib.parse import unquote from urllib.parse import unquote
server_name = unquote(server_name) server_name = unquote(server_name)
@@ -59,7 +59,7 @@ class MCPRouterGroup(group.RouterGroup):
@self.route('/servers/<server_name>/test', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) @self.route('/servers/<server_name>/test', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _(server_name: str) -> str: async def _(server_name: str) -> str:
"""测试MCP服务器连接""" """Test an MCP server connection."""
from urllib.parse import unquote from urllib.parse import unquote
server_name = unquote(server_name) server_name = unquote(server_name)
+1 -1
View File
@@ -137,7 +137,7 @@ class MCPService:
await self.ap.tool_mgr.mcp_tool_loader.remove_mcp_server(server_name) await self.ap.tool_mgr.mcp_tool_loader.remove_mcp_server(server_name)
async def test_mcp_server(self, server_name: str, server_data: dict) -> int: async def test_mcp_server(self, server_name: str, server_data: dict) -> int:
"""测试 MCP 服务器连接并返回任务 ID""" """Test an MCP server connection and return the task ID."""
runtime_mcp_session: RuntimeMCPSession | None = None runtime_mcp_session: RuntimeMCPSession | None = None
+3 -3
View File
@@ -211,7 +211,7 @@ class PreProcessor(stage.PipelineStage):
# time instead of the first message/creation time. # time instead of the first message/creation time.
conversation.update_time = now conversation.update_time = now
# 设置query # Attach resolved session state to the query.
query.session = session query.session = session
query.prompt = conversation.prompt.copy() query.prompt = conversation.prompt.copy()
query.messages = await self._resolve_history_messages(runner_id, conversation) query.messages = await self._resolve_history_messages(runner_id, conversation)
@@ -292,7 +292,7 @@ class PreProcessor(stage.PipelineStage):
if me.base64 is not None: if me.base64 is not None:
content_list.append(provider_message.ContentElement.from_image_base64(me.base64)) content_list.append(provider_message.ContentElement.from_image_base64(me.base64))
elif isinstance(me, platform_message.Voice): elif isinstance(me, platform_message.Voice):
# 转成文件链接,让下游 runner 上传到目标模型 # Convert voice input into file content for downstream model upload.
if me.base64: if me.base64:
content_list.append(provider_message.ContentElement.from_file_base64(me.base64, 'voice.silk')) content_list.append(provider_message.ContentElement.from_file_base64(me.base64, 'voice.silk'))
elif me.url: elif me.url:
@@ -334,7 +334,7 @@ class PreProcessor(stage.PipelineStage):
runner_config, runner_config,
) )
# =========== 触发事件 PromptPreProcessing # Emit PromptPreProcessing before the runner receives the query.
event = events.PromptPreProcessing( event = events.PromptPreProcessing(
session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}', session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
+6 -1
View File
@@ -468,7 +468,12 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
) )
file_bytes = download_resp.content file_bytes = download_resp.content
self._extract_deps_metadata(file_bytes, task_context) plugin_author, plugin_name = self._inspect_plugin_package(
file_bytes,
task_context,
)
if task_context is not None and plugin_author and plugin_name:
task_context.metadata['plugin_name'] = f'{plugin_author}/{plugin_name}'
file_key = await self.handler.send_file(file_bytes, 'lbpkg') file_key = await self.handler.send_file(file_bytes, 'lbpkg')
install_info['plugin_file_key'] = file_key install_info['plugin_file_key'] = file_key
self.ap.logger.info(f'Transfered file {file_key} to plugin runtime') self.ap.logger.info(f'Transfered file {file_key} to plugin runtime')
@@ -3,8 +3,6 @@ Legacy Coze API Runner.
DEPRECATED: This runner has been migrated to the AgentRunner plugin format. DEPRECATED: This runner has been migrated to the AgentRunner plugin format.
Use the official `langbot/coze-agent` plugin instead. Use the official `langbot/coze-agent` plugin instead.
Migration target: /home/glwuy/langbot-app/langbot-agent-runner/coze-agent/
""" """
from __future__ import annotations from __future__ import annotations
@@ -3,8 +3,6 @@ Legacy DashScope (阿里云百炼) API Runner.
DEPRECATED: This runner has been migrated to the AgentRunner plugin format. DEPRECATED: This runner has been migrated to the AgentRunner plugin format.
Use the official `langbot/dashscope-agent` plugin instead. Use the official `langbot/dashscope-agent` plugin instead.
Migration target: /home/glwuy/langbot-app/langbot-agent-runner/dashscope-agent/
""" """
from __future__ import annotations from __future__ import annotations
@@ -3,8 +3,6 @@ Legacy Dify Service API Runner.
DEPRECATED: This runner has been migrated to the AgentRunner plugin format. DEPRECATED: This runner has been migrated to the AgentRunner plugin format.
Use the official `langbot/dify-agent` plugin instead. Use the official `langbot/dify-agent` plugin instead.
Migration target: /home/glwuy/langbot-app/langbot-agent-runner/dify-agent/
""" """
from __future__ import annotations from __future__ import annotations
@@ -3,8 +3,6 @@ Legacy Langflow API Runner.
DEPRECATED: This runner has been migrated to the AgentRunner plugin format. DEPRECATED: This runner has been migrated to the AgentRunner plugin format.
Use the official `langbot/langflow-agent` plugin instead. Use the official `langbot/langflow-agent` plugin instead.
Migration target: /home/glwuy/langbot-app/langbot-agent-runner/langflow-agent/
""" """
from __future__ import annotations from __future__ import annotations
@@ -3,8 +3,6 @@ Legacy Local Agent Runner.
DEPRECATED: This runner has been migrated to the AgentRunner plugin format. DEPRECATED: This runner has been migrated to the AgentRunner plugin format.
Use the official `langbot/local-agent` plugin instead. Use the official `langbot/local-agent` plugin instead.
Migration target: /home/glwuy/langbot-app/langbot-local-agent/
""" """
from __future__ import annotations from __future__ import annotations
@@ -3,8 +3,6 @@ Legacy n8n Service API Runner.
DEPRECATED: This runner has been migrated to the AgentRunner plugin format. DEPRECATED: This runner has been migrated to the AgentRunner plugin format.
Use the official `langbot/n8n-agent` plugin instead. Use the official `langbot/n8n-agent` plugin instead.
Migration target: /home/glwuy/langbot-app/langbot-agent-runner/n8n-agent/
""" """
from __future__ import annotations from __future__ import annotations
@@ -3,8 +3,6 @@ Legacy Tbox (蚂蚁百宝箱) API Runner.
DEPRECATED: This runner has been migrated to the AgentRunner plugin format. DEPRECATED: This runner has been migrated to the AgentRunner plugin format.
Use the official `langbot/tbox-agent` plugin instead. Use the official `langbot/tbox-agent` plugin instead.
Migration target: /home/glwuy/langbot-app/langbot-agent-runner/tbox-agent/
""" """
from __future__ import annotations from __future__ import annotations
+6
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}')
@@ -0,0 +1,18 @@
from __future__ import annotations
from typing import Any
async def is_box_backend_available(ap: Any) -> bool:
"""Return whether the configured Box backend is ready for tool execution."""
box_service = getattr(ap, 'box_service', None)
if box_service is None:
return False
if not getattr(box_service, 'available', False):
return False
try:
status = await box_service.get_status()
backend_info = status.get('backend', {})
return bool(backend_info.get('available', False))
except Exception:
return False
+21 -21
View File
@@ -30,7 +30,7 @@ class MCPSessionStatus(enum.Enum):
class RuntimeMCPSession: class RuntimeMCPSession:
"""运行时 MCP 会话""" """Runtime MCP session."""
ap: app.Application ap: app.Application
@@ -352,12 +352,12 @@ class RuntimeMCPSession:
return info return info
async def shutdown(self): async def shutdown(self):
"""关闭会话并清理资源""" """Close the session and release resources."""
try: try:
# 设置shutdown事件,通知lifecycle任务退出 # Signal the lifecycle task to exit.
self._shutdown_event.set() self._shutdown_event.set()
# 等待lifecycle任务完成(带超时) # Wait for the lifecycle task with a bounded timeout.
if self._lifecycle_task and not self._lifecycle_task.done(): if self._lifecycle_task and not self._lifecycle_task.done():
try: try:
await asyncio.wait_for(self._lifecycle_task, timeout=5.0) await asyncio.wait_for(self._lifecycle_task, timeout=5.0)
@@ -411,9 +411,9 @@ class RuntimeMCPSession:
# @loader.loader_class('mcp') # @loader.loader_class('mcp')
class MCPLoader(loader.ToolLoader): class MCPLoader(loader.ToolLoader):
"""MCP 工具加载器。 """MCP tool loader.
在此加载器中管理所有与 MCP Server 的连接。 This loader owns all active MCP server connections.
""" """
sessions: dict[str, RuntimeMCPSession] sessions: dict[str, RuntimeMCPSession]
@@ -468,14 +468,14 @@ class MCPLoader(loader.ToolLoader):
self.ap.logger.debug(f'Started MCP server {server_config["name"]}({server_config["uuid"]})') self.ap.logger.debug(f'Started MCP server {server_config["name"]}({server_config["uuid"]})')
async def load_mcp_server(self, server_config: dict) -> RuntimeMCPSession: async def load_mcp_server(self, server_config: dict) -> RuntimeMCPSession:
"""加载 MCP 服务器到运行时 """Load an MCP server into the runtime.
Args: Args:
server_config: 服务器配置字典,必须包含: server_config: Server config dict. Must include:
- name: 服务器名称 - name: Server name.
- mode: 连接模式 (stdio/sse/http) - mode: Connection mode (stdio/sse/http).
- enable: 是否启用 - enable: Whether the server is enabled.
- extra_args: 额外的配置参数 (可选) - extra_args: Optional extra config.
""" """
uuid_ = server_config.get('uuid') uuid_ = server_config.get('uuid')
if not uuid_: if not uuid_:
@@ -518,7 +518,7 @@ class MCPLoader(loader.ToolLoader):
return all_functions return all_functions
async def has_tool(self, name: str) -> bool: async def has_tool(self, name: str) -> bool:
"""检查工具是否存在""" """Return whether a loaded MCP tool exists."""
for session in self.sessions.values(): for session in self.sessions.values():
for function in session.get_tools(): for function in session.get_tools():
if function.name == name: if function.name == name:
@@ -541,7 +541,7 @@ class MCPLoader(loader.ToolLoader):
return None return None
async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query | None) -> typing.Any: async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query | None) -> typing.Any:
"""执行工具调用""" """Invoke a loaded MCP tool."""
for session in self.sessions.values(): for session in self.sessions.values():
for function in session.get_tools(): for function in session.get_tools():
if function.name == name: if function.name == name:
@@ -557,7 +557,7 @@ class MCPLoader(loader.ToolLoader):
raise ValueError(f'Tool not found: {name}') raise ValueError(f'Tool not found: {name}')
async def remove_mcp_server(self, server_name: str): async def remove_mcp_server(self, server_name: str):
"""移除 MCP 服务器""" """Remove an MCP server from the runtime."""
if server_name not in self.sessions: if server_name not in self.sessions:
self.ap.logger.warning(f'MCP server {server_name} not found in sessions, skipping removal') self.ap.logger.warning(f'MCP server {server_name} not found in sessions, skipping removal')
return return
@@ -567,24 +567,24 @@ class MCPLoader(loader.ToolLoader):
self.ap.logger.info(f'Removed MCP server: {server_name}') self.ap.logger.info(f'Removed MCP server: {server_name}')
def get_session(self, server_name: str) -> RuntimeMCPSession | None: def get_session(self, server_name: str) -> RuntimeMCPSession | None:
"""获取指定名称的 MCP 会话""" """Get an MCP session by server name."""
return self.sessions.get(server_name) return self.sessions.get(server_name)
def has_session(self, server_name: str) -> bool: def has_session(self, server_name: str) -> bool:
"""检查是否存在指定名称的 MCP 会话""" """Return whether a session exists for the server name."""
return server_name in self.sessions return server_name in self.sessions
def get_all_server_names(self) -> list[str]: def get_all_server_names(self) -> list[str]:
"""获取所有已加载的 MCP 服务器名称""" """Return all loaded MCP server names."""
return list(self.sessions.keys()) return list(self.sessions.keys())
def get_server_tool_count(self, server_name: str) -> int: def get_server_tool_count(self, server_name: str) -> int:
"""获取指定服务器的工具数量""" """Return the number of tools exposed by one MCP server."""
session = self.get_session(server_name) session = self.get_session(server_name)
return len(session.get_tools()) if session else 0 return len(session.get_tools()) if session else 0
def get_all_servers_info(self) -> dict[str, dict]: def get_all_servers_info(self) -> dict[str, dict]:
"""获取所有服务器的信息""" """Return runtime information for all loaded MCP servers."""
info = {} info = {}
for server_name, session in self.sessions.items(): for server_name, session in self.sessions.items():
tools = session.get_tools() tools = session.get_tools()
@@ -598,7 +598,7 @@ class MCPLoader(loader.ToolLoader):
return info return info
async def shutdown(self): async def shutdown(self):
"""关闭所有工具""" """Shut down all MCP sessions."""
self.ap.logger.info('Shutting down all MCP sessions...') self.ap.logger.info('Shutting down all MCP sessions...')
for server_name, session in list(self.sessions.items()): for server_name, session in list(self.sessions.items()):
try: try:
@@ -8,6 +8,8 @@ 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 .availability import is_box_backend_available
from . import skill as skill_loader from . import skill as skill_loader
EXEC_TOOL_NAME = 'exec' EXEC_TOOL_NAME = 'exec'
@@ -52,18 +54,7 @@ class NativeToolLoader(loader.ToolLoader):
async def _check_backend_available(self) -> bool: async def _check_backend_available(self) -> bool:
"""Check if the box backend is truly available (not just the runtime).""" """Check if the box backend is truly available (not just the runtime)."""
box_service = getattr(self.ap, 'box_service', None) return await is_box_backend_available(self.ap)
if box_service is None:
return False
if not getattr(box_service, 'available', False):
return False
# Check if backend is truly available via get_status
try:
status = await box_service.get_status()
backend_info = status.get('backend', {})
return backend_info.get('available', False)
except Exception:
return False
async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]: async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]:
if not self._is_sandbox_available(): if not self._is_sandbox_available():
@@ -100,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
@@ -6,6 +6,7 @@ import typing
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
from .. import loader from .. import loader
from .availability import is_box_backend_available
# Align with Claude Code's Skill tool design: # Align with Claude Code's Skill tool design:
# - activate: Activate a skill via Tool Call, returns SKILL.md content # - activate: Activate a skill via Tool Call, returns SKILL.md content
@@ -45,18 +46,7 @@ class SkillToolLoader(loader.ToolLoader):
async def _check_sandbox_available(self) -> bool: async def _check_sandbox_available(self) -> bool:
"""Check if the box backend is truly available (not just the runtime).""" """Check if the box backend is truly available (not just the runtime)."""
box_service = getattr(self.ap, 'box_service', None) return await is_box_backend_available(self.ap)
if box_service is None:
return False
if not getattr(box_service, 'available', False):
return False
# Check if backend is truly available via get_status
try:
status = await box_service.get_status()
backend_info = status.get('backend', {})
return backend_info.get('available', False)
except Exception:
return False
async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]: async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]:
if not self._is_available(): if not self._is_available():
+4 -2
View File
@@ -6,6 +6,8 @@ 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 .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 (
@@ -128,7 +130,7 @@ class ToolManager:
return tools return tools
async def execute_func_call(self, name: str, parameters: dict, query: pipeline_query.Query | None) -> typing.Any: async def execute_func_call(self, name: str, parameters: dict, query: pipeline_query.Query | None) -> typing.Any:
"""执行函数调用""" """Execute a tool call through the active tool loaders."""
if await self.native_tool_loader.has_tool(name): if await self.native_tool_loader.has_tool(name):
return await self.native_tool_loader.invoke_tool(name, parameters, query) return await self.native_tool_loader.invoke_tool(name, parameters, query)
@@ -138,7 +140,7 @@ class ToolManager:
return await self.mcp_tool_loader.invoke_tool(name, parameters, query) return await self.mcp_tool_loader.invoke_tool(name, parameters, query)
if await self.skill_tool_loader.has_tool(name): if await self.skill_tool_loader.has_tool(name):
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()
+3 -3
View File
@@ -5,10 +5,10 @@ import typing
def import_modules_in_pkg(pkg: typing.Any) -> None: def import_modules_in_pkg(pkg: typing.Any) -> None:
""" """Import all Python modules inside a package.
导入一个包内的所有模块
Args: Args:
pkg: 要导入的包对象 pkg: Package object to import from.
""" """
pkg_path = os.path.dirname(pkg.__file__) pkg_path = os.path.dirname(pkg.__file__)
import_dir(pkg_path) import_dir(pkg_path)