test_router_api.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. """测试 Router 核心接口
  2. 用法:
  3. uv run python tests/test_router_api.py # 只跑 health check
  4. uv run python tests/test_router_api.py --search # 搜索工具列表
  5. uv run python tests/test_router_api.py --search image # 关键词搜索
  6. uv run python tests/test_router_api.py --status # 工具运行状态
  7. uv run python tests/test_router_api.py --select image_stitcher # 调用指定工具
  8. uv run python tests/test_router_api.py --stitch # 测试图片拼接
  9. uv run python tests/test_router_api.py --create # 默认任务
  10. uv run python tests/test_router_api.py --create image_stitcher # 指定任务文件
  11. """
  12. import argparse
  13. import base64
  14. import json
  15. import sys
  16. import time
  17. from pathlib import Path
  18. import httpx
  19. BASE_URL = "http://43.106.118.91:8001"
  20. TASKS_DIR = Path(__file__).parent / "tasks"
  21. TEST_IMAGES_DIR = TASKS_DIR / "stitcher_images"
  22. OUTPUT_DIR = Path(__file__).parent / "output"
  23. def check_connection():
  24. try:
  25. httpx.get(f"{BASE_URL}/health", timeout=3)
  26. except httpx.ConnectError:
  27. print(f"ERROR: Cannot connect to {BASE_URL}")
  28. print("Please start the service first:")
  29. print(" uv run python -m tool_agent")
  30. sys.exit(1)
  31. def test_health():
  32. print("=== Health Check ===")
  33. resp = httpx.get(f"{BASE_URL}/health")
  34. print(f" Status : {resp.status_code}")
  35. print(f" Body : {json.dumps(resp.json(), ensure_ascii=False, indent=4)}")
  36. assert resp.status_code == 200
  37. print(" [PASS]")
  38. def test_search_tools(keyword: str = None):
  39. print(f"=== Search Tools{f' (keyword={keyword!r})' if keyword else ''} ===")
  40. payload = {"keyword": keyword} if keyword else {}
  41. resp = httpx.post(f"{BASE_URL}/search_tools", json=payload)
  42. print(f" Status : {resp.status_code}")
  43. if resp.status_code != 200:
  44. print(f" Body : {resp.text}")
  45. print(" [FAIL]")
  46. return
  47. data = resp.json()
  48. print(f" Total : {data['total']}")
  49. for t in data["tools"]:
  50. print(f"\n [{t['tool_id']}]")
  51. print(f" name : {t['name']}")
  52. print(f" category : {t.get('category', '')}")
  53. print(f" state : {t['state']}")
  54. print(f" runtime : {t.get('runtime_type', '')} host_dir={t.get('host_dir', '')}")
  55. print(f" endpoint : {t.get('http_method', '')} {t.get('endpoint_path', '')} port={t.get('port')}")
  56. print(f" stream_support: {t.get('stream_support', False)}")
  57. print(f" description : {t.get('description', '')}")
  58. print(f" params ({len(t.get('params', []))}):")
  59. for p in t.get("params", []):
  60. req_mark = "*" if p["required"] else " "
  61. default_str = f" default={p['default']}" if p.get("default") is not None else ""
  62. enum_str = f" enum={p['enum']}" if p.get("enum") else ""
  63. print(f" {req_mark} {p['name']:<25} {p['type']:<12} {p.get('description', '')}{default_str}{enum_str}")
  64. if t.get("output_schema"):
  65. out_props = t["output_schema"].get("properties", {})
  66. print(f" output ({len(out_props)}):")
  67. for oname, odef in out_props.items():
  68. print(f" {oname:<25} {odef.get('type', ''):<12} {odef.get('description', '')}")
  69. print("\n [PASS]")
  70. def test_tools_status():
  71. print("=== Tools Status ===")
  72. resp = httpx.get(f"{BASE_URL}/tools/status")
  73. print(f" Status : {resp.status_code}")
  74. data = resp.json()
  75. print(f" Total : {len(data['tools'])}")
  76. for t in data["tools"]:
  77. print(f" - {t['tool_id']}")
  78. print(f" state : {t['state']}")
  79. print(f" port : {t.get('port')}")
  80. print(f" pid : {t.get('pid')}")
  81. print(f" sources: {[s['type'] for s in t.get('sources', [])]}")
  82. if t.get("last_error"):
  83. print(f" error : {t['last_error']}")
  84. print(" [PASS]")
  85. def test_select_tool(tool_id: str):
  86. print(f"=== Select Tool (tool_id={tool_id!r}) ===")
  87. resp = httpx.post(f"{BASE_URL}/select_tool", json={
  88. "tool_id": tool_id,
  89. "params": {}
  90. }, timeout=30)
  91. print(f" Status : {resp.status_code}")
  92. data = resp.json()
  93. print(f" Result :")
  94. print(f" status: {data.get('status')}")
  95. if data.get("error"):
  96. print(f" error : {data['error']}")
  97. else:
  98. result_str = json.dumps(data.get("result"), ensure_ascii=False, indent=6)
  99. print(f" result: {result_str[:500]}")
  100. print(" [PASS]")
  101. def test_stitch_images():
  102. print("=== Test Image Stitcher ===")
  103. if not TEST_IMAGES_DIR.exists():
  104. print(f" ERROR: Test images directory not found: {TEST_IMAGES_DIR}")
  105. print(" [SKIP]")
  106. return
  107. image_files = sorted(TEST_IMAGES_DIR.glob("*.png"))
  108. if len(image_files) < 2:
  109. print(f" ERROR: Need at least 2 images, found {len(image_files)}")
  110. print(" [SKIP]")
  111. return
  112. print(f" Images : {len(image_files)} found")
  113. images_b64 = []
  114. for img_path in image_files[:6]:
  115. with open(img_path, "rb") as f:
  116. images_b64.append(base64.b64encode(f.read()).decode())
  117. print(f" - {img_path.name}")
  118. print(f" Calling image_stitcher (grid, 2 columns)...")
  119. try:
  120. resp = httpx.post(f"{BASE_URL}/select_tool", json={
  121. "tool_id": "image_stitcher",
  122. "params": {
  123. "images": images_b64,
  124. "direction": "grid",
  125. "columns": 2,
  126. "spacing": 10,
  127. "background_color": "#FFFFFF",
  128. }
  129. }, timeout=60)
  130. print(f" Status : {resp.status_code}")
  131. data = resp.json()
  132. if data["status"] == "success":
  133. result = data["result"]
  134. print(f" Result :")
  135. print(f" width : {result['width']}")
  136. print(f" height: {result['height']}")
  137. OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
  138. output_path = OUTPUT_DIR / "stitched_result.png"
  139. with open(output_path, "wb") as f:
  140. f.write(base64.b64decode(result["image"]))
  141. print(f" saved : {output_path}")
  142. print(" [PASS]")
  143. else:
  144. print(f" ERROR : {data.get('error', 'unknown')}")
  145. print(" [FAIL]")
  146. except httpx.TimeoutException:
  147. print(" ERROR : Request timeout")
  148. print(" [FAIL]")
  149. except Exception as e:
  150. print(f" ERROR : {e}")
  151. print(" [FAIL]")
  152. def load_task_spec(task_name: str) -> dict:
  153. task_file = TASKS_DIR / f"{task_name}.json"
  154. if not task_file.exists():
  155. print(f" ERROR: Task file not found: {task_file}")
  156. print(" Available tasks:")
  157. if TASKS_DIR.exists():
  158. for f in TASKS_DIR.glob("*.json"):
  159. print(f" - {f.stem}")
  160. sys.exit(1)
  161. with open(task_file, "r", encoding="utf-8") as f:
  162. return json.load(f)
  163. def test_create_tool(task_name: str = None):
  164. print(f"=== Create Tool{f' (task={task_name!r})' if task_name else ''} ===")
  165. if task_name:
  166. task_data = load_task_spec(task_name)
  167. print(f" File : tests/tasks/{task_name}.json")
  168. print(f" Description: {task_data['description'][:80]}")
  169. else:
  170. task_data = {"description": "创建一个简单的文本计数工具,输入文本,返回字数和字符数"}
  171. print(f" Description: {task_data['description']}")
  172. resp = httpx.post(f"{BASE_URL}/create_tool", json=task_data)
  173. data = resp.json()
  174. task_id = data["task_id"]
  175. print(f" Task ID : {task_id}")
  176. print(f" Status : {data['status']}")
  177. assert data["status"] == "pending"
  178. print(" [SUBMITTED]")
  179. print(f"\n Polling task {task_id} (timeout 10min)...")
  180. for i in range(120):
  181. time.sleep(5)
  182. resp = httpx.get(f"{BASE_URL}/tasks/{task_id}", timeout=30)
  183. task = resp.json()
  184. status = task["status"]
  185. if i % 6 == 0:
  186. print(f" [{i*5}s] status={status}")
  187. if status == "completed":
  188. print(f"\n Completed!")
  189. print(f" Result : {str(task.get('result', ''))[:300]}")
  190. resp2 = httpx.post(f"{BASE_URL}/search_tools", json={})
  191. tools = resp2.json()["tools"]
  192. print(f" Registered : {[t['tool_id'] for t in tools]}")
  193. print(" [PASS]")
  194. return
  195. if status == "failed":
  196. print(f"\n Failed!")
  197. print(f" Error : {task.get('error', 'unknown')}")
  198. print(" [FAIL]")
  199. return
  200. print(f"\n Timeout after 600s")
  201. print(" [TIMEOUT]")
  202. def main():
  203. parser = argparse.ArgumentParser(description="Router API Test")
  204. parser.add_argument("--search", nargs="?", const="", metavar="KEYWORD",
  205. help="search tools, optional keyword")
  206. parser.add_argument("--status", action="store_true",
  207. help="show tools status")
  208. parser.add_argument("--select", metavar="TOOL_ID",
  209. help="call a tool by tool_id")
  210. parser.add_argument("--stitch", action="store_true",
  211. help="test image stitcher with sample images")
  212. parser.add_argument("--create", nargs="?", const="", metavar="TASK_NAME",
  213. help="create tool, optional task file name")
  214. args = parser.parse_args()
  215. print(f"Target: {BASE_URL}\n")
  216. check_connection()
  217. # 始终跑 health check
  218. test_health()
  219. ran_any = False
  220. if args.search is not None:
  221. print()
  222. test_search_tools(args.search or None)
  223. ran_any = True
  224. if args.status:
  225. print()
  226. test_tools_status()
  227. ran_any = True
  228. if args.select:
  229. print()
  230. test_select_tool(args.select)
  231. ran_any = True
  232. if args.stitch:
  233. print()
  234. test_stitch_images()
  235. ran_any = True
  236. if args.create is not None:
  237. print()
  238. test_create_tool(args.create or None)
  239. ran_any = True
  240. if not ran_any:
  241. print()
  242. print("No test specified. Available options:")
  243. parser.print_help()
  244. print("\n=== DONE ===")
  245. if __name__ == "__main__":
  246. main()