sandbox.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. """
  2. Sandbox Tools (Async)
  3. 通过 HTTP 异步调用沙盒管理服务的客户端库。
  4. """
  5. import json
  6. import httpx
  7. from typing import Optional, List, Dict, Any
  8. from agent.tools import tool, ToolResult, ToolContext
  9. # 服务地址,可根据实际部署情况修改
  10. # SANDBOX_SERVER_URL = "http://192.168.100.20:9998"
  11. SANDBOX_SERVER_URL = "http://61.48.133.26:9998"
  12. # 默认超时时间(秒)
  13. DEFAULT_TIMEOUT = 300.0
  14. @tool(
  15. display={
  16. "zh": {
  17. "name": "创建沙盒环境",
  18. "params": {
  19. "image": "Docker 镜像",
  20. "mem_limit": "内存限制",
  21. "nano_cpus": "CPU 限制",
  22. "ports": "端口列表",
  23. "use_gpu": "启用 GPU",
  24. "gpu_count": "GPU 数量"
  25. }
  26. },
  27. "en": {
  28. "name": "Create Sandbox",
  29. "params": {
  30. "image": "Docker image",
  31. "mem_limit": "Memory limit",
  32. "nano_cpus": "CPU limit",
  33. "ports": "Port list",
  34. "use_gpu": "Enable GPU",
  35. "gpu_count": "GPU count"
  36. }
  37. }
  38. }
  39. )
  40. async def sandbox_create_environment(
  41. image: str = "agent-sandbox:latest",
  42. mem_limit: str = "512m",
  43. nano_cpus: int = 500000000,
  44. ports: Optional[List[int]] = None,
  45. use_gpu: bool = False,
  46. gpu_count: int = -1,
  47. server_url: str = None,
  48. timeout: float = DEFAULT_TIMEOUT,
  49. context: Optional[ToolContext] = None,
  50. ) -> ToolResult:
  51. """
  52. 创建一个隔离的 Docker 开发环境。
  53. Args:
  54. image: Docker 镜像名称,默认为 "agent-sandbox:latest"。
  55. 可以使用其他镜像如 "python:3.12-slim", "node:18-slim" 等。
  56. mem_limit: 容器最大内存限制,默认为 "512m"。
  57. nano_cpus: 容器最大 CPU 限制(纳秒),默认为 500000000(0.5 CPU)。
  58. ports: 需要映射的端口列表,如 [8080, 3000]。
  59. use_gpu: 是否启用 GPU 支持,默认为 False。需要宿主机安装 nvidia-container-toolkit。
  60. gpu_count: 使用的 GPU 数量,-1 表示使用所有可用 GPU,默认为 -1。
  61. server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。
  62. timeout: 请求超时时间(秒),默认 300 秒。
  63. context: 工具上下文
  64. Returns:
  65. ToolResult: 包含沙盒创建结果
  66. """
  67. url = f"{server_url or SANDBOX_SERVER_URL}/api/create_environment"
  68. payload = {
  69. "image": image,
  70. "mem_limit": mem_limit,
  71. "nano_cpus": nano_cpus,
  72. "use_gpu": use_gpu,
  73. "gpu_count": gpu_count
  74. }
  75. if ports:
  76. payload["ports"] = ports
  77. try:
  78. async with httpx.AsyncClient(timeout=timeout) as client:
  79. response = await client.post(url, json=payload)
  80. response.raise_for_status()
  81. data = response.json()
  82. sandbox_id = data.get("sandbox_id", "")
  83. port_mapping = data.get("port_mapping", {})
  84. access_urls = data.get("access_urls", [])
  85. output_parts = [f"沙盒 ID: {sandbox_id}"]
  86. if port_mapping:
  87. output_parts.append(f"端口映射: {json.dumps(port_mapping)}")
  88. if access_urls:
  89. output_parts.append(f"访问地址: {', '.join(access_urls)}")
  90. return ToolResult(
  91. title="沙盒环境创建成功",
  92. output="\n".join(output_parts),
  93. metadata=data
  94. )
  95. except httpx.HTTPStatusError as e:
  96. return ToolResult(
  97. title="沙盒创建失败",
  98. output=f"HTTP 错误: {e.response.status_code}",
  99. error=str(e)
  100. )
  101. except httpx.RequestError as e:
  102. return ToolResult(
  103. title="沙盒创建失败",
  104. output=f"网络请求失败: {str(e)}",
  105. error=str(e)
  106. )
  107. @tool(
  108. display={
  109. "zh": {
  110. "name": "执行沙盒命令",
  111. "params": {
  112. "sandbox_id": "沙盒 ID",
  113. "command": "Shell 命令",
  114. "is_background": "后台执行",
  115. "timeout": "超时时间"
  116. }
  117. },
  118. "en": {
  119. "name": "Run Shell in Sandbox",
  120. "params": {
  121. "sandbox_id": "Sandbox ID",
  122. "command": "Shell command",
  123. "is_background": "Run in background",
  124. "timeout": "Timeout"
  125. }
  126. }
  127. }
  128. )
  129. async def sandbox_run_shell(
  130. sandbox_id: str,
  131. command: str,
  132. is_background: bool = False,
  133. timeout: int = 120,
  134. server_url: str = None,
  135. request_timeout: float = DEFAULT_TIMEOUT,
  136. context: Optional[ToolContext] = None,
  137. ) -> ToolResult:
  138. """
  139. 在指定的沙盒中执行 Shell 命令。
  140. Args:
  141. sandbox_id: 沙盒 ID,由 create_environment 返回。
  142. command: 要执行的 Shell 命令,如 "pip install flask" 或 "python app.py"。
  143. is_background: 是否后台执行,默认为 False。
  144. - False:前台执行,等待命令完成并返回输出
  145. - True:后台执行,适合启动长期运行的服务
  146. timeout: 前台命令的超时时间(秒),默认 120 秒。后台命令不受此限制。
  147. server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。
  148. request_timeout: HTTP 请求超时时间(秒),默认 300 秒。
  149. context: 工具上下文
  150. Returns:
  151. ToolResult: 命令执行结果
  152. """
  153. url = f"{server_url or SANDBOX_SERVER_URL}/api/run_shell"
  154. payload = {
  155. "sandbox_id": sandbox_id,
  156. "command": command,
  157. "is_background": is_background,
  158. "timeout": timeout
  159. }
  160. try:
  161. async with httpx.AsyncClient(timeout=request_timeout) as client:
  162. response = await client.post(url, json=payload)
  163. response.raise_for_status()
  164. data = response.json()
  165. if is_background:
  166. status = data.get("status", "")
  167. message = data.get("message", "")
  168. log_file = data.get("log_file", "")
  169. output = f"状态: {status}\n消息: {message}"
  170. if log_file:
  171. output += f"\n日志文件: {log_file}"
  172. return ToolResult(
  173. title=f"后台命令已启动: {command[:50]}",
  174. output=output,
  175. metadata=data
  176. )
  177. else:
  178. exit_code = data.get("exit_code", -1)
  179. stdout = data.get("stdout", "")
  180. stderr = data.get("stderr", "")
  181. output_parts = []
  182. if stdout:
  183. output_parts.append(stdout)
  184. if stderr:
  185. if output_parts:
  186. output_parts.append("\n--- stderr ---")
  187. output_parts.append(stderr)
  188. if not output_parts:
  189. output_parts.append("(命令无输出)")
  190. success = exit_code == 0
  191. title = f"命令: {command[:50]}"
  192. if not success:
  193. title += f" (exit code: {exit_code})"
  194. return ToolResult(
  195. title=title,
  196. output="\n".join(output_parts),
  197. metadata=data,
  198. error=None if success else f"Command failed with exit code {exit_code}"
  199. )
  200. except httpx.HTTPStatusError as e:
  201. return ToolResult(
  202. title="命令执行失败",
  203. output=f"HTTP 错误: {e.response.status_code}",
  204. error=str(e)
  205. )
  206. except httpx.RequestError as e:
  207. return ToolResult(
  208. title="命令执行失败",
  209. output=f"网络请求失败: {str(e)}",
  210. error=str(e)
  211. )
  212. @tool(
  213. display={
  214. "zh": {
  215. "name": "重建沙盒端口",
  216. "params": {
  217. "sandbox_id": "沙盒 ID",
  218. "ports": "端口列表",
  219. "mem_limit": "内存限制",
  220. "nano_cpus": "CPU 限制",
  221. "use_gpu": "启用 GPU",
  222. "gpu_count": "GPU 数量"
  223. }
  224. },
  225. "en": {
  226. "name": "Rebuild Sandbox Ports",
  227. "params": {
  228. "sandbox_id": "Sandbox ID",
  229. "ports": "Port list",
  230. "mem_limit": "Memory limit",
  231. "nano_cpus": "CPU limit",
  232. "use_gpu": "Enable GPU",
  233. "gpu_count": "GPU count"
  234. }
  235. }
  236. }
  237. )
  238. async def sandbox_rebuild_with_ports(
  239. sandbox_id: str,
  240. ports: List[int],
  241. mem_limit: str = "1g",
  242. nano_cpus: int = 1000000000,
  243. use_gpu: bool = False,
  244. gpu_count: int = -1,
  245. server_url: str = None,
  246. timeout: float = DEFAULT_TIMEOUT,
  247. context: Optional[ToolContext] = None,
  248. ) -> ToolResult:
  249. """
  250. 重建沙盒并应用新的端口映射。
  251. 使用场景:先创建沙盒克隆项目,阅读 README 后才知道需要暴露哪些端口,
  252. 此时调用此函数重建沙盒,应用正确的端口映射。
  253. 注意:重建后会返回新的 sandbox_id,后续操作需要使用新 ID。
  254. 容器内的所有文件(克隆的代码、安装的依赖等)都会保留。
  255. Args:
  256. sandbox_id: 当前沙盒 ID。
  257. ports: 需要映射的端口列表,如 [8080, 3306, 6379]。
  258. mem_limit: 容器最大内存限制,默认为 "1g"。
  259. nano_cpus: 容器最大 CPU 限制(纳秒),默认为 1000000000(1 CPU)。
  260. use_gpu: 是否启用 GPU 支持,默认为 False。需要宿主机安装 nvidia-container-toolkit。
  261. gpu_count: 使用的 GPU 数量,-1 表示使用所有可用 GPU,默认为 -1。
  262. server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。
  263. timeout: 请求超时时间(秒),默认 300 秒。
  264. context: 工具上下文
  265. Returns:
  266. ToolResult: 重建结果
  267. """
  268. url = f"{server_url or SANDBOX_SERVER_URL}/api/rebuild_with_ports"
  269. payload = {
  270. "sandbox_id": sandbox_id,
  271. "ports": ports,
  272. "mem_limit": mem_limit,
  273. "nano_cpus": nano_cpus,
  274. "use_gpu": use_gpu,
  275. "gpu_count": gpu_count
  276. }
  277. try:
  278. async with httpx.AsyncClient(timeout=timeout) as client:
  279. response = await client.post(url, json=payload)
  280. response.raise_for_status()
  281. data = response.json()
  282. old_id = data.get("old_sandbox_id", "")
  283. new_id = data.get("new_sandbox_id", "")
  284. port_mapping = data.get("port_mapping", {})
  285. access_urls = data.get("access_urls", [])
  286. output_parts = [
  287. f"旧沙盒 ID: {old_id} (已销毁)",
  288. f"新沙盒 ID: {new_id}"
  289. ]
  290. if port_mapping:
  291. output_parts.append(f"端口映射: {json.dumps(port_mapping)}")
  292. if access_urls:
  293. output_parts.append(f"访问地址: {', '.join(access_urls)}")
  294. return ToolResult(
  295. title="沙盒重建成功",
  296. output="\n".join(output_parts),
  297. metadata=data
  298. )
  299. except httpx.HTTPStatusError as e:
  300. return ToolResult(
  301. title="沙盒重建失败",
  302. output=f"HTTP 错误: {e.response.status_code}",
  303. error=str(e)
  304. )
  305. except httpx.RequestError as e:
  306. return ToolResult(
  307. title="沙盒重建失败",
  308. output=f"网络请求失败: {str(e)}",
  309. error=str(e)
  310. )
  311. @tool(
  312. requires_confirmation=True,
  313. display={
  314. "zh": {
  315. "name": "销毁沙盒环境",
  316. "params": {
  317. "sandbox_id": "沙盒 ID"
  318. }
  319. },
  320. "en": {
  321. "name": "Destroy Sandbox",
  322. "params": {
  323. "sandbox_id": "Sandbox ID"
  324. }
  325. }
  326. }
  327. )
  328. async def sandbox_destroy_environment(
  329. sandbox_id: str,
  330. server_url: str = None,
  331. timeout: float = DEFAULT_TIMEOUT,
  332. context: Optional[ToolContext] = None,
  333. ) -> ToolResult:
  334. """
  335. 销毁沙盒环境,释放资源。
  336. Args:
  337. sandbox_id: 沙盒 ID。
  338. server_url: 服务地址,默认使用全局配置 SANDBOX_SERVER_URL。
  339. timeout: 请求超时时间(秒),默认 300 秒。
  340. context: 工具上下文
  341. Returns:
  342. ToolResult: 销毁结果
  343. """
  344. url = f"{server_url or SANDBOX_SERVER_URL}/api/destroy_environment"
  345. payload = {
  346. "sandbox_id": sandbox_id
  347. }
  348. try:
  349. async with httpx.AsyncClient(timeout=timeout) as client:
  350. response = await client.post(url, json=payload)
  351. response.raise_for_status()
  352. data = response.json()
  353. status = data.get("status", "")
  354. message = data.get("message", "")
  355. removed_tools = data.get("removed_tools", [])
  356. output_parts = [f"状态: {status}", f"消息: {message}"]
  357. if removed_tools:
  358. output_parts.append(f"已移除的工具: {', '.join(removed_tools)}")
  359. return ToolResult(
  360. title="沙盒环境已销毁",
  361. output="\n".join(output_parts),
  362. metadata=data
  363. )
  364. except httpx.HTTPStatusError as e:
  365. return ToolResult(
  366. title="沙盒销毁失败",
  367. output=f"HTTP 错误: {e.response.status_code}",
  368. error=str(e)
  369. )
  370. except httpx.RequestError as e:
  371. return ToolResult(
  372. title="沙盒销毁失败",
  373. output=f"网络请求失败: {str(e)}",
  374. error=str(e)
  375. )