launch_paas.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. #!/usr/bin/env python3
  2. """
  3. pptagent67/launch_paas.py — 通过 AgentPaaS API 或 Claude Code CLI 启动 PPT 生成
  4. ================================================================================
  5. 两种运行模式:
  6. 1. AgentPaaS API 模式: 启动 PaaS 服务,注册所有 agent,通过 REST API 执行
  7. 2. Claude Code CLI 桥接模式: 利用 Claude Max Plan 的 claude CLI 作为 LLM 后端
  8. 用法:
  9. # 模式 1: PaaS 全流程
  10. python launch_paas.py all --port 8000
  11. # 模式 2: 使用 Claude Code CLI (Max Plan)
  12. python launch_paas.py claude-code
  13. python launch_paas.py claude-code --topic "AI 在教育中的应用" --template business
  14. python launch_paas.py claude-code --workspace ./workspace/claude_run_xxx # 断点续跑
  15. """
  16. from __future__ import annotations
  17. import argparse
  18. import json
  19. import os
  20. import subprocess
  21. import sys
  22. import time
  23. import urllib.request
  24. import urllib.error
  25. from pathlib import Path
  26. from typing import Dict, Optional
  27. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
  28. AGENT_DIR = Path(__file__).resolve().parent
  29. sys.path.insert(0, str(PROJECT_ROOT))
  30. PAAS_URL = os.environ.get("AGENTPAAS_URL", "http://127.0.0.1:8000")
  31. API_KEY = os.environ.get("AGENTPAAS_API_KEY", "")
  32. AGENT_CONFIGS = [
  33. ("ppt-67-orchestrator", "PPT 主编排器", AGENT_DIR / "orchestrator.yml"),
  34. ("ppt-67-knowledge-extractor", "知识提取师", AGENT_DIR / "agents" / "knowledge-extractor.yml"),
  35. ("ppt-67-outline-generator", "大纲生成师", AGENT_DIR / "agents" / "outline-generator.yml"),
  36. ("ppt-67-content-refiner", "内容精修师", AGENT_DIR / "agents" / "content-refiner.yml"),
  37. ("ppt-67-renderer", "PPT 渲染师", AGENT_DIR / "agents" / "ppt-renderer.yml"),
  38. ]
  39. # 迭代控制参数
  40. MAX_ROUNDS = 3
  41. QUALITY_THRESHOLD = 0.70
  42. MAX_TURNS_PER_PHASE = 25
  43. PHASE_TIMEOUT = 1800
  44. API_RETRY_MAX = 3
  45. API_RETRY_BACKOFF = 30
  46. PHASE_COOLDOWN = 10
  47. # ═══════════════════════════════════════════════════════════
  48. # 1. PaaS API Helpers
  49. # ═══════════════════════════════════════════════════════════
  50. def api_call(method: str, path: str, data: dict = None) -> dict:
  51. url = f"{PAAS_URL}/api/v1{path}"
  52. body = json.dumps(data).encode("utf-8") if data else None
  53. req = urllib.request.Request(url, data=body, method=method)
  54. req.add_header("Content-Type", "application/json")
  55. req.add_header("Authorization", f"Bearer {API_KEY}")
  56. try:
  57. with urllib.request.urlopen(req, timeout=600) as resp:
  58. return json.loads(resp.read())
  59. except urllib.error.HTTPError as e:
  60. body = e.read().decode()
  61. print(f" API Error {e.code}: {body[:500]}")
  62. return {"error": body}
  63. except urllib.error.URLError as e:
  64. print(f" Connection error: {e}")
  65. return {"error": str(e)}
  66. def wait_for_server(timeout: int = 30):
  67. for i in range(timeout):
  68. try:
  69. req = urllib.request.Request(f"{PAAS_URL}/health")
  70. with urllib.request.urlopen(req, timeout=2):
  71. return True
  72. except Exception:
  73. time.sleep(1)
  74. return False
  75. # ═══════════════════════════════════════════════════════════
  76. # 2. 命令: serve / register / run / all
  77. # ═══════════════════════════════════════════════════════════
  78. def cmd_serve(args):
  79. print("Starting AgentPaaS server...")
  80. proc = subprocess.Popen(
  81. [sys.executable, "-m", "agentpaas", "serve", "--port", str(args.port), "--dev"],
  82. cwd=str(PROJECT_ROOT),
  83. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  84. )
  85. print(f" PID: {proc.pid}, URL: http://127.0.0.1:{args.port}")
  86. if not wait_for_server(30):
  87. print(" ERROR: Server failed to start")
  88. proc.kill()
  89. return None
  90. print(" ✓ Server ready")
  91. return proc
  92. def cmd_register(args):
  93. import yaml
  94. print(f"\nRegistering {len(AGENT_CONFIGS)} agents...")
  95. agent_ids = {}
  96. for agent_id, name, yml_path in AGENT_CONFIGS:
  97. with open(yml_path, "r", encoding="utf-8") as f:
  98. config = yaml.safe_load(f)
  99. result = api_call("POST", "/agents", {
  100. "name": name,
  101. "description": config.get("description", ""),
  102. "config": config,
  103. "tags": ["pptagent67", config.get("type", "react")],
  104. })
  105. if "agent_id" in result:
  106. agent_ids[agent_id] = result["agent_id"]
  107. print(f" ✓ {name:20s} → {result['agent_id']}")
  108. else:
  109. print(f" ✗ {name:20s} → {result.get('error', 'Unknown')}")
  110. mapping_path = AGENT_DIR / ".paas_agent_ids.json"
  111. mapping_path.write_text(json.dumps(agent_ids, indent=2), encoding="utf-8")
  112. return agent_ids
  113. def cmd_run(args):
  114. mapping_path = AGENT_DIR / ".paas_agent_ids.json"
  115. if not mapping_path.exists():
  116. print("ERROR: Run 'register' first.")
  117. return
  118. agent_ids = json.loads(mapping_path.read_text())
  119. orchestrator_id = agent_ids.get("ppt-67-orchestrator")
  120. if not orchestrator_id:
  121. print("ERROR: Orchestrator not registered.")
  122. return
  123. topic = args.topic if hasattr(args, "topic") and args.topic else "[默认主题]"
  124. result = api_call("POST", f"/agents/{orchestrator_id}/run", {
  125. "input": topic,
  126. "parameters": {"template": getattr(args, "template", "academic")},
  127. "context": {"sub_agent_ids": agent_ids},
  128. })
  129. if "error" not in result:
  130. print(f" ✓ Run completed: {result.get('run_id')}")
  131. else:
  132. print(f" ✗ Failed: {result['error'][:500]}")
  133. def cmd_all(args):
  134. global API_KEY
  135. proc = cmd_serve(args)
  136. if not proc:
  137. return
  138. try:
  139. key_result = subprocess.run(
  140. [sys.executable, "-m", "agentpaas", "create-tenant", "--name", "ppt-lab"],
  141. capture_output=True, text=True, cwd=str(PROJECT_ROOT),
  142. )
  143. for line in key_result.stdout.splitlines():
  144. if line.startswith("API Key:"):
  145. API_KEY = line.split(":", 1)[1].strip()
  146. os.environ["AGENTPAAS_API_KEY"] = API_KEY
  147. cmd_register(args)
  148. cmd_run(args)
  149. finally:
  150. proc.terminate()
  151. proc.wait(timeout=5)
  152. # ═══════════════════════════════════════════════════════════
  153. # 3. 命令: claude-code — 使用 Claude Code CLI
  154. # ═══════════════════════════════════════════════════════════
  155. def run_phase(phase_dir: Path, agent_name: str, desc: str,
  156. prompt_content: str, round_num: int) -> str:
  157. """执行单个阶段,带自动重试"""
  158. phase_dir.mkdir(parents=True, exist_ok=True)
  159. (phase_dir / "artifacts").mkdir(exist_ok=True)
  160. (phase_dir / "prompt.md").write_text(prompt_content, encoding="utf-8")
  161. print(f"\n{'━'*60}")
  162. print(f" Phase: {phase_dir.name} — {desc}")
  163. print(f" Agent: {agent_name} | Round: {round_num}")
  164. print(f"{'━'*60}")
  165. cmd = [
  166. "claude", "-p",
  167. "--model", "claude-opus-4-6",
  168. "--max-turns", str(MAX_TURNS_PER_PHASE),
  169. "--allowedTools", "Edit,Write,Read,Bash,Glob,Grep,WebSearch,WebFetch",
  170. ]
  171. for attempt in range(1, API_RETRY_MAX + 1):
  172. t0 = time.time()
  173. try:
  174. result = subprocess.run(
  175. cmd, input=prompt_content,
  176. capture_output=True, text=True,
  177. timeout=PHASE_TIMEOUT, cwd=str(phase_dir),
  178. )
  179. elapsed = time.time() - t0
  180. output = result.stdout or ""
  181. stderr = result.stderr or ""
  182. is_api_error = any(
  183. p in output or p in stderr
  184. for p in ["Request timed out", "overloaded_error", "529", "rate_limit"]
  185. )
  186. if is_api_error and attempt < API_RETRY_MAX:
  187. wait = API_RETRY_BACKOFF * (2 ** (attempt - 1))
  188. print(f" ⚠ API error after {elapsed:.0f}s. Retrying in {wait}s...")
  189. time.sleep(wait)
  190. continue
  191. if result.returncode != 0 and stderr:
  192. output = f"[STDERR] {stderr[:500]}\n{output}"
  193. (phase_dir / "claude_output.md").write_text(output, encoding="utf-8")
  194. print(f" ✓ Done in {elapsed:.0f}s | {len(output)} chars")
  195. return output
  196. except subprocess.TimeoutExpired:
  197. msg = f"[TIMEOUT] {phase_dir.name} timed out after {PHASE_TIMEOUT}s"
  198. print(f" ✗ {msg}")
  199. (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
  200. return msg
  201. except FileNotFoundError:
  202. print(" ✗ 'claude' CLI not found.")
  203. return "[ERROR] claude CLI not found"
  204. except Exception as e:
  205. if attempt < API_RETRY_MAX:
  206. time.sleep(API_RETRY_BACKOFF * attempt)
  207. else:
  208. msg = f"[ERROR] {e}"
  209. (phase_dir / "claude_output.md").write_text(msg, encoding="utf-8")
  210. return msg
  211. return "[FAILED] All retries exhausted"
  212. def build_prompt(workspace: Path, phase_dir: Path, desc: str,
  213. system_prompt: str, topic: str, round_num: int,
  214. template: str, prev_outputs: list,
  215. revision_context: str = "") -> str:
  216. input_parts = [
  217. f"workspace: {workspace}",
  218. f"phase_dir: {phase_dir}",
  219. f"round: {round_num}",
  220. f"template: {template}",
  221. f"\n## PPT 主题:\n{topic[:8000]}",
  222. ]
  223. if revision_context:
  224. input_parts.append(f"\n## 修改建议 (Round {round_num - 1}):\n{revision_context[:4000]}")
  225. for prev in prev_outputs[-2:]:
  226. input_parts.append(f"\n## Previous phase output:\n{prev[:3000]}")
  227. return f"""# {desc}
  228. ## System Instructions
  229. {system_prompt[:8000]}
  230. ## Input Context
  231. {chr(10).join(input_parts)}
  232. ## Important
  233. - 将所有产出文件写入 {phase_dir}/
  234. - 先写 work_plan.md,再执行,最后写 report.json 和 report.md
  235. - report.json 必须包含 _meta 字段(含 phase, round, status)
  236. - 这是 Round {round_num}{',请根据修改建议重点改进' if round_num > 1 else ''}
  237. """
  238. def cmd_claude_code(args):
  239. """通过 Claude Code CLI 执行 PPT 生成流程"""
  240. import yaml
  241. from datetime import datetime
  242. global MAX_ROUNDS, MAX_TURNS_PER_PHASE, PHASE_TIMEOUT, PHASE_COOLDOWN
  243. if hasattr(args, "max_rounds") and args.max_rounds:
  244. MAX_ROUNDS = args.max_rounds
  245. if hasattr(args, "max_turns") and args.max_turns:
  246. MAX_TURNS_PER_PHASE = args.max_turns
  247. topic = args.topic if hasattr(args, "topic") and args.topic else ""
  248. template = args.template if hasattr(args, "template") and args.template else "academic"
  249. if not topic:
  250. topic_path = AGENT_DIR / "TOPIC.md"
  251. if topic_path.exists():
  252. topic = topic_path.read_text(encoding="utf-8")
  253. else:
  254. print("ERROR: No topic. Use --topic or create TOPIC.md")
  255. return
  256. print(f"""
  257. ╔══════════════════════════════════════════════════════════════╗
  258. ║ PPTAgent-67 via Claude Code CLI ║
  259. ║ Template: {template:<10s} | Threshold: {QUALITY_THRESHOLD:.0%} | Max rounds: {MAX_ROUNDS} ║
  260. ╚══════════════════════════════════════════════════════════════╝
  261. """)
  262. # 工作区
  263. if hasattr(args, "workspace") and args.workspace:
  264. workspace = Path(args.workspace)
  265. else:
  266. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  267. workspace = AGENT_DIR / "workspace" / f"claude_run_{ts}"
  268. workspace.mkdir(parents=True, exist_ok=True)
  269. print(f" Workspace: {workspace}")
  270. print(f" Topic: {topic[:80]}...")
  271. MAIN_PHASES = [
  272. ("01_knowledge_extraction", "knowledge-extractor", "从知识库提取信息"),
  273. ("02_outline_generation", "outline-generator", "生成 PPT 大纲"),
  274. ("03_content_refinement", "content-refiner", "精修内容 & 质量评估"),
  275. ("04_ppt_rendering", "ppt-renderer", "渲染 .pptx 文件"),
  276. ]
  277. for round_num in range(1, MAX_ROUNDS + 1):
  278. print(f"\n{'═'*60}")
  279. print(f" ROUND {round_num}/{MAX_ROUNDS}")
  280. print(f"{'═'*60}")
  281. prev_outputs = []
  282. revision_context = ""
  283. for phase_idx, (phase_name, agent_name, desc) in enumerate(MAIN_PHASES):
  284. phase_dir = workspace / f"round_{round_num}" / phase_name
  285. # 跳过已完成
  286. if (phase_dir / "report.json").exists():
  287. print(f"\n ⏭ Skipping {phase_name} (exists)")
  288. existing = (phase_dir / "claude_output.md").read_text(encoding="utf-8") \
  289. if (phase_dir / "claude_output.md").exists() else ""
  290. prev_outputs.append(existing[:5000])
  291. continue
  292. agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
  293. with open(agent_yml, "r", encoding="utf-8") as f:
  294. config = yaml.safe_load(f)
  295. prompt = build_prompt(
  296. workspace, phase_dir, desc,
  297. config.get("systemPrompt", ""), topic,
  298. round_num, template, prev_outputs,
  299. revision_context=revision_context,
  300. )
  301. output = run_phase(phase_dir, agent_name, desc, prompt, round_num)
  302. prev_outputs.append(output[:5000])
  303. # 精修阶段后检查质量
  304. if phase_name == "03_content_refinement":
  305. report_path = phase_dir / "report.json"
  306. quality = 0.0
  307. if report_path.exists():
  308. try:
  309. data = json.loads(report_path.read_text(encoding="utf-8"))
  310. quality = data.get("quality_score", 0)
  311. except Exception:
  312. pass
  313. print(f"\n Quality: {quality:.2f} (threshold: {QUALITY_THRESHOLD})")
  314. if quality < QUALITY_THRESHOLD and round_num < MAX_ROUNDS:
  315. print(f" ⚠ Below threshold. Will iterate.")
  316. revision_context = output[:4000]
  317. break # 跳过渲染,进入下一轮
  318. if phase_idx < len(MAIN_PHASES) - 1:
  319. time.sleep(PHASE_COOLDOWN)
  320. # 检查是否渲染完成
  321. pptx_dir = workspace / f"round_{round_num}" / "04_ppt_rendering"
  322. if (pptx_dir / "report.json").exists():
  323. print(f"\n ✅ PPT rendered! Round {round_num} complete.")
  324. break
  325. print(f"\n{'═'*60}")
  326. print(f" Pipeline complete. Workspace: {workspace}")
  327. print(f"{'═'*60}\n")
  328. # ═══════════════════════════════════════════════════════════
  329. # CLI
  330. # ═══════════════════════════════════════════════════════════
  331. def main():
  332. parser = argparse.ArgumentParser(description="PPTAgent-67 PaaS Launcher")
  333. sub = parser.add_subparsers(dest="command")
  334. serve_p = sub.add_parser("serve")
  335. serve_p.add_argument("--port", type=int, default=8000)
  336. sub.add_parser("register")
  337. run_p = sub.add_parser("run")
  338. run_p.add_argument("--topic", type=str, default="")
  339. run_p.add_argument("--template", type=str, default="academic")
  340. all_p = sub.add_parser("all")
  341. all_p.add_argument("--port", type=int, default=8000)
  342. all_p.add_argument("--topic", type=str, default="")
  343. all_p.add_argument("--template", type=str, default="academic")
  344. cc_p = sub.add_parser("claude-code")
  345. cc_p.add_argument("--topic", type=str, default="")
  346. cc_p.add_argument("--template", type=str, default="academic",
  347. choices=["academic", "business", "minimal", "tech"])
  348. cc_p.add_argument("--workspace", type=str, default=None)
  349. cc_p.add_argument("--max-rounds", type=int, default=None)
  350. cc_p.add_argument("--max-turns", type=int, default=None)
  351. args = parser.parse_args()
  352. commands = {
  353. "serve": cmd_serve,
  354. "register": cmd_register,
  355. "run": cmd_run,
  356. "all": cmd_all,
  357. "claude-code": cmd_claude_code,
  358. }
  359. if args.command in commands:
  360. commands[args.command](args)
  361. else:
  362. parser.print_help()
  363. print("\nExamples:")
  364. print(' python launch_paas.py claude-code --topic "AI 在医疗中的应用" --template business')
  365. print(" python launch_paas.py all --port 8000")
  366. if __name__ == "__main__":
  367. main()