main.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. """
  2. agentpaas CLI — Command-line interface for managing AgentPaaS platform.
  3. All commands (except serve/create-tenant/provider) call the AgentPaaS HTTP API.
  4. Usage:
  5. agentpaas serve [--dev] [--port 8000]
  6. agentpaas create-tenant --name "My Org"
  7. # Agent management
  8. agentpaas agent list
  9. agentpaas agent create --name my-agent --config agent.yml
  10. agentpaas agent get ag_xxx
  11. agentpaas agent delete ag_xxx
  12. agentpaas agent update ag_xxx --config new.yml
  13. agentpaas agent rollback ag_xxx --version 2
  14. agentpaas agent versions ag_xxx
  15. # Execution
  16. agentpaas run ag_xxx "Your input here"
  17. agentpaas run ag_xxx --input-file task.txt
  18. agentpaas run ag_xxx "input" --stream
  19. # Status & monitoring
  20. agentpaas status
  21. agentpaas status agents
  22. agentpaas status agent ag_xxx
  23. agentpaas health ag_xxx
  24. # Runs & traces
  25. agentpaas runs ag_xxx
  26. agentpaas trace run_xxx
  27. # Keys
  28. agentpaas key create --name prod-key
  29. agentpaas key list
  30. agentpaas key revoke key_xxx
  31. # Billing
  32. agentpaas usage
  33. agentpaas metrics
  34. # Provider management
  35. agentpaas provider list
  36. agentpaas provider add dashscope --api-key sk-xxx
  37. agentpaas provider test dashscope
  38. agentpaas provider models dashscope
  39. agentpaas provider remove xxx
  40. """
  41. from __future__ import annotations
  42. import argparse
  43. import json
  44. import os
  45. import sys
  46. import time
  47. import urllib.request
  48. import urllib.error
  49. import yaml
  50. # ============================================================
  51. # Config: server URL + API key
  52. # ============================================================
  53. CONFIG_FILE = os.path.expanduser("~/.agentpaas/config.json")
  54. PROVIDERS_FILE = os.path.expanduser("~/.agentpaas/providers.json")
  55. DEFAULT_PROVIDERS = {
  56. "anthropic": {"name": "Anthropic", "type": "anthropic", "base_url": "https://api.anthropic.com",
  57. "models": ["claude-sonnet-4-20250514", "claude-opus-4-20250514"], "env_key": "ANTHROPIC_API_KEY"},
  58. "openai": {"name": "OpenAI", "type": "openai", "base_url": "https://api.openai.com/v1",
  59. "models": ["gpt-4o", "gpt-4o-mini"], "env_key": "OPENAI_API_KEY"},
  60. "dashscope": {"name": "DashScope (Qwen)", "type": "openai_compatible",
  61. "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
  62. "models": ["qwen3-max-2026-01-23", "qwen-max", "qwen-plus"], "env_key": "DASHSCOPE_API_KEY"},
  63. "deepseek": {"name": "DeepSeek", "type": "openai_compatible", "base_url": "https://api.deepseek.com/v1",
  64. "models": ["deepseek-chat", "deepseek-reasoner"], "env_key": "DEEPSEEK_API_KEY"},
  65. "zhipu": {"name": "Zhipu AI (GLM)", "type": "openai_compatible",
  66. "base_url": "https://open.bigmodel.cn/api/paas/v4",
  67. "models": ["glm-4-plus", "glm-4-flash"], "env_key": "ZHIPU_API_KEY"},
  68. "moonshot": {"name": "Moonshot (Kimi)", "type": "openai_compatible",
  69. "base_url": "https://api.moonshot.cn/v1",
  70. "models": ["moonshot-v1-128k"], "env_key": "MOONSHOT_API_KEY"},
  71. "ollama": {"name": "Ollama (Local)", "type": "openai_compatible",
  72. "base_url": "http://localhost:11434/v1",
  73. "models": ["llama3", "qwen2"], "env_key": "", "api_key": "ollama"},
  74. }
  75. def _load_config():
  76. if os.path.exists(CONFIG_FILE):
  77. with open(CONFIG_FILE) as f:
  78. return json.load(f)
  79. return {"server": "http://localhost:8000", "api_key": ""}
  80. def _save_config(cfg):
  81. os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
  82. with open(CONFIG_FILE, 'w') as f:
  83. json.dump(cfg, f, indent=2)
  84. def _load_providers():
  85. if os.path.exists(PROVIDERS_FILE):
  86. with open(PROVIDERS_FILE) as f:
  87. return json.load(f)
  88. return dict(DEFAULT_PROVIDERS)
  89. def _save_providers(p):
  90. os.makedirs(os.path.dirname(PROVIDERS_FILE), exist_ok=True)
  91. with open(PROVIDERS_FILE, 'w') as f:
  92. json.dump(p, f, indent=2, ensure_ascii=False)
  93. # ============================================================
  94. # HTTP client helper
  95. # ============================================================
  96. def api(method, path, data=None, stream=False):
  97. """Call AgentPaaS API. Returns parsed JSON or raises."""
  98. cfg = _load_config()
  99. url = "{}{}".format(cfg["server"].rstrip("/"), path)
  100. headers = {"Content-Type": "application/json"}
  101. if cfg.get("api_key"):
  102. headers["Authorization"] = "Bearer {}".format(cfg["api_key"])
  103. body = json.dumps(data, ensure_ascii=False).encode() if data else None
  104. req = urllib.request.Request(url, data=body, headers=headers, method=method)
  105. try:
  106. with urllib.request.urlopen(req, timeout=300) as resp:
  107. raw = resp.read().decode()
  108. return json.loads(raw) if raw else {}
  109. except urllib.error.HTTPError as e:
  110. body = e.read().decode()
  111. try:
  112. err = json.loads(body)
  113. msg = err.get("error", {}).get("message", body)
  114. except Exception:
  115. msg = body
  116. print("Error {}: {}".format(e.code, msg))
  117. sys.exit(1)
  118. except urllib.error.URLError as e:
  119. print("Connection failed: {}".format(e))
  120. print("Is the server running? Try: agentpaas serve --dev")
  121. sys.exit(1)
  122. def pp(data):
  123. """Pretty print JSON."""
  124. print(json.dumps(data, indent=2, ensure_ascii=False))
  125. # ============================================================
  126. # Commands: config
  127. # ============================================================
  128. def cmd_config_set(args):
  129. cfg = _load_config()
  130. if args.server:
  131. cfg["server"] = args.server
  132. if args.api_key:
  133. cfg["api_key"] = args.api_key
  134. _save_config(cfg)
  135. print("Config saved to {}".format(CONFIG_FILE))
  136. print(" Server: {}".format(cfg["server"]))
  137. print(" API Key: {}...".format(cfg["api_key"][:12]) if cfg.get("api_key") else " API Key: (not set)")
  138. def cmd_config_show(args):
  139. cfg = _load_config()
  140. print("Server: {}".format(cfg.get("server", "")))
  141. print("API Key: {}".format("{}...".format(cfg["api_key"][:12]) if cfg.get("api_key") else "(not set)"))
  142. print("Config: {}".format(CONFIG_FILE))
  143. # ============================================================
  144. # Commands: serve / create-tenant
  145. # ============================================================
  146. def cmd_serve(args):
  147. import uvicorn
  148. uvicorn.run("agentpaas.api.app:app", host=args.host, port=args.port, reload=args.dev)
  149. def cmd_create_tenant(args):
  150. from agentpaas.db.session import get_db
  151. from agentpaas.db.models import gen_id, now_utc
  152. from agentpaas.api.middleware.auth import hash_key
  153. import secrets
  154. db = get_db()
  155. tid = gen_id("tn_")
  156. uid = gen_id("usr_")
  157. key = "ap_{}".format(secrets.token_hex(16))
  158. now = now_utc()
  159. db.execute("INSERT INTO tenants (id,name,plan,status,created_at) VALUES (?,?,?,'active',?)", (tid, args.name, args.plan, now))
  160. db.execute("INSERT INTO users (id,tenant_id,email,role,created_at) VALUES (?,?,?,'admin',?)", (uid, tid, args.email, now))
  161. db.execute("INSERT INTO api_keys (id,tenant_id,user_id,key_hash,key_prefix,name,scopes,rate_limit,status,created_at) VALUES (?,?,?,?,?,'admin',?,600,'active',?)",
  162. (gen_id("key_"), tid, uid, hash_key(key), key[:8], json.dumps(["agents:*","keys:*","billing:read","admin:*"]), now))
  163. db.commit()
  164. print("Tenant: {}".format(tid))
  165. print("API Key: {}".format(key))
  166. # Auto-save to config
  167. cfg = _load_config()
  168. cfg["api_key"] = key
  169. _save_config(cfg)
  170. print("API key saved to config. Ready to use.")
  171. # ============================================================
  172. # Commands: agent
  173. # ============================================================
  174. def cmd_agent_list(args):
  175. data = api("GET", "/api/v1/agents")
  176. agents = data.get("agents", [])
  177. if not agents:
  178. print("No agents found.")
  179. return
  180. print("{:<16} {:<20} {:>4} {:<12} {}".format("ID", "Name", "Ver", "Status", "Updated"))
  181. print("-" * 80)
  182. for a in agents:
  183. print("{:<16} {:<20} {:>4} {:<12} {}".format(
  184. a["id"], a["name"][:20], a.get("current_version", "?"),
  185. a.get("status", ""), (a.get("updated_at") or "")[:19]))
  186. def cmd_agent_create(args):
  187. if args.config:
  188. with open(args.config) as f:
  189. config = yaml.safe_load(f)
  190. else:
  191. config = {"type": "simple", "systemPrompt": args.prompt or "You are a helpful assistant.",
  192. "model": {"provider": "dashscope", "name": "qwen3-max-2026-01-23"}}
  193. data = api("POST", "/api/v1/agents", {
  194. "name": args.name, "description": args.description or "",
  195. "config": config, "tags": args.tags.split(",") if args.tags else [],
  196. })
  197. print("Agent created: {}".format(data["agent_id"]))
  198. print("Version: {}".format(data["version"]))
  199. print("Endpoint: {}".format(data["endpoint"]))
  200. def cmd_agent_get(args):
  201. pp(api("GET", "/api/v1/agents/{}".format(args.agent_id)))
  202. def cmd_agent_delete(args):
  203. api("DELETE", "/api/v1/agents/{}".format(args.agent_id))
  204. print("Agent {} deleted.".format(args.agent_id))
  205. def cmd_agent_update(args):
  206. with open(args.config) as f:
  207. config = yaml.safe_load(f)
  208. data = api("PUT", "/api/v1/agents/{}".format(args.agent_id),
  209. {"config": config, "changelog": args.changelog or ""})
  210. print("Updated: {} -> v{}".format(args.agent_id, data.get("version")))
  211. def cmd_agent_rollback(args):
  212. data = api("POST", "/api/v1/agents/{}/rollback".format(args.agent_id),
  213. {"target_version": args.version})
  214. print("Rolled back to v{}".format(args.version))
  215. def cmd_agent_versions(args):
  216. data = api("GET", "/api/v1/agents/{}/versions".format(args.agent_id))
  217. versions = data.get("versions", [])
  218. print("{:>4} {:<8} {:<20} {}".format("Ver", "Current", "Created", "Changelog"))
  219. print("-" * 60)
  220. for v in versions:
  221. cur = "*" if v.get("is_current") else ""
  222. print("{:>4} {:<8} {:<20} {}".format(v["version"], cur, (v.get("created_at") or "")[:19], v.get("changelog", "")[:30]))
  223. # ============================================================
  224. # Commands: run
  225. # ============================================================
  226. def cmd_run(args):
  227. agent_id = args.agent_id
  228. if args.input_file:
  229. with open(args.input_file) as f:
  230. input_text = f.read()
  231. else:
  232. input_text = args.input
  233. if not input_text:
  234. print("Error: provide input text or --input-file")
  235. sys.exit(1)
  236. params = {}
  237. if args.temperature is not None:
  238. params["model.temperature"] = args.temperature
  239. if args.max_steps is not None:
  240. params["react.maxSteps"] = args.max_steps
  241. print("Running agent {}...".format(agent_id))
  242. t0 = time.time()
  243. data = api("POST", "/api/v1/agents/{}/run".format(agent_id),
  244. {"input": input_text, "parameters": params})
  245. elapsed = time.time() - t0
  246. print("\n--- Result ({:.1f}s) ---".format(elapsed))
  247. print(data.get("output", ""))
  248. print("--- Stats ---")
  249. usage = data.get("usage", {})
  250. print(" Run ID: {}".format(data.get("run_id")))
  251. print(" Tokens: {} (in: {}, out: {})".format(
  252. usage.get("total_tokens", 0), usage.get("input_tokens", 0), usage.get("output_tokens", 0)))
  253. print(" Steps: {}".format(usage.get("steps", 0)))
  254. print(" Duration: {}ms".format(usage.get("duration_ms", 0)))
  255. # ============================================================
  256. # Commands: chat (interactive)
  257. # ============================================================
  258. # ── Streaming colors ──
  259. _DIM = "\033[2m"
  260. _CYAN = "\033[36m"
  261. _YELLOW = "\033[33m"
  262. _GREEN = "\033[32m"
  263. _RED = "\033[31m"
  264. _BOLD = "\033[1m"
  265. _RESET = "\033[0m"
  266. def _chat_stream(agent_id, agent_name, input_text):
  267. """Stream agent execution via SSE. Returns final output or None if unavailable."""
  268. cfg = _load_config()
  269. url = "{}/api/v1/agents/{}/run/stream".format(cfg["server"].rstrip("/"), agent_id)
  270. headers = {"Content-Type": "application/json"}
  271. if cfg.get("api_key"):
  272. headers["Authorization"] = "Bearer {}".format(cfg["api_key"])
  273. body = json.dumps({"input": input_text}, ensure_ascii=False).encode()
  274. req = urllib.request.Request(url, data=body, headers=headers, method="POST")
  275. try:
  276. resp = urllib.request.urlopen(req, timeout=1800) # 30min for long agent runs
  277. except Exception:
  278. return None # Fallback to sync
  279. final_output = ""
  280. step_count = 0
  281. current_event = ""
  282. for raw_line in resp:
  283. line = raw_line.decode("utf-8", errors="replace").rstrip("\n").rstrip("\r")
  284. if line.startswith("event: "):
  285. current_event = line[7:]
  286. elif line.startswith("data: "):
  287. try:
  288. data = json.loads(line[6:])
  289. except json.JSONDecodeError:
  290. continue
  291. step = data.get("step", 0)
  292. content = data.get("content", "")
  293. tool = data.get("tool", "")
  294. if current_event == "think":
  295. step_count = step + 1
  296. short = content.split("\n")[0][:120]
  297. print("{} [Step {}] 💭 {}{}".format(_DIM, step + 1, short, _RESET))
  298. elif current_event == "tool_call":
  299. print("{} [Step {}] 🔧 {}{} {}".format(
  300. _CYAN, step + 1, tool, _RESET, _DIM + content[:80] + _RESET))
  301. elif current_event == "tool_result":
  302. short = content.replace("\n", " ")[:100]
  303. print("{} [Step {}] ✅ {} → {}{}".format(
  304. _GREEN, step + 1, tool, short, _RESET))
  305. elif current_event == "error":
  306. print("{} [Step {}] ❌ {}{}".format(_RED, step + 1, content[:200], _RESET))
  307. elif current_event == "answer":
  308. final_output = content
  309. elif current_event == "done":
  310. if data.get("output"):
  311. final_output = data["output"]
  312. steps = data.get("steps", step_count)
  313. tokens = data.get("total_tokens", 0)
  314. print()
  315. print("{}📊 {}: {}{}".format(_BOLD, agent_name, _RESET, final_output))
  316. if tokens:
  317. print("{} [{} steps | {} tokens]{}".format(_DIM, steps, tokens, _RESET))
  318. current_event = ""
  319. elif line == "":
  320. current_event = ""
  321. resp.close()
  322. return final_output
  323. def cmd_chat(args):
  324. """Interactive chat with an agent via PaaS API."""
  325. target = args.target
  326. # Resolve agent: by ID, by name, or auto-detect
  327. if not target:
  328. # Auto: list agents, pick the first (or only) one
  329. data = api("GET", "/api/v1/agents")
  330. agents = data.get("agents", [])
  331. if not agents:
  332. print("No agents found. Create one first:")
  333. print(" agentpaas agent create --name lambda --config agent-config.yml")
  334. return
  335. if len(agents) == 1:
  336. agent = agents[0]
  337. else:
  338. print("Multiple agents found. Select one:\n")
  339. for i, a in enumerate(agents):
  340. print(" [{}] {} — {} (v{})".format(i + 1, a["id"], a["name"], a.get("current_version", "?")))
  341. print()
  342. try:
  343. choice = input("Enter number or agent ID: ").strip()
  344. if choice.isdigit() and 1 <= int(choice) <= len(agents):
  345. agent = agents[int(choice) - 1]
  346. else:
  347. agent = next((a for a in agents if a["id"] == choice), agents[0])
  348. except (EOFError, KeyboardInterrupt):
  349. return
  350. elif target.startswith("ag_"):
  351. # By ID
  352. agent = api("GET", "/api/v1/agents/{}".format(target))
  353. else:
  354. # By name: search
  355. data = api("GET", "/api/v1/agents")
  356. agents = data.get("agents", [])
  357. agent = next((a for a in agents if a.get("name", "").lower() == target.lower()), None)
  358. if not agent:
  359. # Fuzzy match
  360. agent = next((a for a in agents if target.lower() in a.get("name", "").lower()), None)
  361. if not agent:
  362. print("Agent '{}' not found. Available agents:".format(target))
  363. for a in agents:
  364. print(" {} — {}".format(a["id"], a["name"]))
  365. return
  366. agent_id = agent["id"]
  367. agent_name = agent.get("name", agent_id)
  368. agent_version = agent.get("current_version", "?")
  369. print()
  370. print("╔══════════════════════════════════════════════════════════╗")
  371. print("║ 🐑 {} — via AgentPaaS".format(agent_name.ljust(44))[:56] + " ║")
  372. print("║ Agent: {} Version: {}".format(agent_id, agent_version).ljust(56) + " ║")
  373. print("╠══════════════════════════════════════════════════════════╣")
  374. print("║ 输入消息开始对话,exit 退出,history 查看历史 ║")
  375. print("╚══════════════════════════════════════════════════════════╝")
  376. print()
  377. conversation = []
  378. while True:
  379. try:
  380. user_input = input("You: ").strip()
  381. except (EOFError, KeyboardInterrupt):
  382. print("\n👋 对话结束")
  383. break
  384. if not user_input:
  385. continue
  386. if user_input.lower() in ("exit", "quit", "bye"):
  387. print("👋 对话结束")
  388. break
  389. if user_input.lower() == "history":
  390. if not conversation:
  391. print(" (无对话历史)")
  392. else:
  393. for entry in conversation:
  394. role = entry["role"]
  395. content = entry["content"][:150]
  396. print(" [{}] {}".format(role, content))
  397. print()
  398. continue
  399. # Build input with conversation history
  400. if conversation:
  401. history_text = "\n".join(
  402. "[{}] {}".format(e["role"], e["content"][:300])
  403. for e in conversation[-10:] # last 10 turns
  404. )
  405. full_input = "=== 对话历史 ===\n{}\n=== 历史结束 ===\n\n[用户] {}".format(
  406. history_text, user_input
  407. )
  408. else:
  409. full_input = user_input
  410. conversation.append({"role": "用户", "content": user_input})
  411. # Call PaaS API (stream first, fallback to sync)
  412. print()
  413. t0 = time.time()
  414. output = _chat_stream(agent_id, agent_name, full_input)
  415. elapsed = time.time() - t0
  416. if output is None:
  417. # Fallback to sync
  418. data = api("POST", "/api/v1/agents/{}/run".format(agent_id),
  419. {"input": full_input})
  420. elapsed = time.time() - t0
  421. output = data.get("output", "")
  422. usage = data.get("usage", {})
  423. print("📊 {}: {}".format(agent_name, output))
  424. print()
  425. print(" [{:.1f}s | {} tokens | {} steps]".format(
  426. elapsed, usage.get("total_tokens", 0), usage.get("steps", 0)))
  427. else:
  428. print()
  429. print(" [{:.1f}s]".format(elapsed))
  430. print()
  431. conversation.append({"role": agent_name, "content": (output or "")[:500]})
  432. # ============================================================
  433. # Commands: status / health
  434. # ============================================================
  435. def cmd_status(args):
  436. if args.target == "agents":
  437. data = api("GET", "/api/v1/status/agents?sort={}".format(args.sort or "last_run"))
  438. agents = data.get("agents", [])
  439. if not agents:
  440. print("No agents.")
  441. return
  442. print("{:<16} {:<16} {:>5} {:>6} {:>6} {:>8} {:>8} {}".format(
  443. "ID", "Name", "Score", "Runs", "Fail", "Succ%", "Lat(ms)", "Last Run"))
  444. print("-" * 100)
  445. for a in agents:
  446. h = a.get("health", {})
  447. score = h.get("score", -1)
  448. level = h.get("level", "?")
  449. icon = {"healthy": "+", "degraded": "~", "warning": "!", "critical": "X"}.get(level, "?")
  450. print("{:<16} {:<16} [{}]{:>3} {:>6} {:>6} {:>8} {:>8} {}".format(
  451. a["agent_id"], a["name"][:16], icon, score,
  452. a["runs_total"], a["runs_failed"], a["success_rate"],
  453. a["avg_latency_ms"], (a.get("last_run") or "never")[:19]))
  454. elif args.target and args.target.startswith("ag_"):
  455. pp(api("GET", "/api/v1/status/agents/{}".format(args.target)))
  456. else:
  457. data = api("GET", "/api/v1/status")
  458. print("=== AgentPaaS Status ===")
  459. print(" Active agents: {}".format(data.get("agents_active", 0)))
  460. print(" Total runs: {}".format(data.get("runs_total", 0)))
  461. print(" Running now: {}".format(data.get("runs_running", 0)))
  462. print(" Success rate: {}".format(data.get("success_rate", "N/A")))
  463. print(" Avg latency: {}ms".format(data.get("avg_latency_ms", 0)))
  464. print(" Total tokens: {}".format(data.get("total_tokens", 0)))
  465. def cmd_health(args):
  466. data = api("GET", "/api/v1/status/agents/{}/health".format(args.agent_id))
  467. score = data.get("health_score", -1)
  468. level = data.get("health_level", "?")
  469. factors = data.get("factors", {})
  470. icon = {"healthy": "OK", "degraded": "!!", "warning": "!!", "critical": "XX"}.get(level, "??")
  471. print("[{}] Agent {} health: {}/100 ({})".format(icon, args.agent_id, score, level))
  472. for k, v in factors.items():
  473. print(" {}: {}".format(k, v))
  474. # ============================================================
  475. # Commands: runs / trace
  476. # ============================================================
  477. def cmd_runs(args):
  478. data = api("GET", "/api/v1/agents/{}/runs?limit={}".format(args.agent_id, args.limit))
  479. runs = data.get("runs", [])
  480. if not runs:
  481. print("No runs.")
  482. return
  483. print("{:<16} {:<10} {:>8} {:>6} {:>8} {}".format("Run ID", "Status", "Dur(ms)", "Steps", "Tokens", "Created"))
  484. print("-" * 80)
  485. for r in runs:
  486. tokens = (r.get("input_tokens") or 0) + (r.get("output_tokens") or 0)
  487. print("{:<16} {:<10} {:>8} {:>6} {:>8} {}".format(
  488. r["id"], r["status"], r.get("duration_ms", 0), r.get("steps", 0),
  489. tokens, (r.get("created_at") or "")[:19]))
  490. def cmd_trace(args):
  491. pp(api("GET", "/api/v1/traces/{}".format(args.run_id)))
  492. # ============================================================
  493. # Commands: key
  494. # ============================================================
  495. def cmd_key_create(args):
  496. data = api("POST", "/api/v1/auth/keys",
  497. {"name": args.name, "scopes": args.scopes.split(",") if args.scopes else ["agents:read", "agents:execute"],
  498. "rate_limit": args.rate_limit})
  499. print("Key created: {}".format(data.get("key_id")))
  500. print("API Key: {}".format(data.get("api_key")))
  501. print("Store this key securely!")
  502. def cmd_key_list(args):
  503. data = api("GET", "/api/v1/auth/keys")
  504. keys = data.get("keys", [])
  505. print("{:<16} {:<10} {:<16} {:<8} {}".format("ID", "Prefix", "Name", "Status", "Last Used"))
  506. print("-" * 70)
  507. for k in keys:
  508. print("{:<16} {:<10} {:<16} {:<8} {}".format(
  509. k["id"], k["key_prefix"], k.get("name", ""), k["status"],
  510. (k.get("last_used_at") or "never")[:19]))
  511. def cmd_key_revoke(args):
  512. api("DELETE", "/api/v1/auth/keys/{}".format(args.key_id))
  513. print("Key {} revoked.".format(args.key_id))
  514. # ============================================================
  515. # Commands: billing / metrics
  516. # ============================================================
  517. def cmd_usage(args):
  518. data = api("GET", "/api/v1/billing/usage?group_by={}".format(args.group_by))
  519. pp(data)
  520. def cmd_metrics(args):
  521. data = api("GET", "/api/v1/metrics/overview")
  522. pp(data)
  523. # ============================================================
  524. # Commands: provider (local, no API call)
  525. # ============================================================
  526. def cmd_provider_list(args):
  527. providers = _load_providers()
  528. print("{:<15} {:<25} {:<45} {}".format("ID", "Name", "Base URL", "Status"))
  529. print("-" * 100)
  530. for pid, p in providers.items():
  531. env = p.get("env_key", "")
  532. has_key = bool(os.environ.get(env, p.get("api_key", "")))
  533. status = "OK" if has_key else "NO KEY ({})".format(env)
  534. print("{:<15} {:<25} {:<45} [{}]".format(pid, p["name"], p.get("base_url", "")[:45], status))
  535. def cmd_provider_add(args):
  536. providers = _load_providers()
  537. pid = args.provider_id
  538. if pid in providers:
  539. if args.api_key:
  540. providers[pid]["api_key"] = args.api_key
  541. if args.base_url:
  542. providers[pid]["base_url"] = args.base_url
  543. else:
  544. providers[pid] = {
  545. "name": args.name or pid, "type": "openai_compatible",
  546. "base_url": args.base_url or "", "models": args.models.split(",") if args.models else [],
  547. "env_key": "{}_API_KEY".format(pid.upper()),
  548. }
  549. if args.api_key:
  550. providers[pid]["api_key"] = args.api_key
  551. _save_providers(providers)
  552. if args.api_key:
  553. env_key = providers[pid].get("env_key", "")
  554. if env_key:
  555. os.environ[env_key] = args.api_key
  556. print("Provider '{}' configured.".format(pid))
  557. def cmd_provider_test(args):
  558. providers = _load_providers()
  559. p = providers.get(args.provider_id)
  560. if not p:
  561. print("Unknown provider: {}".format(args.provider_id)); return
  562. env_key = p.get("env_key", "")
  563. api_key = args.api_key or os.environ.get(env_key, "") or p.get("api_key", "")
  564. if not api_key:
  565. print("No API key. Set {} or use --api-key".format(env_key)); return
  566. base_url = p.get("base_url", "")
  567. model = args.model or (p.get("models", [""])[0])
  568. prompt = args.prompt or "Say hello in one sentence."
  569. print("Testing {} ({})...".format(args.provider_id, model))
  570. if p.get("type") == "anthropic":
  571. url = "{}/v1/messages".format(base_url.rstrip("/"))
  572. body = json.dumps({"model": model, "max_tokens": 256, "messages": [{"role": "user", "content": prompt}]}).encode()
  573. headers = {"Content-Type": "application/json", "x-api-key": api_key, "anthropic-version": "2023-06-01"}
  574. else:
  575. url = "{}/chat/completions".format(base_url.rstrip("/"))
  576. body = json.dumps({"model": model, "max_tokens": 256, "temperature": 0.3,
  577. "messages": [{"role": "user", "content": prompt}]}).encode()
  578. headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(api_key)}
  579. t0 = time.time()
  580. try:
  581. req = urllib.request.Request(url, data=body, headers=headers)
  582. with urllib.request.urlopen(req, timeout=30) as resp:
  583. data = json.loads(resp.read().decode())
  584. text = data["content"][0]["text"] if p.get("type") == "anthropic" else data["choices"][0]["message"]["content"]
  585. print("OK ({:.1f}s): {}".format(time.time() - t0, text.strip()[:100]))
  586. except Exception as e:
  587. print("FAILED: {}".format(e))
  588. def cmd_provider_remove(args):
  589. providers = _load_providers()
  590. if args.provider_id in providers:
  591. del providers[args.provider_id]
  592. _save_providers(providers)
  593. print("Removed '{}'.".format(args.provider_id))
  594. else:
  595. print("Not found: {}".format(args.provider_id))
  596. def cmd_provider_models(args):
  597. providers = _load_providers()
  598. p = providers.get(args.provider_id)
  599. if not p:
  600. print("Not found."); return
  601. api_key = os.environ.get(p.get("env_key", ""), "") or p.get("api_key", "")
  602. if p.get("type") == "anthropic":
  603. print("Models: {}".format(", ".join(p.get("models", [])))); return
  604. try:
  605. url = "{}/models".format(p["base_url"].rstrip("/"))
  606. req = urllib.request.Request(url, headers={"Authorization": "Bearer {}".format(api_key)})
  607. with urllib.request.urlopen(req, timeout=10) as resp:
  608. data = json.loads(resp.read().decode())
  609. for m in sorted(m["id"] for m in data.get("data", []))[:20]:
  610. print(" {}".format(m))
  611. except Exception as e:
  612. print("Query failed: {}. Configured: {}".format(e, ", ".join(p.get("models", []))))
  613. # ============================================================
  614. # Main
  615. # ============================================================
  616. def main():
  617. parser = argparse.ArgumentParser(prog="agentpaas", description="AgentPaaS CLI — manage agents, runs, providers")
  618. sub = parser.add_subparsers(dest="command")
  619. # config
  620. cfg_p = sub.add_parser("config", help="Configure CLI (server URL, API key)")
  621. cfg_sub = cfg_p.add_subparsers(dest="config_cmd")
  622. cs = cfg_sub.add_parser("set"); cs.add_argument("--server", default=""); cs.add_argument("--api-key", default="")
  623. cfg_sub.add_parser("show")
  624. # serve
  625. s = sub.add_parser("serve", help="Start API server")
  626. s.add_argument("--host", default="0.0.0.0"); s.add_argument("--port", type=int, default=8000); s.add_argument("--dev", action="store_true")
  627. # create-tenant
  628. ct = sub.add_parser("create-tenant", help="Create tenant + API key")
  629. ct.add_argument("--name", required=True); ct.add_argument("--plan", default="free"); ct.add_argument("--email", default="")
  630. # agent
  631. ag = sub.add_parser("agent", help="Agent management")
  632. ag_sub = ag.add_subparsers(dest="agent_cmd")
  633. ag_sub.add_parser("list")
  634. ac = ag_sub.add_parser("create"); ac.add_argument("--name", required=True); ac.add_argument("--config", default=""); ac.add_argument("--prompt", default=""); ac.add_argument("--description", default=""); ac.add_argument("--tags", default="")
  635. ag_get = ag_sub.add_parser("get"); ag_get.add_argument("agent_id")
  636. ag_del = ag_sub.add_parser("delete"); ag_del.add_argument("agent_id")
  637. ag_up = ag_sub.add_parser("update"); ag_up.add_argument("agent_id"); ag_up.add_argument("--config", required=True); ag_up.add_argument("--changelog", default="")
  638. ag_rb = ag_sub.add_parser("rollback"); ag_rb.add_argument("agent_id"); ag_rb.add_argument("--version", type=int, required=True)
  639. ag_ver = ag_sub.add_parser("versions"); ag_ver.add_argument("agent_id")
  640. # run
  641. r = sub.add_parser("run", help="Execute an agent (single shot)")
  642. r.add_argument("agent_id"); r.add_argument("input", nargs="?", default=""); r.add_argument("--input-file", default="")
  643. r.add_argument("--temperature", type=float, default=None); r.add_argument("--max-steps", type=int, default=None)
  644. r.add_argument("--stream", action="store_true")
  645. # chat (interactive)
  646. ch = sub.add_parser("chat", help="Interactive chat with an agent (via PaaS)")
  647. ch.add_argument("target", nargs="?", default="", help="Agent ID (ag_xxx) or name")
  648. # status
  649. st = sub.add_parser("status", help="Platform/agent status")
  650. st.add_argument("target", nargs="?", default=""); st.add_argument("--sort", default="last_run")
  651. # health
  652. hl = sub.add_parser("health", help="Agent health score")
  653. hl.add_argument("agent_id")
  654. # runs
  655. rn = sub.add_parser("runs", help="List runs for an agent")
  656. rn.add_argument("agent_id"); rn.add_argument("--limit", type=int, default=20)
  657. # trace
  658. tr = sub.add_parser("trace", help="Get run trace details")
  659. tr.add_argument("run_id")
  660. # key
  661. ky = sub.add_parser("key", help="API key management")
  662. ky_sub = ky.add_subparsers(dest="key_cmd")
  663. kc = ky_sub.add_parser("create"); kc.add_argument("--name", default="default"); kc.add_argument("--scopes", default=""); kc.add_argument("--rate-limit", type=int, default=60)
  664. ky_sub.add_parser("list")
  665. kr = ky_sub.add_parser("revoke"); kr.add_argument("key_id")
  666. # usage / metrics
  667. us = sub.add_parser("usage", help="Billing usage"); us.add_argument("--group-by", default="agent")
  668. sub.add_parser("metrics", help="Platform metrics")
  669. # provider
  670. pv = sub.add_parser("provider", help="LLM provider management")
  671. pv_sub = pv.add_subparsers(dest="prov_cmd")
  672. pv_sub.add_parser("list")
  673. pa = pv_sub.add_parser("add"); pa.add_argument("provider_id"); pa.add_argument("--api-key", default=""); pa.add_argument("--base-url", default=""); pa.add_argument("--name", default=""); pa.add_argument("--models", default="")
  674. prm = pv_sub.add_parser("remove"); prm.add_argument("provider_id")
  675. pt = pv_sub.add_parser("test"); pt.add_argument("provider_id"); pt.add_argument("--api-key", default=""); pt.add_argument("--model", default=""); pt.add_argument("--prompt", default="")
  676. pm = pv_sub.add_parser("models"); pm.add_argument("provider_id")
  677. args = parser.parse_args()
  678. handlers = {
  679. "config": lambda: cmd_config_set(args) if args.config_cmd == "set" else cmd_config_show(args),
  680. "serve": lambda: cmd_serve(args),
  681. "create-tenant": lambda: cmd_create_tenant(args),
  682. "agent": lambda: {"list": cmd_agent_list, "create": cmd_agent_create, "get": cmd_agent_get,
  683. "delete": cmd_agent_delete, "update": cmd_agent_update,
  684. "rollback": cmd_agent_rollback, "versions": cmd_agent_versions
  685. }.get(args.agent_cmd, lambda a: ag.print_help())(args),
  686. "run": lambda: cmd_run(args),
  687. "chat": lambda: cmd_chat(args),
  688. "status": lambda: cmd_status(args),
  689. "health": lambda: cmd_health(args),
  690. "runs": lambda: cmd_runs(args),
  691. "trace": lambda: cmd_trace(args),
  692. "key": lambda: {"create": cmd_key_create, "list": cmd_key_list, "revoke": cmd_key_revoke
  693. }.get(args.key_cmd, lambda a: ky.print_help())(args),
  694. "usage": lambda: cmd_usage(args),
  695. "metrics": lambda: cmd_metrics(args),
  696. "provider": lambda: {"list": cmd_provider_list, "add": cmd_provider_add, "remove": cmd_provider_remove,
  697. "test": cmd_provider_test, "models": cmd_provider_models
  698. }.get(args.prov_cmd, lambda a: pv.print_help())(args),
  699. }
  700. if args.command in handlers:
  701. handlers[args.command]()
  702. else:
  703. parser.print_help()
  704. print("\nQuick start:")
  705. print(" agentpaas serve --dev # 1. Start server")
  706. print(" agentpaas create-tenant --name Org # 2. Get API key")
  707. print(" agentpaas agent create --name lambda --config agent.yml # 3. Deploy")
  708. print(" agentpaas chat lambda # 4. Interactive chat!")
  709. print()
  710. print("Other commands:")
  711. print(" agentpaas agent list # List agents")
  712. print(" agentpaas run ag_xxx 'Hello' # Single-shot run")
  713. print(" agentpaas status # Platform status")
  714. if __name__ == "__main__":
  715. main()