diff --git a/src/langbot/pkg/api/http/controller/groups/resources/mcp.py b/src/langbot/pkg/api/http/controller/groups/resources/mcp.py index e6bc2e77..0365cb0d 100644 --- a/src/langbot/pkg/api/http/controller/groups/resources/mcp.py +++ b/src/langbot/pkg/api/http/controller/groups/resources/mcp.py @@ -12,7 +12,7 @@ class MCPRouterGroup(group.RouterGroup): async def initialize(self) -> None: @self.route('/servers', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: - """获取MCP服务器列表""" + """List MCP servers or create a new MCP server.""" if quart.request.method == 'GET': servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True) @@ -30,7 +30,7 @@ class MCPRouterGroup(group.RouterGroup): @self.route('/servers/', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN) async def _(server_name: str) -> str: - """获取、更新或删除MCP服务器配置""" + """Get, update, or delete an MCP server configuration.""" from urllib.parse import unquote server_name = unquote(server_name) @@ -59,7 +59,7 @@ class MCPRouterGroup(group.RouterGroup): @self.route('/servers//test', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) async def _(server_name: str) -> str: - """测试MCP服务器连接""" + """Test an MCP server connection.""" from urllib.parse import unquote server_name = unquote(server_name) diff --git a/src/langbot/pkg/api/http/service/mcp.py b/src/langbot/pkg/api/http/service/mcp.py index aadbcf11..bcca5604 100644 --- a/src/langbot/pkg/api/http/service/mcp.py +++ b/src/langbot/pkg/api/http/service/mcp.py @@ -137,7 +137,7 @@ class MCPService: 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: - """测试 MCP 服务器连接并返回任务 ID""" + """Test an MCP server connection and return the task ID.""" runtime_mcp_session: RuntimeMCPSession | None = None diff --git a/src/langbot/pkg/pipeline/preproc/preproc.py b/src/langbot/pkg/pipeline/preproc/preproc.py index 9cc3b73f..8898f368 100644 --- a/src/langbot/pkg/pipeline/preproc/preproc.py +++ b/src/langbot/pkg/pipeline/preproc/preproc.py @@ -211,7 +211,7 @@ class PreProcessor(stage.PipelineStage): # time instead of the first message/creation time. conversation.update_time = now - # 设置query + # Attach resolved session state to the query. query.session = session query.prompt = conversation.prompt.copy() query.messages = await self._resolve_history_messages(runner_id, conversation) @@ -292,7 +292,7 @@ class PreProcessor(stage.PipelineStage): if me.base64 is not None: content_list.append(provider_message.ContentElement.from_image_base64(me.base64)) elif isinstance(me, platform_message.Voice): - # 转成文件链接,让下游 runner 上传到目标模型 + # Convert voice input into file content for downstream model upload. if me.base64: content_list.append(provider_message.ContentElement.from_file_base64(me.base64, 'voice.silk')) elif me.url: @@ -334,7 +334,7 @@ class PreProcessor(stage.PipelineStage): runner_config, ) - # =========== 触发事件 PromptPreProcessing + # Emit PromptPreProcessing before the runner receives the query. event = events.PromptPreProcessing( session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}', diff --git a/src/langbot/pkg/plugin/connector.py b/src/langbot/pkg/plugin/connector.py index b929c570..a1b97f8f 100644 --- a/src/langbot/pkg/plugin/connector.py +++ b/src/langbot/pkg/plugin/connector.py @@ -468,7 +468,12 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): ) 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') install_info['plugin_file_key'] = file_key self.ap.logger.info(f'Transfered file {file_key} to plugin runtime') diff --git a/src/langbot/pkg/provider/runners/cozeapi.py b/src/langbot/pkg/provider/runners/cozeapi.py index 00eacaaf..fbc59fcc 100644 --- a/src/langbot/pkg/provider/runners/cozeapi.py +++ b/src/langbot/pkg/provider/runners/cozeapi.py @@ -3,8 +3,6 @@ Legacy Coze API Runner. DEPRECATED: This runner has been migrated to the AgentRunner plugin format. Use the official `langbot/coze-agent` plugin instead. - -Migration target: /home/glwuy/langbot-app/langbot-agent-runner/coze-agent/ """ from __future__ import annotations diff --git a/src/langbot/pkg/provider/runners/dashscopeapi.py b/src/langbot/pkg/provider/runners/dashscopeapi.py index 6b3cf4e3..325d7d84 100644 --- a/src/langbot/pkg/provider/runners/dashscopeapi.py +++ b/src/langbot/pkg/provider/runners/dashscopeapi.py @@ -3,8 +3,6 @@ Legacy DashScope (阿里云百炼) API Runner. DEPRECATED: This runner has been migrated to the AgentRunner plugin format. Use the official `langbot/dashscope-agent` plugin instead. - -Migration target: /home/glwuy/langbot-app/langbot-agent-runner/dashscope-agent/ """ from __future__ import annotations diff --git a/src/langbot/pkg/provider/runners/difysvapi.py b/src/langbot/pkg/provider/runners/difysvapi.py index 40d6c463..f734b924 100644 --- a/src/langbot/pkg/provider/runners/difysvapi.py +++ b/src/langbot/pkg/provider/runners/difysvapi.py @@ -3,8 +3,6 @@ Legacy Dify Service API Runner. DEPRECATED: This runner has been migrated to the AgentRunner plugin format. Use the official `langbot/dify-agent` plugin instead. - -Migration target: /home/glwuy/langbot-app/langbot-agent-runner/dify-agent/ """ from __future__ import annotations diff --git a/src/langbot/pkg/provider/runners/langflowapi.py b/src/langbot/pkg/provider/runners/langflowapi.py index 5bff7891..13849ec9 100644 --- a/src/langbot/pkg/provider/runners/langflowapi.py +++ b/src/langbot/pkg/provider/runners/langflowapi.py @@ -3,8 +3,6 @@ Legacy Langflow API Runner. DEPRECATED: This runner has been migrated to the AgentRunner plugin format. Use the official `langbot/langflow-agent` plugin instead. - -Migration target: /home/glwuy/langbot-app/langbot-agent-runner/langflow-agent/ """ from __future__ import annotations diff --git a/src/langbot/pkg/provider/runners/localagent.py b/src/langbot/pkg/provider/runners/localagent.py index 439021fe..811cf352 100644 --- a/src/langbot/pkg/provider/runners/localagent.py +++ b/src/langbot/pkg/provider/runners/localagent.py @@ -3,8 +3,6 @@ Legacy Local Agent Runner. DEPRECATED: This runner has been migrated to the AgentRunner plugin format. Use the official `langbot/local-agent` plugin instead. - -Migration target: /home/glwuy/langbot-app/langbot-local-agent/ """ from __future__ import annotations diff --git a/src/langbot/pkg/provider/runners/n8nsvapi.py b/src/langbot/pkg/provider/runners/n8nsvapi.py index 252dbe69..87d18d3f 100644 --- a/src/langbot/pkg/provider/runners/n8nsvapi.py +++ b/src/langbot/pkg/provider/runners/n8nsvapi.py @@ -3,8 +3,6 @@ Legacy n8n Service API Runner. DEPRECATED: This runner has been migrated to the AgentRunner plugin format. Use the official `langbot/n8n-agent` plugin instead. - -Migration target: /home/glwuy/langbot-app/langbot-agent-runner/n8n-agent/ """ from __future__ import annotations diff --git a/src/langbot/pkg/provider/runners/tboxapi.py b/src/langbot/pkg/provider/runners/tboxapi.py index fc7a19da..12ce205a 100644 --- a/src/langbot/pkg/provider/runners/tboxapi.py +++ b/src/langbot/pkg/provider/runners/tboxapi.py @@ -3,8 +3,6 @@ Legacy Tbox (蚂蚁百宝箱) API Runner. DEPRECATED: This runner has been migrated to the AgentRunner plugin format. Use the official `langbot/tbox-agent` plugin instead. - -Migration target: /home/glwuy/langbot-app/langbot-agent-runner/tbox-agent/ """ from __future__ import annotations 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/loaders/availability.py b/src/langbot/pkg/provider/tools/loaders/availability.py new file mode 100644 index 00000000..58d79586 --- /dev/null +++ b/src/langbot/pkg/provider/tools/loaders/availability.py @@ -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 diff --git a/src/langbot/pkg/provider/tools/loaders/mcp.py b/src/langbot/pkg/provider/tools/loaders/mcp.py index 42bcb06f..4e12ca42 100644 --- a/src/langbot/pkg/provider/tools/loaders/mcp.py +++ b/src/langbot/pkg/provider/tools/loaders/mcp.py @@ -30,7 +30,7 @@ class MCPSessionStatus(enum.Enum): class RuntimeMCPSession: - """运行时 MCP 会话""" + """Runtime MCP session.""" ap: app.Application @@ -352,12 +352,12 @@ class RuntimeMCPSession: return info async def shutdown(self): - """关闭会话并清理资源""" + """Close the session and release resources.""" try: - # 设置shutdown事件,通知lifecycle任务退出 + # Signal the lifecycle task to exit. self._shutdown_event.set() - # 等待lifecycle任务完成(带超时) + # Wait for the lifecycle task with a bounded timeout. if self._lifecycle_task and not self._lifecycle_task.done(): try: await asyncio.wait_for(self._lifecycle_task, timeout=5.0) @@ -411,9 +411,9 @@ class RuntimeMCPSession: # @loader.loader_class('mcp') class MCPLoader(loader.ToolLoader): - """MCP 工具加载器。 + """MCP tool loader. - 在此加载器中管理所有与 MCP Server 的连接。 + This loader owns all active MCP server connections. """ 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"]})') async def load_mcp_server(self, server_config: dict) -> RuntimeMCPSession: - """加载 MCP 服务器到运行时 + """Load an MCP server into the runtime. Args: - server_config: 服务器配置字典,必须包含: - - name: 服务器名称 - - mode: 连接模式 (stdio/sse/http) - - enable: 是否启用 - - extra_args: 额外的配置参数 (可选) + server_config: Server config dict. Must include: + - name: Server name. + - mode: Connection mode (stdio/sse/http). + - enable: Whether the server is enabled. + - extra_args: Optional extra config. """ uuid_ = server_config.get('uuid') if not uuid_: @@ -518,7 +518,7 @@ class MCPLoader(loader.ToolLoader): return all_functions async def has_tool(self, name: str) -> bool: - """检查工具是否存在""" + """Return whether a loaded MCP tool exists.""" for session in self.sessions.values(): for function in session.get_tools(): if function.name == name: @@ -541,7 +541,7 @@ class MCPLoader(loader.ToolLoader): return None 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 function in session.get_tools(): if function.name == name: @@ -557,7 +557,7 @@ class MCPLoader(loader.ToolLoader): raise ValueError(f'Tool not found: {name}') async def remove_mcp_server(self, server_name: str): - """移除 MCP 服务器""" + """Remove an MCP server from the runtime.""" if server_name not in self.sessions: self.ap.logger.warning(f'MCP server {server_name} not found in sessions, skipping removal') return @@ -567,24 +567,24 @@ class MCPLoader(loader.ToolLoader): self.ap.logger.info(f'Removed MCP server: {server_name}') def get_session(self, server_name: str) -> RuntimeMCPSession | None: - """获取指定名称的 MCP 会话""" + """Get an MCP session by server name.""" return self.sessions.get(server_name) def has_session(self, server_name: str) -> bool: - """检查是否存在指定名称的 MCP 会话""" + """Return whether a session exists for the server name.""" return server_name in self.sessions def get_all_server_names(self) -> list[str]: - """获取所有已加载的 MCP 服务器名称""" + """Return all loaded MCP server names.""" return list(self.sessions.keys()) 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) return len(session.get_tools()) if session else 0 def get_all_servers_info(self) -> dict[str, dict]: - """获取所有服务器的信息""" + """Return runtime information for all loaded MCP servers.""" info = {} for server_name, session in self.sessions.items(): tools = session.get_tools() @@ -598,7 +598,7 @@ class MCPLoader(loader.ToolLoader): return info async def shutdown(self): - """关闭所有工具""" + """Shut down all MCP sessions.""" self.ap.logger.info('Shutting down all MCP sessions...') for server_name, session in list(self.sessions.items()): try: diff --git a/src/langbot/pkg/provider/tools/loaders/native.py b/src/langbot/pkg/provider/tools/loaders/native.py index ca2b3f97..4d5e7eee 100644 --- a/src/langbot/pkg/provider/tools/loaders/native.py +++ b/src/langbot/pkg/provider/tools/loaders/native.py @@ -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 .. import loader +from ..errors import ToolNotFoundError +from .availability import is_box_backend_available from . import skill as skill_loader EXEC_TOOL_NAME = 'exec' @@ -52,18 +54,7 @@ class NativeToolLoader(loader.ToolLoader): async def _check_backend_available(self) -> bool: """Check if the box backend is truly available (not just the runtime).""" - box_service = getattr(self.ap, 'box_service', None) - 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 + return await is_box_backend_available(self.ap) async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]: if not self._is_sandbox_available(): @@ -100,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/skill_authoring.py b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py index 9d0fe6e9..e758ccac 100644 --- a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py +++ b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py @@ -6,6 +6,7 @@ import typing import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from .. import loader +from .availability import is_box_backend_available # Align with Claude Code's Skill tool design: # - 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: """Check if the box backend is truly available (not just the runtime).""" - box_service = getattr(self.ap, 'box_service', None) - 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 + return await is_box_backend_available(self.ap) async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]: if not self._is_available(): diff --git a/src/langbot/pkg/provider/tools/toolmgr.py b/src/langbot/pkg/provider/tools/toolmgr.py index 7e59d56f..35d0c84c 100644 --- a/src/langbot/pkg/provider/tools/toolmgr.py +++ b/src/langbot/pkg/provider/tools/toolmgr.py @@ -6,6 +6,8 @@ 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 .errors import ToolNotFoundError + if TYPE_CHECKING: from ...core import app from langbot.pkg.provider.tools.loaders import ( @@ -128,7 +130,7 @@ class ToolManager: return tools 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): 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) if await self.skill_tool_loader.has_tool(name): 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/src/langbot/pkg/utils/importutil.py b/src/langbot/pkg/utils/importutil.py index a35052a6..a2d28c58 100644 --- a/src/langbot/pkg/utils/importutil.py +++ b/src/langbot/pkg/utils/importutil.py @@ -5,10 +5,10 @@ import typing def import_modules_in_pkg(pkg: typing.Any) -> None: - """ - 导入一个包内的所有模块 + """Import all Python modules inside a package. + Args: - pkg: 要导入的包对象 + pkg: Package object to import from. """ pkg_path = os.path.dirname(pkg.__file__) import_dir(pkg_path)