launch.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. """
  2. agentpaas launch — 一键启动 agent
  3. 流程:
  4. 1. 启动 PaaS 服务 (后台)
  5. 2. 创建租户 + API Key (如果需要)
  6. 3. 注册 agent (从 YAML 配置)
  7. 4. 开始对话 (支持语音 + 文字)
  8. 用法:
  9. agentpaas launch agent-config.yml
  10. agentpaas launch agent-config.yml --daemon
  11. agentpaas launch agent-config.yml --port 9000
  12. """
  13. from __future__ import annotations
  14. import json
  15. import os
  16. import secrets
  17. import signal
  18. import subprocess
  19. import sys
  20. import time
  21. import urllib.request
  22. import urllib.error
  23. from pathlib import Path
  24. def _wait_for_server(url, timeout=30):
  25. """等待 PaaS 服务就绪。"""
  26. for _ in range(timeout):
  27. try:
  28. req = urllib.request.Request("{}/health".format(url))
  29. with urllib.request.urlopen(req, timeout=2):
  30. return True
  31. except Exception:
  32. time.sleep(1)
  33. return False
  34. def _api_call(base_url, api_key, method, path, data=None):
  35. """调用 PaaS API。"""
  36. url = "{}{}".format(base_url.rstrip("/"), path)
  37. headers = {"Content-Type": "application/json"}
  38. if api_key:
  39. headers["Authorization"] = "Bearer {}".format(api_key)
  40. body = json.dumps(data, ensure_ascii=False).encode() if data else None
  41. req = urllib.request.Request(url, data=body, headers=headers, method=method)
  42. try:
  43. with urllib.request.urlopen(req, timeout=30) as resp:
  44. return json.loads(resp.read().decode())
  45. except urllib.error.HTTPError as e:
  46. err_body = e.read().decode()
  47. return {"error": err_body}
  48. except urllib.error.URLError as e:
  49. return {"error": str(e)}
  50. def _start_server(port, dev=False):
  51. """后台启动 PaaS 服务,返回进程。"""
  52. print("🚀 启动 AgentPaaS 服务 (port {})...".format(port))
  53. proc = subprocess.Popen(
  54. [sys.executable, "-m", "agentpaas", "serve", "--port", str(port)]
  55. + (["--dev"] if dev else []),
  56. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  57. )
  58. print(" PID: {}".format(proc.pid))
  59. if not _wait_for_server("http://127.0.0.1:{}".format(port)):
  60. print(" ✗ 服务启动超时")
  61. proc.kill()
  62. return None
  63. print(" ✓ 服务就绪")
  64. return proc
  65. def _ensure_tenant(port):
  66. """确保有租户和 API Key,返回 api_key。"""
  67. from agentpaas.__main__ import _load_config, _save_config
  68. cfg = _load_config()
  69. if cfg.get("api_key"):
  70. # 测试现有 key 是否有效
  71. result = _api_call(
  72. "http://127.0.0.1:{}".format(port), cfg["api_key"],
  73. "GET", "/api/v1/agents")
  74. if "error" not in result:
  75. return cfg["api_key"]
  76. # 创建新租户
  77. print("\n📋 创建租户...")
  78. from agentpaas.db.session import get_db
  79. from agentpaas.db.models import gen_id, now_utc
  80. from agentpaas.api.middleware.auth import hash_key
  81. db = get_db()
  82. tid = gen_id("tn_")
  83. uid = gen_id("usr_")
  84. key = "ap_{}".format(secrets.token_hex(16))
  85. now = now_utc()
  86. db.execute(
  87. "INSERT INTO tenants (id,name,plan,status,created_at) VALUES (?,?,?,'active',?)",
  88. (tid, "launch", "free", now))
  89. db.execute(
  90. "INSERT INTO users (id,tenant_id,email,role,created_at) VALUES (?,?,?,'admin',?)",
  91. (uid, tid, "", now))
  92. db.execute(
  93. "INSERT INTO api_keys (id,tenant_id,user_id,key_hash,key_prefix,name,scopes,rate_limit,status,created_at) "
  94. "VALUES (?,?,?,?,?,'admin',?,600,'active',?)",
  95. (gen_id("key_"), tid, uid, hash_key(key), key[:8],
  96. json.dumps(["agents:*", "keys:*", "billing:read", "admin:*"]), now))
  97. db.commit()
  98. cfg["server"] = "http://127.0.0.1:{}".format(port)
  99. cfg["api_key"] = key
  100. _save_config(cfg)
  101. print(" ✓ 租户已创建, API Key: {}...".format(key[:12]))
  102. return key
  103. def _register_agent(port, api_key, config_path):
  104. """注册 agent 到 PaaS,返回 agent_id。"""
  105. import yaml
  106. config_path = Path(config_path).resolve()
  107. if not config_path.exists():
  108. print("✗ 配置文件不存在: {}".format(config_path))
  109. return None
  110. with open(config_path, "r", encoding="utf-8") as f:
  111. config = yaml.safe_load(f)
  112. name = config.get("name", config_path.stem)
  113. desc = config.get("description", "")
  114. tags = config.get("tags", [])
  115. if isinstance(tags, str):
  116. tags = [t.strip() for t in tags.split(",")]
  117. print("\n📝 注册 agent: {}".format(name))
  118. result = _api_call(
  119. "http://127.0.0.1:{}".format(port), api_key,
  120. "POST", "/api/v1/agents", {
  121. "name": name,
  122. "description": desc,
  123. "config": config,
  124. "tags": tags,
  125. })
  126. if "agent_id" in result:
  127. agent_id = result["agent_id"]
  128. version = result["version"]
  129. print(" ✓ Agent ID: {} (v{})".format(agent_id, version))
  130. return agent_id
  131. else:
  132. print(" ✗ 注册失败: {}".format(result.get("error", "")))
  133. return None
  134. def _chat_loop(agent_id, port, api_key, claude_code=True):
  135. """交互式对话循环(文字模式)。"""
  136. # 尝试加载 agent67 的 PersonalAssistant (Claude Code 模式)
  137. assistant = None
  138. if claude_code:
  139. try:
  140. project_root = Path(__file__).resolve().parent.parent
  141. agent_dir = project_root / "agentexample"
  142. if str(agent_dir) not in sys.path:
  143. sys.path.insert(0, str(agent_dir))
  144. if str(project_root) not in sys.path:
  145. sys.path.insert(0, str(project_root))
  146. # 从 PaaS 获取配置
  147. result = _api_call(
  148. "http://127.0.0.1:{}".format(port), api_key,
  149. "GET", "/api/v1/agents/{}".format(agent_id))
  150. config = result.get("config", {})
  151. system_prompt = config.get("systemPrompt", "")
  152. from agent67.core.claude_lam import ClaudeLam
  153. from agent67.core.assistant import PersonalAssistant
  154. from lambdagent import Context
  155. assistant = PersonalAssistant.__new__(PersonalAssistant)
  156. assistant.brain = ClaudeLam(
  157. "launch_agent", prompt=system_prompt,
  158. model="sonnet", max_tokens=config.get("model", {}).get("maxTokens", 4096))
  159. assistant.ctx = Context()
  160. assistant.conversation_history = []
  161. assistant.max_history = config.get("memory", {}).get("size", 20)
  162. except Exception as e:
  163. print(" ⚠️ Claude Code 模式不可用 ({}), 使用 PaaS API 模式".format(e))
  164. assistant = None
  165. mode = "Claude Code CLI" if assistant else "PaaS API"
  166. print("\n" + "═" * 55)
  167. print(" 对话已就绪! Agent: {}".format(agent_id))
  168. print(" 后端: {}".format(mode))
  169. print(" 输入 exit 退出 | runs 查看历史")
  170. print("═" * 55 + "\n")
  171. while True:
  172. try:
  173. user_input = input("You: ").strip()
  174. except (EOFError, KeyboardInterrupt):
  175. print("\n👋 再见!")
  176. break
  177. if not user_input:
  178. continue
  179. if user_input.lower() in ("exit", "quit", "bye"):
  180. print("👋 再见!")
  181. break
  182. if user_input.lower() == "runs":
  183. result = _api_call(
  184. "http://127.0.0.1:{}".format(port), api_key,
  185. "GET", "/api/v1/agents/{}/runs?limit=10".format(agent_id))
  186. runs = result.get("runs", [])
  187. if runs:
  188. for r in runs:
  189. print(" {} | {} | {}ms | {}".format(
  190. r["id"], r.get("status"), r.get("duration_ms", 0),
  191. (r.get("created_at") or "")[:19]))
  192. else:
  193. print(" (暂无记录)")
  194. continue
  195. if user_input.lower() == "trace" and assistant:
  196. assistant.print_trace()
  197. continue
  198. print()
  199. t0 = time.time()
  200. if assistant:
  201. response = assistant.chat(user_input)
  202. duration_ms = int((time.time() - t0) * 1000)
  203. steps = len(assistant.ctx.trace)
  204. # 回写到 PaaS
  205. _api_call(
  206. "http://127.0.0.1:{}".format(port), api_key,
  207. "POST", "/api/v1/agents/{}/runs/record".format(agent_id), {
  208. "input": user_input[:500], "output": response[:2000],
  209. "status": "completed", "duration_ms": duration_ms,
  210. "steps": steps, "source": "launch-claude-code",
  211. })
  212. else:
  213. result = _api_call(
  214. "http://127.0.0.1:{}".format(port), api_key,
  215. "POST", "/api/v1/agents/{}/run".format(agent_id),
  216. {"input": user_input})
  217. response = result.get("output", result.get("error", "(无回复)"))
  218. duration_ms = int((time.time() - t0) * 1000)
  219. print("🐑 lambda: {}".format(response))
  220. print(" [{:.1f}s]\n".format(duration_ms / 1000))
  221. # ── 公开入口 ────────────────────────────────────────────────
  222. def cmd_launch(config_path, port=8000):
  223. """一键启动: 服务 + 注册 + 对话。退出时自动关闭服务。"""
  224. proc = _start_server(port)
  225. if not proc:
  226. return
  227. try:
  228. api_key = _ensure_tenant(port)
  229. agent_id = _register_agent(port, api_key, config_path)
  230. if not agent_id:
  231. return
  232. _chat_loop(agent_id, port, api_key)
  233. except KeyboardInterrupt:
  234. print("\n\n🛑 中断")
  235. finally:
  236. print("\n关闭 PaaS 服务...")
  237. proc.terminate()
  238. try:
  239. proc.wait(timeout=5)
  240. except subprocess.TimeoutExpired:
  241. proc.kill()
  242. print(" ✓ 已关闭")
  243. LAUNCH_PID_FILE = Path.home() / ".agentpaas" / "launch_daemon.pid"
  244. LAUNCH_INFO_FILE = Path.home() / ".agentpaas" / "launch_daemon.json"
  245. def cmd_launch_daemon(config_path, port=8000):
  246. """后台启动: 服务 + 注册 agent,然后返回。不进入对话。
  247. 之后可以用 wechat / 其他终端 chat 连接。"""
  248. proc = _start_server(port)
  249. if not proc:
  250. return
  251. api_key = _ensure_tenant(port)
  252. agent_id = _register_agent(port, api_key, config_path)
  253. if not agent_id:
  254. proc.kill()
  255. return
  256. # 保存 daemon 信息
  257. LAUNCH_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
  258. LAUNCH_PID_FILE.write_text(str(proc.pid))
  259. LAUNCH_INFO_FILE.write_text(json.dumps({
  260. "pid": proc.pid,
  261. "port": port,
  262. "agent_id": agent_id,
  263. "api_key": api_key,
  264. "config": str(Path(config_path).resolve()),
  265. "started_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
  266. }, indent=2))
  267. print("\n✅ 后台运行中")
  268. print(" PID: {}".format(proc.pid))
  269. print(" Port: {}".format(port))
  270. print(" Agent: {}".format(agent_id))
  271. print(" API: http://127.0.0.1:{}".format(port))
  272. print()
  273. print("连接方式:")
  274. print(" agentpaas run {} 'Hello' # 单次调用".format(agent_id))
  275. print(" agentpaas wechat start {} # 微信桥接".format(agent_id))
  276. print()
  277. print("停止: kill {}".format(proc.pid))