launch_paas.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. #!/usr/bin/env python3
  2. """
  3. agent67v2/launch_paas.py — 通过 AgentPaaS 运行 lambda v2 🐑
  4. =============================================================
  5. 多智能体版本的 PaaS 部署。注册协调者 + 5个微代理。
  6. 与 agent67/launch_paas.py 保持一致的启动流程:
  7. PaaS 启动 → 创建租户 → 注册代理 → Claude Code CLI 对话 + PaaS 记录
  8. 用法:
  9. # 一键启动(推荐,无需 API Key)
  10. python launch_paas.py all
  11. # 分步执行
  12. python launch_paas.py serve # 启动 PaaS 服务
  13. python launch_paas.py register # 注册全部代理 (协调者 + 5微代理)
  14. python launch_paas.py chat # 通过 PaaS 与 lambda v2 对话
  15. python launch_paas.py chat --claude-code # PaaS 追踪 + Claude Code 执行
  16. python launch_paas.py status # 查看状态
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import json
  21. import os
  22. import shutil
  23. import subprocess
  24. import sys
  25. import time
  26. import urllib.request
  27. import urllib.error
  28. from pathlib import Path
  29. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent # lambdagentpaas/
  30. AGENT_DIR = Path(__file__).resolve().parent # agent67v2/
  31. AGENTS_DIR = AGENT_DIR / "agents"
  32. sys.path.insert(0, str(PROJECT_ROOT))
  33. sys.path.insert(0, str(AGENT_DIR.parent)) # agentexample/
  34. PAAS_URL = os.environ.get("AGENTPAAS_URL", "http://127.0.0.1:8000")
  35. API_KEY = os.environ.get("AGENTPAAS_API_KEY", "")
  36. # ═══════════════════════════════════════════════════════════
  37. # PaaS API Helpers (与 agent67 一致)
  38. # ═══════════════════════════════════════════════════════════
  39. def api_call(method: str, path: str, data: dict = None) -> dict:
  40. """调用 AgentPaaS REST API"""
  41. url = f"{PAAS_URL}/api/v1{path}"
  42. body = json.dumps(data).encode("utf-8") if data else None
  43. req = urllib.request.Request(url, data=body, method=method)
  44. req.add_header("Content-Type", "application/json")
  45. req.add_header("Authorization", f"Bearer {API_KEY}")
  46. try:
  47. with urllib.request.urlopen(req, timeout=300) as resp:
  48. return json.loads(resp.read())
  49. except urllib.error.HTTPError as e:
  50. body = e.read().decode()
  51. print(f" ✗ API Error {e.code}: {body[:300]}")
  52. return {"error": body}
  53. except urllib.error.URLError as e:
  54. print(f" ✗ Connection error: {e}")
  55. return {"error": str(e)}
  56. def wait_for_server(timeout: int = 30) -> bool:
  57. """等待 PaaS 服务就绪"""
  58. for _ in range(timeout):
  59. try:
  60. req = urllib.request.Request(f"{PAAS_URL}/health")
  61. with urllib.request.urlopen(req, timeout=2):
  62. return True
  63. except Exception:
  64. time.sleep(1)
  65. return False
  66. def load_agent_configs() -> dict:
  67. """加载所有 YAML 配置"""
  68. import yaml
  69. configs = {}
  70. # 5 个微代理
  71. for yml_file in sorted(AGENTS_DIR.glob("*.yml")):
  72. with open(yml_file, "r", encoding="utf-8") as f:
  73. configs[yml_file.stem] = yaml.safe_load(f)
  74. # 协调者
  75. orchestrator_path = AGENT_DIR / "orchestrator.yml"
  76. with open(orchestrator_path, "r", encoding="utf-8") as f:
  77. configs["coordinator"] = yaml.safe_load(f)
  78. return configs
  79. # ═══════════════════════════════════════════════════════════
  80. # 命令: serve — 启动 AgentPaaS 服务
  81. # ═══════════════════════════════════════════════════════════
  82. def cmd_serve(args):
  83. """启动 AgentPaaS 服务"""
  84. print("🚀 启动 AgentPaaS 服务...")
  85. proc = subprocess.Popen(
  86. [sys.executable, "-m", "agentpaas", "serve", "--port", str(args.port), "--dev"],
  87. cwd=str(PROJECT_ROOT),
  88. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  89. )
  90. print(f" PID: {proc.pid}")
  91. print(f" URL: http://127.0.0.1:{args.port}")
  92. print(" 等待服务就绪...")
  93. if not wait_for_server(30):
  94. print(" ✗ 服务启动超时")
  95. proc.kill()
  96. return None
  97. print(" ✓ PaaS 服务就绪")
  98. return proc
  99. def cmd_create_tenant(args):
  100. """创建租户并返回 API key"""
  101. print("\n📋 创建租户...")
  102. result = subprocess.run(
  103. [sys.executable, "-m", "agentpaas", "create-tenant", "--name", "agent67v2-lab"],
  104. capture_output=True, text=True, cwd=str(PROJECT_ROOT),
  105. )
  106. for line in result.stdout.splitlines():
  107. print(f" {line}")
  108. if line.startswith("API Key:"):
  109. return line.split(":", 1)[1].strip()
  110. return None
  111. # ═══════════════════════════════════════════════════════════
  112. # 命令: register — 注册全部代理到 PaaS
  113. # ═══════════════════════════════════════════════════════════
  114. def cmd_register(args):
  115. """注册协调者 + 5个微代理到 PaaS"""
  116. configs = load_agent_configs()
  117. agent_ids = {}
  118. # 1. 注册 5 个微代理
  119. print("\n📝 注册 5 个微代理...")
  120. for name, config in configs.items():
  121. if name == "coordinator":
  122. continue
  123. agent_name = config.get("name", name)
  124. print(f" 注册 {agent_name}...", end=" ")
  125. result = api_call("POST", "/agents", {
  126. "name": agent_name,
  127. "description": config.get("description", ""),
  128. "config": config,
  129. "tags": config.get("skill", {}).get("tags", []) + ["agent67v2", "micro-agent"],
  130. "environment": "production",
  131. })
  132. if "agent_id" in result:
  133. agent_ids[agent_name] = result["agent_id"]
  134. print(f"✅ {result['agent_id']}")
  135. else:
  136. print(f"❌ {result.get('error', 'Unknown')[:80]}")
  137. # 2. 注册协调者
  138. print(f"\n 注册协调者...", end=" ")
  139. orch_config = configs["coordinator"]
  140. result = api_call("POST", "/agents", {
  141. "name": "lambda-v2",
  142. "description": orch_config.get("description", ""),
  143. "config": orch_config,
  144. "tags": ["agent67v2", "coordinator", "multi-agent"],
  145. "environment": "production",
  146. "metadata": {"sub_agents": agent_ids},
  147. })
  148. if "agent_id" in result:
  149. agent_ids["coordinator"] = result["agent_id"]
  150. print(f"✅ {result['agent_id']}")
  151. else:
  152. print(f"❌ {result.get('error', 'Unknown')[:80]}")
  153. # 保存映射
  154. mapping_path = AGENT_DIR / ".paas_agent_ids.json"
  155. mapping_path.write_text(json.dumps(agent_ids, indent=2))
  156. print(f"\n ✅ 共注册 {len(agent_ids)} 个代理")
  157. return agent_ids
  158. # ═══════════════════════════════════════════════════════════
  159. # 命令: chat — 通过 PaaS 与 lambda v2 对话
  160. # ═══════════════════════════════════════════════════════════
  161. def cmd_chat(args):
  162. """通过 AgentPaaS 与 lambda v2 对话(交互式)"""
  163. # 加载 agent IDs
  164. mapping_path = AGENT_DIR / ".paas_agent_ids.json"
  165. if not mapping_path.exists():
  166. print("✗ 未找到 agent IDs,请先运行 register")
  167. return
  168. agent_ids = json.loads(mapping_path.read_text())
  169. coordinator_id = agent_ids.get("coordinator")
  170. if not coordinator_id:
  171. print("✗ 未找到协调者 ID,请重新 register")
  172. return
  173. use_claude_code = args.claude_code or not os.environ.get("ANTHROPIC_API_KEY")
  174. print(f"\n{'═' * 60}")
  175. print(f" 🐑 lambda v2 — 多智能体协作版 (通过 AgentPaaS 运行)")
  176. print(f" Coordinator ID: {coordinator_id}")
  177. print(f" 微代理: {', '.join(k for k in agent_ids if k != 'coordinator')}")
  178. if use_claude_code:
  179. print(f" 后端: Claude Code Max Plan (无需 API Key)")
  180. else:
  181. print(f" 后端: AgentPaaS 标准执行 (from_config)")
  182. print(f"{'═' * 60}")
  183. if use_claude_code:
  184. _chat_claude_code(coordinator_id, args)
  185. else:
  186. _chat_paas_api(coordinator_id, args)
  187. def _chat_paas_api(coordinator_id: str, args):
  188. """纯 PaaS API 模式:所有执行都通过 PaaS"""
  189. print("\n 💡 所有请求通过 PaaS API 路由,执行记录自动保存")
  190. print(" 输入 'exit' 退出 | 'runs' 查看执行历史\n")
  191. while True:
  192. try:
  193. user_input = input("You: ").strip()
  194. except (EOFError, KeyboardInterrupt):
  195. print("\n👋 lambda v2 下线了!")
  196. break
  197. if not user_input:
  198. continue
  199. if user_input.lower() in ("exit", "quit", "bye"):
  200. print("👋 lambda v2 下线了!")
  201. break
  202. if user_input.lower() == "runs":
  203. _show_runs(coordinator_id)
  204. continue
  205. print()
  206. result = api_call("POST", f"/agents/{coordinator_id}/run", {
  207. "input": user_input,
  208. "parameters": {},
  209. "context": {},
  210. })
  211. if "error" not in result:
  212. print(f"🐑 lambda v2: {result.get('output', '')}")
  213. usage = result.get("usage", {})
  214. print(f" 📊 [run_id={result['run_id']} | "
  215. f"steps={usage.get('steps', 0)} | "
  216. f"tokens={usage.get('total_tokens', 0)} | "
  217. f"{usage.get('duration_ms', 0)}ms]")
  218. else:
  219. print(f"❌ 执行失败: {result['error'][:300]}")
  220. print()
  221. def _chat_claude_code(coordinator_id: str, args):
  222. """
  223. Claude Code 混合模式 (与 agent67 一致):
  224. - LLM 调用 → claude CLI (Max Plan, 无需 API Key)
  225. - 工具执行 → 本地 Python (微代理)
  226. - 执行追踪 → 写入 PaaS (runs 表)
  227. 兼顾:
  228. ✅ 不需要 API Key
  229. ✅ 每次对话记录都在 PaaS 中追踪
  230. ✅ 版本管理、执行历史由 PaaS 管理
  231. ✅ 多智能体协作
  232. """
  233. from agent67v2.core.coordinator import CoordinatorAssistant
  234. print("\n 💡 LLM 调用走 Claude Code CLI,执行记录同步到 PaaS")
  235. print(" 输入 'exit' 退出 | 'runs' 查看执行历史 | 'trace' 追踪 | 'stats' 统计\n")
  236. # 检测后端
  237. try:
  238. from agent67.core.config import detect_backend
  239. model, use_api, backend = detect_backend()
  240. except ImportError:
  241. # 如果 agent67 不可用,直接检测 claude CLI
  242. if shutil.which("claude"):
  243. model, use_api, backend = "sonnet", False, "claude_code"
  244. print("✅ 使用 Claude Code Max Plan (无需 API Key)")
  245. else:
  246. model, use_api, backend = "sonnet", False, ""
  247. print("⚠️ 未检测到 claude CLI,将使用标准 Lam")
  248. assistant = CoordinatorAssistant(
  249. model=model or "sonnet", use_api=use_api, backend=backend
  250. )
  251. while True:
  252. try:
  253. user_input = input("You: ").strip()
  254. except (EOFError, KeyboardInterrupt):
  255. print("\n👋 lambda v2 下线了!")
  256. break
  257. if not user_input:
  258. continue
  259. if user_input.lower() in ("exit", "quit", "bye"):
  260. print("👋 lambda v2 下线了!")
  261. break
  262. if user_input.lower() == "runs":
  263. _show_runs(coordinator_id)
  264. continue
  265. if user_input.lower() == "trace":
  266. assistant.print_trace()
  267. continue
  268. if user_input.lower() == "stats":
  269. assistant.print_stats()
  270. continue
  271. if user_input.lower() == "skills":
  272. from lambdagent.skills import SkillRegistry
  273. registry = SkillRegistry()
  274. print(f"\n📦 已注册技能 ({len(registry)} 个):")
  275. for name in registry.list_all():
  276. skill = registry.get(name)
  277. print(f" • {name}: {skill.description}")
  278. print()
  279. continue
  280. print()
  281. t0 = time.time()
  282. # 通过 CoordinatorAssistant + 微代理执行
  283. response = assistant.chat(user_input)
  284. duration_ms = int((time.time() - t0) * 1000)
  285. steps = len(assistant.ctx.trace)
  286. # 显示结果(流式模式下已在 chat() 中显示)
  287. is_streaming = hasattr(assistant.brain, 'stream') and assistant.brain.stream
  288. if not is_streaming:
  289. print(f"🐑 lambda v2: {response}")
  290. # 同步执行记录到 PaaS
  291. _record_run_to_paas(coordinator_id, user_input, response, duration_ms,
  292. steps, assistant._total_delegations)
  293. print()
  294. def _record_run_to_paas(coordinator_id: str, input_text: str, output: str,
  295. duration_ms: int, steps: int, delegations: int = 0):
  296. """
  297. 将执行记录写入 PaaS(仅记录,不触发执行)。
  298. 调用 /agents/{id}/runs/record 端点,
  299. 与 /agents/{id}/run 不同,这个端点不会调 from_config/Lam,
  300. 只写入 runs 表。
  301. """
  302. result = api_call("POST", f"/agents/{coordinator_id}/runs/record", {
  303. "input": input_text[:500],
  304. "output": output[:2000],
  305. "status": "completed",
  306. "duration_ms": duration_ms,
  307. "steps": steps,
  308. "source": "claude-code-cli",
  309. "metadata": {"delegations": delegations, "version": "v2-multi-agent"},
  310. })
  311. if "run_id" in result:
  312. run_id = result["run_id"]
  313. print(f" 📊 [PaaS: run_id={run_id} | steps={steps} | "
  314. f"delegations={delegations} | {duration_ms}ms]")
  315. def _show_runs(coordinator_id: str):
  316. """显示最近的执行历史"""
  317. result = api_call("GET", f"/agents/{coordinator_id}/runs?limit=10")
  318. runs = result.get("runs", [])
  319. if not runs:
  320. print(" (暂无执行记录)")
  321. return
  322. print(f"\n📜 最近 {len(runs)} 次执行:")
  323. for r in runs:
  324. status_icon = "✅" if r.get("status") == "completed" else "❌"
  325. print(f" {status_icon} {r['id']} | {r.get('status')} | "
  326. f"{r.get('duration_ms', 0)}ms | {r.get('created_at', '')[:19]}")
  327. if r.get("input"):
  328. print(f" 输入: {r['input'][:80]}")
  329. print()
  330. # ═══════════════════════════════════════════════════════════
  331. # 命令: all — 一键启动
  332. # ═══════════════════════════════════════════════════════════
  333. def cmd_all(args):
  334. """一键: 启动 PaaS + 创建租户 + 注册代理 + 开始对话"""
  335. global API_KEY
  336. proc = cmd_serve(args)
  337. if not proc:
  338. return
  339. try:
  340. # 创建租户
  341. key = cmd_create_tenant(args)
  342. if key:
  343. API_KEY = key
  344. os.environ["AGENTPAAS_API_KEY"] = key
  345. # 注册代理 (协调者 + 5微代理)
  346. agent_ids = cmd_register(args)
  347. if not agent_ids or "coordinator" not in agent_ids:
  348. return
  349. # 开始对话
  350. print(f"\n{'═' * 60}")
  351. print(f" 🐑 lambda v2 已就绪! 多智能体协作模式")
  352. print(f" 协调者 + {len(agent_ids) - 1} 个微代理")
  353. print(f"{'═' * 60}")
  354. cmd_chat(args)
  355. except KeyboardInterrupt:
  356. print("\n\n🛑 中断")
  357. finally:
  358. print("\n关闭 PaaS 服务...")
  359. proc.terminate()
  360. try:
  361. proc.wait(timeout=5)
  362. except subprocess.TimeoutExpired:
  363. proc.kill()
  364. print(" ✓ 服务已关闭")
  365. # ═══════════════════════════════════════════════════════════
  366. # 命令: status — 查看代理状态
  367. # ═══════════════════════════════════════════════════════════
  368. def cmd_status(args):
  369. """查看所有代理在 PaaS 中的状态"""
  370. mapping_path = AGENT_DIR / ".paas_agent_ids.json"
  371. if not mapping_path.exists():
  372. print("✗ 未注册,请先运行: python launch_paas.py register")
  373. return
  374. agent_ids = json.loads(mapping_path.read_text())
  375. print(f"\n📋 lambda v2 🐑 状态查询 ({len(agent_ids)} 个代理)")
  376. for name, aid in agent_ids.items():
  377. role = "🎯 协调者" if name == "coordinator" else "🔧 微代理"
  378. print(f"\n {role}: {name}")
  379. print(f" Agent ID: {aid}")
  380. result = api_call("GET", f"/agents/{aid}")
  381. if "error" not in result:
  382. print(f" 版本: v{result.get('current_version', '?')}")
  383. print(f" 状态: {result.get('status', 'unknown')}")
  384. # 协调者执行历史
  385. coordinator_id = agent_ids.get("coordinator")
  386. if coordinator_id:
  387. _show_runs(coordinator_id)
  388. # ═══════════════════════════════════════════════════════════
  389. # CLI 入口
  390. # ═══════════════════════════════════════════════════════════
  391. def main():
  392. parser = argparse.ArgumentParser(
  393. description="🐑 lambda v2 — 多智能体协作版 (通过 AgentPaaS 运行)",
  394. formatter_class=argparse.RawDescriptionHelpFormatter,
  395. epilog="""
  396. 示例:
  397. python launch_paas.py all # 一键启动 (推荐, 无需 API Key)
  398. python launch_paas.py serve # 只启动 PaaS 服务
  399. python launch_paas.py register # 注册协调者 + 5微代理
  400. python launch_paas.py chat --claude-code # Claude Code 模式对话
  401. python launch_paas.py status # 查看状态
  402. """
  403. )
  404. sub = parser.add_subparsers(dest="command")
  405. # serve
  406. p_serve = sub.add_parser("serve", help="启动 AgentPaaS 服务")
  407. p_serve.add_argument("--port", type=int, default=8000)
  408. # register
  409. sub.add_parser("register", help="注册全部代理到 PaaS")
  410. # chat
  411. p_chat = sub.add_parser("chat", help="通过 PaaS 与 lambda v2 对话")
  412. p_chat.add_argument("--claude-code", action="store_true",
  413. help="使用 Claude Code CLI 作为 LLM 后端")
  414. # all
  415. p_all = sub.add_parser("all", help="一键启动全流程")
  416. p_all.add_argument("--port", type=int, default=8000)
  417. p_all.add_argument("--claude-code", action="store_true", default=True,
  418. help="使用 Claude Code CLI (默认)")
  419. # status
  420. sub.add_parser("status", help="查看代理状态")
  421. args = parser.parse_args()
  422. if not args.command:
  423. parser.print_help()
  424. return
  425. # 确保 claude-code 属性存在
  426. if not hasattr(args, "claude_code"):
  427. args.claude_code = bool(shutil.which("claude"))
  428. commands = {
  429. "serve": lambda: cmd_serve(args),
  430. "register": lambda: cmd_register(args),
  431. "chat": lambda: cmd_chat(args),
  432. "all": lambda: cmd_all(args),
  433. "status": lambda: cmd_status(args),
  434. }
  435. commands[args.command]()
  436. if __name__ == "__main__":
  437. main()