@_vrsen/openswarm 0.1.0

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 (316) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/bin/openswarm.js +38 -0
  4. package/config.py +34 -0
  5. package/data_analyst_agent/.cursor/rules/data_analyst.mdc +43 -0
  6. package/data_analyst_agent/__init__.py +3 -0
  7. package/data_analyst_agent/__pycache__/__init__.cpython-312.pyc +0 -0
  8. package/data_analyst_agent/__pycache__/data_analyst_agent.cpython-312.pyc +0 -0
  9. package/data_analyst_agent/data_analyst_agent.py +45 -0
  10. package/data_analyst_agent/instructions.md +173 -0
  11. package/data_analyst_agent/test_files/test_file.csv +21 -0
  12. package/data_analyst_agent/tools/__init__.py +6 -0
  13. package/deep_research/__init__.py +1 -0
  14. package/deep_research/__pycache__/__init__.cpython-312.pyc +0 -0
  15. package/deep_research/__pycache__/deep_research.cpython-312.pyc +0 -0
  16. package/deep_research/deep_research.py +27 -0
  17. package/deep_research/instructions.md +104 -0
  18. package/deep_research/tools/__init__.py +1 -0
  19. package/docs_agent/__init__.py +3 -0
  20. package/docs_agent/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/docs_agent/__pycache__/docs_agent.cpython-312.pyc +0 -0
  22. package/docs_agent/docs_agent.py +61 -0
  23. package/docs_agent/instructions.md +418 -0
  24. package/docs_agent/tools/ConvertDocument.py +323 -0
  25. package/docs_agent/tools/CreateDocument.py +287 -0
  26. package/docs_agent/tools/ListDocuments.py +134 -0
  27. package/docs_agent/tools/ModifyDocument.py +247 -0
  28. package/docs_agent/tools/RestoreDocument.py +79 -0
  29. package/docs_agent/tools/ViewDocument.py +153 -0
  30. package/docs_agent/tools/__init__.py +1 -0
  31. package/docs_agent/tools/__pycache__/ConvertDocument.cpython-312.pyc +0 -0
  32. package/docs_agent/tools/__pycache__/CreateDocument.cpython-312.pyc +0 -0
  33. package/docs_agent/tools/__pycache__/ListDocuments.cpython-312.pyc +0 -0
  34. package/docs_agent/tools/__pycache__/ModifyDocument.cpython-312.pyc +0 -0
  35. package/docs_agent/tools/__pycache__/RestoreDocument.cpython-312.pyc +0 -0
  36. package/docs_agent/tools/__pycache__/ViewDocument.cpython-312.pyc +0 -0
  37. package/docs_agent/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  38. package/docs_agent/tools/utils/__init__.py +1 -0
  39. package/docs_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  40. package/docs_agent/tools/utils/__pycache__/doc_file_utils.cpython-312.pyc +0 -0
  41. package/docs_agent/tools/utils/__pycache__/html_docx_blocks.cpython-312.pyc +0 -0
  42. package/docs_agent/tools/utils/__pycache__/html_docx_constants.cpython-312.pyc +0 -0
  43. package/docs_agent/tools/utils/__pycache__/html_docx_core.cpython-312.pyc +0 -0
  44. package/docs_agent/tools/utils/__pycache__/html_docx_css.cpython-312.pyc +0 -0
  45. package/docs_agent/tools/utils/__pycache__/html_docx_images.cpython-312.pyc +0 -0
  46. package/docs_agent/tools/utils/__pycache__/html_docx_page.cpython-312.pyc +0 -0
  47. package/docs_agent/tools/utils/__pycache__/html_docx_paragraphs.cpython-312.pyc +0 -0
  48. package/docs_agent/tools/utils/__pycache__/html_docx_playwright.cpython-312.pyc +0 -0
  49. package/docs_agent/tools/utils/__pycache__/html_docx_selectors.cpython-312.pyc +0 -0
  50. package/docs_agent/tools/utils/__pycache__/html_docx_shared.cpython-312.pyc +0 -0
  51. package/docs_agent/tools/utils/__pycache__/html_validation.cpython-312.pyc +0 -0
  52. package/docs_agent/tools/utils/doc_file_utils.py +29 -0
  53. package/docs_agent/tools/utils/html_docx_blocks.py +262 -0
  54. package/docs_agent/tools/utils/html_docx_constants.py +78 -0
  55. package/docs_agent/tools/utils/html_docx_core.py +138 -0
  56. package/docs_agent/tools/utils/html_docx_css.py +262 -0
  57. package/docs_agent/tools/utils/html_docx_images.py +293 -0
  58. package/docs_agent/tools/utils/html_docx_page.py +185 -0
  59. package/docs_agent/tools/utils/html_docx_paragraphs.py +342 -0
  60. package/docs_agent/tools/utils/html_docx_playwright.py +184 -0
  61. package/docs_agent/tools/utils/html_docx_selectors.py +196 -0
  62. package/docs_agent/tools/utils/html_docx_shared.py +23 -0
  63. package/docs_agent/tools/utils/html_docx_tables.py +942 -0
  64. package/docs_agent/tools/utils/html_validation.py +102 -0
  65. package/helpers.py +59 -0
  66. package/image_generation_agent/__init__.py +1 -0
  67. package/image_generation_agent/__pycache__/__init__.cpython-312.pyc +0 -0
  68. package/image_generation_agent/__pycache__/image_generation_agent.cpython-312.pyc +0 -0
  69. package/image_generation_agent/image_generation_agent.py +31 -0
  70. package/image_generation_agent/instructions.md +80 -0
  71. package/image_generation_agent/tools/CombineImages.py +211 -0
  72. package/image_generation_agent/tools/EditImages.py +200 -0
  73. package/image_generation_agent/tools/GenerateImages.py +184 -0
  74. package/image_generation_agent/tools/RemoveBackground.py +108 -0
  75. package/image_generation_agent/tools/__init__.py +2 -0
  76. package/image_generation_agent/tools/__pycache__/CombineImages.cpython-312.pyc +0 -0
  77. package/image_generation_agent/tools/__pycache__/EditImages.cpython-312.pyc +0 -0
  78. package/image_generation_agent/tools/__pycache__/GenerateImages.cpython-312.pyc +0 -0
  79. package/image_generation_agent/tools/__pycache__/RemoveBackground.cpython-312.pyc +0 -0
  80. package/image_generation_agent/tools/utils/__init__.py +2 -0
  81. package/image_generation_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  82. package/image_generation_agent/tools/utils/__pycache__/image_io.cpython-312.pyc +0 -0
  83. package/image_generation_agent/tools/utils/image_io.py +308 -0
  84. package/onboard.py +325 -0
  85. package/orchestrator/__init__.py +3 -0
  86. package/orchestrator/__pycache__/__init__.cpython-312.pyc +0 -0
  87. package/orchestrator/__pycache__/orchestrator.cpython-312.pyc +0 -0
  88. package/orchestrator/instructions.md +90 -0
  89. package/orchestrator/orchestrator.py +33 -0
  90. package/package.json +49 -0
  91. package/patches/__init__.py +1 -0
  92. package/patches/__pycache__/__init__.cpython-312.pyc +0 -0
  93. package/patches/__pycache__/patch_agency_swarm_dual_comms.cpython-312.pyc +0 -0
  94. package/patches/__pycache__/patch_file_attachment_refs.cpython-312.pyc +0 -0
  95. package/patches/__pycache__/patch_ipython_interpreter_composio.cpython-312.pyc +0 -0
  96. package/patches/dom-to-pptx+1.1.5.patch +133440 -0
  97. package/patches/patch_agency_swarm_dual_comms.py +199 -0
  98. package/patches/patch_file_attachment_refs.py +74 -0
  99. package/patches/patch_ipython_interpreter_composio.py +54 -0
  100. package/pyproject.toml +67 -0
  101. package/run.py +343 -0
  102. package/server.py +26 -0
  103. package/shared_instructions.md +119 -0
  104. package/shared_tools/CopyFile.py +68 -0
  105. package/shared_tools/ExecuteTool.py +184 -0
  106. package/shared_tools/FindTools.py +101 -0
  107. package/shared_tools/ManageConnections.py +43 -0
  108. package/shared_tools/SearchTools.py +44 -0
  109. package/shared_tools/__init__.py +7 -0
  110. package/shared_tools/__pycache__/CopyFile.cpython-312.pyc +0 -0
  111. package/shared_tools/__pycache__/ExecuteTool.cpython-312.pyc +0 -0
  112. package/shared_tools/__pycache__/FindTools.cpython-312.pyc +0 -0
  113. package/shared_tools/__pycache__/ManageConnections.cpython-312.pyc +0 -0
  114. package/shared_tools/__pycache__/SearchTools.cpython-312.pyc +0 -0
  115. package/shared_tools/__pycache__/__init__.cpython-312.pyc +0 -0
  116. package/slides_agent/.cursor/rules/slides-agent-workflow.mdc +9 -0
  117. package/slides_agent/__init__.py +1 -0
  118. package/slides_agent/__pycache__/__init__.cpython-312.pyc +0 -0
  119. package/slides_agent/__pycache__/slides_agent.cpython-312.pyc +0 -0
  120. package/slides_agent/instructions.md +298 -0
  121. package/slides_agent/pptx/SKILL.md +528 -0
  122. package/slides_agent/pptx/html2pptx.md +625 -0
  123. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  124. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  125. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  126. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  127. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  128. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  129. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  130. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  131. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  132. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  133. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  134. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  135. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  136. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  137. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  138. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  139. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  140. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  141. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  142. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  143. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  144. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  145. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  146. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  147. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  148. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  149. package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  150. package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  151. package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  152. package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  153. package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  154. package/slides_agent/pptx/ooxml/schemas/mce/mc.xsd +75 -0
  155. package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  156. package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  157. package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  158. package/slides_agent/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  159. package/slides_agent/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  160. package/slides_agent/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  161. package/slides_agent/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  162. package/slides_agent/pptx/ooxml/scripts/pack.py +169 -0
  163. package/slides_agent/pptx/ooxml/scripts/unpack.py +29 -0
  164. package/slides_agent/pptx/ooxml/scripts/validate.py +69 -0
  165. package/slides_agent/pptx/ooxml/scripts/validation/__init__.py +15 -0
  166. package/slides_agent/pptx/ooxml/scripts/validation/base.py +951 -0
  167. package/slides_agent/pptx/ooxml/scripts/validation/docx.py +274 -0
  168. package/slides_agent/pptx/ooxml/scripts/validation/pptx.py +315 -0
  169. package/slides_agent/pptx/ooxml/scripts/validation/redlining.py +279 -0
  170. package/slides_agent/pptx/ooxml.md +427 -0
  171. package/slides_agent/pptx/scripts/html2pptx.js +1092 -0
  172. package/slides_agent/pptx/scripts/inventory.py +1020 -0
  173. package/slides_agent/pptx/scripts/rearrange.py +231 -0
  174. package/slides_agent/pptx/scripts/replace.py +385 -0
  175. package/slides_agent/pptx/scripts/thumbnail.py +451 -0
  176. package/slides_agent/slides_agent.py +109 -0
  177. package/slides_agent/test_deck/_theme.css +92 -0
  178. package/slides_agent/test_deck/assets/placeholder.svg +11 -0
  179. package/slides_agent/test_deck/slide_01_title.html +10 -0
  180. package/slides_agent/test_deck/slide_02_image_split.html +23 -0
  181. package/slides_agent/test_deck/slide_03_kpi.html +21 -0
  182. package/slides_agent/tools/ApplyPptxTextReplacements.py +91 -0
  183. package/slides_agent/tools/BuildPptxFromHtmlSlides.py +221 -0
  184. package/slides_agent/tools/CheckSlide.py +218 -0
  185. package/slides_agent/tools/CheckSlideCanvasOverflow.py +221 -0
  186. package/slides_agent/tools/CreateImageMontage.py +261 -0
  187. package/slides_agent/tools/CreatePptxThumbnailGrid.py +168 -0
  188. package/slides_agent/tools/DeleteSlide.py +78 -0
  189. package/slides_agent/tools/DownloadImage.py +79 -0
  190. package/slides_agent/tools/EnsureRasterImage.py +157 -0
  191. package/slides_agent/tools/ExtractPptxTextInventory.py +104 -0
  192. package/slides_agent/tools/GenerateImage.py +189 -0
  193. package/slides_agent/tools/ImageSearch.py +127 -0
  194. package/slides_agent/tools/InsertNewSlides.py +393 -0
  195. package/slides_agent/tools/ManageTheme.py +112 -0
  196. package/slides_agent/tools/ModifySlide.py +563 -0
  197. package/slides_agent/tools/ReadSlide.py +26 -0
  198. package/slides_agent/tools/RearrangePptxSlidesFromTemplate.py +114 -0
  199. package/slides_agent/tools/RestoreSnapshot.py +89 -0
  200. package/slides_agent/tools/SlideScreenshot.py +66 -0
  201. package/slides_agent/tools/__init__.py +54 -0
  202. package/slides_agent/tools/__pycache__/ApplyPptxTextReplacements.cpython-312.pyc +0 -0
  203. package/slides_agent/tools/__pycache__/BuildPptxFromHtmlSlides.cpython-312.pyc +0 -0
  204. package/slides_agent/tools/__pycache__/CheckSlide.cpython-312.pyc +0 -0
  205. package/slides_agent/tools/__pycache__/CheckSlideCanvasOverflow.cpython-312.pyc +0 -0
  206. package/slides_agent/tools/__pycache__/CreateImageMontage.cpython-312.pyc +0 -0
  207. package/slides_agent/tools/__pycache__/CreatePptxThumbnailGrid.cpython-312.pyc +0 -0
  208. package/slides_agent/tools/__pycache__/DeleteSlide.cpython-312.pyc +0 -0
  209. package/slides_agent/tools/__pycache__/DownloadImage.cpython-312.pyc +0 -0
  210. package/slides_agent/tools/__pycache__/EnsureRasterImage.cpython-312.pyc +0 -0
  211. package/slides_agent/tools/__pycache__/ExtractPptxTextInventory.cpython-312.pyc +0 -0
  212. package/slides_agent/tools/__pycache__/GenerateImage.cpython-312.pyc +0 -0
  213. package/slides_agent/tools/__pycache__/ImageSearch.cpython-312.pyc +0 -0
  214. package/slides_agent/tools/__pycache__/InsertNewSlides.cpython-312.pyc +0 -0
  215. package/slides_agent/tools/__pycache__/ManageTheme.cpython-312.pyc +0 -0
  216. package/slides_agent/tools/__pycache__/ModifySlide.cpython-312.pyc +0 -0
  217. package/slides_agent/tools/__pycache__/ReadSlide.cpython-312.pyc +0 -0
  218. package/slides_agent/tools/__pycache__/RearrangePptxSlidesFromTemplate.cpython-312.pyc +0 -0
  219. package/slides_agent/tools/__pycache__/RestoreSnapshot.cpython-312.pyc +0 -0
  220. package/slides_agent/tools/__pycache__/SlideScreenshot.cpython-312.pyc +0 -0
  221. package/slides_agent/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  222. package/slides_agent/tools/__pycache__/slide_file_utils.cpython-312.pyc +0 -0
  223. package/slides_agent/tools/__pycache__/slide_html_utils.cpython-312.pyc +0 -0
  224. package/slides_agent/tools/__pycache__/template_registry.cpython-312.pyc +0 -0
  225. package/slides_agent/tools/deck_utils.py +31 -0
  226. package/slides_agent/tools/html2pptx_runner.js +1183 -0
  227. package/slides_agent/tools/html_writer_instructions.md +149 -0
  228. package/slides_agent/tools/slide_file_utils.py +108 -0
  229. package/slides_agent/tools/slide_html_utils.py +354 -0
  230. package/slides_agent/tools/template_registry.py +55 -0
  231. package/swarm.py +82 -0
  232. package/video_generation_agent/__init__.py +1 -0
  233. package/video_generation_agent/__pycache__/__init__.cpython-312.pyc +0 -0
  234. package/video_generation_agent/__pycache__/video_generation_agent.cpython-312.pyc +0 -0
  235. package/video_generation_agent/instructions.md +178 -0
  236. package/video_generation_agent/tools/AddSubtitles.py +425 -0
  237. package/video_generation_agent/tools/CombineImages.py +166 -0
  238. package/video_generation_agent/tools/CombineVideos.py +113 -0
  239. package/video_generation_agent/tools/EditAudio.py +297 -0
  240. package/video_generation_agent/tools/EditImage.py +144 -0
  241. package/video_generation_agent/tools/EditVideoContent.py +369 -0
  242. package/video_generation_agent/tools/GenerateImage.py +133 -0
  243. package/video_generation_agent/tools/GenerateVideo.py +556 -0
  244. package/video_generation_agent/tools/TrimVideo.py +233 -0
  245. package/video_generation_agent/tools/__init__.py +1 -0
  246. package/video_generation_agent/tools/__pycache__/AddSubtitles.cpython-312.pyc +0 -0
  247. package/video_generation_agent/tools/__pycache__/CombineImages.cpython-312.pyc +0 -0
  248. package/video_generation_agent/tools/__pycache__/CombineVideos.cpython-312.pyc +0 -0
  249. package/video_generation_agent/tools/__pycache__/EditAudio.cpython-312.pyc +0 -0
  250. package/video_generation_agent/tools/__pycache__/EditImage.cpython-312.pyc +0 -0
  251. package/video_generation_agent/tools/__pycache__/EditVideoContent.cpython-312.pyc +0 -0
  252. package/video_generation_agent/tools/__pycache__/GenerateImage.cpython-312.pyc +0 -0
  253. package/video_generation_agent/tools/__pycache__/GenerateVideo.cpython-312.pyc +0 -0
  254. package/video_generation_agent/tools/__pycache__/TrimVideo.cpython-312.pyc +0 -0
  255. package/video_generation_agent/tools/utils/__init__.py +1 -0
  256. package/video_generation_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  257. package/video_generation_agent/tools/utils/__pycache__/image_utils.cpython-312.pyc +0 -0
  258. package/video_generation_agent/tools/utils/__pycache__/video_utils.cpython-312.pyc +0 -0
  259. package/video_generation_agent/tools/utils/image_utils.py +174 -0
  260. package/video_generation_agent/tools/utils/video_utils.py +522 -0
  261. package/video_generation_agent/video_generation_agent.py +26 -0
  262. package/virtual_assistant/__init__.py +1 -0
  263. package/virtual_assistant/__pycache__/__init__.cpython-312.pyc +0 -0
  264. package/virtual_assistant/__pycache__/virtual_assistant.cpython-312.pyc +0 -0
  265. package/virtual_assistant/instructions.md +206 -0
  266. package/virtual_assistant/tools/AddLabelToEmail.py +154 -0
  267. package/virtual_assistant/tools/CheckEventsForDate.py +218 -0
  268. package/virtual_assistant/tools/CheckUnreadSlackMessages.py +216 -0
  269. package/virtual_assistant/tools/CreateCalendarEvent.py +261 -0
  270. package/virtual_assistant/tools/DeleteCalendarEvent.py +137 -0
  271. package/virtual_assistant/tools/DeleteDraft.py +95 -0
  272. package/virtual_assistant/tools/DraftEmail.py +239 -0
  273. package/virtual_assistant/tools/EditFile.py +113 -0
  274. package/virtual_assistant/tools/FindEmails.py +330 -0
  275. package/virtual_assistant/tools/GetCurrentTime.py +69 -0
  276. package/virtual_assistant/tools/GetSlackUserInfo.py +117 -0
  277. package/virtual_assistant/tools/ListDirectory.py +113 -0
  278. package/virtual_assistant/tools/ListSkills.py +94 -0
  279. package/virtual_assistant/tools/ManageLabels.py +295 -0
  280. package/virtual_assistant/tools/ProductSearch.py +254 -0
  281. package/virtual_assistant/tools/ReadEmail.py +251 -0
  282. package/virtual_assistant/tools/ReadFile.py +108 -0
  283. package/virtual_assistant/tools/ReadSlackMessages.py +191 -0
  284. package/virtual_assistant/tools/RemoveLabelFromEmail.py +137 -0
  285. package/virtual_assistant/tools/RescheduleCalendarEvent.py +227 -0
  286. package/virtual_assistant/tools/ScholarSearch.py +216 -0
  287. package/virtual_assistant/tools/SendDraft.py +101 -0
  288. package/virtual_assistant/tools/SendSlackMessage.py +148 -0
  289. package/virtual_assistant/tools/WriteFile.py +95 -0
  290. package/virtual_assistant/tools/__init__.py +1 -0
  291. package/virtual_assistant/tools/__pycache__/AddLabelToEmail.cpython-312.pyc +0 -0
  292. package/virtual_assistant/tools/__pycache__/CheckEventsForDate.cpython-312.pyc +0 -0
  293. package/virtual_assistant/tools/__pycache__/CheckUnreadSlackMessages.cpython-312.pyc +0 -0
  294. package/virtual_assistant/tools/__pycache__/CreateCalendarEvent.cpython-312.pyc +0 -0
  295. package/virtual_assistant/tools/__pycache__/DeleteCalendarEvent.cpython-312.pyc +0 -0
  296. package/virtual_assistant/tools/__pycache__/DeleteDraft.cpython-312.pyc +0 -0
  297. package/virtual_assistant/tools/__pycache__/DraftEmail.cpython-312.pyc +0 -0
  298. package/virtual_assistant/tools/__pycache__/EditFile.cpython-312.pyc +0 -0
  299. package/virtual_assistant/tools/__pycache__/FindEmails.cpython-312.pyc +0 -0
  300. package/virtual_assistant/tools/__pycache__/GetCurrentTime.cpython-312.pyc +0 -0
  301. package/virtual_assistant/tools/__pycache__/GetSlackUserInfo.cpython-312.pyc +0 -0
  302. package/virtual_assistant/tools/__pycache__/ListDirectory.cpython-312.pyc +0 -0
  303. package/virtual_assistant/tools/__pycache__/ListSkills.cpython-312.pyc +0 -0
  304. package/virtual_assistant/tools/__pycache__/ManageLabels.cpython-312.pyc +0 -0
  305. package/virtual_assistant/tools/__pycache__/ProductSearch.cpython-312.pyc +0 -0
  306. package/virtual_assistant/tools/__pycache__/ReadEmail.cpython-312.pyc +0 -0
  307. package/virtual_assistant/tools/__pycache__/ReadFile.cpython-312.pyc +0 -0
  308. package/virtual_assistant/tools/__pycache__/ReadSlackMessages.cpython-312.pyc +0 -0
  309. package/virtual_assistant/tools/__pycache__/RemoveLabelFromEmail.cpython-312.pyc +0 -0
  310. package/virtual_assistant/tools/__pycache__/RescheduleCalendarEvent.cpython-312.pyc +0 -0
  311. package/virtual_assistant/tools/__pycache__/ScholarSearch.cpython-312.pyc +0 -0
  312. package/virtual_assistant/tools/__pycache__/SendDraft.cpython-312.pyc +0 -0
  313. package/virtual_assistant/tools/__pycache__/SendSlackMessage.cpython-312.pyc +0 -0
  314. package/virtual_assistant/tools/__pycache__/WriteFile.cpython-312.pyc +0 -0
  315. package/virtual_assistant/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  316. package/virtual_assistant/virtual_assistant.py +52 -0
@@ -0,0 +1,563 @@
1
+ """
2
+ Modify an existing slide by generating HTML with a sub-agent.
3
+
4
+ Flow: InsertNewSlides creates blank placeholders + plan, then ModifySlide generates/edits slide HTML.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import base64
11
+ import mimetypes
12
+ import os
13
+ import re
14
+ import tempfile
15
+ import threading
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from agency_swarm import Agent, ModelSettings, Reasoning
21
+ from agency_swarm.tools import BaseTool, ToolOutputText, tool_output_image_from_path
22
+ from agents.extensions.models.litellm_model import LitellmModel
23
+ from pydantic import Field
24
+
25
+ from .slide_file_utils import get_project_dir
26
+ from .slide_html_utils import (
27
+ ensure_full_html,
28
+ list_slide_filenames,
29
+ validate_html,
30
+ _strip_html_to_text,
31
+ )
32
+ from .template_registry import load_template_index, save_template_index, template_path
33
+ # Per-project locks for the template-index read-modify-write.
34
+ _index_locks: dict[str, threading.Lock] = {}
35
+ _index_locks_guard = threading.Lock()
36
+
37
+
38
+ def _index_lock_for(project_dir: Path) -> threading.Lock:
39
+ key = str(project_dir)
40
+ with _index_locks_guard:
41
+ if key not in _index_locks:
42
+ _index_locks[key] = threading.Lock()
43
+ return _index_locks[key]
44
+
45
+
46
+ def _strip_base64_images(html: str) -> str:
47
+ """Replace base64 data URI references with short placeholders.
48
+
49
+ Covers both src="data:..." attributes and url('data:...') CSS values.
50
+ Prevents context-window overflow when feeding previously-processed HTML
51
+ back to the sub-agent as a baseline.
52
+ """
53
+ # Only strip data:image/ URIs — never touch data:text/css or other non-image blobs
54
+ html = re.sub(r'src=(["\'])data:image/[^"\']+\1', r'src=\1[image]\1', html)
55
+ html = re.sub(r'url\((["\']?)data:image/[^"\')\s]+\1\)', r'url(\1[image]\1)', html)
56
+ html = re.sub(r'(href|xlink:href|data)=(["\'])data:image/[^"\']+\2', r'\1=\2[image]\2', html)
57
+ return html
58
+
59
+
60
+ def _convert_css_bg_images_to_img_tags(html: str) -> str:
61
+ """Convert CSS background-image to <img> tags so dom-to-pptx can render them.
62
+
63
+ Handles two patterns:
64
+ 1. Inline style: <div style="background-image: url(img.png)">
65
+ 2. Class-based: .cls { background-image: url(img.png) } + <div class="cls">
66
+
67
+ For each match an absolutely-positioned <img> is injected as the first child
68
+ and background-image/size/position/repeat are stripped from the CSS.
69
+ Accepts both local paths and data: URIs (data URIs are kept as-is in the src).
70
+ """
71
+ _BG_STRIP_RE = re.compile(
72
+ r'\bbackground-image\s*:\s*url\([^)]*\)\s*;?\s*'
73
+ r'|\bbackground-size\s*:\s*[^;]+;\s*'
74
+ r'|\bbackground-position\s*:\s*[^;]+;\s*'
75
+ r'|\bbackground-repeat\s*:\s*[^;]+;\s*',
76
+ re.IGNORECASE,
77
+ )
78
+
79
+ def _img_tag(src: str) -> str:
80
+ return (
81
+ f'<img src="{src}" alt="" '
82
+ f'style="position:absolute;top:0;left:0;width:100%;height:100%;'
83
+ f'object-fit:cover;z-index:0;" />'
84
+ )
85
+
86
+ def _should_convert(url_arg: str) -> bool:
87
+ """Convert both local image paths and data:image/ URIs."""
88
+ if url_arg.startswith("data:image/"):
89
+ return True
90
+ if url_arg.startswith(("data:", "http://", "https://", "file://")):
91
+ return False
92
+ return _is_image_path(url_arg)
93
+
94
+ # ── 1. Inline style="...background-image: url(...)..." ───────────────────
95
+ inline_re = re.compile(
96
+ r'(<[a-zA-Z][^>]*?style=["\'])([^"\']*?background-image\s*:\s*url\(([^)]+)\)[^"\']*?)(["\'][^>]*>)',
97
+ re.IGNORECASE,
98
+ )
99
+
100
+ def rewrite_inline(m: re.Match) -> str:
101
+ before, style_val, url_raw, after = m.group(1), m.group(2), m.group(3), m.group(4)
102
+ url_arg = url_raw.strip("\"' ")
103
+ if not _should_convert(url_arg):
104
+ return m.group(0)
105
+ clean = _BG_STRIP_RE.sub('', style_val).strip().rstrip(';')
106
+ return f'{before}{clean}{after}{_img_tag(url_arg)}'
107
+
108
+ html = inline_re.sub(rewrite_inline, html)
109
+
110
+ # ── 2. Class-based rules in <style> blocks ───────────────────────────────
111
+ # Collect class → url mapping from <style> blocks
112
+ style_block_re = re.compile(r'<style[^>]*>(.*?)</style>', re.IGNORECASE | re.DOTALL)
113
+ css_class_bg_re = re.compile(
114
+ r'\.([a-zA-Z_-][\w-]*)\s*\{([^}]*?background-image\s*:\s*url\(([^)]+)\)[^}]*?)\}',
115
+ re.IGNORECASE | re.DOTALL,
116
+ )
117
+
118
+ class_to_url: dict[str, str] = {}
119
+ for style_m in style_block_re.finditer(html):
120
+ for rule_m in css_class_bg_re.finditer(style_m.group(1)):
121
+ cls = rule_m.group(1)
122
+ url_arg = rule_m.group(3).strip("\"' ")
123
+ if _should_convert(url_arg):
124
+ class_to_url[cls] = url_arg
125
+
126
+ if not class_to_url:
127
+ return html
128
+
129
+ # Strip background-image from matching rules in <style> blocks
130
+ def rewrite_style_block(style_m: re.Match) -> str:
131
+ css = style_m.group(1)
132
+ def clean_rule(rule_m: re.Match) -> str:
133
+ cls = rule_m.group(1)
134
+ if cls not in class_to_url:
135
+ return rule_m.group(0)
136
+ cleaned_body = _BG_STRIP_RE.sub('', rule_m.group(2)).strip().rstrip(';')
137
+ return f'.{cls} {{{cleaned_body}}}'
138
+ return f'<style>{css_class_bg_re.sub(clean_rule, css)}</style>'
139
+
140
+ html = style_block_re.sub(rewrite_style_block, html)
141
+
142
+ # Inject <img> as first child of elements that carry a matched class
143
+ class_pattern = '|'.join(re.escape(c) for c in class_to_url)
144
+ element_re = re.compile(
145
+ rf'(<[a-zA-Z][^>]*?class=["\'][^"\']*?(?:{class_pattern})[^"\']*?["\'][^>]*>)',
146
+ re.IGNORECASE,
147
+ )
148
+
149
+ def inject_img(m: re.Match) -> str:
150
+ opening = m.group(1)
151
+ # Find which class matched
152
+ classes = re.search(r'class=["\']([^"\']+)["\']', opening, re.IGNORECASE)
153
+ if not classes:
154
+ return opening
155
+ for cls in classes.group(1).split():
156
+ if cls in class_to_url:
157
+ return f'{opening}{_img_tag(class_to_url[cls])}'
158
+ return opening
159
+
160
+ html = element_re.sub(inject_img, html)
161
+ return html
162
+
163
+
164
+ _IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp", ".avif"}
165
+
166
+
167
+ def _is_image_path(src: str) -> bool:
168
+ return Path(src.split("?")[0]).suffix.lower() in _IMAGE_EXTENSIONS
169
+
170
+
171
+ def _embed_local_images_as_base64(html: str, project_dir: Path) -> str:
172
+ """Replace local image references with base64 data URIs.
173
+
174
+ Handles HTML src=, CSS url(), SVG href/xlink:href, and <object data=>.
175
+ Only processes paths with known image file extensions to avoid
176
+ accidentally encoding scripts, stylesheets, or fonts.
177
+ """
178
+ def _encode(src: str) -> str | None:
179
+ if (
180
+ src.startswith("data:")
181
+ or src.startswith("http://")
182
+ or src.startswith("https://")
183
+ or src.startswith("file://")
184
+ or not _is_image_path(src)
185
+ ):
186
+ return None
187
+ img_path = (project_dir / src).resolve()
188
+ if not img_path.exists():
189
+ return None
190
+ mime, _ = mimetypes.guess_type(str(img_path))
191
+ mime = mime or "image/png"
192
+ encoded = base64.b64encode(img_path.read_bytes()).decode("ascii")
193
+ return f"data:{mime};base64,{encoded}"
194
+
195
+ def replace_src(match: re.Match) -> str:
196
+ quote, src = match.group(1), match.group(2)
197
+ data_uri = _encode(src)
198
+ return f"src={quote}{data_uri}{quote}" if data_uri else match.group(0)
199
+
200
+ def replace_css_url(match: re.Match) -> str:
201
+ quote, src = match.group(1), match.group(2)
202
+ data_uri = _encode(src)
203
+ return f"url({quote}{data_uri}{quote})" if data_uri else match.group(0)
204
+
205
+ def replace_href(match: re.Match) -> str:
206
+ attr, quote, src = match.group(1), match.group(2), match.group(3)
207
+ data_uri = _encode(src)
208
+ return f'{attr}={quote}{data_uri}{quote}' if data_uri else match.group(0)
209
+
210
+ html = re.sub(r'src=(["\'])((?!data:|https?://|file://)[^"\']+)\1', replace_src, html)
211
+ html = re.sub(r'url\((["\']?)((?!data:|https?://|file://)[^"\')\s]+)\1\)', replace_css_url, html)
212
+ html = re.sub(
213
+ r'(href|xlink:href|data)=(["\'])((?!data:|https?://|file://|#)[^"\']+)\2',
214
+ replace_href,
215
+ html,
216
+ )
217
+ return html
218
+
219
+
220
+ _HTML_WRITER_MODEL_CLAUDE = "anthropic/claude-sonnet-4-6"
221
+ _HTML_WRITER_MODEL_OAI = "gpt-5.2-codex"
222
+ _HTML_WRITER_MAX_ATTEMPTS = 3
223
+
224
+
225
+ def _make_html_writer_agent(caller_model=None) -> Agent:
226
+ """Create a fresh, stateless agent instance for one ModifySlide call.
227
+
228
+ Model priority:
229
+ 1. ANTHROPIC_API_KEY in env → Claude Sonnet 4.6 (best HTML quality)
230
+ 2. caller_model → inherit from the calling agent (covers /auth TUI flow)
231
+ """
232
+ anthropic_key = os.getenv("ANTHROPIC_API_KEY")
233
+ if anthropic_key:
234
+ model = LitellmModel(model=_HTML_WRITER_MODEL_CLAUDE, api_key=anthropic_key)
235
+ else:
236
+ model = caller_model or _HTML_WRITER_MODEL_OAI
237
+ return Agent(
238
+ name="Slide HTML Writer",
239
+ description="Generates complete slide HTML from task briefs.",
240
+ instructions=_read_html_writer_instructions(),
241
+ tools=[],
242
+ model=model,
243
+ model_settings=ModelSettings(
244
+ reasoning=Reasoning(effort="high", summary="auto"),
245
+ verbosity="medium",
246
+ ),
247
+ )
248
+
249
+
250
+ def _extract_html_from_output(text: str) -> str:
251
+ raw = (text or "").strip()
252
+ if not raw:
253
+ return ""
254
+ code_block = re.search(r"```(?:html)?\s*(.*?)```", raw, flags=re.IGNORECASE | re.DOTALL)
255
+ if code_block:
256
+ return code_block.group(1).strip()
257
+
258
+ html_start = re.search(r"(?is)(<!doctype html>|<html\b)", raw)
259
+ if html_start:
260
+ return raw[html_start.start() :].strip()
261
+ body_start = re.search(r"(?is)<body\b", raw)
262
+ if body_start:
263
+ return raw[body_start.start() :].strip()
264
+ return raw
265
+
266
+
267
+ def _read_html_writer_instructions() -> str:
268
+ path = Path(__file__).with_name("html_writer_instructions.md")
269
+ try:
270
+ return path.read_text(encoding="utf-8").strip()
271
+ except Exception:
272
+ return "You generate slide HTML. Return only HTML content."
273
+
274
+
275
+ def _read_theme_css(project_dir: Path) -> str:
276
+ theme_path = project_dir / "_theme.css"
277
+ if not theme_path.exists():
278
+ return ""
279
+ try:
280
+ return theme_path.read_text(encoding="utf-8").strip()
281
+ except Exception:
282
+ return ""
283
+
284
+
285
+ def _extract_used_classes(html_content: str, limit: int = 120) -> list[str]:
286
+ classes: list[str] = []
287
+ seen: set[str] = set()
288
+ for match in re.finditer(r'class\s*=\s*["\']([^"\']+)["\']', html_content, flags=re.IGNORECASE):
289
+ raw = match.group(1)
290
+ for cls in re.split(r"\s+", raw.strip()):
291
+ if not cls or cls in seen:
292
+ continue
293
+ seen.add(cls)
294
+ classes.append(cls)
295
+ if len(classes) >= limit:
296
+ return classes
297
+ return classes
298
+
299
+
300
+ def _build_main_text_contents(project_dir: Path, current_slide: str) -> str:
301
+ """Return a MAIN_TEXT_CONTENTS block with a one-line text snippet per slide.
302
+
303
+ Mirrors the block the main agent receives each turn so the HTML writer
304
+ sub-agent has the same deck-wide context (what's on each slide, what slide
305
+ number it is currently editing, total count).
306
+ """
307
+ slides = list_slide_filenames(project_dir)
308
+ if not slides:
309
+ return ""
310
+ lines = ["<MAIN_TEXT_CONTENTS>"]
311
+ for i, name in enumerate(slides, 1):
312
+ try:
313
+ text = _strip_html_to_text((project_dir / name).read_text(encoding="utf-8"))
314
+ except Exception:
315
+ text = "(unreadable)"
316
+ marker = " ← YOU ARE EDITING THIS SLIDE" if name == current_slide else ""
317
+ lines.append(f" <SLIDE_{i}>{text}</SLIDE_{i}>{marker}")
318
+ lines.append("</MAIN_TEXT_CONTENTS>")
319
+ return "\n".join(lines)
320
+
321
+
322
+
323
+ def _build_sub_run_prompt(
324
+ *,
325
+ task_brief: str,
326
+ slide_name: str,
327
+ total_pages: int,
328
+ main_text_contents: str,
329
+ base_html: str,
330
+ current_html: str | None = None,
331
+ theme_css: str,
332
+ retry_validation_error: str = "",
333
+ previous_failed_html: str | None = None,
334
+ ) -> str:
335
+ """Build the per-call user message for the HTML writer sub-agent.
336
+
337
+ Design guidelines and validation rules are in the agent's system prompt
338
+ (set once at agent creation). This message carries only the dynamic,
339
+ per-slide context that changes with every call.
340
+
341
+ When `current_html` is provided it means a saved template (not the slide itself)
342
+ is being used as the layout baseline. The current slide content is shown
343
+ separately so the writer knows what already exists on the slide.
344
+
345
+ When `previous_failed_html` is provided (retry ≥ 2), the writer receives
346
+ its own previous output so it can surgically fix the specific violations
347
+ rather than regenerating from scratch.
348
+ """
349
+ deck_context = (
350
+ f"Deck overview — {total_pages} slide(s) total:\n{main_text_contents}"
351
+ if main_text_contents
352
+ else f"Total slides in deck: {total_pages}"
353
+ )
354
+ retry_block = (
355
+ f"\n\nVALIDATION FEEDBACK FROM PREVIOUS ATTEMPT (fix these before returning):\n{retry_validation_error}"
356
+ if retry_validation_error
357
+ else ""
358
+ )
359
+ previous_attempt_block = (
360
+ "\n\nYOUR PREVIOUS ATTEMPT (the HTML you returned that failed validation — fix it, do not regenerate from scratch):\n"
361
+ "<PREVIOUS_ATTEMPT>\n"
362
+ f"{previous_failed_html}\n"
363
+ "</PREVIOUS_ATTEMPT>"
364
+ if previous_failed_html
365
+ else ""
366
+ )
367
+
368
+ if current_html is not None:
369
+ # Template mode: base_html is a saved layout skeleton, current_html is the
370
+ # live slide. Show both so the writer uses the template structure but
371
+ # understands the slide's existing content.
372
+ html_section = (
373
+ "LAYOUT_TEMPLATE_HTML (use this as the structural/design baseline — "
374
+ "adopt its layout, colours, and component patterns):\n"
375
+ "<LAYOUT_TEMPLATE>\n"
376
+ f"{base_html}\n"
377
+ "</LAYOUT_TEMPLATE>\n\n"
378
+ "CURRENT_SLIDE_HTML (the slide as it exists now — understand its content "
379
+ "but replace the layout with the template above):\n"
380
+ "<CURRENT_SLIDE>\n"
381
+ f"{current_html}\n"
382
+ "</CURRENT_SLIDE>\n"
383
+ )
384
+ else:
385
+ # Direct edit mode: base_html IS the current slide. Modify it in place.
386
+ html_section = (
387
+ "CURRENT_SLIDE_HTML (edit this slide in place — preserve everything "
388
+ "not mentioned in the task brief):\n"
389
+ "<CURRENT_SLIDE>\n"
390
+ f"{base_html}\n"
391
+ "</CURRENT_SLIDE>\n"
392
+ )
393
+
394
+ return (
395
+ f"Target slide: {slide_name}\n"
396
+ f"{deck_context}\n\n"
397
+ "TASK_BRIEF:\n"
398
+ f"{task_brief.strip()}\n\n"
399
+ f"{html_section}\n"
400
+ "CURRENT_THEME_CSS (authoritative design tokens — reuse, do not contradict):\n"
401
+ "<THEME_CSS>\n"
402
+ f"{theme_css}\n"
403
+ "</THEME_CSS>\n\n"
404
+ "USED_CLASSES_IN_CURRENT_SLIDE:\n"
405
+ f"{', '.join(_extract_used_classes(base_html))}"
406
+ f"{previous_attempt_block}"
407
+ f"{retry_block}"
408
+ )
409
+
410
+
411
+ def _screenshot_html_slide(html_path: Path) -> tuple[Any | None, str]:
412
+ """Render the slide HTML in a headless browser and return (ToolOutputImage | None, error_msg).
413
+
414
+ Returns (None, reason) on failure so the caller can include the reason in the tool output.
415
+ """
416
+ try:
417
+ from playwright.sync_api import sync_playwright
418
+
419
+ with sync_playwright() as pw:
420
+ browser = pw.chromium.launch(headless=True)
421
+ page = browser.new_page(viewport={"width": 1280, "height": 720})
422
+ page.goto(html_path.resolve().as_uri(), wait_until="load", timeout=20_000)
423
+ page.wait_for_timeout(800) # let JS and fonts settle
424
+ tmp = Path(tempfile.mktemp(suffix=".jpg"))
425
+ page.screenshot(
426
+ path=str(tmp),
427
+ clip={"x": 0, "y": 0, "width": 1280, "height": 720},
428
+ type="jpeg",
429
+ quality=80,
430
+ )
431
+ browser.close()
432
+ return tool_output_image_from_path(tmp), ""
433
+ except Exception as exc:
434
+ return None, str(exc)
435
+
436
+
437
+
438
+
439
+ class ModifySlide(BaseTool):
440
+ """Generate/update slide HTML from task brief via sub-agent."""
441
+
442
+ project_name: str = Field(..., description="Presentation project folder name under ./mnt/<project_name>/presentations. Only provide the project_name in this field.")
443
+ slide_name: str = Field(..., description="Slide filename (e.g., slide_01 or slide_01.html)")
444
+ task_brief: str = Field(..., description="What to change on this slide. Do not include any HTML in the tool input. HTML is written by the sub-agent inside this tool.")
445
+ existing_template_key: str | None = Field(default=None, description="Optional template key to load as baseline")
446
+ save_as_template_key: str | None = Field(default=None, description="Optional template key to save resulting slide")
447
+ save_as_template_name: str | None = Field(default=None, description="Optional display name for saved template")
448
+
449
+ async def run(self):
450
+ project_dir = get_project_dir(self.project_name)
451
+ if not project_dir.exists():
452
+ return f"Project not found: {project_dir}"
453
+
454
+ slide_filename = self.slide_name if self.slide_name.lower().endswith(".html") else f"{self.slide_name}.html"
455
+ slide_path = project_dir / slide_filename
456
+ if not slide_path.exists():
457
+ return f"Slide not found: {slide_filename}"
458
+
459
+ index_data = load_template_index(project_dir)
460
+ current_html = _strip_base64_images(slide_path.read_text(encoding="utf-8"))
461
+ base_html = current_html
462
+ using_template = False
463
+
464
+ if self.existing_template_key:
465
+ key = self.existing_template_key.strip()
466
+ meta = index_data.get(key)
467
+ if not meta:
468
+ return f"Template key not found: {key}"
469
+ path = template_path(project_dir, key)
470
+ if not path.exists():
471
+ return f"Template file missing for key '{key}': {path.name}"
472
+ base_html = _strip_base64_images(path.read_text(encoding="utf-8"))
473
+ using_template = True
474
+
475
+ total_pages = len([p for p in project_dir.glob("*.html")])
476
+ theme_css = _read_theme_css(project_dir)
477
+ main_text_contents = _build_main_text_contents(project_dir, slide_filename)
478
+
479
+ writer = _make_html_writer_agent(caller_model=getattr(self._caller_agent, "model", None))
480
+
481
+ sub_results: list[Any] = []
482
+ last_validation_error = ""
483
+ previous_failed_html: str | None = None
484
+ final_html = ""
485
+ used_scaffold = False
486
+
487
+ for attempt in range(1, _HTML_WRITER_MAX_ATTEMPTS + 1):
488
+ prompt = _build_sub_run_prompt(
489
+ task_brief=self.task_brief,
490
+ slide_name=slide_filename,
491
+ total_pages=total_pages,
492
+ main_text_contents=main_text_contents,
493
+ base_html=base_html,
494
+ current_html=current_html if using_template else None,
495
+ theme_css=theme_css,
496
+ retry_validation_error=last_validation_error,
497
+ previous_failed_html=previous_failed_html,
498
+ )
499
+
500
+ try:
501
+ final_result = await writer.get_response(prompt)
502
+ except Exception as exc:
503
+ last_validation_error = f"Sub-agent error (attempt {attempt}): {exc}"
504
+ continue
505
+ sub_results.append(final_result)
506
+
507
+ # No result object means the framework swallowed an API-level error
508
+ # (e.g. rate limit). Extract whatever error detail is available.
509
+ if final_result is None:
510
+ last_validation_error = f"Sub-agent returned no result on attempt {attempt} (possible rate limit or API error)."
511
+ continue
512
+ api_error = getattr(final_result, "error", None) or getattr(final_result, "last_error", None)
513
+ if api_error:
514
+ last_validation_error = f"Sub-agent API error (attempt {attempt}): {api_error}"
515
+ continue
516
+
517
+ output_text = str(getattr(final_result, "final_output", "") or "")
518
+ candidate_html = _extract_html_from_output(output_text)
519
+ if not candidate_html:
520
+ last_validation_error = f"Model returned empty output on attempt {attempt}."
521
+ continue
522
+
523
+ full_html, used_scaffold = ensure_full_html(candidate_html)
524
+ validation = await asyncio.to_thread(validate_html, full_html, project_dir, used_scaffold)
525
+ if validation.get("valid"):
526
+ final_html = full_html
527
+ break
528
+ last_validation_error = str(validation.get("error", "Unknown validation error")).strip()
529
+ previous_failed_html = full_html
530
+
531
+ if not final_html:
532
+ return f"HTML validation failed after {_HTML_WRITER_MAX_ATTEMPTS} attempts:\n{last_validation_error}"
533
+
534
+ final_html = _convert_css_bg_images_to_img_tags(final_html)
535
+ final_html = _embed_local_images_as_base64(final_html, project_dir)
536
+ slide_path.write_text(final_html, encoding="utf-8")
537
+ save_note = ""
538
+ if self.save_as_template_key:
539
+ key = self.save_as_template_key.strip()
540
+ t_path = template_path(project_dir, key)
541
+ t_path.write_text(final_html, encoding="utf-8")
542
+ with _index_lock_for(project_dir):
543
+ fresh_index = load_template_index(project_dir)
544
+ fresh_index[key] = {
545
+ "name": (self.save_as_template_name or key).strip(),
546
+ "source_slide": slide_filename,
547
+ "updated_at": datetime.now(timezone.utc).isoformat(),
548
+ }
549
+ save_template_index(project_dir, fresh_index)
550
+ save_note = f"\nSaved template: {key}."
551
+
552
+ success_msg = f"Updated {slide_filename}.{save_note}"
553
+
554
+ screenshot, screenshot_err = await asyncio.to_thread(_screenshot_html_slide, slide_path)
555
+ if screenshot is not None:
556
+ return [ToolOutputText(text=success_msg), screenshot]
557
+ if screenshot_err:
558
+ return f"{success_msg}\n Screenshot failed: {screenshot_err}"
559
+ return success_msg
560
+
561
+ if __name__ == "__main__":
562
+ modify_slide = ModifySlide(project_name="universe_5slide_deck", slide_name="slide_06", task_brief="""Generate a plain string saying hello world""")
563
+ print(asyncio.run(modify_slide.run()))
@@ -0,0 +1,26 @@
1
+ """Read the raw HTML of a slide file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agency_swarm.tools import BaseTool
6
+ from pydantic import Field
7
+
8
+ from .slide_file_utils import get_project_dir
9
+
10
+
11
+ class ReadSlide(BaseTool):
12
+ """Return the raw HTML source of a slide so the agent can inspect its current design and content."""
13
+
14
+ project_name: str = Field(..., description="Project folder name")
15
+ slide_name: str = Field(
16
+ ...,
17
+ description="Slide filename (e.g. 'slide_01_title' or 'slide_01_title.html')",
18
+ )
19
+
20
+ def run(self) -> str:
21
+ project_dir = get_project_dir(self.project_name)
22
+ slide_name = self.slide_name if self.slide_name.endswith(".html") else f"{self.slide_name}.html"
23
+ slide_path = project_dir / slide_name
24
+ if not slide_path.exists():
25
+ return f"Error: slide not found at {slide_path}"
26
+ return slide_path.read_text(encoding="utf-8")