sandbox.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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 import tool
  9. from agent.tools.models import ToolResult
  10. # 服务地址,可根据实际部署情况修改
  11. SANDBOX_SERVER_URL = "http://61.48.133.26:9999"
  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. }
  24. },
  25. "en": {
  26. "name": "Create Sandbox",
  27. "params": {
  28. "image": "Docker image",
  29. "mem_limit": "Memory limit",
  30. "nano_cpus": "CPU limit",
  31. "ports": "Port list"
  32. }
  33. }
  34. }
  35. )
  36. async def sandbox_create_environment(
  37. image: str = "agent-sandbox:latest",
  38. mem_limit: str = "512m",
  39. nano_cpus: int = 500000000,
  40. ports: Optional[List[int]] = None,
  41. uid: str = ""
  42. ) -> ToolResult:
  43. """
  44. 创建一个隔离的 Docker 开发环境。
  45. Args:
  46. image: Docker 镜像名称,默认为 "agent-sandbox:latest"。
  47. 可以使用其他镜像如 "python:3.12-slim", "node:18-slim" 等。
  48. mem_limit: 容器最大内存限制,默认为 "512m"。
  49. nano_cpus: 容器最大 CPU 限制(纳秒),默认为 500000000(0.5 CPU)。
  50. ports: 需要映射的端口列表,如 [8080, 3000]。
  51. uid: 用户ID(自动注入)
  52. Returns:
  53. ToolResult 包含:
  54. - sandbox_id (str): 沙盒唯一标识,后续操作需要用到
  55. - message (str): 提示信息
  56. - port_mapping (dict): 端口映射关系,如 {8080: 32001}
  57. - access_urls (list): 访问 URL 列表
  58. """
  59. try:
  60. url = f"{SANDBOX_SERVER_URL}/api/create_environment"
  61. payload = {
  62. "image": image,
  63. "mem_limit": mem_limit,
  64. "nano_cpus": nano_cpus
  65. }
  66. if ports:
  67. payload["ports"] = ports
  68. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  69. response = await client.post(url, json=payload)
  70. response.raise_for_status()
  71. data = response.json()
  72. return ToolResult(
  73. title="Sandbox Created",
  74. output=json.dumps(data, ensure_ascii=False, indent=2),
  75. long_term_memory=f"Created sandbox: {data.get('sandbox_id', 'unknown')}"
  76. )
  77. except httpx.HTTPStatusError as e:
  78. return ToolResult(
  79. title="Create Sandbox Failed",
  80. output="",
  81. error=f"HTTP error {e.response.status_code}: {e.response.text}"
  82. )
  83. except Exception as e:
  84. return ToolResult(
  85. title="Create Sandbox Failed",
  86. output="",
  87. error=str(e)
  88. )
  89. @tool(
  90. display={
  91. "zh": {
  92. "name": "执行沙盒命令",
  93. "params": {
  94. "sandbox_id": "沙盒 ID",
  95. "command": "Shell 命令",
  96. "is_background": "后台执行",
  97. "timeout": "超时时间"
  98. }
  99. },
  100. "en": {
  101. "name": "Run Shell in Sandbox",
  102. "params": {
  103. "sandbox_id": "Sandbox ID",
  104. "command": "Shell command",
  105. "is_background": "Run in background",
  106. "timeout": "Timeout"
  107. }
  108. }
  109. }
  110. )
  111. async def sandbox_run_shell(
  112. sandbox_id: str,
  113. command: str,
  114. is_background: bool = False,
  115. timeout: int = 120,
  116. uid: str = ""
  117. ) -> ToolResult:
  118. """
  119. 在指定的沙盒中执行 Shell 命令。
  120. Args:
  121. sandbox_id: 沙盒 ID,由 create_environment 返回。
  122. command: 要执行的 Shell 命令,如 "pip install flask" 或 "python app.py"。
  123. is_background: 是否后台执行,默认为 False。
  124. - False:前台执行,等待命令完成并返回输出
  125. - True:后台执行,适合启动长期运行的服务
  126. timeout: 前台命令的超时时间(秒),默认 120 秒。后台命令不受此限制。
  127. uid: 用户ID(自动注入)
  128. Returns:
  129. ToolResult 包含:
  130. 前台执行:
  131. - exit_code (int): 命令退出码
  132. - stdout (str): 标准输出
  133. - stderr (str): 标准错误
  134. 后台执行:
  135. - status (str): 状态
  136. - message (str): 提示信息
  137. - log_file (str): 日志文件路径
  138. """
  139. try:
  140. url = f"{SANDBOX_SERVER_URL}/api/run_shell"
  141. payload = {
  142. "sandbox_id": sandbox_id,
  143. "command": command,
  144. "is_background": is_background,
  145. "timeout": timeout
  146. }
  147. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  148. response = await client.post(url, json=payload)
  149. response.raise_for_status()
  150. data = response.json()
  151. return ToolResult(
  152. title=f"Shell: {command[:50]}{'...' if len(command) > 50 else ''}",
  153. output=json.dumps(data, ensure_ascii=False, indent=2),
  154. long_term_memory=f"Executed in sandbox {sandbox_id}: {command[:100]}"
  155. )
  156. except httpx.HTTPStatusError as e:
  157. return ToolResult(
  158. title="Run Shell Failed",
  159. output="",
  160. error=f"HTTP error {e.response.status_code}: {e.response.text}"
  161. )
  162. except Exception as e:
  163. return ToolResult(
  164. title="Run Shell Failed",
  165. output="",
  166. error=str(e)
  167. )
  168. @tool(
  169. display={
  170. "zh": {
  171. "name": "重建沙盒端口",
  172. "params": {
  173. "sandbox_id": "沙盒 ID",
  174. "ports": "端口列表",
  175. "mem_limit": "内存限制",
  176. "nano_cpus": "CPU 限制"
  177. }
  178. },
  179. "en": {
  180. "name": "Rebuild Sandbox Ports",
  181. "params": {
  182. "sandbox_id": "Sandbox ID",
  183. "ports": "Port list",
  184. "mem_limit": "Memory limit",
  185. "nano_cpus": "CPU limit"
  186. }
  187. }
  188. }
  189. )
  190. async def sandbox_rebuild_with_ports(
  191. sandbox_id: str,
  192. ports: List[int],
  193. mem_limit: str = "1g",
  194. nano_cpus: int = 1000000000,
  195. uid: str = ""
  196. ) -> ToolResult:
  197. """
  198. 重建沙盒并应用新的端口映射。
  199. 使用场景:先创建沙盒克隆项目,阅读 README 后才知道需要暴露哪些端口,
  200. 此时调用此函数重建沙盒,应用正确的端口映射。
  201. 注意:重建后会返回新的 sandbox_id,后续操作需要使用新 ID。
  202. 容器内的所有文件(克隆的代码、安装的依赖等)都会保留。
  203. Args:
  204. sandbox_id: 当前沙盒 ID。
  205. ports: 需要映射的端口列表,如 [8080, 3306, 6379]。
  206. mem_limit: 容器最大内存限制,默认为 "1g"。
  207. nano_cpus: 容器最大 CPU 限制(纳秒),默认为 1000000000(1 CPU)。
  208. uid: 用户ID(自动注入)
  209. Returns:
  210. ToolResult 包含:
  211. - old_sandbox_id (str): 旧沙盒 ID(已销毁)
  212. - new_sandbox_id (str): 新沙盒 ID(后续使用这个)
  213. - port_mapping (dict): 端口映射关系
  214. - access_urls (list): 访问 URL 列表
  215. """
  216. try:
  217. url = f"{SANDBOX_SERVER_URL}/api/rebuild_with_ports"
  218. payload = {
  219. "sandbox_id": sandbox_id,
  220. "ports": ports,
  221. "mem_limit": mem_limit,
  222. "nano_cpus": nano_cpus
  223. }
  224. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  225. response = await client.post(url, json=payload)
  226. response.raise_for_status()
  227. data = response.json()
  228. return ToolResult(
  229. title="Sandbox Rebuilt",
  230. output=json.dumps(data, ensure_ascii=False, indent=2),
  231. long_term_memory=f"Rebuilt sandbox {sandbox_id} -> {data.get('new_sandbox_id', 'unknown')} with ports {ports}"
  232. )
  233. except httpx.HTTPStatusError as e:
  234. return ToolResult(
  235. title="Rebuild Sandbox Failed",
  236. output="",
  237. error=f"HTTP error {e.response.status_code}: {e.response.text}"
  238. )
  239. except Exception as e:
  240. return ToolResult(
  241. title="Rebuild Sandbox Failed",
  242. output="",
  243. error=str(e)
  244. )
  245. @tool(
  246. requires_confirmation=True,
  247. display={
  248. "zh": {
  249. "name": "销毁沙盒环境",
  250. "params": {
  251. "sandbox_id": "沙盒 ID"
  252. }
  253. },
  254. "en": {
  255. "name": "Destroy Sandbox",
  256. "params": {
  257. "sandbox_id": "Sandbox ID"
  258. }
  259. }
  260. }
  261. )
  262. async def sandbox_destroy_environment(
  263. sandbox_id: str,
  264. uid: str = ""
  265. ) -> ToolResult:
  266. """
  267. 销毁沙盒环境,释放资源。
  268. Args:
  269. sandbox_id: 沙盒 ID。
  270. uid: 用户ID(自动注入)
  271. Returns:
  272. ToolResult 包含:
  273. - status (str): 操作状态,如 "success"
  274. - message (str): 提示信息
  275. - removed_tools (list): 被移除的工具列表(如有关联的已注册工具)
  276. """
  277. try:
  278. url = f"{SANDBOX_SERVER_URL}/api/destroy_environment"
  279. payload = {
  280. "sandbox_id": sandbox_id
  281. }
  282. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  283. response = await client.post(url, json=payload)
  284. response.raise_for_status()
  285. data = response.json()
  286. return ToolResult(
  287. title="Sandbox Destroyed",
  288. output=json.dumps(data, ensure_ascii=False, indent=2),
  289. long_term_memory=f"Destroyed sandbox: {sandbox_id}"
  290. )
  291. except httpx.HTTPStatusError as e:
  292. return ToolResult(
  293. title="Destroy Sandbox Failed",
  294. output="",
  295. error=f"HTTP error {e.response.status_code}: {e.response.text}"
  296. )
  297. except Exception as e:
  298. return ToolResult(
  299. title="Destroy Sandbox Failed",
  300. output="",
  301. error=str(e)
  302. )
  303. @tool(
  304. display={
  305. "zh": {
  306. "name": "注册沙盒工具",
  307. "params": {
  308. "tool_name": "工具名称",
  309. "description": "工具描述",
  310. "input_schema": "参数定义",
  311. "sandbox_id": "沙盒 ID",
  312. "internal_port": "内部端口",
  313. "endpoint_path": "API 路径",
  314. "http_method": "HTTP 方法"
  315. }
  316. },
  317. "en": {
  318. "name": "Register Sandbox Tool",
  319. "params": {
  320. "tool_name": "Tool name",
  321. "description": "Tool description",
  322. "input_schema": "Input schema",
  323. "sandbox_id": "Sandbox ID",
  324. "internal_port": "Internal port",
  325. "endpoint_path": "API path",
  326. "http_method": "HTTP method"
  327. }
  328. }
  329. }
  330. )
  331. async def sandbox_register_tool(
  332. tool_name: str,
  333. description: str,
  334. input_schema: Dict[str, Any],
  335. sandbox_id: str,
  336. internal_port: int,
  337. endpoint_path: str = "/",
  338. http_method: str = "POST",
  339. metadata: Optional[Dict[str, Any]] = None,
  340. uid: str = ""
  341. ) -> ToolResult:
  342. """
  343. 将部署好的服务注册为工具。
  344. 注册后,该工具会出现在统一 MCP Server 的工具列表中,可被上游服务调用。
  345. Args:
  346. tool_name: 工具唯一标识(字母开头,只能包含字母、数字、下划线),
  347. 如 "rendercv_api"。
  348. description: 工具描述,描述该工具的功能。
  349. input_schema: JSON Schema 格式的参数定义,定义工具接收的参数。
  350. sandbox_id: 服务所在的沙盒 ID。
  351. internal_port: 服务在容器内的端口。
  352. endpoint_path: API 路径,默认 "/"。
  353. http_method: HTTP 方法,默认 "POST"。
  354. metadata: 额外元数据(可选)。
  355. uid: 用户ID(自动注入)
  356. Returns:
  357. ToolResult 包含:
  358. - status (str): 操作状态,"success" 或 "error"
  359. - message (str): 提示信息
  360. - tool_info (dict): 工具信息(成功时)
  361. """
  362. try:
  363. url = f"{SANDBOX_SERVER_URL}/api/register_tool"
  364. payload = {
  365. "tool_name": tool_name,
  366. "description": description,
  367. "input_schema": input_schema,
  368. "sandbox_id": sandbox_id,
  369. "internal_port": internal_port,
  370. "endpoint_path": endpoint_path,
  371. "http_method": http_method
  372. }
  373. if metadata:
  374. payload["metadata"] = metadata
  375. async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
  376. response = await client.post(url, json=payload)
  377. response.raise_for_status()
  378. data = response.json()
  379. return ToolResult(
  380. title=f"Tool Registered: {tool_name}",
  381. output=json.dumps(data, ensure_ascii=False, indent=2),
  382. long_term_memory=f"Registered tool '{tool_name}' on sandbox {sandbox_id}:{internal_port}"
  383. )
  384. except httpx.HTTPStatusError as e:
  385. return ToolResult(
  386. title="Register Tool Failed",
  387. output="",
  388. error=f"HTTP error {e.response.status_code}: {e.response.text}"
  389. )
  390. except Exception as e:
  391. return ToolResult(
  392. title="Register Tool Failed",
  393. output="",
  394. error=str(e)
  395. )