run.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. #!/usr/bin/env python3
  2. """
  3. pptagent67/run.py — 简化版 Runner
  4. ===================================
  5. 用本地工具 stub 替代尚不存在的 MCP 服务,直接通过 lambdagent runtime
  6. 执行 pptagent67 多智能体 PPT 生成流程。
  7. 使用 Claude Opus 4.6 (1M context) 的原生能力:
  8. - web_search → 补充知识库之外的信息
  9. - python_exec → 辅助脚本执行(marp-cli 渲染)
  10. - local fs → 替代 fs-mcp
  11. 用法:
  12. export ANTHROPIC_API_KEY=sk-ant-...
  13. python run.py # 完整流程
  14. python run.py --phase outline # 只跑某个阶段
  15. python run.py --topic "AI 在医疗中的应用" # 指定主题
  16. python run.py --template business # 指定模板风格
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import json
  21. import os
  22. import subprocess
  23. import sys
  24. import time
  25. import shutil
  26. from datetime import datetime, timezone
  27. from pathlib import Path
  28. from typing import Any, Dict, List, Optional
  29. # ── 路径设置 ──────────────────────────────────────────────
  30. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
  31. AGENT_DIR = Path(__file__).resolve().parent
  32. sys.path.insert(0, str(PROJECT_ROOT))
  33. from lambdagent.fromconfig import from_config
  34. from lambdagent.agentruntime.runtime import Runtime, RuntimeResult
  35. from lambdagent.agentruntime.config import RuntimeConfig
  36. from lambdagent.core import Context
  37. # ═══════════════════════════════════════════════════════════
  38. # 1. 本地工具 Stubs — 替代 MCP 服务
  39. # ═══════════════════════════════════════════════════════════
  40. class LocalToolkit:
  41. """
  42. 将 MCP 服务的功能用本地实现替代。
  43. 每个方法签名对齐 MCP tool 的 (input: str) -> str 接口。
  44. """
  45. def __init__(self, workspace: Path):
  46. self.workspace = workspace
  47. # ── fs-mcp 替代 ──────────────────────────────────────
  48. def read_file(self, path: str) -> str:
  49. p = self._resolve(path)
  50. if not p.exists():
  51. return f"[ERROR] File not found: {p}"
  52. return p.read_text(encoding="utf-8")
  53. def write_file(self, args: str) -> str:
  54. """args = JSON: {"path": "...", "content": "..."}"""
  55. d = json.loads(args)
  56. p = self._resolve(d["path"])
  57. p.parent.mkdir(parents=True, exist_ok=True)
  58. p.write_text(d["content"], encoding="utf-8")
  59. return f"Written {len(d['content'])} bytes to {p}"
  60. def list_dir(self, path: str) -> str:
  61. p = self._resolve(path)
  62. if not p.is_dir():
  63. return f"[ERROR] Not a directory: {p}"
  64. entries = sorted(p.iterdir())
  65. return "\n".join(f"{'[DIR] ' if e.is_dir() else ''}{e.name}" for e in entries)
  66. def mkdir(self, path: str) -> str:
  67. p = self._resolve(path)
  68. p.mkdir(parents=True, exist_ok=True)
  69. return f"Created: {p}"
  70. def copy_file(self, args: str) -> str:
  71. d = json.loads(args)
  72. src, dst = self._resolve(d["src"]), self._resolve(d["dst"])
  73. dst.parent.mkdir(parents=True, exist_ok=True)
  74. shutil.copy2(src, dst)
  75. return f"Copied {src} → {dst}"
  76. # ── Marp 渲染 ─────────────────────────────────────
  77. def render_marp(self, args: str) -> str:
  78. """使用 marp-cli 将 Marp Markdown 转换为 .pptx 和 .pdf"""
  79. d = json.loads(args)
  80. md_path = self._resolve(d.get("input", "presentation.md"))
  81. output_dir = self._resolve(d.get("output_dir", "."))
  82. if not md_path.exists():
  83. return f"[ERROR] Markdown not found: {md_path}"
  84. results = []
  85. # 检测 marp-cli
  86. marp_cmd = self._find_marp()
  87. if not marp_cmd:
  88. return "[ERROR] marp-cli not found. Install: npm install -g @marp-team/marp-cli"
  89. # 转换为 PPTX
  90. pptx_path = output_dir / "presentation.pptx"
  91. try:
  92. r = subprocess.run(
  93. [*marp_cmd, "--pptx", str(md_path), "-o", str(pptx_path), "--allow-local-files"],
  94. capture_output=True, text=True, timeout=120,
  95. cwd=str(md_path.parent),
  96. )
  97. if r.returncode == 0 and pptx_path.exists():
  98. results.append(f"PPTX: {pptx_path} ({pptx_path.stat().st_size} bytes)")
  99. else:
  100. results.append(f"[PPTX_ERROR] {r.stderr[:500]}")
  101. except Exception as e:
  102. results.append(f"[PPTX_ERROR] {e}")
  103. # 转换为 PDF
  104. pdf_path = output_dir / "presentation.pdf"
  105. try:
  106. r = subprocess.run(
  107. [*marp_cmd, "--pdf", str(md_path), "-o", str(pdf_path), "--allow-local-files"],
  108. capture_output=True, text=True, timeout=120,
  109. cwd=str(md_path.parent),
  110. )
  111. if r.returncode == 0 and pdf_path.exists():
  112. results.append(f"PDF: {pdf_path} ({pdf_path.stat().st_size} bytes)")
  113. else:
  114. results.append(f"[PDF_WARN] {r.stderr[:300]}")
  115. except Exception as e:
  116. results.append(f"[PDF_WARN] {e}")
  117. return "\n".join(results)
  118. def _find_marp(self) -> list:
  119. """查找 marp-cli 可执行文件"""
  120. # 优先全局安装
  121. for cmd in ["marp", "npx"]:
  122. try:
  123. if cmd == "npx":
  124. r = subprocess.run(
  125. ["npx", "@marp-team/marp-cli", "--version"],
  126. capture_output=True, text=True, timeout=30,
  127. )
  128. if r.returncode == 0:
  129. return ["npx", "@marp-team/marp-cli"]
  130. else:
  131. r = subprocess.run(
  132. ["marp", "--version"],
  133. capture_output=True, text=True, timeout=10,
  134. )
  135. if r.returncode == 0:
  136. return ["marp"]
  137. except FileNotFoundError:
  138. continue
  139. return []
  140. def python_exec(self, code: str) -> str:
  141. """在本地执行 Python 代码"""
  142. try:
  143. result = subprocess.run(
  144. [sys.executable, "-c", code],
  145. capture_output=True, text=True, timeout=120,
  146. cwd=str(self.workspace),
  147. )
  148. output = result.stdout
  149. if result.returncode != 0:
  150. output += f"\n[STDERR] {result.stderr}"
  151. return output[:5000]
  152. except subprocess.TimeoutExpired:
  153. return "[TIMEOUT] Python execution exceeded 120s"
  154. except Exception as e:
  155. return f"[EXEC_ERROR] {e}"
  156. def install_package(self, pkg: str) -> str:
  157. try:
  158. result = subprocess.run(
  159. [sys.executable, "-m", "pip", "install", pkg, "-q"],
  160. capture_output=True, text=True, timeout=60,
  161. )
  162. return result.stdout + result.stderr
  163. except Exception as e:
  164. return f"[PIP_ERROR] {e}"
  165. # ── 特殊本地工具 ─────────────────────────────────────
  166. def terminate(self, result: str) -> str:
  167. return result
  168. def read_topic(self, path: str = "") -> str:
  169. """读取 PPT 主题文件"""
  170. topic_path = AGENT_DIR / "TOPIC.md"
  171. if not topic_path.exists():
  172. return f"[ERROR] TOPIC.md not found at {topic_path}"
  173. return topic_path.read_text(encoding="utf-8")
  174. def save_checkpoint(self, data: str) -> str:
  175. ckpt_dir = self.workspace / ".checkpoints"
  176. ckpt_dir.mkdir(parents=True, exist_ok=True)
  177. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  178. path = ckpt_dir / f"checkpoint_{ts}.json"
  179. path.write_text(data, encoding="utf-8")
  180. return f"Checkpoint saved: {path}"
  181. def load_checkpoint(self, pattern: str = "latest") -> str:
  182. ckpt_dir = self.workspace / ".checkpoints"
  183. if not ckpt_dir.exists():
  184. return "[ERROR] No checkpoints found"
  185. files = sorted(ckpt_dir.glob("checkpoint_*.json"))
  186. if not files:
  187. return "[ERROR] No checkpoints found"
  188. return files[-1].read_text(encoding="utf-8")
  189. def human_feedback(self, question: str) -> str:
  190. print(f"\n{'='*60}")
  191. print(f"[HUMAN FEEDBACK REQUESTED]")
  192. print(f"{question}")
  193. print(f"{'='*60}")
  194. try:
  195. answer = input("Your response (or press Enter to skip): ").strip()
  196. return answer if answer else "[NO FEEDBACK]"
  197. except (EOFError, KeyboardInterrupt):
  198. return "[NO FEEDBACK]"
  199. def dispatch_agent(self, args: str) -> str:
  200. """调度子智能体执行"""
  201. d = json.loads(args)
  202. agent_name = d.get("agent", "")
  203. agent_input = d.get("input", "")
  204. agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
  205. if not agent_yml.exists():
  206. return f"[ERROR] Agent config not found: {agent_yml}"
  207. print(f"\n >> Dispatching sub-agent: {agent_name}")
  208. print(f" Config: {agent_yml}")
  209. try:
  210. result = Runtime.execute(str(agent_yml), agent_input)
  211. return result.result
  212. except Exception as e:
  213. return f"[AGENT_ERROR] {agent_name} failed: {e}"
  214. # ── 内部辅助 ─────────────────────────────────────────
  215. def _resolve(self, path: str) -> Path:
  216. p = Path(path)
  217. if p.is_absolute():
  218. return p
  219. return self.workspace / p
  220. def get_tool_map(self) -> Dict[str, callable]:
  221. """返回工具名 → 函数的映射,用于注入 runtime"""
  222. return {
  223. # fs
  224. "read_file": self.read_file,
  225. "write_file": self.write_file,
  226. "list_dir": self.list_dir,
  227. "mkdir": self.mkdir,
  228. "copy_file": self.copy_file,
  229. # marp
  230. "render_marp": self.render_marp,
  231. "python_exec": self.python_exec,
  232. "install_package": self.install_package,
  233. # local
  234. "terminate": self.terminate,
  235. "read_topic": self.read_topic,
  236. "save_checkpoint": self.save_checkpoint,
  237. "load_checkpoint": self.load_checkpoint,
  238. "human_feedback": self.human_feedback,
  239. "dispatch_agent": self.dispatch_agent,
  240. }
  241. # ═══════════════════════════════════════════════════════════
  242. # 2. Runner — 驱动 PPT 生成流程
  243. # ═══════════════════════════════════════════════════════════
  244. PHASES = [
  245. ("01_knowledge_extraction", "knowledge-extractor"),
  246. ("02_outline_generation", "outline-generator"),
  247. ("03_content_refinement", "content-refiner"),
  248. ("04_ppt_rendering", "ppt-renderer"),
  249. ]
  250. def create_workspace() -> Path:
  251. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  252. ws = AGENT_DIR / "workspace" / f"run_{ts}"
  253. ws.mkdir(parents=True, exist_ok=True)
  254. return ws
  255. def run_phase(
  256. phase_name: str,
  257. agent_name: str,
  258. workspace: Path,
  259. round_num: int,
  260. toolkit: LocalToolkit,
  261. prev_reports: List[str],
  262. revision_context: str = "",
  263. topic: str = "",
  264. template: str = "academic",
  265. ) -> Dict[str, Any]:
  266. """运行单个阶段的子智能体"""
  267. phase_dir = workspace / f"round_{round_num}" / phase_name
  268. phase_dir.mkdir(parents=True, exist_ok=True)
  269. (phase_dir / "artifacts").mkdir(exist_ok=True)
  270. agent_yml = AGENT_DIR / "agents" / f"{agent_name}.yml"
  271. # 构建输入上下文
  272. context_parts = [
  273. f"workspace: {workspace}",
  274. f"phase_dir: {phase_dir}",
  275. f"round: {round_num}",
  276. f"template: {template}",
  277. f"dependencies: {json.dumps(prev_reports)}",
  278. ]
  279. if revision_context:
  280. context_parts.append(f"revision_context: {revision_context}")
  281. # 读取主题
  282. if topic:
  283. context_parts.append(f"\n## PPT 主题:\n{topic}")
  284. else:
  285. topic_content = toolkit.read_topic()
  286. context_parts.append(f"\n## TOPIC.md Content:\n{topic_content}")
  287. # 附上前序阶段的关键结果摘要
  288. for rp in prev_reports[-3:]:
  289. rp_path = workspace / rp
  290. if rp_path.exists():
  291. try:
  292. data = json.loads(rp_path.read_text(encoding="utf-8"))
  293. summary = {k: v for k, v in data.items() if k == "_meta" or not isinstance(v, (list, dict))}
  294. context_parts.append(f"\n## Previous report ({rp}):\n{json.dumps(summary, ensure_ascii=False, indent=2)[:2000]}")
  295. except Exception:
  296. pass
  297. input_text = "\n".join(context_parts)
  298. print(f"\n{'━'*60}")
  299. print(f" Phase: {phase_name}")
  300. print(f" Agent: {agent_name} → {agent_yml.name}")
  301. print(f" Output: {phase_dir}")
  302. print(f"{'━'*60}")
  303. t0 = time.time()
  304. try:
  305. result = Runtime.execute(str(agent_yml), input_text)
  306. duration = time.time() - t0
  307. print(f" ✓ Completed in {duration:.1f}s, {len(result.trace)} trace steps")
  308. # 保存 trace
  309. trace_path = phase_dir / "trace.json"
  310. trace_data = [
  311. {"step": t.step, "term": t.term_name, "type": t.term_type,
  312. "duration_ms": t.duration_ms, "output": t.output[:200]}
  313. for t in result.trace
  314. ]
  315. trace_path.write_text(json.dumps(trace_data, indent=2), encoding="utf-8")
  316. # 尝试解析 report.json
  317. report_path = phase_dir / "report.json"
  318. if report_path.exists():
  319. report = json.loads(report_path.read_text(encoding="utf-8"))
  320. else:
  321. try:
  322. report = json.loads(result.result)
  323. report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  324. except (json.JSONDecodeError, TypeError):
  325. report = {"_meta": {"status": "completed"}, "raw_output": result.result[:3000]}
  326. report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  327. return report
  328. except Exception as e:
  329. duration = time.time() - t0
  330. print(f" ✗ Failed after {duration:.1f}s: {e}")
  331. error_report = {
  332. "_meta": {"status": "failed", "error": str(e), "duration_seconds": duration},
  333. }
  334. (phase_dir / "report.json").write_text(json.dumps(error_report, indent=2), encoding="utf-8")
  335. return error_report
  336. def run_full_pipeline(
  337. topic: str = "",
  338. template: str = "academic",
  339. start_phase: Optional[str] = None,
  340. max_rounds: int = 3,
  341. ):
  342. """运行完整的 PPT 生成流程"""
  343. if not os.environ.get("ANTHROPIC_API_KEY"):
  344. print("ERROR: ANTHROPIC_API_KEY environment variable not set.")
  345. print(" export ANTHROPIC_API_KEY=sk-ant-...")
  346. sys.exit(1)
  347. workspace = create_workspace()
  348. toolkit = LocalToolkit(workspace)
  349. # 读取主题
  350. if not topic:
  351. topic_path = AGENT_DIR / "TOPIC.md"
  352. if topic_path.exists():
  353. topic = topic_path.read_text(encoding="utf-8")
  354. else:
  355. print("ERROR: No topic provided. Use --topic or create TOPIC.md")
  356. sys.exit(1)
  357. print(f"""
  358. ╔══════════════════════════════════════════════════════════╗
  359. ║ PPTAgent-67 — 知识驱动 PPT 生成 ║
  360. ║ Model: Claude Opus 4.6 (1M context) ║
  361. ║ Template: {template:<39s} ║
  362. ╚══════════════════════════════════════════════════════════╝
  363. Workspace: {workspace}
  364. Topic: {topic[:60]}...
  365. Max rounds: {max_rounds}
  366. """)
  367. # 写入项目计划
  368. project_plan = f"""# PPTAgent-67 Project Plan
  369. ## Topic
  370. {topic[:500]}
  371. ## Template
  372. {template}
  373. ## Model
  374. Claude Opus 4.6 (1M context) — claude-opus-4-6
  375. ## Phases
  376. {chr(10).join(f'{i+1}. {name} → {agent}' for i, (name, agent) in enumerate(PHASES))}
  377. ## Target
  378. Content quality score ≥ 0.7
  379. ## Created
  380. {datetime.now(timezone.utc).isoformat()}
  381. """
  382. (workspace / "project_plan.md").write_text(project_plan, encoding="utf-8")
  383. # 保存 TOPIC.md 到工作区
  384. (workspace / "TOPIC.md").write_text(topic, encoding="utf-8")
  385. progress = {
  386. "project_id": workspace.name,
  387. "topic": topic[:200],
  388. "template": template,
  389. "current_round": 1,
  390. "current_phase": PHASES[0][0],
  391. "max_rounds": max_rounds,
  392. "best_score": 0.0,
  393. "rounds": [],
  394. }
  395. for round_num in range(1, max_rounds + 1):
  396. print(f"\n{'═'*60}")
  397. print(f" ROUND {round_num} / {max_rounds}")
  398. print(f"{'═'*60}")
  399. progress["current_round"] = round_num
  400. round_reports = []
  401. prev_reports = []
  402. # 确定本轮需要跑哪些阶段
  403. phases_to_run = PHASES
  404. if round_num > 1 and "revision_target" in progress:
  405. target = progress["revision_target"]
  406. start_idx = next((i for i, (name, _) in enumerate(PHASES) if name == target), 0)
  407. phases_to_run = PHASES[start_idx:]
  408. print(f" Revision: starting from {target}")
  409. if start_phase and round_num == 1:
  410. start_idx = next((i for i, (name, _) in enumerate(PHASES) if start_phase in name), 0)
  411. phases_to_run = PHASES[start_idx:]
  412. for phase_name, agent_name in phases_to_run:
  413. progress["current_phase"] = phase_name
  414. _save_progress(workspace, progress)
  415. report = run_phase(
  416. phase_name, agent_name, workspace, round_num, toolkit,
  417. prev_reports,
  418. revision_context=progress.get("revision_context", ""),
  419. topic=topic,
  420. template=template,
  421. )
  422. report_rel = f"round_{round_num}/{phase_name}/report.json"
  423. prev_reports.append(report_rel)
  424. round_reports.append({"phase": phase_name, "status": report.get("_meta", {}).get("status", "unknown")})
  425. # 在精修阶段检查质量分数
  426. if phase_name == "03_content_refinement":
  427. quality = report.get("quality_score", 0)
  428. progress["best_score"] = max(progress.get("best_score", 0), quality)
  429. print(f"\n Quality Score: {quality:.2f}")
  430. print(f" Best Score: {progress['best_score']:.2f}")
  431. if quality >= 0.7:
  432. print(f" ✓ Quality threshold met! Proceeding to render.")
  433. else:
  434. # 决定回退
  435. dims = report.get("dimension_scores", {})
  436. if dims:
  437. lowest = min(dims.items(), key=lambda x: x[1])
  438. dimension = lowest[0]
  439. target_map = {
  440. "structure": "02_outline_generation",
  441. "clarity": "03_content_refinement",
  442. "depth": "01_knowledge_extraction",
  443. "visual": "03_content_refinement",
  444. "narrative": "02_outline_generation",
  445. }
  446. if round_num < max_rounds:
  447. progress["revision_target"] = target_map.get(dimension, "03_content_refinement")
  448. suggestions = report.get("revision_suggestions", [])
  449. progress["revision_context"] = json.dumps(suggestions[:3], ensure_ascii=False)
  450. print(f" Lowest dimension: {dimension} → Will retry from {progress['revision_target']}")
  451. # 不进入渲染阶段,直接跳到下一轮
  452. break
  453. progress["rounds"].append({"round": round_num, "phases": round_reports})
  454. # 检查是否已经渲染完成
  455. pptx_report = workspace / f"round_{round_num}" / "04_ppt_rendering" / "report.json"
  456. if pptx_report.exists():
  457. print(f"\n ✓ PPT rendered successfully!")
  458. _finalize(workspace, round_num, toolkit, progress)
  459. break
  460. else:
  461. # 达到 max_rounds,强制渲染当前最佳版本
  462. print(f"\n Max rounds ({max_rounds}) reached. Rendering current best version.")
  463. # 执行最终渲染
  464. run_phase(
  465. "04_ppt_rendering", "ppt-renderer", workspace, round_num, toolkit,
  466. prev_reports, topic=topic, template=template,
  467. )
  468. _finalize(workspace, round_num, toolkit, progress)
  469. _save_progress(workspace, progress)
  470. print(f"\n{'═'*60}")
  471. print(f" Pipeline complete. Workspace: {workspace}")
  472. print(f"{'═'*60}\n")
  473. def _save_progress(workspace: Path, progress: dict):
  474. (workspace / "progress.json").write_text(
  475. json.dumps(progress, ensure_ascii=False, indent=2), encoding="utf-8"
  476. )
  477. def _finalize(workspace: Path, last_round: int, toolkit: LocalToolkit, progress: dict):
  478. """归档最终产出到 final/ 目录"""
  479. final_dir = workspace / "final"
  480. final_dir.mkdir(exist_ok=True)
  481. round_dir = workspace / f"round_{last_round}"
  482. # 复制产出文件
  483. for src_name, dst_name in [
  484. ("04_ppt_rendering/artifacts/presentation.pptx", "presentation.pptx"),
  485. ("04_ppt_rendering/artifacts/presentation.pdf", "presentation.pdf"),
  486. ("03_content_refinement/artifacts/presentation.md", "presentation.md"),
  487. ("02_outline_generation/artifacts/outline.json", "outline.json"),
  488. ("02_outline_generation/artifacts/outline.md", "outline.md"),
  489. ]:
  490. src = round_dir / src_name
  491. if src.exists():
  492. shutil.copy2(src, final_dir / dst_name)
  493. # 生成总结
  494. summary = {
  495. "topic": progress.get("topic", ""),
  496. "template_used": progress.get("template", "academic"),
  497. "total_rounds": last_round,
  498. "best_quality_score": progress.get("best_score", 0),
  499. "final_pptx_path": "final/presentation.pptx",
  500. "workspace": str(workspace),
  501. "completed_at": datetime.now(timezone.utc).isoformat(),
  502. }
  503. (final_dir / "generation_summary.json").write_text(
  504. json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
  505. )
  506. progress["final_output"] = str(final_dir)
  507. print(f"\n Final output archived to: {final_dir}")
  508. # ═══════════════════════════════════════════════════════════
  509. # 3. CLI
  510. # ═══════════════════════════════════════════════════════════
  511. def main():
  512. parser = argparse.ArgumentParser(description="PPTAgent-67 Runner")
  513. parser.add_argument("--topic", type=str, default="",
  514. help="PPT topic (or create TOPIC.md)")
  515. parser.add_argument("--template", type=str, default="academic",
  516. choices=["academic", "business", "minimal", "tech"],
  517. help="PPT template style (default: academic)")
  518. parser.add_argument("--phase", type=str, default=None,
  519. help="Start from a specific phase (e.g., outline, rendering)")
  520. parser.add_argument("--max-rounds", type=int, default=3,
  521. help="Maximum iteration rounds (default: 3)")
  522. parser.add_argument("--test-tools", action="store_true",
  523. help="Test local toolkit connectivity and exit")
  524. args = parser.parse_args()
  525. if args.test_tools:
  526. _test_tools()
  527. return
  528. run_full_pipeline(
  529. topic=args.topic,
  530. template=args.template,
  531. start_phase=args.phase,
  532. max_rounds=args.max_rounds,
  533. )
  534. def _test_tools():
  535. """测试本地工具是否可用"""
  536. ws = Path("/tmp/pptagent67_test")
  537. ws.mkdir(exist_ok=True)
  538. toolkit = LocalToolkit(ws)
  539. tests = [
  540. ("python_exec", lambda: toolkit.python_exec("import sys; print(f'Python {sys.version}')")),
  541. ("marp-cli", lambda: toolkit.python_exec("import subprocess; r=subprocess.run(['marp','--version'],capture_output=True,text=True); print(r.stdout.strip() or 'not found')")),
  542. ("write_file", lambda: toolkit.write_file(json.dumps({"path": "test.txt", "content": "hello"}))),
  543. ("read_file", lambda: toolkit.read_file("test.txt")),
  544. ("list_dir", lambda: toolkit.list_dir(".")),
  545. ]
  546. print("Testing local toolkit...\n")
  547. for name, fn in tests:
  548. try:
  549. t0 = time.time()
  550. result = fn()
  551. dt = time.time() - t0
  552. ok = "[ERROR]" not in result and "[STUB]" not in result
  553. status = "✓" if ok else "⚠"
  554. print(f" {status} {name:30s} ({dt:.1f}s) {result[:80]}")
  555. except Exception as e:
  556. print(f" ✗ {name:30s} ERROR: {e}")
  557. shutil.rmtree(ws, ignore_errors=True)
  558. print("\nDone.")
  559. if __name__ == "__main__":
  560. main()