launch_paas.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. #!/usr/bin/env python3
  2. """
  3. agentbuilder67/launch_paas.py — 多智能体 AgentBuilder 启动器
  4. =============================================================
  5. 用户输入一句话描述 → 6 个智能体协作 → 输出可部署的 agent YAML 配置
  6. 两种运行模式:
  7. 1. AgentPaaS API 模式: 启动 PaaS 服务,注册 builder agents,通过 REST API 执行
  8. 2. Claude Code CLI 模式: 利用 Claude CLI 作为 LLM 后端,本地执行多阶段流水线
  9. 用法:
  10. # 模式 1: PaaS API
  11. python launch_paas.py serve # 启动服务
  12. python launch_paas.py register # 注册所有 builder agents
  13. python launch_paas.py build "做一个股票分析agent" # 通过 API 构建
  14. # 模式 2: Claude Code CLI (推荐)
  15. python launch_paas.py claude-code "做一个能分析CSV数据的agent"
  16. python launch_paas.py claude-code --interactive # 交互模式
  17. # 优化已部署的 agent
  18. python launch_paas.py optimize <agent_id>
  19. """
  20. from __future__ import annotations
  21. import argparse
  22. import json
  23. import os
  24. import subprocess
  25. import sys
  26. import time
  27. from datetime import datetime
  28. from pathlib import Path
  29. from typing import Dict, List, Optional
  30. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
  31. BUILDER_DIR = Path(__file__).resolve().parent
  32. sys.path.insert(0, str(PROJECT_ROOT))
  33. PAAS_URL = os.environ.get("AGENTPAAS_URL", "http://127.0.0.1:8000")
  34. API_KEY = os.environ.get("AGENTPAAS_API_KEY", "")
  35. # ── Builder Agent 配置 ────────────────────────────────────
  36. AGENT_CONFIGS = [
  37. ("builder-orchestrator", "AgentBuilder 总指挥", BUILDER_DIR / "orchestrator.yml"),
  38. ("builder-analyst", "需求分析师", BUILDER_DIR / "agents" / "analyst.yml"),
  39. ("builder-retriever", "配置检索员", BUILDER_DIR / "agents" / "retriever.yml"),
  40. ("builder-prompter", "Prompt 工匠", BUILDER_DIR / "agents" / "prompter.yml"),
  41. ("builder-assembler", "配置组装师", BUILDER_DIR / "agents" / "assembler.yml"),
  42. ("builder-critic", "配置审查官", BUILDER_DIR / "agents" / "critic.yml"),
  43. ("builder-optimizer", "持续优化器", BUILDER_DIR / "agents" / "optimizer.yml"),
  44. ]
  45. # ── 运行参数 ──────────────────────────────────────────────
  46. MAX_REVISION_ROUNDS = 3 # Critic 不通过时最大修正轮数
  47. MAX_TURNS_PER_PHASE = 20 # 每个 agent 最大工具调用轮数
  48. PHASE_TIMEOUT = 300 # 每个 phase 超时 (5 分钟,builder 比 research 快)
  49. API_RETRY_MAX = 3 # API 失败最大重试次数
  50. API_RETRY_BACKOFF = 15 # 重试间隔基数 (秒)
  51. PHASE_COOLDOWN = 5 # 阶段间冷却 (秒)
  52. CRITIC_PASS_SCORE = 7.0 # Critic 通过分数线
  53. # ═══════════════════════════════════════════════════════════
  54. # 1. PaaS API Helpers
  55. # ═══════════════════════════════════════════════════════════
  56. def api_call(method: str, path: str, data: dict = None) -> dict:
  57. import urllib.request
  58. import urllib.error
  59. url = f"{PAAS_URL}/api/v1{path}"
  60. body = json.dumps(data).encode("utf-8") if data else None
  61. req = urllib.request.Request(url, data=body, method=method)
  62. req.add_header("Content-Type", "application/json")
  63. if API_KEY:
  64. req.add_header("Authorization", f"Bearer {API_KEY}")
  65. try:
  66. with urllib.request.urlopen(req, timeout=600) as resp:
  67. return json.loads(resp.read())
  68. except urllib.error.HTTPError as e:
  69. body = e.read().decode()
  70. print(f" API Error {e.code}: {body[:500]}")
  71. return {"error": body}
  72. except urllib.error.URLError as e:
  73. print(f" Connection error: {e}")
  74. return {"error": str(e)}
  75. def wait_for_server(timeout: int = 30) -> bool:
  76. import urllib.request
  77. for _ in range(timeout):
  78. try:
  79. req = urllib.request.Request(f"{PAAS_URL}/health")
  80. with urllib.request.urlopen(req, timeout=2):
  81. return True
  82. except Exception:
  83. time.sleep(1)
  84. return False
  85. # ═══════════════════════════════════════════════════════════
  86. # 2. 命令: serve — 启动 AgentPaaS 服务
  87. # ═══════════════════════════════════════════════════════════
  88. def cmd_serve(args):
  89. print("Starting AgentPaaS server...")
  90. proc = subprocess.Popen(
  91. [sys.executable, "-m", "agentpaas", "serve", "--port", str(args.port), "--dev"],
  92. cwd=str(PROJECT_ROOT),
  93. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  94. )
  95. print(f" PID: {proc.pid}")
  96. print(f" URL: http://127.0.0.1:{args.port}")
  97. if not wait_for_server(30):
  98. print(" ERROR: Server failed to start within 30s")
  99. proc.kill()
  100. return None
  101. print(" ✓ Server ready")
  102. return proc
  103. # ═══════════════════════════════════════════════════════════
  104. # 3. 命令: register — 注册所有 builder agents
  105. # ═══════════════════════════════════════════════════════════
  106. def cmd_register(args):
  107. import yaml
  108. print(f"\nRegistering {len(AGENT_CONFIGS)} builder agents to AgentPaaS...")
  109. agent_ids = {}
  110. for agent_id, name, yml_path in AGENT_CONFIGS:
  111. with open(yml_path, "r", encoding="utf-8") as f:
  112. config = yaml.safe_load(f)
  113. result = api_call("POST", "/agents", {
  114. "name": name,
  115. "description": config.get("description", ""),
  116. "config": config,
  117. "tags": ["agentbuilder", config.get("type", "react")],
  118. "environment": "production",
  119. })
  120. if "agent_id" in result:
  121. agent_ids[agent_id] = result["agent_id"]
  122. print(f" ✓ {name:20s} → {result['agent_id']} (v{result['version']})")
  123. else:
  124. print(f" ✗ {name:20s} → {result.get('error', 'Unknown error')}")
  125. mapping_path = BUILDER_DIR / ".paas_agent_ids.json"
  126. mapping_path.write_text(json.dumps(agent_ids, indent=2), encoding="utf-8")
  127. print(f"\n Agent IDs saved to {mapping_path}")
  128. return agent_ids
  129. # ═══════════════════════════════════════════════════════════
  130. # 4. 命令: build — 通过 PaaS API 构建 agent
  131. # ═══════════════════════════════════════════════════════════
  132. def cmd_build(args):
  133. mapping_path = BUILDER_DIR / ".paas_agent_ids.json"
  134. if not mapping_path.exists():
  135. print("ERROR: No agent ID mapping. Run 'register' first.")
  136. return
  137. agent_ids = json.loads(mapping_path.read_text())
  138. orchestrator_id = agent_ids.get("builder-orchestrator")
  139. if not orchestrator_id:
  140. print("ERROR: Orchestrator not registered.")
  141. return
  142. user_desc = args.description
  143. print(f"\n{'═'*60}")
  144. print(f" AgentBuilder — building agent from description")
  145. print(f" Input: {user_desc[:80]}...")
  146. print(f"{'═'*60}\n")
  147. result = api_call("POST", f"/agents/{orchestrator_id}/run", {
  148. "input": user_desc,
  149. "parameters": {},
  150. "context": {"sub_agent_ids": agent_ids},
  151. })
  152. if "error" not in result:
  153. print(f"\n ✓ Build completed")
  154. print(f" Run ID: {result.get('run_id')}")
  155. output = result.get("output", "")
  156. print(f"\n{output}")
  157. else:
  158. print(f"\n ✗ Build failed: {result['error'][:500]}")
  159. # ═══════════════════════════════════════════════════════════
  160. # 5. Claude Code CLI — 核心多阶段流水线
  161. # ═══════════════════════════════════════════════════════════
  162. def run_claude_phase(phase_dir: Path, phase_name: str, desc: str,
  163. prompt_content: str) -> str:
  164. """执行单个 Claude CLI 阶段,带重试。"""
  165. phase_dir.mkdir(parents=True, exist_ok=True)
  166. (phase_dir / "prompt.md").write_text(prompt_content, encoding="utf-8")
  167. print(f"\n{'━'*60}")
  168. print(f" Phase: {phase_name} — {desc}")
  169. print(f" Timeout: {PHASE_TIMEOUT}s | Max turns: {MAX_TURNS_PER_PHASE}")
  170. print(f"{'━'*60}")
  171. cmd = [
  172. "claude",
  173. "-p",
  174. "--model", "claude-sonnet-4-20250514",
  175. "--max-turns", str(MAX_TURNS_PER_PHASE),
  176. "--allowedTools", "Read,Glob,Grep",
  177. ]
  178. for attempt in range(1, API_RETRY_MAX + 1):
  179. t0 = time.time()
  180. label = f"[attempt {attempt}/{API_RETRY_MAX}]"
  181. try:
  182. print(f" 🚀 {label} Running claude CLI ...")
  183. result = subprocess.run(
  184. cmd,
  185. input=prompt_content,
  186. capture_output=True, text=True,
  187. timeout=PHASE_TIMEOUT,
  188. cwd=str(phase_dir),
  189. )
  190. elapsed = time.time() - t0
  191. output = result.stdout or ""
  192. stderr = result.stderr or ""
  193. # 检测 API 错误
  194. is_api_error = False
  195. for err_pattern in ["Request timed out", "overloaded_error", "529",
  196. "rate_limit", "500 Internal"]:
  197. if err_pattern in output or err_pattern in stderr:
  198. is_api_error = True
  199. break
  200. if is_api_error and attempt < API_RETRY_MAX:
  201. wait = API_RETRY_BACKOFF * (2 ** (attempt - 1))
  202. print(f" ⚠ {label} API error after {elapsed:.0f}s. Retrying in {wait}s ...")
  203. time.sleep(wait)
  204. continue
  205. (phase_dir / "output.md").write_text(output, encoding="utf-8")
  206. print(f" ✓ {label} Done in {elapsed:.0f}s | {len(output)} chars")
  207. return output
  208. except subprocess.TimeoutExpired:
  209. elapsed = time.time() - t0
  210. msg = f"[TIMEOUT] Phase {phase_name} timed out after {elapsed:.0f}s"
  211. print(f" ✗ {msg}")
  212. (phase_dir / "output.md").write_text(msg, encoding="utf-8")
  213. return msg
  214. except FileNotFoundError:
  215. print(" ✗ 'claude' CLI not found. Is Claude Code installed?")
  216. return "[ERROR] claude CLI not found"
  217. return f"[FAILED] All {API_RETRY_MAX} attempts failed"
  218. def load_reference_docs() -> str:
  219. """加载 schema 参考和示例文档。"""
  220. parts = []
  221. schema_path = BUILDER_DIR / "prompts" / "schema_reference.md"
  222. examples_path = BUILDER_DIR / "prompts" / "examples.md"
  223. if schema_path.exists():
  224. parts.append(schema_path.read_text(encoding="utf-8"))
  225. if examples_path.exists():
  226. parts.append(examples_path.read_text(encoding="utf-8"))
  227. return "\n\n---\n\n".join(parts)
  228. def load_agent_prompt(agent_name: str) -> str:
  229. """从 YAML 配置中读取 agent 的 systemPrompt。"""
  230. import yaml
  231. yml_path = BUILDER_DIR / "agents" / f"{agent_name}.yml"
  232. if not yml_path.exists():
  233. return ""
  234. with open(yml_path, "r", encoding="utf-8") as f:
  235. config = yaml.safe_load(f)
  236. return config.get("systemPrompt", "")
  237. def build_phase_prompt(agent_name: str, user_desc: str,
  238. context: Dict = None, revision: str = "") -> str:
  239. """构建阶段 prompt。"""
  240. system_prompt = load_agent_prompt(agent_name)
  241. reference = load_reference_docs()
  242. parts = [
  243. f"# Agent Configuration Builder — {agent_name}",
  244. f"\n## System Instructions\n{system_prompt}",
  245. f"\n## User Description\n{user_desc}",
  246. ]
  247. if context:
  248. parts.append("\n## Previous Phase Outputs")
  249. for key, value in context.items():
  250. parts.append(f"\n### {key}\n{str(value)[:4000]}")
  251. if revision:
  252. parts.append(f"\n## Revision Instructions (from Critic)\n{revision}")
  253. parts.append(f"\n## Reference: YAML Schema & Examples\n{reference[:6000]}")
  254. parts.append("""
  255. ## Output Instructions
  256. - Output ONLY the result in the specified format (JSON or YAML)
  257. - Do not wrap output in markdown code blocks unless it is YAML
  258. - Be precise and concise
  259. """)
  260. return "\n".join(parts)
  261. def parse_critic_output(output: str) -> Dict:
  262. """从 Critic 输出中提取 JSON 评分。"""
  263. # 尝试直接解析
  264. try:
  265. return json.loads(output)
  266. except (json.JSONDecodeError, TypeError):
  267. pass
  268. # 尝试从 markdown 代码块中提取
  269. import re
  270. json_match = re.search(r'```(?:json)?\s*\n(.*?)\n```', output, re.DOTALL)
  271. if json_match:
  272. try:
  273. return json.loads(json_match.group(1))
  274. except (json.JSONDecodeError, TypeError):
  275. pass
  276. # 尝试找 { } 块
  277. brace_match = re.search(r'\{[^{}]*"overall_score"[^{}]*\}', output, re.DOTALL)
  278. if brace_match:
  279. try:
  280. return json.loads(brace_match.group(0))
  281. except (json.JSONDecodeError, TypeError):
  282. pass
  283. return {"overall_score": 0, "verdict": "unknown", "parse_error": True}
  284. def extract_yaml_from_output(output: str) -> str:
  285. """从 Assembler 输出中提取 YAML 配置。"""
  286. import re
  287. # 尝试从 ```yaml 代码块中提取
  288. yaml_match = re.search(r'```ya?ml\s*\n(.*?)\n```', output, re.DOTALL)
  289. if yaml_match:
  290. return yaml_match.group(1).strip()
  291. # 尝试找以 agentId: 开头的块
  292. agent_match = re.search(r'(agentId:.*)', output, re.DOTALL)
  293. if agent_match:
  294. return agent_match.group(1).strip()
  295. return output.strip()
  296. def cmd_claude_code(args):
  297. """
  298. 通过 Claude Code CLI 执行多智能体构建流水线。
  299. Layer 1 (并行): Analyst + Retriever + Prompter
  300. Layer 2 (串行): Assembler → Critic
  301. Loop: 如果 Critic 不通过,修正后重新走 Layer 2
  302. """
  303. import yaml
  304. user_desc = args.description
  305. if not user_desc:
  306. print("ERROR: Please provide a description. Example:")
  307. print(' python launch_paas.py claude-code "做一个能分析股票K线的agent"')
  308. return
  309. # 检查 claude CLI
  310. try:
  311. r = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=10)
  312. print(f" Claude CLI: {r.stdout.strip()}")
  313. except FileNotFoundError:
  314. print(" ERROR: 'claude' not found. Install Claude Code CLI.")
  315. return
  316. # 创建工作区
  317. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  318. workspace = BUILDER_DIR / "workspace" / f"build_{ts}"
  319. workspace.mkdir(parents=True, exist_ok=True)
  320. # 保存输入
  321. (workspace / "user_input.txt").write_text(user_desc, encoding="utf-8")
  322. print(f"""
  323. ╔══════════════════════════════════════════════════════════════╗
  324. ║ AgentBuilder — Multi-Agent Configuration Generator ║
  325. ║ Model: Claude Sonnet 4 ║
  326. ║ Max revision rounds: {MAX_REVISION_ROUNDS} ║
  327. ╚══════════════════════════════════════════════════════════════╝
  328. Input: {user_desc}
  329. Workspace: {workspace}
  330. """)
  331. # ═══ Layer 1: 并行分析 ═══════════════════════════════
  332. print(f"\n{'═'*60}")
  333. print(f" LAYER 1 — Parallel Analysis (Analyst + Retriever + Prompter)")
  334. print(f"{'═'*60}")
  335. # Layer 1 的三个阶段按顺序执行(CLI 模式下不方便真正并行,但各自独立)
  336. # Analyst
  337. analyst_output = run_claude_phase(
  338. workspace / "01_analyst", "analyst", "需求分析",
  339. build_phase_prompt("analyst", user_desc),
  340. )
  341. time.sleep(PHASE_COOLDOWN)
  342. # Retriever
  343. retriever_output = run_claude_phase(
  344. workspace / "02_retriever", "retriever", "配置检索",
  345. build_phase_prompt("retriever", user_desc, context={"analyst": analyst_output}),
  346. )
  347. time.sleep(PHASE_COOLDOWN)
  348. # Prompter
  349. prompter_output = run_claude_phase(
  350. workspace / "03_prompter", "prompter", "Prompt 撰写",
  351. build_phase_prompt("prompter", user_desc, context={"analyst": analyst_output}),
  352. )
  353. time.sleep(PHASE_COOLDOWN)
  354. # ═══ Layer 2: 组装 + 审查(可迭代)═══════════════════
  355. assembler_context = {
  356. "analyst_output": analyst_output,
  357. "retriever_output": retriever_output,
  358. "prompter_output": prompter_output,
  359. }
  360. final_config = None
  361. final_score = 0
  362. revision_instructions = ""
  363. for round_num in range(1, MAX_REVISION_ROUNDS + 1):
  364. print(f"\n{'═'*60}")
  365. print(f" LAYER 2 — Assembly + Review (Round {round_num}/{MAX_REVISION_ROUNDS})")
  366. print(f"{'═'*60}")
  367. # Assembler
  368. assembler_output = run_claude_phase(
  369. workspace / f"04_assembler_r{round_num}", "assembler",
  370. f"配置组装 (Round {round_num})",
  371. build_phase_prompt("assembler", user_desc,
  372. context=assembler_context,
  373. revision=revision_instructions),
  374. )
  375. time.sleep(PHASE_COOLDOWN)
  376. # 提取 YAML
  377. generated_yaml = extract_yaml_from_output(assembler_output)
  378. (workspace / f"04_assembler_r{round_num}" / "generated.yml").write_text(
  379. generated_yaml, encoding="utf-8"
  380. )
  381. # Lint 检查
  382. from tools.config_lint import lint_config
  383. try:
  384. lint_result = lint_config(generated_yaml)
  385. (workspace / f"04_assembler_r{round_num}" / "lint_result.json").write_text(
  386. json.dumps(lint_result, indent=2, ensure_ascii=False), encoding="utf-8"
  387. )
  388. print(f" Lint: valid={lint_result['valid']}, score={lint_result['score']}, "
  389. f"errors={len(lint_result['errors'])}, warnings={len(lint_result['warnings'])}")
  390. except Exception as e:
  391. lint_result = {"valid": False, "errors": [str(e)], "warnings": [], "score": 0}
  392. print(f" Lint error: {e}")
  393. # Critic
  394. critic_context = {
  395. "user_description": user_desc,
  396. "generated_config": generated_yaml,
  397. "lint_result": json.dumps(lint_result, ensure_ascii=False),
  398. }
  399. critic_output = run_claude_phase(
  400. workspace / f"05_critic_r{round_num}", "critic",
  401. f"配置审查 (Round {round_num})",
  402. build_phase_prompt("critic", user_desc, context=critic_context),
  403. )
  404. # 解析 Critic 评分
  405. critic_result = parse_critic_output(critic_output)
  406. (workspace / f"05_critic_r{round_num}" / "scores.json").write_text(
  407. json.dumps(critic_result, indent=2, ensure_ascii=False), encoding="utf-8"
  408. )
  409. overall_score = critic_result.get("overall_score", 0)
  410. verdict = critic_result.get("verdict", "unknown")
  411. print(f"\n 📊 Critic: score={overall_score}/10, verdict={verdict}")
  412. if overall_score >= CRITIC_PASS_SCORE or verdict == "approve":
  413. print(f" ✅ Approved! Score {overall_score} >= {CRITIC_PASS_SCORE}")
  414. final_config = generated_yaml
  415. final_score = overall_score
  416. break
  417. elif round_num < MAX_REVISION_ROUNDS:
  418. # 提取修改指令
  419. revision_instructions = ""
  420. rev_inst = critic_result.get("revision_instructions", {})
  421. if isinstance(rev_inst, dict):
  422. for target, instruction in rev_inst.items():
  423. if instruction:
  424. revision_instructions += f"\n[{target}]: {instruction}"
  425. critical = critic_result.get("critical_issues", [])
  426. if critical:
  427. revision_instructions += "\n\nCritical issues:\n" + "\n".join(
  428. f"- {issue}" for issue in critical
  429. )
  430. suggestions = critic_result.get("suggestions", [])
  431. if suggestions:
  432. revision_instructions += "\n\nSuggestions:\n" + "\n".join(
  433. f"- {s}" for s in suggestions
  434. )
  435. print(f" ⚠ Score {overall_score} < {CRITIC_PASS_SCORE}, revising...")
  436. print(f" Revision notes: {revision_instructions[:200]}...")
  437. time.sleep(PHASE_COOLDOWN)
  438. else:
  439. print(f" ❌ Max rounds reached. Using best result (score={overall_score})")
  440. final_config = generated_yaml
  441. final_score = overall_score
  442. # ═══ 输出最终结果 ═══════════════════════════════════
  443. if final_config:
  444. output_path = workspace / "final_agent.yml"
  445. output_path.write_text(final_config, encoding="utf-8")
  446. print(f"\n{'═'*60}")
  447. print(f" ✅ AgentBuilder Complete!")
  448. print(f"{'═'*60}")
  449. print(f" Score: {final_score}/10")
  450. print(f" Output: {output_path}")
  451. print(f" Workspace: {workspace}")
  452. print(f"\n Generated config:")
  453. print(f"{'─'*60}")
  454. print(final_config[:2000])
  455. if len(final_config) > 2000:
  456. print(f" ... ({len(final_config)} chars total)")
  457. print(f"{'─'*60}")
  458. # 尝试解析并展示摘要
  459. try:
  460. config = yaml.safe_load(final_config)
  461. if isinstance(config, dict):
  462. print(f"\n Summary:")
  463. print(f" Agent ID: {config.get('agentId', '?')}")
  464. print(f" Name: {config.get('name', '?')}")
  465. print(f" Type: {config.get('type', '?')}")
  466. print(f" Model: {config.get('model', {}).get('provider', '?')}"
  467. f"/{config.get('model', {}).get('name', '?')}")
  468. tools = config.get('mcp', {}).get('localTools', [])
  469. print(f" Tools: {', '.join(tools)}")
  470. except Exception:
  471. pass
  472. else:
  473. print(f"\n ❌ Build failed. Check workspace: {workspace}")
  474. # ═══════════════════════════════════════════════════════════
  475. # 6. 命令: interactive — 交互式构建
  476. # ═══════════════════════════════════════════════════════════
  477. def cmd_interactive(args):
  478. """交互式 AgentBuilder:对话式构建 + 实时预览。"""
  479. print(f"""
  480. ╔══════════════════════════════════════════════════════════════╗
  481. ║ AgentBuilder Interactive Mode ║
  482. ║ 输入你想要的 agent 描述,我来生成配置 ║
  483. ║ 输入 quit 退出 | 输入 save 保存当前配置 ║
  484. ╚══════════════════════════════════════════════════════════════╝
  485. """)
  486. current_config = None
  487. while True:
  488. try:
  489. user_input = input("\n🔧 描述你的 agent > ").strip()
  490. except (EOFError, KeyboardInterrupt):
  491. print("\nBye!")
  492. break
  493. if not user_input:
  494. continue
  495. if user_input.lower() == "quit":
  496. break
  497. if user_input.lower() == "save" and current_config:
  498. save_path = BUILDER_DIR / "workspace" / f"agent_{datetime.now().strftime('%H%M%S')}.yml"
  499. save_path.parent.mkdir(parents=True, exist_ok=True)
  500. save_path.write_text(current_config, encoding="utf-8")
  501. print(f" ✓ Saved to {save_path}")
  502. continue
  503. # 构建 agent(复用 claude-code 逻辑)
  504. args.description = user_input
  505. cmd_claude_code(args)
  506. # ═══════════════════════════════════════════════════════════
  507. # 7. 命令: optimize — 优化已部署的 agent
  508. # ═══════════════════════════════════════════════════════════
  509. def cmd_optimize(args):
  510. """通过 Claude CLI 运行 Optimizer agent。"""
  511. agent_id = args.agent_id
  512. print(f"\n{'═'*60}")
  513. print(f" AgentBuilder Optimizer")
  514. print(f" Target: {agent_id}")
  515. print(f"{'═'*60}\n")
  516. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  517. workspace = BUILDER_DIR / "workspace" / f"optimize_{ts}"
  518. workspace.mkdir(parents=True, exist_ok=True)
  519. optimizer_prompt = load_agent_prompt("optimizer")
  520. prompt = f"""# Agent Optimizer
  521. ## System Instructions
  522. {optimizer_prompt}
  523. ## Target Agent
  524. agent_id: {agent_id}
  525. ## Task
  526. 1. 调用 get_run_stats 获取运行数据
  527. 2. 调用 get_user_feedback 获取用户反馈
  528. 3. 调用 get_current_config 获取当前配置
  529. 4. 分析数据,找出优化点
  530. 5. 生成优化补丁
  531. ## Environment
  532. - AgentPaaS API: {PAAS_URL}
  533. - API Key: {'set' if API_KEY else 'not set'}
  534. 输出优化建议 JSON。
  535. """
  536. output = run_claude_phase(
  537. workspace / "optimizer", "optimizer",
  538. f"优化 {agent_id}",
  539. prompt,
  540. )
  541. print(f"\n Optimizer output:")
  542. print(output[:2000])
  543. # ═══════════════════════════════════════════════════════════
  544. # 8. 命令: all — 一键启动全流程
  545. # ═══════════════════════════════════════════════════════════
  546. def cmd_all(args):
  547. """一键: 启动服务 + 注册 agents + 构建。"""
  548. global API_KEY
  549. proc = cmd_serve(args)
  550. if not proc:
  551. return
  552. try:
  553. # 创建租户
  554. result = subprocess.run(
  555. [sys.executable, "-m", "agentpaas", "create-tenant", "--name", "builder-lab"],
  556. capture_output=True, text=True, cwd=str(PROJECT_ROOT),
  557. )
  558. print(result.stdout)
  559. for line in result.stdout.splitlines():
  560. if line.startswith("API Key:"):
  561. API_KEY = line.split(":", 1)[1].strip()
  562. os.environ["AGENTPAAS_API_KEY"] = API_KEY
  563. cmd_register(args)
  564. cmd_build(args)
  565. finally:
  566. print("\nShutting down server...")
  567. proc.terminate()
  568. proc.wait(timeout=5)
  569. # ═══════════════════════════════════════════════════════════
  570. # CLI Entry Point
  571. # ═══════════════════════════════════════════════════════════
  572. def _apply_overrides(args):
  573. global MAX_REVISION_ROUNDS, MAX_TURNS_PER_PHASE, PHASE_TIMEOUT
  574. if hasattr(args, "max_rounds") and args.max_rounds:
  575. MAX_REVISION_ROUNDS = args.max_rounds
  576. if hasattr(args, "max_turns") and args.max_turns:
  577. MAX_TURNS_PER_PHASE = args.max_turns
  578. if hasattr(args, "timeout") and args.timeout:
  579. PHASE_TIMEOUT = args.timeout
  580. def main():
  581. parser = argparse.ArgumentParser(
  582. description="AgentBuilder — Multi-agent Configuration Generator",
  583. formatter_class=argparse.RawDescriptionHelpFormatter,
  584. epilog="""
  585. Examples:
  586. python launch_paas.py claude-code "做一个能分析CSV数据的agent"
  587. python launch_paas.py claude-code "帮我做一个能上网搜索的调研助手"
  588. python launch_paas.py claude-code --interactive
  589. python launch_paas.py optimize agent-abc123
  590. python launch_paas.py all --port 8000 -d "股票K线分析agent"
  591. """)
  592. sub = parser.add_subparsers(dest="command")
  593. # serve
  594. serve_p = sub.add_parser("serve", help="Start AgentPaaS server")
  595. serve_p.add_argument("--port", type=int, default=8000)
  596. # register
  597. sub.add_parser("register", help="Register builder agents to PaaS")
  598. # build (via PaaS API)
  599. build_p = sub.add_parser("build", help="Build agent via PaaS API")
  600. build_p.add_argument("description", type=str, help="Agent description")
  601. # claude-code (via CLI)
  602. cc_p = sub.add_parser("claude-code", help="Build agent via Claude Code CLI")
  603. cc_p.add_argument("description", type=str, nargs="?", default="",
  604. help="Agent description")
  605. cc_p.add_argument("--interactive", "-i", action="store_true",
  606. help="Interactive mode")
  607. cc_p.add_argument("--max-rounds", type=int, default=None,
  608. help=f"Max revision rounds (default: {MAX_REVISION_ROUNDS})")
  609. cc_p.add_argument("--max-turns", type=int, default=None,
  610. help=f"Max turns per phase (default: {MAX_TURNS_PER_PHASE})")
  611. cc_p.add_argument("--timeout", type=int, default=None,
  612. help=f"Phase timeout in seconds (default: {PHASE_TIMEOUT})")
  613. # optimize
  614. opt_p = sub.add_parser("optimize", help="Optimize a deployed agent")
  615. opt_p.add_argument("agent_id", type=str, help="Target agent ID")
  616. # all
  617. all_p = sub.add_parser("all", help="Serve + register + build (one-shot)")
  618. all_p.add_argument("--port", type=int, default=8000)
  619. all_p.add_argument("-d", "--description", type=str, required=True,
  620. help="Agent description")
  621. args = parser.parse_args()
  622. # 应用参数覆盖
  623. _apply_overrides(args)
  624. commands = {
  625. "serve": cmd_serve,
  626. "register": cmd_register,
  627. "build": cmd_build,
  628. "claude-code": lambda a: cmd_interactive(a) if getattr(a, "interactive", False) else cmd_claude_code(a),
  629. "optimize": cmd_optimize,
  630. "all": cmd_all,
  631. }
  632. if args.command in commands:
  633. commands[args.command](args)
  634. else:
  635. parser.print_help()
  636. if __name__ == "__main__":
  637. main()