launch_paas.py 17 KB

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