ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (535) hide show
  1. agentui/.prettierrc +15 -0
  2. agentui/QUICKSTART.md +272 -0
  3. agentui/README.md +59 -0
  4. agentui/env.example +16 -0
  5. agentui/jsconfig.json +14 -0
  6. agentui/package-lock.json +4242 -0
  7. agentui/package.json +34 -0
  8. agentui/scripts/postinstall/apply-patches.mjs +260 -0
  9. agentui/src/app.css +61 -0
  10. agentui/src/app.d.ts +13 -0
  11. agentui/src/app.html +12 -0
  12. agentui/src/components/LoadingSpinner.svelte +64 -0
  13. agentui/src/components/ThemeSwitcher.svelte +159 -0
  14. agentui/src/components/index.js +4 -0
  15. agentui/src/lib/api/bots.ts +60 -0
  16. agentui/src/lib/api/chat.ts +22 -0
  17. agentui/src/lib/api/http.ts +25 -0
  18. agentui/src/lib/components/BotCard.svelte +33 -0
  19. agentui/src/lib/components/ChatBubble.svelte +63 -0
  20. agentui/src/lib/components/Toast.svelte +21 -0
  21. agentui/src/lib/config.ts +20 -0
  22. agentui/src/lib/stores/auth.svelte.ts +73 -0
  23. agentui/src/lib/stores/theme.svelte.js +64 -0
  24. agentui/src/lib/stores/toast.svelte.ts +31 -0
  25. agentui/src/lib/utils/conversation.ts +39 -0
  26. agentui/src/routes/+layout.svelte +20 -0
  27. agentui/src/routes/+page.svelte +232 -0
  28. agentui/src/routes/login/+page.svelte +200 -0
  29. agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
  30. agentui/src/routes/talk/[agentId]/+page.ts +7 -0
  31. agentui/static/README.md +1 -0
  32. agentui/svelte.config.js +11 -0
  33. agentui/tailwind.config.ts +53 -0
  34. agentui/tsconfig.json +3 -0
  35. agentui/vite.config.ts +10 -0
  36. ai_parrot-0.17.2.dist-info/METADATA +472 -0
  37. ai_parrot-0.17.2.dist-info/RECORD +535 -0
  38. ai_parrot-0.17.2.dist-info/WHEEL +6 -0
  39. ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
  40. ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
  41. ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
  42. crew-builder/.prettierrc +15 -0
  43. crew-builder/QUICKSTART.md +259 -0
  44. crew-builder/README.md +113 -0
  45. crew-builder/env.example +17 -0
  46. crew-builder/jsconfig.json +14 -0
  47. crew-builder/package-lock.json +4182 -0
  48. crew-builder/package.json +37 -0
  49. crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
  50. crew-builder/src/app.css +62 -0
  51. crew-builder/src/app.d.ts +13 -0
  52. crew-builder/src/app.html +12 -0
  53. crew-builder/src/components/LoadingSpinner.svelte +64 -0
  54. crew-builder/src/components/ThemeSwitcher.svelte +149 -0
  55. crew-builder/src/components/index.js +9 -0
  56. crew-builder/src/lib/api/bots.ts +60 -0
  57. crew-builder/src/lib/api/chat.ts +80 -0
  58. crew-builder/src/lib/api/client.ts +56 -0
  59. crew-builder/src/lib/api/crew/crew.ts +136 -0
  60. crew-builder/src/lib/api/index.ts +5 -0
  61. crew-builder/src/lib/api/o365/auth.ts +65 -0
  62. crew-builder/src/lib/auth/auth.ts +54 -0
  63. crew-builder/src/lib/components/AgentNode.svelte +43 -0
  64. crew-builder/src/lib/components/BotCard.svelte +33 -0
  65. crew-builder/src/lib/components/ChatBubble.svelte +67 -0
  66. crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
  67. crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
  68. crew-builder/src/lib/components/JsonViewer.svelte +24 -0
  69. crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
  70. crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
  71. crew-builder/src/lib/components/Toast.svelte +67 -0
  72. crew-builder/src/lib/components/Toolbar.svelte +157 -0
  73. crew-builder/src/lib/components/index.ts +10 -0
  74. crew-builder/src/lib/config.ts +8 -0
  75. crew-builder/src/lib/stores/auth.svelte.ts +228 -0
  76. crew-builder/src/lib/stores/crewStore.ts +369 -0
  77. crew-builder/src/lib/stores/theme.svelte.js +145 -0
  78. crew-builder/src/lib/stores/toast.svelte.ts +69 -0
  79. crew-builder/src/lib/utils/conversation.ts +39 -0
  80. crew-builder/src/lib/utils/markdown.ts +122 -0
  81. crew-builder/src/lib/utils/talkHistory.ts +47 -0
  82. crew-builder/src/routes/+layout.svelte +20 -0
  83. crew-builder/src/routes/+page.svelte +539 -0
  84. crew-builder/src/routes/agents/+page.svelte +247 -0
  85. crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
  86. crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
  87. crew-builder/src/routes/builder/+page.svelte +204 -0
  88. crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
  89. crew-builder/src/routes/crew/ask/+page.ts +1 -0
  90. crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
  91. crew-builder/src/routes/login/+page.svelte +197 -0
  92. crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
  93. crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
  94. crew-builder/static/README.md +1 -0
  95. crew-builder/svelte.config.js +11 -0
  96. crew-builder/tailwind.config.ts +53 -0
  97. crew-builder/tsconfig.json +3 -0
  98. crew-builder/vite.config.ts +10 -0
  99. mcp_servers/calculator_server.py +309 -0
  100. parrot/__init__.py +27 -0
  101. parrot/__pycache__/__init__.cpython-310.pyc +0 -0
  102. parrot/__pycache__/version.cpython-310.pyc +0 -0
  103. parrot/_version.py +34 -0
  104. parrot/a2a/__init__.py +48 -0
  105. parrot/a2a/client.py +658 -0
  106. parrot/a2a/discovery.py +89 -0
  107. parrot/a2a/mixin.py +257 -0
  108. parrot/a2a/models.py +376 -0
  109. parrot/a2a/server.py +770 -0
  110. parrot/agents/__init__.py +29 -0
  111. parrot/bots/__init__.py +12 -0
  112. parrot/bots/a2a_agent.py +19 -0
  113. parrot/bots/abstract.py +3139 -0
  114. parrot/bots/agent.py +1129 -0
  115. parrot/bots/basic.py +9 -0
  116. parrot/bots/chatbot.py +669 -0
  117. parrot/bots/data.py +1618 -0
  118. parrot/bots/database/__init__.py +5 -0
  119. parrot/bots/database/abstract.py +3071 -0
  120. parrot/bots/database/cache.py +286 -0
  121. parrot/bots/database/models.py +468 -0
  122. parrot/bots/database/prompts.py +154 -0
  123. parrot/bots/database/retries.py +98 -0
  124. parrot/bots/database/router.py +269 -0
  125. parrot/bots/database/sql.py +41 -0
  126. parrot/bots/db/__init__.py +6 -0
  127. parrot/bots/db/abstract.py +556 -0
  128. parrot/bots/db/bigquery.py +602 -0
  129. parrot/bots/db/cache.py +85 -0
  130. parrot/bots/db/documentdb.py +668 -0
  131. parrot/bots/db/elastic.py +1014 -0
  132. parrot/bots/db/influx.py +898 -0
  133. parrot/bots/db/mock.py +96 -0
  134. parrot/bots/db/multi.py +783 -0
  135. parrot/bots/db/prompts.py +185 -0
  136. parrot/bots/db/sql.py +1255 -0
  137. parrot/bots/db/tools.py +212 -0
  138. parrot/bots/document.py +680 -0
  139. parrot/bots/hrbot.py +15 -0
  140. parrot/bots/kb.py +170 -0
  141. parrot/bots/mcp.py +36 -0
  142. parrot/bots/orchestration/README.md +463 -0
  143. parrot/bots/orchestration/__init__.py +1 -0
  144. parrot/bots/orchestration/agent.py +155 -0
  145. parrot/bots/orchestration/crew.py +3330 -0
  146. parrot/bots/orchestration/fsm.py +1179 -0
  147. parrot/bots/orchestration/hr.py +434 -0
  148. parrot/bots/orchestration/storage/__init__.py +4 -0
  149. parrot/bots/orchestration/storage/memory.py +100 -0
  150. parrot/bots/orchestration/storage/mixin.py +119 -0
  151. parrot/bots/orchestration/verify.py +202 -0
  152. parrot/bots/product.py +204 -0
  153. parrot/bots/prompts/__init__.py +96 -0
  154. parrot/bots/prompts/agents.py +155 -0
  155. parrot/bots/prompts/data.py +216 -0
  156. parrot/bots/prompts/output_generation.py +8 -0
  157. parrot/bots/scraper/__init__.py +3 -0
  158. parrot/bots/scraper/models.py +122 -0
  159. parrot/bots/scraper/scraper.py +1173 -0
  160. parrot/bots/scraper/templates.py +115 -0
  161. parrot/bots/stores/__init__.py +5 -0
  162. parrot/bots/stores/local.py +172 -0
  163. parrot/bots/webdev.py +81 -0
  164. parrot/cli.py +17 -0
  165. parrot/clients/__init__.py +16 -0
  166. parrot/clients/base.py +1491 -0
  167. parrot/clients/claude.py +1191 -0
  168. parrot/clients/factory.py +129 -0
  169. parrot/clients/google.py +4567 -0
  170. parrot/clients/gpt.py +1975 -0
  171. parrot/clients/grok.py +432 -0
  172. parrot/clients/groq.py +986 -0
  173. parrot/clients/hf.py +582 -0
  174. parrot/clients/models.py +18 -0
  175. parrot/conf.py +395 -0
  176. parrot/embeddings/__init__.py +9 -0
  177. parrot/embeddings/base.py +157 -0
  178. parrot/embeddings/google.py +98 -0
  179. parrot/embeddings/huggingface.py +74 -0
  180. parrot/embeddings/openai.py +84 -0
  181. parrot/embeddings/processor.py +88 -0
  182. parrot/exceptions.c +13868 -0
  183. parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
  184. parrot/exceptions.pxd +22 -0
  185. parrot/exceptions.pxi +15 -0
  186. parrot/exceptions.pyx +44 -0
  187. parrot/generators/__init__.py +29 -0
  188. parrot/generators/base.py +200 -0
  189. parrot/generators/html.py +293 -0
  190. parrot/generators/react.py +205 -0
  191. parrot/generators/streamlit.py +203 -0
  192. parrot/generators/template.py +105 -0
  193. parrot/handlers/__init__.py +4 -0
  194. parrot/handlers/agent.py +861 -0
  195. parrot/handlers/agents/__init__.py +1 -0
  196. parrot/handlers/agents/abstract.py +900 -0
  197. parrot/handlers/bots.py +338 -0
  198. parrot/handlers/chat.py +915 -0
  199. parrot/handlers/creation.sql +192 -0
  200. parrot/handlers/crew/ARCHITECTURE.md +362 -0
  201. parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
  202. parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
  203. parrot/handlers/crew/__init__.py +0 -0
  204. parrot/handlers/crew/handler.py +801 -0
  205. parrot/handlers/crew/models.py +229 -0
  206. parrot/handlers/crew/redis_persistence.py +523 -0
  207. parrot/handlers/jobs/__init__.py +10 -0
  208. parrot/handlers/jobs/job.py +384 -0
  209. parrot/handlers/jobs/mixin.py +627 -0
  210. parrot/handlers/jobs/models.py +115 -0
  211. parrot/handlers/jobs/worker.py +31 -0
  212. parrot/handlers/models.py +596 -0
  213. parrot/handlers/o365_auth.py +105 -0
  214. parrot/handlers/stream.py +337 -0
  215. parrot/interfaces/__init__.py +6 -0
  216. parrot/interfaces/aws.py +143 -0
  217. parrot/interfaces/credentials.py +113 -0
  218. parrot/interfaces/database.py +27 -0
  219. parrot/interfaces/google.py +1123 -0
  220. parrot/interfaces/hierarchy.py +1227 -0
  221. parrot/interfaces/http.py +651 -0
  222. parrot/interfaces/images/__init__.py +0 -0
  223. parrot/interfaces/images/plugins/__init__.py +24 -0
  224. parrot/interfaces/images/plugins/abstract.py +58 -0
  225. parrot/interfaces/images/plugins/analisys.py +148 -0
  226. parrot/interfaces/images/plugins/classify.py +150 -0
  227. parrot/interfaces/images/plugins/classifybase.py +182 -0
  228. parrot/interfaces/images/plugins/detect.py +150 -0
  229. parrot/interfaces/images/plugins/exif.py +1103 -0
  230. parrot/interfaces/images/plugins/hash.py +52 -0
  231. parrot/interfaces/images/plugins/vision.py +104 -0
  232. parrot/interfaces/images/plugins/yolo.py +66 -0
  233. parrot/interfaces/images/plugins/zerodetect.py +197 -0
  234. parrot/interfaces/o365.py +978 -0
  235. parrot/interfaces/onedrive.py +822 -0
  236. parrot/interfaces/sharepoint.py +1435 -0
  237. parrot/interfaces/soap.py +257 -0
  238. parrot/loaders/__init__.py +8 -0
  239. parrot/loaders/abstract.py +1131 -0
  240. parrot/loaders/audio.py +199 -0
  241. parrot/loaders/basepdf.py +53 -0
  242. parrot/loaders/basevideo.py +1568 -0
  243. parrot/loaders/csv.py +409 -0
  244. parrot/loaders/docx.py +116 -0
  245. parrot/loaders/epubloader.py +316 -0
  246. parrot/loaders/excel.py +199 -0
  247. parrot/loaders/factory.py +55 -0
  248. parrot/loaders/files/__init__.py +0 -0
  249. parrot/loaders/files/abstract.py +39 -0
  250. parrot/loaders/files/html.py +26 -0
  251. parrot/loaders/files/text.py +63 -0
  252. parrot/loaders/html.py +152 -0
  253. parrot/loaders/markdown.py +442 -0
  254. parrot/loaders/pdf.py +373 -0
  255. parrot/loaders/pdfmark.py +320 -0
  256. parrot/loaders/pdftables.py +506 -0
  257. parrot/loaders/ppt.py +476 -0
  258. parrot/loaders/qa.py +63 -0
  259. parrot/loaders/splitters/__init__.py +10 -0
  260. parrot/loaders/splitters/base.py +138 -0
  261. parrot/loaders/splitters/md.py +228 -0
  262. parrot/loaders/splitters/token.py +143 -0
  263. parrot/loaders/txt.py +26 -0
  264. parrot/loaders/video.py +89 -0
  265. parrot/loaders/videolocal.py +218 -0
  266. parrot/loaders/videounderstanding.py +377 -0
  267. parrot/loaders/vimeo.py +167 -0
  268. parrot/loaders/web.py +599 -0
  269. parrot/loaders/youtube.py +504 -0
  270. parrot/manager/__init__.py +5 -0
  271. parrot/manager/manager.py +1030 -0
  272. parrot/mcp/__init__.py +28 -0
  273. parrot/mcp/adapter.py +105 -0
  274. parrot/mcp/cli.py +174 -0
  275. parrot/mcp/client.py +119 -0
  276. parrot/mcp/config.py +75 -0
  277. parrot/mcp/integration.py +842 -0
  278. parrot/mcp/oauth.py +933 -0
  279. parrot/mcp/server.py +225 -0
  280. parrot/mcp/transports/__init__.py +3 -0
  281. parrot/mcp/transports/base.py +279 -0
  282. parrot/mcp/transports/grpc_session.py +163 -0
  283. parrot/mcp/transports/http.py +312 -0
  284. parrot/mcp/transports/mcp.proto +108 -0
  285. parrot/mcp/transports/quic.py +1082 -0
  286. parrot/mcp/transports/sse.py +330 -0
  287. parrot/mcp/transports/stdio.py +309 -0
  288. parrot/mcp/transports/unix.py +395 -0
  289. parrot/mcp/transports/websocket.py +547 -0
  290. parrot/memory/__init__.py +16 -0
  291. parrot/memory/abstract.py +209 -0
  292. parrot/memory/agent.py +32 -0
  293. parrot/memory/cache.py +175 -0
  294. parrot/memory/core.py +555 -0
  295. parrot/memory/file.py +153 -0
  296. parrot/memory/mem.py +131 -0
  297. parrot/memory/redis.py +613 -0
  298. parrot/models/__init__.py +46 -0
  299. parrot/models/basic.py +118 -0
  300. parrot/models/compliance.py +208 -0
  301. parrot/models/crew.py +395 -0
  302. parrot/models/detections.py +654 -0
  303. parrot/models/generation.py +85 -0
  304. parrot/models/google.py +223 -0
  305. parrot/models/groq.py +23 -0
  306. parrot/models/openai.py +30 -0
  307. parrot/models/outputs.py +285 -0
  308. parrot/models/responses.py +938 -0
  309. parrot/notifications/__init__.py +743 -0
  310. parrot/openapi/__init__.py +3 -0
  311. parrot/openapi/components.yaml +641 -0
  312. parrot/openapi/config.py +322 -0
  313. parrot/outputs/__init__.py +32 -0
  314. parrot/outputs/formats/__init__.py +108 -0
  315. parrot/outputs/formats/altair.py +359 -0
  316. parrot/outputs/formats/application.py +122 -0
  317. parrot/outputs/formats/base.py +351 -0
  318. parrot/outputs/formats/bokeh.py +356 -0
  319. parrot/outputs/formats/card.py +424 -0
  320. parrot/outputs/formats/chart.py +436 -0
  321. parrot/outputs/formats/d3.py +255 -0
  322. parrot/outputs/formats/echarts.py +310 -0
  323. parrot/outputs/formats/generators/__init__.py +0 -0
  324. parrot/outputs/formats/generators/abstract.py +61 -0
  325. parrot/outputs/formats/generators/panel.py +145 -0
  326. parrot/outputs/formats/generators/streamlit.py +86 -0
  327. parrot/outputs/formats/generators/terminal.py +63 -0
  328. parrot/outputs/formats/holoviews.py +310 -0
  329. parrot/outputs/formats/html.py +147 -0
  330. parrot/outputs/formats/jinja2.py +46 -0
  331. parrot/outputs/formats/json.py +87 -0
  332. parrot/outputs/formats/map.py +933 -0
  333. parrot/outputs/formats/markdown.py +172 -0
  334. parrot/outputs/formats/matplotlib.py +237 -0
  335. parrot/outputs/formats/mixins/__init__.py +0 -0
  336. parrot/outputs/formats/mixins/emaps.py +855 -0
  337. parrot/outputs/formats/plotly.py +341 -0
  338. parrot/outputs/formats/seaborn.py +310 -0
  339. parrot/outputs/formats/table.py +397 -0
  340. parrot/outputs/formats/template_report.py +138 -0
  341. parrot/outputs/formats/yaml.py +125 -0
  342. parrot/outputs/formatter.py +152 -0
  343. parrot/outputs/templates/__init__.py +95 -0
  344. parrot/pipelines/__init__.py +0 -0
  345. parrot/pipelines/abstract.py +210 -0
  346. parrot/pipelines/detector.py +124 -0
  347. parrot/pipelines/models.py +90 -0
  348. parrot/pipelines/planogram.py +3002 -0
  349. parrot/pipelines/table.sql +97 -0
  350. parrot/plugins/__init__.py +106 -0
  351. parrot/plugins/importer.py +80 -0
  352. parrot/py.typed +0 -0
  353. parrot/registry/__init__.py +18 -0
  354. parrot/registry/registry.py +594 -0
  355. parrot/scheduler/__init__.py +1189 -0
  356. parrot/scheduler/models.py +60 -0
  357. parrot/security/__init__.py +16 -0
  358. parrot/security/prompt_injection.py +268 -0
  359. parrot/security/security_events.sql +25 -0
  360. parrot/services/__init__.py +1 -0
  361. parrot/services/mcp/__init__.py +8 -0
  362. parrot/services/mcp/config.py +13 -0
  363. parrot/services/mcp/server.py +295 -0
  364. parrot/services/o365_remote_auth.py +235 -0
  365. parrot/stores/__init__.py +7 -0
  366. parrot/stores/abstract.py +352 -0
  367. parrot/stores/arango.py +1090 -0
  368. parrot/stores/bigquery.py +1377 -0
  369. parrot/stores/cache.py +106 -0
  370. parrot/stores/empty.py +10 -0
  371. parrot/stores/faiss_store.py +1157 -0
  372. parrot/stores/kb/__init__.py +9 -0
  373. parrot/stores/kb/abstract.py +68 -0
  374. parrot/stores/kb/cache.py +165 -0
  375. parrot/stores/kb/doc.py +325 -0
  376. parrot/stores/kb/hierarchy.py +346 -0
  377. parrot/stores/kb/local.py +457 -0
  378. parrot/stores/kb/prompt.py +28 -0
  379. parrot/stores/kb/redis.py +659 -0
  380. parrot/stores/kb/store.py +115 -0
  381. parrot/stores/kb/user.py +374 -0
  382. parrot/stores/models.py +59 -0
  383. parrot/stores/pgvector.py +3 -0
  384. parrot/stores/postgres.py +2853 -0
  385. parrot/stores/utils/__init__.py +0 -0
  386. parrot/stores/utils/chunking.py +197 -0
  387. parrot/telemetry/__init__.py +3 -0
  388. parrot/telemetry/mixin.py +111 -0
  389. parrot/template/__init__.py +3 -0
  390. parrot/template/engine.py +259 -0
  391. parrot/tools/__init__.py +23 -0
  392. parrot/tools/abstract.py +644 -0
  393. parrot/tools/agent.py +363 -0
  394. parrot/tools/arangodbsearch.py +537 -0
  395. parrot/tools/arxiv_tool.py +188 -0
  396. parrot/tools/calculator/__init__.py +3 -0
  397. parrot/tools/calculator/operations/__init__.py +38 -0
  398. parrot/tools/calculator/operations/calculus.py +80 -0
  399. parrot/tools/calculator/operations/statistics.py +76 -0
  400. parrot/tools/calculator/tool.py +150 -0
  401. parrot/tools/cloudwatch.py +988 -0
  402. parrot/tools/codeinterpreter/__init__.py +127 -0
  403. parrot/tools/codeinterpreter/executor.py +371 -0
  404. parrot/tools/codeinterpreter/internals.py +473 -0
  405. parrot/tools/codeinterpreter/models.py +643 -0
  406. parrot/tools/codeinterpreter/prompts.py +224 -0
  407. parrot/tools/codeinterpreter/tool.py +664 -0
  408. parrot/tools/company_info/__init__.py +6 -0
  409. parrot/tools/company_info/tool.py +1138 -0
  410. parrot/tools/correlationanalysis.py +437 -0
  411. parrot/tools/database/abstract.py +286 -0
  412. parrot/tools/database/bq.py +115 -0
  413. parrot/tools/database/cache.py +284 -0
  414. parrot/tools/database/models.py +95 -0
  415. parrot/tools/database/pg.py +343 -0
  416. parrot/tools/databasequery.py +1159 -0
  417. parrot/tools/db.py +1800 -0
  418. parrot/tools/ddgo.py +370 -0
  419. parrot/tools/decorators.py +271 -0
  420. parrot/tools/dftohtml.py +282 -0
  421. parrot/tools/document.py +549 -0
  422. parrot/tools/ecs.py +819 -0
  423. parrot/tools/edareport.py +368 -0
  424. parrot/tools/elasticsearch.py +1049 -0
  425. parrot/tools/employees.py +462 -0
  426. parrot/tools/epson/__init__.py +96 -0
  427. parrot/tools/excel.py +683 -0
  428. parrot/tools/file/__init__.py +13 -0
  429. parrot/tools/file/abstract.py +76 -0
  430. parrot/tools/file/gcs.py +378 -0
  431. parrot/tools/file/local.py +284 -0
  432. parrot/tools/file/s3.py +511 -0
  433. parrot/tools/file/tmp.py +309 -0
  434. parrot/tools/file/tool.py +501 -0
  435. parrot/tools/file_reader.py +129 -0
  436. parrot/tools/flowtask/__init__.py +19 -0
  437. parrot/tools/flowtask/tool.py +761 -0
  438. parrot/tools/gittoolkit.py +508 -0
  439. parrot/tools/google/__init__.py +18 -0
  440. parrot/tools/google/base.py +169 -0
  441. parrot/tools/google/tools.py +1251 -0
  442. parrot/tools/googlelocation.py +5 -0
  443. parrot/tools/googleroutes.py +5 -0
  444. parrot/tools/googlesearch.py +5 -0
  445. parrot/tools/googlesitesearch.py +5 -0
  446. parrot/tools/googlevoice.py +2 -0
  447. parrot/tools/gvoice.py +695 -0
  448. parrot/tools/ibisworld/README.md +225 -0
  449. parrot/tools/ibisworld/__init__.py +11 -0
  450. parrot/tools/ibisworld/tool.py +366 -0
  451. parrot/tools/jiratoolkit.py +1718 -0
  452. parrot/tools/manager.py +1098 -0
  453. parrot/tools/math.py +152 -0
  454. parrot/tools/metadata.py +476 -0
  455. parrot/tools/msteams.py +1621 -0
  456. parrot/tools/msword.py +635 -0
  457. parrot/tools/multidb.py +580 -0
  458. parrot/tools/multistoresearch.py +369 -0
  459. parrot/tools/networkninja.py +167 -0
  460. parrot/tools/nextstop/__init__.py +4 -0
  461. parrot/tools/nextstop/base.py +286 -0
  462. parrot/tools/nextstop/employee.py +733 -0
  463. parrot/tools/nextstop/store.py +462 -0
  464. parrot/tools/notification.py +435 -0
  465. parrot/tools/o365/__init__.py +42 -0
  466. parrot/tools/o365/base.py +295 -0
  467. parrot/tools/o365/bundle.py +522 -0
  468. parrot/tools/o365/events.py +554 -0
  469. parrot/tools/o365/mail.py +992 -0
  470. parrot/tools/o365/onedrive.py +497 -0
  471. parrot/tools/o365/sharepoint.py +641 -0
  472. parrot/tools/openapi_toolkit.py +904 -0
  473. parrot/tools/openweather.py +527 -0
  474. parrot/tools/pdfprint.py +1001 -0
  475. parrot/tools/powerbi.py +518 -0
  476. parrot/tools/powerpoint.py +1113 -0
  477. parrot/tools/pricestool.py +146 -0
  478. parrot/tools/products/__init__.py +246 -0
  479. parrot/tools/prophet_tool.py +171 -0
  480. parrot/tools/pythonpandas.py +630 -0
  481. parrot/tools/pythonrepl.py +910 -0
  482. parrot/tools/qsource.py +436 -0
  483. parrot/tools/querytoolkit.py +395 -0
  484. parrot/tools/quickeda.py +827 -0
  485. parrot/tools/resttool.py +553 -0
  486. parrot/tools/retail/__init__.py +0 -0
  487. parrot/tools/retail/bby.py +528 -0
  488. parrot/tools/sandboxtool.py +703 -0
  489. parrot/tools/sassie/__init__.py +352 -0
  490. parrot/tools/scraping/__init__.py +7 -0
  491. parrot/tools/scraping/docs/select.md +466 -0
  492. parrot/tools/scraping/documentation.md +1278 -0
  493. parrot/tools/scraping/driver.py +436 -0
  494. parrot/tools/scraping/models.py +576 -0
  495. parrot/tools/scraping/options.py +85 -0
  496. parrot/tools/scraping/orchestrator.py +517 -0
  497. parrot/tools/scraping/readme.md +740 -0
  498. parrot/tools/scraping/tool.py +3115 -0
  499. parrot/tools/seasonaldetection.py +642 -0
  500. parrot/tools/shell_tool/__init__.py +5 -0
  501. parrot/tools/shell_tool/actions.py +408 -0
  502. parrot/tools/shell_tool/engine.py +155 -0
  503. parrot/tools/shell_tool/models.py +322 -0
  504. parrot/tools/shell_tool/tool.py +442 -0
  505. parrot/tools/site_search.py +214 -0
  506. parrot/tools/textfile.py +418 -0
  507. parrot/tools/think.py +378 -0
  508. parrot/tools/toolkit.py +298 -0
  509. parrot/tools/webapp_tool.py +187 -0
  510. parrot/tools/whatif.py +1279 -0
  511. parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
  512. parrot/tools/workday/__init__.py +6 -0
  513. parrot/tools/workday/models.py +1389 -0
  514. parrot/tools/workday/tool.py +1293 -0
  515. parrot/tools/yfinance_tool.py +306 -0
  516. parrot/tools/zipcode.py +217 -0
  517. parrot/utils/__init__.py +2 -0
  518. parrot/utils/helpers.py +73 -0
  519. parrot/utils/parsers/__init__.py +5 -0
  520. parrot/utils/parsers/toml.c +12078 -0
  521. parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
  522. parrot/utils/parsers/toml.pyx +21 -0
  523. parrot/utils/toml.py +11 -0
  524. parrot/utils/types.cpp +20936 -0
  525. parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
  526. parrot/utils/types.pyx +213 -0
  527. parrot/utils/uv.py +11 -0
  528. parrot/version.py +10 -0
  529. parrot/yaml-rs/Cargo.lock +350 -0
  530. parrot/yaml-rs/Cargo.toml +19 -0
  531. parrot/yaml-rs/pyproject.toml +19 -0
  532. parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
  533. parrot/yaml-rs/src/lib.rs +222 -0
  534. requirements/docker-compose.yml +24 -0
  535. requirements/requirements-dev.txt +21 -0
@@ -0,0 +1,1113 @@
1
+ """
2
+ PowerPoint Tool migrated to use AbstractDocumentTool framework.
3
+ """
4
+ import re
5
+ from pathlib import Path
6
+ import tempfile
7
+ from typing import Dict, List, Optional, Any, Union
8
+ import io
9
+ import traceback
10
+ import requests
11
+ from pptx import Presentation
12
+ from pptx.util import Pt, Inches, Cm
13
+ from pptx.dml.color import RGBColor
14
+ from pptx.enum.text import PP_ALIGN
15
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
16
+ from jinja2 import Environment, FileSystemLoader
17
+ from pydantic import Field, field_validator
18
+ import markdown
19
+ from bs4 import BeautifulSoup, NavigableString
20
+ from navconfig import BASE_DIR
21
+ from .document import (
22
+ AbstractDocumentTool,
23
+ DocumentGenerationArgs
24
+ )
25
+
26
+ class PowerPointArgs(DocumentGenerationArgs):
27
+ """Arguments schema for PowerPoint presentation generation."""
28
+
29
+ template_name: Optional[str] = Field(
30
+ None,
31
+ description="Name of the HTML template (e.g., 'presentation.html') to render before conversion"
32
+ )
33
+ template_vars: Optional[Dict[str, Any]] = Field(
34
+ None,
35
+ description="Variables to pass to the HTML template (e.g., title, author, date)"
36
+ )
37
+ pptx_template: Optional[str] = Field(
38
+ None,
39
+ description="Filename of PowerPoint template file (.pptx or .potx) to use as base"
40
+ )
41
+ pptx_template_path: Optional[Path] = Field(
42
+ None,
43
+ description="Path where the PowerPoint template file is located"
44
+ )
45
+ slide_layout: int = Field(
46
+ 1,
47
+ description="Default slide layout index (0=Title Slide, 1=Title and Content, etc.)",
48
+ ge=0,
49
+ le=15
50
+ )
51
+ title_styles: Optional[Dict[str, Any]] = Field(
52
+ None,
53
+ description="Styles to apply to slide titles (font_name, font_size, bold, italic, font_color, alignment)"
54
+ )
55
+ content_styles: Optional[Dict[str, Any]] = Field(
56
+ None,
57
+ description="Styles to apply to slide content (font_name, font_size, bold, italic, font_color, alignment)"
58
+ )
59
+ max_slides: int = Field(
60
+ 50,
61
+ description="Maximum number of slides to generate",
62
+ ge=1,
63
+ le=100
64
+ )
65
+ split_by_headings: bool = Field(
66
+ True,
67
+ description="Whether to split content into slides based on headings (H1, H2, etc.)"
68
+ )
69
+ enable_images: bool = Field(
70
+ True,
71
+ description="Whether to download and include images from URLs in markdown"
72
+ )
73
+ image_width: Optional[float] = Field(
74
+ None,
75
+ description="Default image width in inches (None for auto-sizing)"
76
+ )
77
+ image_height: Optional[float] = Field(
78
+ None,
79
+ description="Default image height in inches (None for auto-sizing)"
80
+ )
81
+ max_image_size: float = Field(
82
+ 6.0,
83
+ description="Maximum image size in inches (width or height)"
84
+ )
85
+ image_quality: str = Field(
86
+ "high",
87
+ description="Image quality: 'high', 'medium', 'low'"
88
+ )
89
+ image_timeout: int = Field(
90
+ 30,
91
+ description="Timeout for image downloads in seconds"
92
+ )
93
+
94
+ @field_validator('template_name')
95
+ @classmethod
96
+ def validate_template_name(cls, v):
97
+ if v and not v.endswith('.html'):
98
+ v = f"{v}.html"
99
+ return v
100
+
101
+ class PowerPointTool(AbstractDocumentTool):
102
+ """
103
+ PowerPoint Presentation Generator Tool.
104
+
105
+ This tool converts text content (including Markdown and HTML) into professionally
106
+ formatted PowerPoint presentations. It automatically splits content into slides
107
+ based on headings and supports custom templates, styling, and layout options.
108
+
109
+ Features:
110
+ - Automatic slide splitting based on headings (H1, H2, H3, etc.)
111
+ - Markdown to PowerPoint conversion with proper formatting
112
+ - HTML to PowerPoint conversion support
113
+ - Custom PowerPoint template support
114
+ - Jinja2 HTML template processing
115
+ - Configurable slide layouts and styling
116
+ - Table, list, and content formatting
117
+ - Professional presentation generation
118
+
119
+ Slide Splitting Logic:
120
+ - H1 (# Title) → Title slide (layout 0)
121
+ - H2 (## Section) → Content slide (layout 1)
122
+ - H3 (### Subsection) → Content slide (layout 1)
123
+ - Content between headings → Added to the slide
124
+ """
125
+
126
+ name = "powerpoint_generator"
127
+ description = (
128
+ "Generate PowerPoint presentations from text, Markdown, or HTML content. "
129
+ "Automatically splits content into slides based on headings. "
130
+ "Supports custom templates, styling, and professional presentation formatting."
131
+ )
132
+ args_schema = PowerPointArgs
133
+
134
+ # Document type configuration
135
+ document_type = "presentation"
136
+ default_extension = "pptx"
137
+ supported_extensions = [".pptx", ".potx"]
138
+
139
+ def __init__(
140
+ self,
141
+ templates_dir: Optional[Path] = None,
142
+ output_dir: Optional[Union[str, Path]] = None,
143
+ pptx_template_path: Optional[Path] = None,
144
+ default_html_template: Optional[str] = None,
145
+ **kwargs
146
+ ):
147
+ """
148
+ Initialize the PowerPoint Tool.
149
+
150
+ Args:
151
+ templates_dir: Directory containing HTML and PowerPoint templates
152
+ output_dir: Directory where generated presentations will be saved
153
+ default_html_template: Default HTML template for content processing
154
+ **kwargs: Additional arguments for AbstractDocumentTool
155
+ """
156
+ # Set up output directory before calling super().__init__
157
+ if output_dir:
158
+ kwargs['output_dir'] = Path(output_dir)
159
+
160
+ super().__init__(templates_dir=templates_dir, **kwargs)
161
+
162
+ self.default_html_template = default_html_template
163
+ self.pptx_template_path = pptx_template_path or BASE_DIR.joinpath('presentations')
164
+
165
+ # Initialize Jinja2 environment for HTML templates
166
+ if self.templates_dir:
167
+ self.html_env = Environment(
168
+ loader=FileSystemLoader(str(self.templates_dir)),
169
+ autoescape=True
170
+ )
171
+ else:
172
+ self.html_env = None
173
+
174
+ def _render_html_template(self, content: str, template_name: Optional[str], template_vars: Optional[Dict[str, Any]]) -> str:
175
+ """Render content through Jinja2 HTML template if provided."""
176
+ if not template_name or not self.html_env:
177
+ return content
178
+
179
+ try:
180
+ template = self.html_env.get_template(template_name)
181
+ vars_dict = template_vars or {}
182
+
183
+ # Add default variables
184
+ vars_dict.setdefault('content', content)
185
+ vars_dict.setdefault('date', self._get_current_date())
186
+ vars_dict.setdefault('timestamp', self._get_current_timestamp())
187
+
188
+ rendered = template.render(**vars_dict)
189
+ self.logger.info(f"Rendered content through HTML template: {template_name}")
190
+ return rendered
191
+
192
+ except Exception as e:
193
+ self.logger.error(f"HTML template rendering failed: {e}")
194
+ return content
195
+
196
+ def _preprocess_markdown(self, text: str) -> str:
197
+ """Preprocess markdown to handle common issues."""
198
+ # Replace placeholder variables with empty strings
199
+ text = re.sub(r'\{[a-zA-Z0-9_]+\}', '', text)
200
+
201
+ # Handle f-strings that weren't evaluated
202
+ text = re.sub(r'f"""(.*?)"""', r'\1', text, flags=re.DOTALL)
203
+ text = re.sub(r"f'''(.*?)'''", r'\1', text, flags=re.DOTALL)
204
+
205
+ # Remove triple backticks and language indicators
206
+ text = re.sub(r'```[a-zA-Z]*\n', '', text)
207
+ text = re.sub(r'```', '', text)
208
+
209
+ # Fix heading issues (ensure space after #) - this should work correctly
210
+ text = re.sub(r'(#+)([^ \n])', r'\1 \2', text)
211
+
212
+ # Fix escaped newlines if any
213
+ text = text.replace('\\n', '\n')
214
+
215
+ # Clean up extra whitespace but preserve line structure
216
+ lines = text.split('\n')
217
+ cleaned_lines = []
218
+ for line in lines:
219
+ # Strip leading/trailing whitespace but preserve the line
220
+ cleaned_line = line.strip()
221
+ cleaned_lines.append(cleaned_line)
222
+
223
+ return '\n'.join(cleaned_lines)
224
+
225
+ def _markdown_to_html(self, markdown_text: str) -> str:
226
+ """Convert markdown to HTML."""
227
+ try:
228
+ self.logger.debug(f"Converting markdown to HTML. Input preview: {markdown_text[:100]}...")
229
+
230
+ html = markdown.markdown(
231
+ markdown_text,
232
+ extensions=['extra', 'codehilite', 'tables'] # Removed 'toc' to avoid issues
233
+ )
234
+
235
+ self.logger.debug(f"HTML conversion result preview: {html[:200]}...")
236
+ return html
237
+
238
+ except Exception as e:
239
+ self.logger.error(f"Markdown conversion failed: {e}")
240
+ # Fallback: wrap in paragraphs
241
+ paragraphs = markdown_text.split('\n\n')
242
+ html_paragraphs = [f'<p>{p.replace(chr(10), "<br>")}</p>' for p in paragraphs if p.strip()]
243
+ fallback_html = '\n'.join(html_paragraphs)
244
+ self.logger.debug(f"Using fallback HTML: {fallback_html[:200]}...")
245
+ return fallback_html
246
+
247
+ def _extract_slides_from_html(self, html_content: str, max_slides: int) -> List[Dict[str, Any]]:
248
+ """Extract slides from HTML content based on headings."""
249
+ soup = BeautifulSoup(html_content, 'html.parser')
250
+ slides = []
251
+
252
+ # Find all heading elements
253
+ headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
254
+
255
+ if not headings:
256
+ # If no headings, create a single slide with all content
257
+ slides.append({
258
+ 'title': 'Presentation',
259
+ 'content': self._extract_content_elements(soup),
260
+ 'level': 1,
261
+ 'layout': None
262
+ })
263
+ return slides
264
+
265
+ # Get all elements in the document to process sequentially
266
+ all_elements = soup.find_all(
267
+ ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul', 'ol', 'table', 'blockquote', 'div', 'img']
268
+ )
269
+
270
+ current_slide = None
271
+
272
+ for element in all_elements:
273
+ if len(slides) >= max_slides:
274
+ self.logger.warning(
275
+ f"Reached maximum slides limit ({max_slides})"
276
+ )
277
+ break
278
+
279
+ # If this is a heading, start a new slide
280
+ if element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
281
+ if current_slide is not None and current_slide['content']:
282
+ slides.append(current_slide)
283
+
284
+ heading_level = int(element.name[1])
285
+ heading_text = self._get_text_content(element)
286
+ heading_text = heading_text.strip()
287
+
288
+ if not heading_text:
289
+ continue
290
+
291
+ current_slide = {
292
+ 'title': heading_text,
293
+ 'level': heading_level,
294
+ 'content': [],
295
+ # 'layout': 0 if (len(slides) == 0 and heading_level == 1) else None
296
+ 'layout': None
297
+ }
298
+ # If this is content and we have a current slide, add it
299
+ elif element.name in ['p', 'ul', 'ol', 'table', 'blockquote', 'div', 'img'] and current_slide is not None:
300
+ if element.name == 'img':
301
+ # Always add images
302
+ current_slide['content'].append(element)
303
+ img_alt = element.get('alt', 'Image')
304
+ self.logger.debug(f"Added image to slide '{current_slide['title']}': {img_alt}")
305
+ else:
306
+ # Add other content if not empty
307
+ content_text = self._get_text_content(element).strip()
308
+ if content_text:
309
+ current_slide['content'].append(element)
310
+
311
+ # Don't forget the last slide
312
+ if current_slide is not None and current_slide['content']:
313
+ slides.append(current_slide)
314
+
315
+ self.logger.info(
316
+ f"Extracted {len(slides)} slides from HTML content"
317
+ )
318
+ return slides
319
+
320
+ def _extract_content_elements(self, soup) -> List:
321
+ """Extract content elements from soup."""
322
+ content_elements = []
323
+ # Get all content elements, including images
324
+ for element in soup.find_all(['p', 'ul', 'ol', 'table', 'blockquote', 'div', 'img']):
325
+ # Skip if this div only contains headings
326
+ if element.name == 'div':
327
+ child_tags = [child.name for child in element.find_all() if hasattr(child, 'name')]
328
+ if all(tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] for tag in child_tags):
329
+ continue
330
+
331
+ # Special handling for paragraphs that contain images
332
+ if element.name == 'p':
333
+ # Check if paragraph contains images
334
+ images = element.find_all('img')
335
+ if images:
336
+ # Add text content if any
337
+ text_content = self._get_text_content(element).strip()
338
+ if text_content:
339
+ content_elements.append(element)
340
+ # Add images separately
341
+ for img in images:
342
+ content_elements.append(img)
343
+ else:
344
+ content_elements.append(element)
345
+ else:
346
+ content_elements.append(element)
347
+
348
+ return content_elements
349
+
350
+ def _create_presentation(self, template_path: Optional[str] = None) -> Presentation:
351
+ """Create or load PowerPoint presentation."""
352
+ if template_path:
353
+ pptx_template = self._get_template_path(template_path)
354
+ if pptx_template and pptx_template.exists():
355
+ self.logger.info(f"Loading PowerPoint template: {pptx_template}")
356
+ return Presentation(str(pptx_template))
357
+
358
+ # Create new presentation
359
+ return Presentation()
360
+
361
+ def _create_slides(
362
+ self,
363
+ prs: Presentation,
364
+ slides_data: List[Dict[str, Any]],
365
+ slide_layout: int,
366
+ title_styles: Optional[Dict[str, Any]],
367
+ content_styles: Optional[Dict[str, Any]],
368
+ **kwargs: Any
369
+ ) -> None:
370
+ """Create slides from extracted data with dynamic layout detection."""
371
+
372
+ # Debug available layouts
373
+ self.logger.info(
374
+ f"Available slide layouts: {len(prs.slide_layouts)}"
375
+ )
376
+ for i, layout in enumerate(prs.slide_layouts):
377
+ self.logger.debug(
378
+ f"Layout {i}: {layout.name}"
379
+ )
380
+ if len(prs.slides) > 0:
381
+ # Remove the empty slide if it exists
382
+ # slide_to_remove = prs.slides[0]
383
+ # slide_id = slide_to_remove.slide_id
384
+ prs.part.drop_rel(prs.slides._sldIdLst[0].rId)
385
+ del prs.slides._sldIdLst[0]
386
+
387
+ for i, slide_data in enumerate(slides_data):
388
+ try:
389
+ if len(prs.slide_layouts) == 1:
390
+ # Template has only one layout - use it for everything
391
+ layout_idx = 0
392
+ elif len(prs.slide_layouts) >= 2:
393
+ # Template has multiple layouts - use layout 1 for content, 0 for title-only
394
+ if slide_data['content']:
395
+ layout_idx = 1 # Content slide
396
+ else:
397
+ layout_idx = 0 # Title-only slide
398
+ self.logger.debug(
399
+ f"Using layout {layout_idx} for slide with content: {bool(slide_data['content'])}"
400
+ )
401
+ else:
402
+ # Fallback - shouldn't happen but just in case
403
+ layout_idx = 0
404
+ # Additional safety check
405
+ if layout_idx >= len(prs.slide_layouts):
406
+ layout_idx = 0
407
+ self.logger.warning(
408
+ f"Layout index {layout_idx} out of range, using layout 0"
409
+ )
410
+
411
+ # Add slide
412
+ slide_layout_obj = prs.slide_layouts[layout_idx]
413
+ slide = prs.slides.add_slide(slide_layout_obj)
414
+
415
+ # FIXED: Better title cleaning
416
+ if slide.shapes.title and slide_data['title']:
417
+ # Remove markdown symbols more thoroughly
418
+ _title = slide_data['title']
419
+ _title = re.sub(r'^#+\s*', '', _title) # Remove leading # symbols
420
+ _title = _title.lstrip('#').strip() # Remove any remaining # symbols
421
+ _title = _title.strip() # Final cleanup
422
+
423
+ slide.shapes.title.text = _title
424
+ self.logger.debug(f"Set slide title: '{_title}'")
425
+
426
+ if title_styles:
427
+ self._apply_text_styles(slide.shapes.title, title_styles)
428
+
429
+ # Add content only if there is content
430
+ if slide_data['content']:
431
+ self._add_slide_content(
432
+ slide, slide_data['content'], content_styles, **kwargs
433
+ )
434
+
435
+ self.logger.debug(
436
+ f"Created slide: '{_title}' using layout {layout_idx}"
437
+ )
438
+
439
+ except Exception as e:
440
+ self.logger.error(
441
+ f"Error creating slide '{slide_data.get('title', 'Unknown')}': {e}"
442
+ )
443
+ # Continue to next slide instead of failing completely
444
+ continue
445
+
446
+ def _find_content_placeholder(self, slide):
447
+ """Find the appropriate content placeholder on a slide."""
448
+ try:
449
+ # Debug: Log all available placeholders
450
+ self.logger.debug(
451
+ f"Available placeholders: {len(slide.shapes.placeholders)}"
452
+ )
453
+ for i, placeholder in enumerate(slide.shapes.placeholders):
454
+ self.logger.debug(
455
+ f"Placeholder {i}: {placeholder.placeholder_format.type}"
456
+ )
457
+
458
+ # Try to find content placeholder
459
+ # Common content placeholder types: BODY (2), OBJECT (7), CONTENT (13)
460
+ content_placeholder_types = [2, 7, 13, 14, 15, 16, 17, 18]
461
+
462
+ for placeholder in slide.shapes.placeholders:
463
+ try:
464
+ if hasattr(placeholder.placeholder_format, 'type'):
465
+ if placeholder.placeholder_format.type in content_placeholder_types:
466
+ if hasattr(placeholder, 'text_frame'):
467
+ self.logger.debug(
468
+ f"Found content placeholder: {placeholder.placeholder_format.type}"
469
+ )
470
+ return placeholder
471
+ except Exception as e:
472
+ self.logger.error(
473
+ f"Error finding content placeholder: {e}"
474
+ )
475
+ continue
476
+
477
+ # Fallback: Try to find any placeholder that's not the title (idx 0)
478
+ if len(slide.shapes.placeholders) > 1:
479
+ # Skip title placeholder (usually index 0) and try others
480
+ for i in range(1, len(slide.shapes.placeholders)):
481
+ placeholder = slide.shapes.placeholders[i]
482
+ if hasattr(placeholder, 'text_frame'):
483
+ self.logger.debug(f"Using placeholder at index {i} as content placeholder")
484
+ return placeholder
485
+
486
+ self.logger.warning(
487
+ "No suitable content placeholder found"
488
+ )
489
+ return None
490
+
491
+ except Exception as e:
492
+ self.logger.error(f"Error finding content placeholder: {e}")
493
+ return None
494
+
495
+ def _add_text_as_textbox(self, slide, content_elements, content_styles):
496
+ """Add text content as a text box when no content placeholder is available."""
497
+ try:
498
+ # FIXED: Better positioning that allows movement
499
+ left = Inches(0.5)
500
+ top = Inches(1.8) # Start below title area
501
+ width = Inches(9) # Full width minus margins
502
+ height = Inches(5) # Reasonable height
503
+
504
+ textbox = slide.shapes.add_textbox(left, top, width, height)
505
+ text_frame = textbox.text_frame
506
+
507
+ # FIXED: Better text frame settings
508
+ text_frame.clear()
509
+ text_frame.margin_left = Inches(0.1)
510
+ text_frame.margin_right = Inches(0.1)
511
+ text_frame.margin_top = Inches(0.1)
512
+ text_frame.margin_bottom = Inches(0.1)
513
+ text_frame.word_wrap = True
514
+
515
+ # Add content to text box with better formatting
516
+ first_paragraph = True
517
+ for element in content_elements:
518
+ if element.name == 'p':
519
+ paragraph_text = self._get_text_content(element)
520
+
521
+ # Split by line breaks for better formatting
522
+ lines = [line.strip() for line in paragraph_text.split('\n') if line.strip()]
523
+
524
+ for line in lines:
525
+ if first_paragraph and not text_frame.paragraphs[0].text:
526
+ p = text_frame.paragraphs[0]
527
+ first_paragraph = False
528
+ else:
529
+ p = text_frame.add_paragraph()
530
+
531
+ p.text = line
532
+ p.space_after = Pt(6) # Add some spacing between lines
533
+
534
+ if content_styles:
535
+ self._apply_paragraph_styles(p, content_styles)
536
+
537
+ elif element.name in ['ul', 'ol']:
538
+ for li in element.find_all('li', recursive=False):
539
+ if first_paragraph and not text_frame.paragraphs[0].text:
540
+ p = text_frame.paragraphs[0]
541
+ first_paragraph = False
542
+ else:
543
+ p = text_frame.add_paragraph()
544
+
545
+ p.text = f"• {self._get_text_content(li)}"
546
+ p.space_after = Pt(3)
547
+
548
+ if content_styles:
549
+ self._apply_paragraph_styles(p, content_styles)
550
+
551
+ elif element.name == 'table':
552
+ table_text = self._extract_table_text(element)
553
+ if first_paragraph and not text_frame.paragraphs[0].text:
554
+ p = text_frame.paragraphs[0]
555
+ first_paragraph = False
556
+ else:
557
+ p = text_frame.add_paragraph()
558
+
559
+ p.text = table_text
560
+ p.space_after = Pt(6)
561
+
562
+ if content_styles:
563
+ self._apply_paragraph_styles(p, content_styles)
564
+
565
+ elif element.name == 'blockquote':
566
+ if first_paragraph and not text_frame.paragraphs[0].text:
567
+ p = text_frame.paragraphs[0]
568
+ first_paragraph = False
569
+ else:
570
+ p = text_frame.add_paragraph()
571
+
572
+ p.text = f'"{self._get_text_content(element)}"'
573
+ p.space_after = Pt(6)
574
+
575
+ if content_styles:
576
+ self._apply_paragraph_styles(p, content_styles)
577
+
578
+ self.logger.debug("Added content as movable textbox")
579
+
580
+ except Exception as e:
581
+ self.logger.error(f"Error adding text as textbox: {e}")
582
+
583
+ def _add_slide_content(
584
+ self,
585
+ slide: Any,
586
+ content_elements: List,
587
+ content_styles: Optional[Dict[str, Any]],
588
+ **kwargs
589
+ ) -> None:
590
+ """Add content to a slide placeholder."""
591
+
592
+ enable_images = kwargs.get('enable_images', True)
593
+ image_width = kwargs.get('image_width')
594
+ image_height = kwargs.get('image_height')
595
+ max_image_size = kwargs.get('max_image_size', 6.0)
596
+
597
+ # Separate images from other content
598
+ images = [elem for elem in content_elements if elem.name == 'img']
599
+ other_content = [elem for elem in content_elements if elem.name != 'img']
600
+
601
+ # Handle text content first
602
+ if other_content:
603
+ content_placeholder = self._find_content_placeholder(slide)
604
+ if content_placeholder:
605
+ try:
606
+ text_frame = content_placeholder.text_frame
607
+ text_frame.clear()
608
+
609
+ for element in other_content:
610
+ if element.name == 'p':
611
+ if len(text_frame.paragraphs) == 1 and not text_frame.paragraphs[0].text:
612
+ p = text_frame.paragraphs[0]
613
+ else:
614
+ p = text_frame.add_paragraph()
615
+ p.text = self._get_text_content(element)
616
+ if content_styles:
617
+ self._apply_paragraph_styles(p, content_styles)
618
+
619
+ elif element.name in ['ul', 'ol']:
620
+ for li in element.find_all('li', recursive=False):
621
+ p = text_frame.add_paragraph()
622
+ p.text = self._get_text_content(li)
623
+ p.level = 1
624
+ if content_styles:
625
+ self._apply_paragraph_styles(p, content_styles)
626
+
627
+ elif element.name == 'table':
628
+ table_text = self._extract_table_text(element)
629
+ p = text_frame.add_paragraph()
630
+ p.text = table_text
631
+ if content_styles:
632
+ self._apply_paragraph_styles(p, content_styles)
633
+
634
+ elif element.name == 'blockquote':
635
+ p = text_frame.add_paragraph()
636
+ p.text = f'"{self._get_text_content(element)}"'
637
+ if content_styles:
638
+ self._apply_paragraph_styles(p, content_styles)
639
+
640
+ except Exception as e:
641
+ self.logger.error(f"Error adding text content to placeholder: {e}")
642
+ # Fallback: Add text as a text box
643
+ self._add_text_as_textbox(slide, other_content, content_styles)
644
+ else:
645
+ # No content placeholder found, add text as a text box
646
+ self.logger.info("No content placeholder found, adding text as textbox")
647
+ self._add_text_as_textbox(slide, other_content, content_styles)
648
+
649
+ # Handle images
650
+ if images and enable_images:
651
+ self._add_images_to_slide(
652
+ slide,
653
+ images,
654
+ image_width,
655
+ image_height,
656
+ max_image_size
657
+ )
658
+
659
+ def _download_image(self, img_url: str, timeout: int = 30) -> Optional[bytes]:
660
+ """Download image from URL."""
661
+ try:
662
+ self.logger.debug(f"Downloading image: {img_url}")
663
+
664
+ headers = {
665
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
666
+ }
667
+
668
+ response = requests.get(img_url, headers=headers, timeout=timeout, stream=True)
669
+ response.raise_for_status()
670
+
671
+ # Check content type
672
+ content_type = response.headers.get('content-type', '').lower()
673
+ if not any(img_type in content_type for img_type in ['image/', 'jpeg', 'jpg', 'png', 'gif', 'bmp']):
674
+ self.logger.warning(f"URL does not appear to be an image: {content_type}")
675
+
676
+ # Read image data
677
+ image_data = response.content
678
+
679
+ if len(image_data) == 0:
680
+ self.logger.error(f"Downloaded image is empty: {img_url}")
681
+ return None
682
+
683
+ self.logger.debug(f"Successfully downloaded image: {len(image_data)} bytes")
684
+ return image_data
685
+
686
+ except requests.exceptions.RequestException as e:
687
+ self.logger.error(f"Failed to download image {img_url}: {e}")
688
+ return None
689
+ except Exception as e:
690
+ self.logger.error(f"Unexpected error downloading image {img_url}: {e}")
691
+ return None
692
+
693
+ def _calculate_image_position(
694
+ self,
695
+ image_index: int,
696
+ total_images: int,
697
+ image_width: Optional[float],
698
+ image_height: Optional[float],
699
+ max_image_size: float
700
+ ) -> tuple:
701
+ """Calculate position and size for image on slide."""
702
+
703
+ # Default dimensions
704
+ slide_width = Inches(10) # Standard slide width
705
+ slide_height = Inches(7.5) # Standard slide height
706
+
707
+ # Calculate default size
708
+ if image_width and image_height:
709
+ width = Inches(image_width)
710
+ height = Inches(image_height)
711
+ else:
712
+ # Auto-size with max constraints
713
+ max_width = Inches(max_image_size)
714
+ max_height = Inches(max_image_size * 0.75) # Maintain aspect ratio
715
+
716
+ # For multiple images, make them smaller
717
+ if total_images > 1:
718
+ max_width = Inches(max_image_size * 0.7)
719
+ max_height = Inches(max_image_size * 0.5)
720
+
721
+ width = max_width
722
+ height = max_height
723
+
724
+ # Calculate position
725
+ if total_images == 1:
726
+ # Center single image
727
+ left = (slide_width - width) / 2
728
+ top = Inches(2) # Below title
729
+ else:
730
+ # Arrange multiple images
731
+ images_per_row = 2 if total_images <= 4 else 3
732
+ row = image_index // images_per_row
733
+ col = image_index % images_per_row
734
+
735
+ margin = Inches(0.5)
736
+ available_width = slide_width - (2 * margin)
737
+ available_height = slide_height - Inches(3) # Account for title
738
+
739
+ img_width = (available_width - (images_per_row - 1) * Inches(0.2)) / images_per_row
740
+ img_height = min(height, available_height / ((total_images - 1) // images_per_row + 1))
741
+
742
+ left = margin + col * (img_width + Inches(0.2))
743
+ top = Inches(2) + row * (img_height + Inches(0.2))
744
+
745
+ width = img_width
746
+ height = img_height
747
+
748
+ return left, top, width, height
749
+
750
+ def _add_images_to_slide(
751
+ self,
752
+ slide,
753
+ images: List,
754
+ image_width: Optional[float],
755
+ image_height: Optional[float],
756
+ max_image_size: float
757
+ ) -> None:
758
+ """Add images to a PowerPoint slide."""
759
+
760
+ for i, img_element in enumerate(images):
761
+ try:
762
+ img_src = img_element.get('src')
763
+ img_alt = img_element.get('alt', f'Image {i+1}')
764
+
765
+ if not img_src:
766
+ self.logger.warning(f"Image has no src attribute: {img_alt}")
767
+ continue
768
+
769
+ # Download image
770
+ image_data = self._download_image(img_src)
771
+ if not image_data:
772
+ self.logger.error(f"Failed to download image: {img_src}")
773
+ continue
774
+
775
+ # Calculate position and size
776
+ left, top, width, height = self._calculate_image_position(
777
+ i, len(images), image_width, image_height, max_image_size
778
+ )
779
+
780
+ # Add image to slide
781
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file:
782
+ tmp_file.write(image_data)
783
+ tmp_file_path = tmp_file.name
784
+
785
+ try:
786
+ picture = slide.shapes.add_picture(tmp_file_path, left, top, width, height)
787
+ self.logger.debug(f"Added image to slide: {img_alt}")
788
+ finally:
789
+ # Clean up temporary file
790
+ Path(tmp_file_path).unlink(missing_ok=True)
791
+
792
+ except Exception as e:
793
+ self.logger.error(f"Error adding image '{img_alt}': {e}")
794
+
795
+ def _extract_table_text(self, table_element) -> str:
796
+ """Extract text from table element."""
797
+ rows = table_element.find_all('tr')
798
+ table_lines = []
799
+
800
+ for row in rows:
801
+ cells = row.find_all(['td', 'th'])
802
+ row_text = ' | '.join([self._get_text_content(cell) for cell in cells])
803
+ table_lines.append(row_text)
804
+
805
+ return '\n'.join(table_lines)
806
+
807
+ def _get_text_content(self, element) -> str:
808
+ """Extract clean text content from HTML element with preserved line breaks."""
809
+ if isinstance(element, NavigableString):
810
+ return str(element).strip()
811
+
812
+ # For HTML elements, get the text content with line break preservation
813
+ if hasattr(element, 'get_text'):
814
+ # Use separator to preserve line breaks between elements
815
+ text = element.get_text(separator='\n', strip=True)
816
+
817
+ # Clean up excessive newlines but preserve intentional line breaks
818
+ # Replace multiple consecutive newlines with single newlines
819
+ text = re.sub(r'\n{3,}', '\n\n', text)
820
+
821
+ # Handle specific markdown formatting that should have line breaks
822
+ # Convert patterns like "**Key:** value" to have proper line breaks
823
+ text = re.sub(r'(\*\*[^*]+\*\*[^*\n]*?)(\*\*[^*]+\*\*)', r'\1\n\2', text)
824
+
825
+ return text
826
+
827
+ # Fallback method for manual text extraction
828
+ text_parts = []
829
+ for content in element.contents:
830
+ if isinstance(content, NavigableString):
831
+ text_parts.append(str(content).strip())
832
+ else:
833
+ text_parts.append(self._get_text_content(content))
834
+
835
+ result = '\n'.join([part for part in text_parts if part.strip()])
836
+
837
+ # Additional cleanup: remove any remaining markdown symbols
838
+ result = re.sub(r'^#+\s*', '', result) # Remove leading hashtags
839
+ result = re.sub(r'\*\*([^*]+)\*\*', r'\1', result) # Remove bold markers
840
+ result = re.sub(r'\*([^*]+)\*', r'\1', result) # Remove italic markers
841
+
842
+ return result
843
+
844
+ def _apply_text_styles(self, shape, styles: Dict[str, Any]) -> None:
845
+ """Apply styles to a text shape."""
846
+ if not shape.has_text_frame:
847
+ return
848
+
849
+ try:
850
+ text_frame = shape.text_frame
851
+ for paragraph in text_frame.paragraphs:
852
+ self._apply_paragraph_styles(paragraph, styles)
853
+ except Exception as e:
854
+ self.logger.error(f"Error applying text styles: {e}")
855
+
856
+ def _apply_paragraph_styles(self, paragraph, styles: Dict[str, Any]) -> None:
857
+ """Apply styles to a paragraph."""
858
+ try:
859
+ # Font styling
860
+ if 'font_name' in styles:
861
+ paragraph.font.name = styles['font_name']
862
+ if 'font_size' in styles:
863
+ paragraph.font.size = Pt(styles['font_size'])
864
+ if 'bold' in styles:
865
+ paragraph.font.bold = styles['bold']
866
+ if 'italic' in styles:
867
+ paragraph.font.italic = styles['italic']
868
+ if 'font_color' in styles:
869
+ # Convert hex color to RGB
870
+ color_hex = styles['font_color'].lstrip('#')
871
+ r, g, b = tuple(int(color_hex[i:i+2], 16) for i in (0, 2, 4))
872
+ paragraph.font.color.rgb = RGBColor(r, g, b)
873
+
874
+ # Alignment
875
+ if 'alignment' in styles:
876
+ alignment_map = {
877
+ 'left': PP_ALIGN.LEFT,
878
+ 'center': PP_ALIGN.CENTER,
879
+ 'right': PP_ALIGN.RIGHT,
880
+ 'justify': PP_ALIGN.JUSTIFY
881
+ }
882
+ paragraph.alignment = alignment_map.get(styles['alignment'], PP_ALIGN.LEFT)
883
+
884
+ except Exception as e:
885
+ self.logger.error(f"Error applying paragraph styles: {e}")
886
+
887
+ def debug_content_parsing(self, content: str) -> Dict[str, Any]:
888
+ """
889
+ Debug method to see how content is being parsed.
890
+
891
+ Args:
892
+ content: Input content to debug
893
+
894
+ Returns:
895
+ Dictionary with debug information
896
+ """
897
+ try:
898
+ # Process the content the same way as in generation
899
+ processed_content = self._preprocess_markdown(content)
900
+ html_content = self._markdown_to_html(processed_content)
901
+
902
+ # Parse with BeautifulSoup
903
+ soup = BeautifulSoup(html_content, 'html.parser')
904
+ headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
905
+
906
+ # Extract slide information
907
+ slides_data = self._extract_slides_from_html(html_content, 50)
908
+
909
+ debug_info = {
910
+ "original_content_length": len(content),
911
+ "original_content_preview": content[:300] + "..." if len(content) > 300 else content,
912
+ "processed_content_preview": processed_content[:300] + "..." if len(processed_content) > 300 else processed_content,
913
+ "html_content": html_content, # Show full HTML to debug
914
+ "headings_found": [
915
+ {
916
+ "tag": h.name,
917
+ "level": int(h.name[1]),
918
+ "raw_html": str(h),
919
+ "extracted_text": self._get_text_content(h),
920
+ "inner_text": h.get_text() if hasattr(h, 'get_text') else "N/A"
921
+ } for h in headings
922
+ ],
923
+ "slides_extracted": [
924
+ {
925
+ "title": slide['title'],
926
+ "level": slide['level'],
927
+ "layout": slide['layout'],
928
+ "content_count": len(slide['content']),
929
+ "content_preview": [
930
+ {
931
+ "tag": elem.name,
932
+ "text": self._get_text_content(elem)[:100]
933
+ } for elem in slide['content'][:3]
934
+ ]
935
+ } for slide in slides_data
936
+ ],
937
+ "total_slides": len(slides_data)
938
+ }
939
+
940
+ return debug_info
941
+
942
+ except Exception as e:
943
+ return {
944
+ "error": str(e),
945
+ "traceback": traceback.format_exc(),
946
+ "message": "Debug parsing failed"
947
+ }
948
+
949
+ async def _generate_document_content(self, content: str, **kwargs) -> bytes:
950
+ """
951
+ Generate PowerPoint presentation content from input.
952
+
953
+ Args:
954
+ content: Input content (text, markdown, or HTML)
955
+ **kwargs: Additional arguments from PowerPointArgs
956
+
957
+ Returns:
958
+ PowerPoint presentation as bytes
959
+ """
960
+ try:
961
+ # Extract arguments
962
+ template_name = kwargs.get('template_name')
963
+ template_vars = kwargs.get('template_vars')
964
+ pptx_template = kwargs.get('pptx_template')
965
+ slide_layout = kwargs.pop('slide_layout', 1)
966
+ title_styles = kwargs.pop('title_styles', None)
967
+ content_styles = kwargs.pop('content_styles', None)
968
+ max_slides = kwargs.pop('max_slides', 50)
969
+ split_by_headings = kwargs.pop('split_by_headings', True)
970
+
971
+ # Process content through HTML template if provided
972
+ processed_content = self._render_html_template(content, template_name, template_vars)
973
+
974
+ if pptx_template:
975
+ pptx_template = self.pptx_template_path.joinpath(pptx_template)
976
+
977
+ # Preprocess markdown
978
+ cleaned_content = self._preprocess_markdown(processed_content)
979
+
980
+ # Convert to HTML
981
+ html_content = self._markdown_to_html(cleaned_content)
982
+
983
+ # Extract slides from HTML
984
+ if split_by_headings:
985
+ slides_data = self._extract_slides_from_html(
986
+ html_content,
987
+ max_slides
988
+ )
989
+ else:
990
+ # Create single slide with all content
991
+ soup = BeautifulSoup(html_content, 'html.parser')
992
+ slides_data = [{
993
+ 'title': 'Presentation',
994
+ 'content': self._extract_content_elements(soup),
995
+ 'level': 1,
996
+ 'layout': 0
997
+ }]
998
+
999
+ self.logger.info(
1000
+ f"Generated {len(slides_data)} slides from content"
1001
+ )
1002
+
1003
+ # Create PowerPoint presentation
1004
+ prs = self._create_presentation(
1005
+ pptx_template
1006
+ )
1007
+
1008
+ # Create slides
1009
+ create_slides_kwargs = {
1010
+ k: v for k, v in kwargs.items()
1011
+ if k not in [
1012
+ 'template_name', 'template_vars', 'pptx_template',
1013
+ 'slide_layout', 'title_styles', 'content_styles',
1014
+ 'max_slides', 'split_by_headings'
1015
+ ]
1016
+ }
1017
+ self._create_slides(
1018
+ prs=prs,
1019
+ slides_data=slides_data,
1020
+ slide_layout=slide_layout,
1021
+ title_styles=title_styles,
1022
+ content_styles=content_styles,
1023
+ **create_slides_kwargs
1024
+ )
1025
+
1026
+ # Save presentation to bytes
1027
+ ppt_bytes = io.BytesIO()
1028
+ prs.save(ppt_bytes)
1029
+ ppt_bytes.seek(0)
1030
+
1031
+ return ppt_bytes.getvalue()
1032
+
1033
+ except Exception as e:
1034
+ self.logger.error(f"Error generating PowerPoint presentation: {e}")
1035
+ raise
1036
+
1037
+ async def _execute(
1038
+ self,
1039
+ content: str,
1040
+ output_filename: Optional[str] = None,
1041
+ file_prefix: str = "presentation",
1042
+ output_dir: Optional[str] = None,
1043
+ overwrite_existing: bool = False,
1044
+ template_name: Optional[str] = None,
1045
+ template_vars: Optional[Dict[str, Any]] = None,
1046
+ pptx_template: Optional[str] = None,
1047
+ slide_layout: int = 1,
1048
+ title_styles: Optional[Dict[str, Any]] = None,
1049
+ content_styles: Optional[Dict[str, Any]] = None,
1050
+ max_slides: int = 50,
1051
+ split_by_headings: bool = True,
1052
+ **kwargs
1053
+ ) -> Dict[str, Any]:
1054
+ """
1055
+ Execute PowerPoint presentation generation (AbstractTool interface).
1056
+
1057
+ Args:
1058
+ content: Content to convert to PowerPoint presentation
1059
+ output_filename: Custom filename (without extension)
1060
+ file_prefix: Prefix for auto-generated filenames
1061
+ output_dir: Custom output directory
1062
+ overwrite_existing: Whether to overwrite existing files
1063
+ template_name: HTML template name for content processing
1064
+ template_vars: Variables for HTML template
1065
+ pptx_template: PowerPoint template file path
1066
+ slide_layout: Default slide layout index
1067
+ title_styles: Styles for slide titles
1068
+ content_styles: Styles for slide content
1069
+ max_slides: Maximum number of slides to generate
1070
+ split_by_headings: Whether to split by headings
1071
+ **kwargs: Additional arguments
1072
+
1073
+ Returns:
1074
+ Dictionary with presentation generation results
1075
+ """
1076
+ try:
1077
+ self.logger.info(f"Starting PowerPoint generation with {len(content)} characters of content")
1078
+
1079
+ # Use the safe document creation workflow
1080
+ result = await self._create_document_safely(
1081
+ content=content,
1082
+ output_filename=output_filename,
1083
+ file_prefix=file_prefix,
1084
+ output_dir=output_dir,
1085
+ overwrite_existing=overwrite_existing,
1086
+ extension="pptx",
1087
+ template_name=template_name,
1088
+ template_vars=template_vars,
1089
+ pptx_template=pptx_template,
1090
+ slide_layout=slide_layout,
1091
+ title_styles=title_styles,
1092
+ content_styles=content_styles,
1093
+ max_slides=max_slides,
1094
+ split_by_headings=split_by_headings
1095
+ )
1096
+
1097
+ if result['status'] == 'success':
1098
+ # Add presentation-specific metadata
1099
+ result['presentation_info'] = {
1100
+ 'max_slides_limit': max_slides,
1101
+ 'split_by_headings': split_by_headings,
1102
+ 'slide_layout_used': slide_layout
1103
+ }
1104
+
1105
+ self.logger.debug(
1106
+ f"PowerPoint presentation created successfully: {result['metadata']['filename']}"
1107
+ )
1108
+
1109
+ return result
1110
+
1111
+ except Exception as e:
1112
+ self.logger.error(f"Error in PowerPoint generation: {e}")
1113
+ raise