@_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,425 @@
1
+ """Tool for adding animated subtitles to videos using OpenAI Whisper API for timing."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import Optional, Literal
6
+
7
+ from pydantic import Field, field_validator
8
+
9
+ from agency_swarm import BaseTool
10
+ from moviepy.editor import VideoFileClip, ImageClip, CompositeVideoClip
11
+ from PIL import Image, ImageDraw, ImageFont
12
+ import numpy as np
13
+ from openai import OpenAI
14
+
15
+ from .utils.video_utils import get_videos_dir
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class AddSubtitles(BaseTool):
21
+ """
22
+ Add animated subtitles to a video.
23
+ Uses OpenAI Whisper API to automatically transcribe audio and extract word-level timestamps.
24
+ Subtitles appear word-by-word or phrase-by-phrase with highlighting effect.
25
+
26
+ Videos are saved to: mnt/{product_name}/generated_videos/
27
+ """
28
+
29
+ product_name: str = Field(
30
+ ...,
31
+ description="Name of the product this video is for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to organize files into product-specific folders.",
32
+ )
33
+ video_name: str = Field(
34
+ ...,
35
+ description="Name of the video file (without extension) to add subtitles to",
36
+ )
37
+ original_script: str = Field(
38
+ ...,
39
+ description="Original script of the video to provide guidance for the subtitles. Should be provided in a single text block, without any formatting.",
40
+ )
41
+ output_name: Optional[str] = Field(
42
+ None,
43
+ description="Output video name (without extension). If not provided, adds '_subtitled' to original name",
44
+ )
45
+ font_size: int = Field(
46
+ default=60,
47
+ description="Font size for subtitles (default: 60, recommended range: 40-80 for vertical videos)",
48
+ )
49
+ position: Literal["center", "bottom", "top"] = Field(
50
+ default="bottom", description="Vertical position of subtitles on screen"
51
+ )
52
+ words_per_clip: int = Field(
53
+ default=6,
54
+ description="Number of words to show per subtitle clip (default: 6, usually 2-6)",
55
+ )
56
+ highlight_color: str = Field(
57
+ default="white",
58
+ description="Color for highlighting text (default: 'yellow', options: 'yellow', 'white', 'cyan', 'green')",
59
+ )
60
+
61
+ @field_validator("video_name")
62
+ @classmethod
63
+ def _validate_video_name(cls, value: str) -> str:
64
+ if not value.strip():
65
+ raise ValueError("video_name must not be empty")
66
+ return value
67
+
68
+ @field_validator("font_size")
69
+ @classmethod
70
+ def _validate_font_size(cls, value: int) -> int:
71
+ if value < 20 or value > 120:
72
+ raise ValueError("font_size must be between 20 and 120")
73
+ return value
74
+
75
+ @field_validator("words_per_clip")
76
+ @classmethod
77
+ def _validate_words_per_clip(cls, value: int) -> int:
78
+ if value < 1 or value > 10:
79
+ raise ValueError("words_per_clip must be between 1 and 10")
80
+ return value
81
+
82
+ def run(self) -> str:
83
+ """Add animated subtitles to the video using Whisper for timing."""
84
+ videos_dir = get_videos_dir(self.product_name)
85
+ video_path = os.path.join(videos_dir, f"{self.video_name}.mp4")
86
+
87
+ if not os.path.exists(video_path):
88
+ raise FileNotFoundError(
89
+ f"Video file not found: {video_path}. "
90
+ f"Make sure the video exists in the {videos_dir} directory."
91
+ )
92
+
93
+ video = VideoFileClip(video_path)
94
+ video_width, video_height = video.size
95
+
96
+ api_key = os.getenv("OPENAI_API_KEY")
97
+ if not api_key:
98
+ raise RuntimeError("OPENAI_API_KEY environment variable is required for subtitles")
99
+ client = OpenAI(api_key=api_key)
100
+
101
+ prompt = (
102
+ "Transcribe the audio of the video into text, make sure to include correct periods and capitalization. "
103
+ "Do not use em dashes, use hyphens instead. "
104
+ f"The original script, use it as a reference to ensure correct spelling: {self.original_script}."
105
+ )
106
+
107
+ # Transcribe with word-level timestamps using OpenAI API
108
+ with open(video_path, "rb") as audio_file:
109
+ transcript = client.audio.transcriptions.create(
110
+ model="whisper-1",
111
+ prompt=prompt,
112
+ file=audio_file,
113
+ response_format="verbose_json",
114
+ timestamp_granularities=["word"],
115
+ temperature=0.0,
116
+ )
117
+
118
+
119
+ # Extract words with timestamps from API response and add punctuation
120
+ words_with_timing = []
121
+
122
+ if hasattr(transcript, "words") and transcript.words:
123
+ # Character replacements for Unicode normalization
124
+ char_replacements = {
125
+ '\u2014': '-', # Em dash
126
+ '\u2013': '-', # En dash
127
+ '\u2212': '-', # Minus sign
128
+ '\u2010': '-', # Hyphen
129
+ '\u2011': '-', # Non-breaking hyphen
130
+ '\ufe63': '-', # Small hyphen-minus
131
+ '\uff0d': '-', # Fullwidth hyphen-minus
132
+ '\u2019': "'", # Right single quote
133
+ '\u2018': "'", # Left single quote
134
+ '\u2032': "'", # Prime
135
+ '\u2035': "'" # Reversed prime
136
+ }
137
+
138
+ # Split full text into words (preserves punctuation)
139
+ full_text_words = transcript.text.split()
140
+
141
+ # Match API words with full text words to get punctuation
142
+ full_text_idx = 0
143
+
144
+ for word_info in transcript.words:
145
+ word = word_info.word.strip()
146
+
147
+ # Try to find matching word in full text (with punctuation)
148
+ final_word = word
149
+ if full_text_idx < len(full_text_words):
150
+ full_word = full_text_words[full_text_idx]
151
+ # Check if the word matches (case-insensitive, ignoring punctuation)
152
+ if word.lower() == full_word.strip('.,!?;:').lower():
153
+ final_word = full_word # Use the version with punctuation
154
+ full_text_idx += 1
155
+ else:
156
+ # Try to find it in the next few words
157
+ for i in range(full_text_idx, min(full_text_idx + 3, len(full_text_words))):
158
+ if word.lower() == full_text_words[i].strip('.,!?;:').lower():
159
+ final_word = full_text_words[i]
160
+ full_text_idx = i + 1
161
+ break
162
+
163
+ # Normalize Unicode characters to ASCII equivalents
164
+ for old_char, new_char in char_replacements.items():
165
+ final_word = final_word.replace(old_char, new_char)
166
+
167
+ words_with_timing.append(
168
+ {
169
+ "word": final_word,
170
+ "start": word_info.start,
171
+ "end": word_info.end,
172
+ }
173
+ )
174
+
175
+ if not words_with_timing:
176
+ raise RuntimeError(
177
+ "No words detected in audio. Video may not have speech or audio track."
178
+ )
179
+
180
+
181
+ chunks = []
182
+ i = 0
183
+
184
+ while i < len(words_with_timing):
185
+ chunk_words = []
186
+ words_added = 0
187
+
188
+ # Add words until we reach max words_per_clip or hit a sentence ending
189
+ while (
190
+ i + words_added < len(words_with_timing)
191
+ and words_added < self.words_per_clip
192
+ ):
193
+ word_data = words_with_timing[i + words_added]
194
+ chunk_words.append(word_data)
195
+ words_added += 1
196
+
197
+ # Check if this word ends a sentence (period, exclamation, question mark)
198
+ word_text = word_data["word"].strip()
199
+ if (
200
+ word_text.endswith(".")
201
+ or word_text.endswith("!")
202
+ or word_text.endswith("?")
203
+ ):
204
+ # End chunk here
205
+ break
206
+
207
+ # Create chunk with combined text and timing
208
+ if chunk_words:
209
+ chunk = {
210
+ "text": " ".join([w["word"] for w in chunk_words]),
211
+ "start": chunk_words[0]["start"],
212
+ "end": chunk_words[-1]["end"],
213
+ }
214
+ chunks.append(chunk)
215
+ i += words_added
216
+ else:
217
+ # Safety: should never happen, but just in case
218
+ i += 1
219
+
220
+
221
+ if self.position == "center":
222
+ y_position = "center"
223
+ elif self.position == "bottom":
224
+ y_position = video_height - 400 # 400px from bottom
225
+ else: # top
226
+ y_position = 150 # 150px from top
227
+
228
+ # Color mapping (RGB format for PIL)
229
+ color_map = {
230
+ "yellow": (255, 255, 0),
231
+ "white": (255, 255, 255),
232
+ "cyan": (0, 255, 255),
233
+ "green": (144, 238, 144),
234
+ }
235
+ text_color = color_map.get(self.highlight_color, (255, 255, 0))
236
+
237
+ # Helper function to create text image using PIL
238
+ def create_text_image(text, font_size, width, color):
239
+ """Create an image with text using PIL, with automatic line wrapping."""
240
+ # Get the fonts directory relative to this tool file
241
+ tools_dir = os.path.dirname(os.path.abspath(__file__))
242
+ fonts_dir = os.path.join(tools_dir, "utils", "fonts")
243
+
244
+ # Montserrat font paths - try multiple variations
245
+ font_paths = [
246
+ os.path.join(fonts_dir, "Montserrat-Bold.ttf"),
247
+ os.path.join(fonts_dir, "Montserrat-SemiBold.ttf"),
248
+ os.path.join(fonts_dir, "Montserrat-Regular.ttf"),
249
+ "C:/Windows/Fonts/Montserrat-Bold.ttf",
250
+ "Montserrat-Bold.ttf",
251
+ ]
252
+
253
+ # Try to load Montserrat font
254
+ font = None
255
+ for font_path in font_paths:
256
+ try:
257
+ font = ImageFont.truetype(font_path, font_size)
258
+ break
259
+ except Exception:
260
+ continue
261
+
262
+ # Fall back to default if no font loaded
263
+ if font is None:
264
+ font = ImageFont.load_default()
265
+
266
+ # Create temporary image to measure text
267
+ temp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
268
+ temp_draw = ImageDraw.Draw(temp_img)
269
+
270
+ # Word wrapping logic
271
+ words = text.split(" ")
272
+ lines = []
273
+ current_line = []
274
+
275
+ max_width = width # Maximum width available for text
276
+
277
+ for word in words:
278
+ # Try adding this word to current line
279
+ test_line = " ".join(current_line + [word])
280
+ bbox = temp_draw.textbbox((0, 0), test_line, font=font)
281
+ line_width = bbox[2] - bbox[0]
282
+
283
+ if line_width <= max_width:
284
+ # Word fits, add it to current line
285
+ current_line.append(word)
286
+ else:
287
+ # Word doesn't fit, start new line
288
+ if current_line:
289
+ lines.append(" ".join(current_line))
290
+ current_line = [word]
291
+ else:
292
+ # Single word is too long, add it anyway
293
+ lines.append(word)
294
+
295
+ # Add remaining words
296
+ if current_line:
297
+ lines.append(" ".join(current_line))
298
+
299
+ # Calculate dimensions for multi-line text
300
+ padding = 20
301
+ line_height = font_size + 10 # Add some spacing between lines
302
+
303
+ # Get max line width
304
+ max_line_width = 0
305
+ for line in lines:
306
+ bbox = temp_draw.textbbox((0, 0), line, font=font)
307
+ line_width = bbox[2] - bbox[0]
308
+ max_line_width = max(max_line_width, line_width)
309
+
310
+ img_width = max_line_width + padding * 2
311
+ img_height = len(lines) * line_height + padding * 2
312
+
313
+ # Create actual image with transparent background
314
+ img = Image.new("RGBA", (img_width, img_height), (0, 0, 0, 0))
315
+ draw = ImageDraw.Draw(img)
316
+
317
+ # Draw each line of text
318
+ stroke_width = 6
319
+ y_offset = padding
320
+
321
+ for line in lines:
322
+ # Get line width for centering
323
+ bbox = draw.textbbox((0, 0), line, font=font)
324
+ line_width = bbox[2] - bbox[0]
325
+ x = (img_width - line_width) // 2
326
+
327
+ # Draw stroke (outline)
328
+ for offset_x in range(-stroke_width, stroke_width + 1):
329
+ for offset_y in range(-stroke_width, stroke_width + 1):
330
+ draw.text(
331
+ (x + offset_x, y_offset + offset_y),
332
+ line,
333
+ font=font,
334
+ fill=(0, 0, 0, 255),
335
+ )
336
+
337
+ # Draw main text
338
+ draw.text((x, y_offset), line, font=font, fill=(*color, 255))
339
+
340
+ # Move to next line
341
+ y_offset += line_height
342
+
343
+ return np.array(img)
344
+
345
+ subtitle_clips = []
346
+
347
+ for i, chunk in enumerate(chunks):
348
+ # Bold, uppercase for visibility
349
+ text = chunk["text"].upper()
350
+ start_time = chunk["start"]
351
+ duration = chunk["end"] - chunk["start"]
352
+
353
+ try:
354
+ # Create text image using PIL
355
+ text_img = create_text_image(
356
+ text, self.font_size, video_width - 100, text_color
357
+ )
358
+
359
+ # Create ImageClip from the text image
360
+ txt_clip = ImageClip(text_img)
361
+
362
+ # Position and time the clip
363
+ txt_clip = txt_clip.set_position(("center", y_position))
364
+ txt_clip = txt_clip.set_start(start_time)
365
+ txt_clip = txt_clip.set_duration(duration)
366
+
367
+ subtitle_clips.append(txt_clip)
368
+
369
+ except Exception:
370
+ continue
371
+ final_video = CompositeVideoClip([video] + subtitle_clips)
372
+ final_video = final_video.set_duration(video.duration)
373
+ final_video = final_video.set_audio(video.audio)
374
+
375
+ if self.output_name:
376
+ output_name = self.output_name
377
+ else:
378
+ output_name = f"{self.video_name}_subtitled"
379
+
380
+ output_path = os.path.join(videos_dir, f"{output_name}.mp4")
381
+
382
+ try:
383
+ final_video.write_videofile(
384
+ output_path,
385
+ codec="libx264",
386
+ audio_codec="aac",
387
+ temp_audiofile="temp-audio.m4a",
388
+ remove_temp=True,
389
+ logger=None,
390
+ fps=video.fps,
391
+ )
392
+ finally:
393
+ # Clean up
394
+ video.close()
395
+ final_video.close()
396
+ for clip in subtitle_clips:
397
+ try:
398
+ clip.close()
399
+ except Exception:
400
+ pass
401
+
402
+ return (
403
+ f"Successfully added animated subtitles to {self.video_name}.mp4\n\n"
404
+ f"Output: {output_name}.mp4\n"
405
+ f"Path: {output_path}\n\n"
406
+ f"Details:\n"
407
+ f" - Duration: {video.duration:.1f} seconds\n"
408
+ f" - Subtitle chunks: {len(subtitle_clips)}\n"
409
+ f" - Style: {self.highlight_color.upper()} text, {self.words_per_clip} words per clip\n"
410
+ f" - Transcribed: {transcript.text[:200]}..."
411
+ )
412
+
413
+ if __name__ == "__main__":
414
+ # Test case
415
+ tool = AddSubtitles(
416
+ product_name="Test_Product",
417
+ video_name="herbaluxe_ad_v3",
418
+ original_script="Does your moisturizer still smell like perfume? Mine did, and my skin hated it. So I switched to HerbaLuxe—an aloe‑first daily moisturizer made with organic ingredients. It's fragrance‑free, with no essential oils and a minimal formula. First swipe feels cool and soothing. Sinks in fast—no stickiness. Layers clean under sunscreen. Skin feels calm, not coated. herbaluxe-cosmetics.com",
419
+ words_per_clip=4,
420
+ position="bottom",
421
+ highlight_color="white",
422
+ font_size=60,
423
+ )
424
+ result = tool.run()
425
+ print(result)
@@ -0,0 +1,166 @@
1
+ """Tool for combining multiple images using Google's Gemini 2.5 Flash Image model."""
2
+
3
+ import io
4
+ from typing import Literal
5
+ from pathlib import Path
6
+
7
+ import os
8
+ from google import genai
9
+ from PIL import Image
10
+ from pydantic import Field, field_validator
11
+
12
+ from agency_swarm import BaseTool
13
+
14
+ from .utils.image_utils import (
15
+ get_images_dir,
16
+ MODEL_NAME,
17
+ load_image_by_name,
18
+ extract_image_parts_from_response,
19
+ extract_usage_metadata,
20
+ process_variant_result,
21
+ split_results_and_usage,
22
+ run_parallel_variants,
23
+ compress_image_for_base64,
24
+ )
25
+
26
+
27
+ class CombineImages(BaseTool):
28
+ """Combine multiple images using Google's Gemini 2.5 Flash Image (Nano Banana) model according to the given text instruction.
29
+
30
+ Images are saved to: mnt/{product_name}/generated_images/
31
+ """
32
+
33
+ product_name: str = Field(
34
+ ...,
35
+ description="Name of the product these images are for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to organize files into product-specific folders.",
36
+ )
37
+ image_names: list[str] = Field(
38
+ ...,
39
+ description="List of image file names (without extension) or full file paths to combine. Can mix both formats.",
40
+ )
41
+ text_instruction: str = Field(
42
+ ...,
43
+ description="Text instruction describing how to combine the images",
44
+ )
45
+ file_name_or_path: str = Field(
46
+ ...,
47
+ description="The name (without extension) or full path for the generated combined image file",
48
+ )
49
+ num_variants: int = Field(
50
+ default=1,
51
+ description="Number of image variants to generate (1-4, default is 1)",
52
+ )
53
+ aspect_ratio: Literal["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"] = Field(
54
+ default="1:1",
55
+ description="The aspect ratio of the generated image (default is 1:1)",
56
+ )
57
+
58
+ @field_validator("image_names")
59
+ @classmethod
60
+ def _validate_image_names(cls, value: list[str]) -> list[str]:
61
+ if not value:
62
+ raise ValueError("image_names must not be empty")
63
+ if len(value) < 2:
64
+ raise ValueError("At least 2 images are required for combining")
65
+ for name in value:
66
+ if not name.strip():
67
+ raise ValueError("Image names must not be empty")
68
+ return value
69
+
70
+ @field_validator("text_instruction")
71
+ @classmethod
72
+ def _instruction_not_blank(cls, value: str) -> str:
73
+ if not value.strip():
74
+ raise ValueError("text_instruction must not be empty")
75
+ return value
76
+
77
+ @field_validator("file_name_or_path")
78
+ @classmethod
79
+ def _filename_not_blank(cls, value: str) -> str:
80
+ if not value.strip():
81
+ raise ValueError("file_name_or_path must not be empty")
82
+ return value
83
+
84
+ @field_validator("num_variants")
85
+ @classmethod
86
+ def _validate_num_variants(cls, value: int) -> int:
87
+ if value < 1 or value > 4:
88
+ raise ValueError("num_variants must be between 1 and 4")
89
+ return value
90
+
91
+ async def run(self) -> list:
92
+ """Combine images using the Gemini API."""
93
+ api_key = os.getenv("GOOGLE_API_KEY")
94
+ if not api_key:
95
+ raise ValueError("GOOGLE_API_KEY is not set. Add it to your .env to use image composition.")
96
+
97
+ client = genai.Client(api_key=api_key)
98
+ images_dir = get_images_dir(self.product_name)
99
+
100
+ images = []
101
+ for image_name_or_path in self.image_names:
102
+ path = Path(image_name_or_path).expanduser().resolve()
103
+ if path.exists():
104
+ images.append(Image.open(path))
105
+ else:
106
+ image, _image_path, load_error = load_image_by_name(
107
+ image_name_or_path, images_dir, [".png", ".jpg", ".jpeg"]
108
+ )
109
+ if load_error:
110
+ raise FileNotFoundError(f"Image not found: '{image_name_or_path}' (tried as path and as name in {images_dir})")
111
+ images.append(image)
112
+
113
+ def combine_single_variant(variant_num: int):
114
+ try:
115
+ response = client.models.generate_content(
116
+ model=MODEL_NAME,
117
+ contents=images + [self.text_instruction],
118
+ config=genai.types.GenerateContentConfig(
119
+ image_config=genai.types.ImageConfig(aspect_ratio=self.aspect_ratio),
120
+ ),
121
+ )
122
+ usage_metadata = extract_usage_metadata(response)
123
+ image_parts = extract_image_parts_from_response(response)
124
+ if not image_parts:
125
+ return None
126
+ combined_image = Image.open(io.BytesIO(image_parts[0]))
127
+ result = process_variant_result(
128
+ variant_num,
129
+ combined_image,
130
+ self.file_name_or_path,
131
+ self.num_variants,
132
+ compress_image_for_base64,
133
+ images_dir,
134
+ )
135
+ result["prompt_tokens"] = float(usage_metadata.get("prompt_token_count") or 0)
136
+ result["candidate_tokens"] = float(usage_metadata.get("candidates_token_count") or 0)
137
+ return result
138
+ except Exception:
139
+ return None
140
+
141
+ raw_results = await run_parallel_variants(combine_single_variant, self.num_variants)
142
+ if not raw_results:
143
+ raise RuntimeError("No variants were successfully generated")
144
+
145
+ results, _usage = split_results_and_usage(raw_results)
146
+ return results
147
+
148
+ if __name__ == "__main__":
149
+ # Example usage with Google Gemini 2.5 Flash Image
150
+ import asyncio
151
+ tool = CombineImages(
152
+ product_name="Test_Product",
153
+ image_names=["laptop_image_variant_2", "logo_image_variant_2"],
154
+ text_instruction=(
155
+ "Take the first image of a laptop on a table. Add the logo from the second image into the middle "
156
+ "of the laptop. Remove the background of the logo and make it transparent. Ensure the laptop and "
157
+ "features remain completely unchanged. The logo should look like it's naturally attached."
158
+ ),
159
+ file_name_or_path="laptop_with_logo",
160
+ num_variants=2,
161
+ )
162
+ try:
163
+ result = asyncio.run(tool.run())
164
+ print(result)
165
+ except Exception as exc:
166
+ print(f"Image combining failed: {exc}")