""" Sandbox Tools (Async) 通过 HTTP 异步调用沙盒管理服务的客户端库。 """ import json import httpx from typing import Optional, List, Dict, Any from agent.tools import tool, ToolResult, ToolContext # 服务地址,可根据实际部署情况修改 # SANDBOX_SERVER_URL = "http://192.168.100.20:9998" SANDBOX_SERVER_URL = "http://61.48.133.26:9998" # 默认超时时间(秒) DEFAULT_TIMEOUT = 300.0 @tool( hidden_params=["context"], display={ "zh": { "name": "创建沙盒环境", "params": { "image": "Docker 镜像", "mem_limit": "内存限制", "nano_cpus": "CPU 限制", "ports": "端口列表", "use_gpu": "启用 GPU", "gpu_count": "GPU 数量" } }, "en": { "name": "Create Sandbox", "params": { "image": "Docker image", "mem_limit": "Memory limit", "nano_cpus": "CPU limit", "ports": "Port list", "use_gpu": "Enable GPU", "gpu_count": "GPU count" } } } ) async def sandbox_create_environment( image: str = "agent-sandbox:latest", mem_limit: str = "512m", nano_cpus: int = 500000000, ports: Optional[List[int]] = None, use_gpu: bool = False, gpu_count: int = -1, server_url: str = None, timeout: float = DEFAULT_TIMEOUT, context: Optional[ToolContext] = None, ) -> ToolResult: """ 创建一个隔离的 Docker 开发环境。 Args: image: Docker 镜像名称,默认为 "agent-sandbox:latest"。 可以使用其他镜像如 "python:3.12-slim", "node:18-slim" 等。 mem_limit: 容器最大内存限制,默认为 "512m"。 nano_cpus: 容器最大 CPU 限制(纳秒),默认为 500000000(0.5 CPU)。 ports: 需要映射的端口列表,如 [8080, 3000]。 use_gpu: 是否启用 GPU 支持,默认为 False。需要宿主机安装 nvidia-container-toolkit。 gpu_count: 使用的 GPU 数量,-1 表示使用所有可用 GPU,默认为 -1。 server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。 timeout: 请求超时时间(秒),默认 300 秒。 context: 工具上下文 Returns: ToolResult: 包含沙盒创建结果 """ url = f"{server_url or SANDBOX_SERVER_URL}/api/create_environment" payload = { "image": image, "mem_limit": mem_limit, "nano_cpus": nano_cpus, "use_gpu": use_gpu, "gpu_count": gpu_count } if ports: payload["ports"] = ports try: async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post(url, json=payload) response.raise_for_status() data = response.json() sandbox_id = data.get("sandbox_id", "") port_mapping = data.get("port_mapping", {}) access_urls = data.get("access_urls", []) output_parts = [f"沙盒 ID: {sandbox_id}"] if port_mapping: output_parts.append(f"端口映射: {json.dumps(port_mapping)}") if access_urls: output_parts.append(f"访问地址: {', '.join(access_urls)}") return ToolResult( title="沙盒环境创建成功", output="\n".join(output_parts), metadata=data ) except httpx.HTTPStatusError as e: return ToolResult( title="沙盒创建失败", output=f"HTTP 错误: {e.response.status_code}", error=str(e) ) except httpx.RequestError as e: return ToolResult( title="沙盒创建失败", output=f"网络请求失败: {str(e)}", error=str(e) ) @tool( hidden_params=["context"], display={ "zh": { "name": "执行沙盒命令", "params": { "sandbox_id": "沙盒 ID", "command": "Shell 命令", "is_background": "后台执行", "timeout": "超时时间" } }, "en": { "name": "Run Shell in Sandbox", "params": { "sandbox_id": "Sandbox ID", "command": "Shell command", "is_background": "Run in background", "timeout": "Timeout" } } } ) async def sandbox_run_shell( sandbox_id: str, command: str, is_background: bool = False, timeout: int = 120, server_url: str = None, request_timeout: float = DEFAULT_TIMEOUT, context: Optional[ToolContext] = None, ) -> ToolResult: """ 在指定的沙盒中执行 Shell 命令。 Args: sandbox_id: 沙盒 ID,由 create_environment 返回。 command: 要执行的 Shell 命令,如 "pip install flask" 或 "python app.py"。 is_background: 是否后台执行,默认为 False。 - False:前台执行,等待命令完成并返回输出 - True:后台执行,适合启动长期运行的服务 timeout: 前台命令的超时时间(秒),默认 120 秒。后台命令不受此限制。 server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。 request_timeout: HTTP 请求超时时间(秒),默认 300 秒。 context: 工具上下文 Returns: ToolResult: 命令执行结果 """ url = f"{server_url or SANDBOX_SERVER_URL}/api/run_shell" payload = { "sandbox_id": sandbox_id, "command": command, "is_background": is_background, "timeout": timeout } try: async with httpx.AsyncClient(timeout=request_timeout) as client: response = await client.post(url, json=payload) response.raise_for_status() data = response.json() if is_background: status = data.get("status", "") message = data.get("message", "") log_file = data.get("log_file", "") output = f"状态: {status}\n消息: {message}" if log_file: output += f"\n日志文件: {log_file}" return ToolResult( title=f"后台命令已启动: {command[:50]}", output=output, metadata=data ) else: exit_code = data.get("exit_code", -1) stdout = data.get("stdout", "") stderr = data.get("stderr", "") output_parts = [] if stdout: output_parts.append(stdout) if stderr: if output_parts: output_parts.append("\n--- stderr ---") output_parts.append(stderr) if not output_parts: output_parts.append("(命令无输出)") success = exit_code == 0 title = f"命令: {command[:50]}" if not success: title += f" (exit code: {exit_code})" return ToolResult( title=title, output="\n".join(output_parts), metadata=data, error=None if success else f"Command failed with exit code {exit_code}" ) except httpx.HTTPStatusError as e: return ToolResult( title="命令执行失败", output=f"HTTP 错误: {e.response.status_code}", error=str(e) ) except httpx.RequestError as e: return ToolResult( title="命令执行失败", output=f"网络请求失败: {str(e)}", error=str(e) ) @tool( hidden_params=["context"], display={ "zh": { "name": "重建沙盒端口", "params": { "sandbox_id": "沙盒 ID", "ports": "端口列表", "mem_limit": "内存限制", "nano_cpus": "CPU 限制", "use_gpu": "启用 GPU", "gpu_count": "GPU 数量" } }, "en": { "name": "Rebuild Sandbox Ports", "params": { "sandbox_id": "Sandbox ID", "ports": "Port list", "mem_limit": "Memory limit", "nano_cpus": "CPU limit", "use_gpu": "Enable GPU", "gpu_count": "GPU count" } } } ) async def sandbox_rebuild_with_ports( sandbox_id: str, ports: List[int], mem_limit: str = "1g", nano_cpus: int = 1000000000, use_gpu: bool = False, gpu_count: int = -1, server_url: str = None, timeout: float = DEFAULT_TIMEOUT, context: Optional[ToolContext] = None, ) -> ToolResult: """ 重建沙盒并应用新的端口映射。 使用场景:先创建沙盒克隆项目,阅读 README 后才知道需要暴露哪些端口, 此时调用此函数重建沙盒,应用正确的端口映射。 注意:重建后会返回新的 sandbox_id,后续操作需要使用新 ID。 容器内的所有文件(克隆的代码、安装的依赖等)都会保留。 Args: sandbox_id: 当前沙盒 ID。 ports: 需要映射的端口列表,如 [8080, 3306, 6379]。 mem_limit: 容器最大内存限制,默认为 "1g"。 nano_cpus: 容器最大 CPU 限制(纳秒),默认为 1000000000(1 CPU)。 use_gpu: 是否启用 GPU 支持,默认为 False。需要宿主机安装 nvidia-container-toolkit。 gpu_count: 使用的 GPU 数量,-1 表示使用所有可用 GPU,默认为 -1。 server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。 timeout: 请求超时时间(秒),默认 300 秒。 context: 工具上下文 Returns: ToolResult: 重建结果 """ url = f"{server_url or SANDBOX_SERVER_URL}/api/rebuild_with_ports" payload = { "sandbox_id": sandbox_id, "ports": ports, "mem_limit": mem_limit, "nano_cpus": nano_cpus, "use_gpu": use_gpu, "gpu_count": gpu_count } try: async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post(url, json=payload) response.raise_for_status() data = response.json() old_id = data.get("old_sandbox_id", "") new_id = data.get("new_sandbox_id", "") port_mapping = data.get("port_mapping", {}) access_urls = data.get("access_urls", []) output_parts = [ f"旧沙盒 ID: {old_id} (已销毁)", f"新沙盒 ID: {new_id}" ] if port_mapping: output_parts.append(f"端口映射: {json.dumps(port_mapping)}") if access_urls: output_parts.append(f"访问地址: {', '.join(access_urls)}") return ToolResult( title="沙盒重建成功", output="\n".join(output_parts), metadata=data ) except httpx.HTTPStatusError as e: return ToolResult( title="沙盒重建失败", output=f"HTTP 错误: {e.response.status_code}", error=str(e) ) except httpx.RequestError as e: return ToolResult( title="沙盒重建失败", output=f"网络请求失败: {str(e)}", error=str(e) ) @tool( requires_confirmation=True, display={ "zh": { "name": "销毁沙盒环境", "params": { "sandbox_id": "沙盒 ID" } }, "en": { "name": "Destroy Sandbox", "params": { "sandbox_id": "Sandbox ID" } } } ) async def sandbox_destroy_environment( sandbox_id: str, server_url: str = None, timeout: float = DEFAULT_TIMEOUT, context: Optional[ToolContext] = None, ) -> ToolResult: """ 销毁沙盒环境,释放资源。 Args: sandbox_id: 沙盒 ID。 server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。 timeout: 请求超时时间(秒),默认 300 秒。 context: 工具上下文 Returns: ToolResult: 销毁结果 """ url = f"{server_url or SANDBOX_SERVER_URL}/api/destroy_environment" payload = { "sandbox_id": sandbox_id } try: async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post(url, json=payload) response.raise_for_status() data = response.json() status = data.get("status", "") message = data.get("message", "") removed_tools = data.get("removed_tools", []) output_parts = [f"状态: {status}", f"消息: {message}"] if removed_tools: output_parts.append(f"已移除的工具: {', '.join(removed_tools)}") return ToolResult( title="沙盒环境已销毁", output="\n".join(output_parts), metadata=data ) except httpx.HTTPStatusError as e: return ToolResult( title="沙盒销毁失败", output=f"HTTP 错误: {e.response.status_code}", error=str(e) ) except httpx.RequestError as e: return ToolResult( title="沙盒销毁失败", output=f"网络请求失败: {str(e)}", error=str(e) )