@_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,113 @@
1
+ """Tool for combining multiple videos into a single video using ffmpeg."""
2
+
3
+ import os
4
+ import subprocess
5
+ import tempfile
6
+
7
+ from pydantic import Field, field_validator
8
+
9
+ from agency_swarm import BaseTool, ToolOutputText
10
+
11
+ from .utils.video_utils import get_videos_dir, resolve_ffmpeg_executable
12
+
13
+
14
+ class CombineVideos(BaseTool):
15
+ """Combine multiple videos into a single video using instant cut transitions (ffmpeg).
16
+
17
+ Videos are saved to: mnt/{product_name}/generated_videos/
18
+ """
19
+
20
+ product_name: str = Field(
21
+ ...,
22
+ description="Name of the product these videos are for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to locate and save videos in product-specific folders.",
23
+ )
24
+ video_names: list[str] = Field(
25
+ ...,
26
+ description="List of video file names (without extension) to combine in order.",
27
+ )
28
+ name: str = Field(
29
+ ...,
30
+ description="The name for the combined video file (without extension)",
31
+ )
32
+
33
+ @field_validator("video_names")
34
+ @classmethod
35
+ def _validate_video_names(cls, value: list[str]) -> list[str]:
36
+ if not value:
37
+ raise ValueError("video_names must not be empty")
38
+ if len(value) < 2:
39
+ raise ValueError("At least 2 videos are required for combining")
40
+ for name in value:
41
+ if not name.strip():
42
+ raise ValueError("Video names must not be empty")
43
+ return value
44
+
45
+ @field_validator("name")
46
+ @classmethod
47
+ def _name_not_blank(cls, value: str) -> str:
48
+ if not value.strip():
49
+ raise ValueError("name must not be empty")
50
+ return value
51
+
52
+ def run(self) -> list:
53
+ """Combine videos using ffmpeg concat demuxer."""
54
+ videos_dir = get_videos_dir(self.product_name)
55
+
56
+ video_paths = []
57
+ for video_name in self.video_names:
58
+ video_path = os.path.join(videos_dir, f"{video_name}.mp4")
59
+ if not os.path.exists(video_path):
60
+ raise FileNotFoundError(
61
+ f"Video file not found: {video_path}. "
62
+ f"Make sure the video exists in the {videos_dir} directory."
63
+ )
64
+ video_paths.append(video_path)
65
+
66
+ output_path = os.path.join(videos_dir, f"{self.name}.mp4")
67
+
68
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
69
+ for path in video_paths:
70
+ abs_path = os.path.abspath(path).replace('\\', '/')
71
+ escaped_path = abs_path.replace("'", "'\\''")
72
+ f.write(f"file '{escaped_path}'\n")
73
+ concat_file = f.name
74
+
75
+ try:
76
+ ffmpeg_executable = resolve_ffmpeg_executable()
77
+ cmd = [
78
+ ffmpeg_executable,
79
+ '-f', 'concat',
80
+ '-safe', '0',
81
+ '-i', concat_file,
82
+ '-c', 'copy', # copy streams without re-encoding
83
+ '-y',
84
+ output_path
85
+ ]
86
+ result = subprocess.run(cmd, capture_output=True, text=True)
87
+ if result.returncode != 0:
88
+ raise RuntimeError(f"FFmpeg concatenation failed: {result.stderr}")
89
+ finally:
90
+ try:
91
+ os.unlink(concat_file)
92
+ except Exception:
93
+ pass
94
+
95
+ lines = [f"Combined {len(self.video_names)} videos:"]
96
+ for i, name in enumerate(self.video_names, 1):
97
+ lines.append(f" {i}. {name}.mp4")
98
+ lines.append(f"\nOutput: {self.name}.mp4")
99
+ lines.append(f"Path: {output_path}")
100
+
101
+ return [ToolOutputText(type="text", text="\n".join(lines))]
102
+
103
+
104
+ if __name__ == "__main__":
105
+ # Example usage
106
+ tool = CombineVideos(
107
+ product_name="Test_Product",
108
+ video_names=["herbaluxe_01_hook_v2","herbaluxe_02_formula","herbaluxe_03_result_consistency_fix","herbaluxe_04_cta"],
109
+ name="x_combine_test",
110
+ )
111
+ result = tool.run()
112
+ print(result)
113
+
@@ -0,0 +1,297 @@
1
+ """Tool for combining audio from one video with visuals from another video."""
2
+
3
+ import os
4
+ import subprocess
5
+ import asyncio
6
+ import cv2
7
+ from typing import Optional
8
+ from pathlib import Path
9
+
10
+ from pydantic import Field, field_validator
11
+ from PIL import Image
12
+
13
+ from agency_swarm import BaseTool, ToolOutputText
14
+
15
+ from .utils.video_utils import (
16
+ get_videos_dir,
17
+ ensure_not_blank,
18
+ generate_spritesheet,
19
+ extract_last_frame,
20
+ create_image_output,
21
+ resolve_ffmpeg_executable,
22
+ )
23
+
24
+
25
+ class EditAudio(BaseTool):
26
+ """
27
+ Combine audio from one video with visuals from another video.
28
+
29
+ Useful for adding b-roll footage over narration, or replacing visuals while keeping original audio.
30
+ Supports padding to offset video timing relative to audio.
31
+
32
+ Videos are saved to: mnt/{product_name}/generated_videos/
33
+ """
34
+
35
+ product_name: str = Field(
36
+ ...,
37
+ 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.",
38
+ )
39
+ audio_source: str = Field(
40
+ ...,
41
+ description=(
42
+ "The video to extract audio from. Can be: "
43
+ "1) Video name without extension (searches generated_videos folder), "
44
+ "2) Full local path to video file."
45
+ ),
46
+ )
47
+ video_source: str = Field(
48
+ ...,
49
+ description=(
50
+ "The video to use for visuals/b-roll. Can be: "
51
+ "1) Video name without extension (searches generated_videos folder), "
52
+ "2) Full local path to video file."
53
+ ),
54
+ )
55
+ output_name: str = Field(
56
+ ...,
57
+ description="The name for the combined video file (without extension)",
58
+ )
59
+ pad_seconds: float = Field(
60
+ default=0.0,
61
+ description=(
62
+ "Seconds to offset the video relative to audio. "
63
+ "Negative = video starts before audio (e.g., -2.0 = video plays 2s before audio), "
64
+ "Positive = video starts after audio (e.g., 2.0 = video plays 2s after audio starts), "
65
+ "Zero = video and audio start together (default: 0.0)"
66
+ ),
67
+ )
68
+
69
+ @field_validator("audio_source")
70
+ @classmethod
71
+ def _audio_not_blank(cls, value: str) -> str:
72
+ return ensure_not_blank(value, "audio_source")
73
+
74
+ @field_validator("video_source")
75
+ @classmethod
76
+ def _video_not_blank(cls, value: str) -> str:
77
+ return ensure_not_blank(value, "video_source")
78
+
79
+ @field_validator("output_name")
80
+ @classmethod
81
+ def _output_not_blank(cls, value: str) -> str:
82
+ return ensure_not_blank(value, "output_name")
83
+
84
+ async def run(self) -> list:
85
+ """Mix audio and video from two different sources."""
86
+ audio_path = self._resolve_video_path(self.audio_source)
87
+ video_path = self._resolve_video_path(self.video_source)
88
+
89
+ videos_dir = get_videos_dir(self.product_name)
90
+ output_path = os.path.join(videos_dir, f"{self.output_name}.mp4")
91
+
92
+ loop = asyncio.get_event_loop()
93
+ await loop.run_in_executor(None, self._mix_audio_video_blocking, audio_path, video_path, output_path)
94
+
95
+ output = []
96
+
97
+ spritesheet_path = os.path.join(videos_dir, f"{self.output_name}_spritesheet.jpg")
98
+ spritesheet = await loop.run_in_executor(None, generate_spritesheet, output_path, spritesheet_path)
99
+ if spritesheet:
100
+ output.extend(create_image_output(spritesheet_path, f"{self.output_name}_spritesheet.jpg"))
101
+
102
+ thumbnail_path = os.path.join(videos_dir, f"{self.output_name}_thumbnail.jpg")
103
+ thumbnail = await loop.run_in_executor(None, self._extract_first_frame, output_path, thumbnail_path)
104
+ if thumbnail:
105
+ output.extend(create_image_output(thumbnail_path, f"{self.output_name}_thumbnail.jpg"))
106
+
107
+ last_frame_path = os.path.join(videos_dir, f"{self.output_name}_last_frame.jpg")
108
+ last_frame = await loop.run_in_executor(None, extract_last_frame, output_path, last_frame_path)
109
+ if last_frame:
110
+ output.extend(create_image_output(last_frame_path, f"{self.output_name}_last_frame.jpg"))
111
+
112
+ pad_info = ""
113
+ if self.pad_seconds != 0.0:
114
+ if self.pad_seconds < 0:
115
+ pad_info = f"\nPadding: Video starts {abs(self.pad_seconds)}s before audio"
116
+ else:
117
+ pad_info = f"\nPadding: Video starts {self.pad_seconds}s after audio"
118
+
119
+ output.append(ToolOutputText(
120
+ type="text",
121
+ text=f"Audio and video mixed successfully!\nSaved to: `{self.output_name}.mp4`\nPath: {output_path}{pad_info}"
122
+ ))
123
+
124
+ return output
125
+
126
+ def _resolve_video_path(self, video_ref: str) -> str:
127
+ """Resolve video reference to full path."""
128
+ # Try as full path first
129
+ path = Path(video_ref).expanduser().resolve()
130
+
131
+ if path.exists():
132
+ return str(path)
133
+
134
+ # Try as video name without extension in generated_videos
135
+ videos_dir = get_videos_dir(self.product_name)
136
+
137
+ for ext in [".mp4", ".mov", ".avi", ".webm"]:
138
+ potential_path = os.path.join(videos_dir, f"{video_ref}{ext}")
139
+ if os.path.exists(potential_path):
140
+ return potential_path
141
+
142
+ raise FileNotFoundError(
143
+ f"Video '{video_ref}' not found in {videos_dir}. "
144
+ f"Tried extensions: .mp4, .mov, .avi, .webm"
145
+ )
146
+
147
+ def _mix_audio_video_blocking(self, audio_path: str, video_path: str, output_path: str) -> None:
148
+ """Mix audio and video using ffmpeg (blocking operation)."""
149
+ cap_audio = cv2.VideoCapture(audio_path)
150
+ fps_audio = cap_audio.get(cv2.CAP_PROP_FPS)
151
+ frames_audio = int(cap_audio.get(cv2.CAP_PROP_FRAME_COUNT))
152
+ audio_duration = frames_audio / fps_audio if fps_audio > 0 else 0
153
+ cap_audio.release()
154
+
155
+ cap_video = cv2.VideoCapture(video_path)
156
+ fps_video = cap_video.get(cv2.CAP_PROP_FPS)
157
+ frames_video = int(cap_video.get(cv2.CAP_PROP_FRAME_COUNT))
158
+ video_duration = frames_video / fps_video if fps_video > 0 else 0
159
+ cap_video.release()
160
+
161
+ ffmpeg_executable = resolve_ffmpeg_executable()
162
+
163
+ if self.pad_seconds == 0.0:
164
+ # Simple case: no padding, just combine audio and video
165
+ ffmpeg_cmd = [
166
+ ffmpeg_executable,
167
+ "-y", # Overwrite output file
168
+ "-i", video_path, # Input 0: video source
169
+ "-i", audio_path, # Input 1: audio source
170
+ "-map", "0:v:0", # Use video from input 0
171
+ "-map", "1:a:0", # Use audio from input 1
172
+ "-c:v", "libx264", # Video codec
173
+ "-preset", "medium",
174
+ "-crf", "23",
175
+ "-c:a", "aac", # Audio codec
176
+ "-b:a", "128k",
177
+ "-movflags", "+faststart",
178
+ "-pix_fmt", "yuv420p",
179
+ "-t", str(video_duration), # Use video duration as master timeline
180
+ output_path
181
+ ]
182
+ else:
183
+ # Complex case: apply padding offset
184
+ # Negative pad = delay audio (video starts first)
185
+ # Positive pad = delay video (audio starts first)
186
+
187
+ if self.pad_seconds < 0:
188
+ # Video starts before audio - delay audio track
189
+ audio_delay = abs(self.pad_seconds)
190
+ ffmpeg_cmd = [
191
+ ffmpeg_executable,
192
+ "-y",
193
+ "-i", video_path,
194
+ "-i", audio_path,
195
+ "-filter_complex",
196
+ f"[1:a]adelay={int(audio_delay * 1000)}|{int(audio_delay * 1000)}[a]", # Delay audio in milliseconds
197
+ "-map", "0:v:0",
198
+ "-map", "[a]",
199
+ "-c:v", "libx264",
200
+ "-preset", "medium",
201
+ "-crf", "23",
202
+ "-c:a", "aac",
203
+ "-b:a", "128k",
204
+ "-movflags", "+faststart",
205
+ "-pix_fmt", "yuv420p",
206
+ "-t", str(video_duration), # Use video duration as master timeline
207
+ output_path
208
+ ]
209
+ else:
210
+ # Audio starts before video - delay video track
211
+ video_delay = self.pad_seconds
212
+ ffmpeg_cmd = [
213
+ ffmpeg_executable,
214
+ "-y",
215
+ "-i", video_path,
216
+ "-i", audio_path,
217
+ "-filter_complex",
218
+ f"[0:v]setpts=PTS+{video_delay}/TB[v]", # Delay video
219
+ "-map", "[v]",
220
+ "-map", "1:a:0",
221
+ "-c:v", "libx264",
222
+ "-preset", "medium",
223
+ "-crf", "23",
224
+ "-c:a", "aac",
225
+ "-b:a", "128k",
226
+ "-movflags", "+faststart",
227
+ "-pix_fmt", "yuv420p",
228
+ "-t", str(video_duration), # Use video duration as master timeline
229
+ output_path
230
+ ]
231
+
232
+ try:
233
+ subprocess.run(
234
+ ffmpeg_cmd,
235
+ stdout=subprocess.PIPE,
236
+ stderr=subprocess.PIPE,
237
+ text=True,
238
+ check=True
239
+ )
240
+ except subprocess.CalledProcessError as e:
241
+ raise RuntimeError(
242
+ f"ffmpeg failed to mix audio and video. Error: {e.stderr}"
243
+ )
244
+ except FileNotFoundError:
245
+ raise RuntimeError(
246
+ "ffmpeg not found. Please install ffmpeg and ensure it's in your PATH. "
247
+ "Download from: https://ffmpeg.org/download.html"
248
+ )
249
+
250
+ def _extract_first_frame(self, video_path: str, output_path: str) -> Optional[object]:
251
+ """Extract the first frame from video as thumbnail."""
252
+ cap = cv2.VideoCapture(video_path)
253
+ ret, frame = cap.read()
254
+ cap.release()
255
+
256
+ if not ret:
257
+ return None
258
+
259
+ # Convert BGR to RGB
260
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
261
+ thumbnail_image = Image.fromarray(frame_rgb)
262
+ thumbnail_image.save(output_path)
263
+
264
+ return thumbnail_image
265
+
266
+
267
+ if __name__ == "__main__":
268
+ # Check if test videos exist
269
+ test_dir = Path(__file__).parent.parent.parent / "mnt" / "Test_Product" / "generated_videos"
270
+ video1 = test_dir / "test_video.mp4"
271
+ video2 = test_dir / "test_video_trimmed_last2s.mp4"
272
+
273
+ if not video1.exists() or not video2.exists():
274
+ print("Test videos not found. Skipping test.")
275
+ print(f"Expected: {video1}")
276
+ print(f"Expected: {video2}")
277
+ else:
278
+ # Example: Combine audio from one video with b-roll from another
279
+ # Audio is 4s, b-roll is 26.8s - b-roll continues after audio ends
280
+ # Video starts 5 seconds after audio (audio pre-roll)
281
+ tool = EditAudio(
282
+ product_name="Test_Product",
283
+ audio_source="test_video", # Audio from this video (4s)
284
+ video_source="Ad2_3seg_AI_Employee_UGC_final", # Visuals from this video (26.8s)
285
+ output_name="mixed_video",
286
+ pad_seconds=-3.0, # Video starts 5 seconds after audio begins
287
+ )
288
+
289
+ try:
290
+ result = asyncio.run(tool.run())
291
+ print("\nMix complete!")
292
+ for item in result:
293
+ if hasattr(item, 'text'):
294
+ print(item.text)
295
+ except Exception as exc:
296
+ print(f"Audio/video mixing failed: {exc}")
297
+
@@ -0,0 +1,144 @@
1
+ """Tool for editing images using Google's Gemini 2.5 Flash Image model."""
2
+
3
+ import asyncio
4
+ import os
5
+ from typing import Literal
6
+
7
+ from google import genai
8
+ from pydantic import Field, field_validator
9
+
10
+ from agency_swarm import BaseTool
11
+
12
+ from .utils.image_utils import (
13
+ get_images_dir,
14
+ MODEL_NAME,
15
+ load_image_by_name,
16
+ extract_image_from_response,
17
+ extract_usage_metadata,
18
+ process_variant_result,
19
+ split_results_and_usage,
20
+ run_parallel_variants,
21
+ compress_image_for_base64,
22
+ )
23
+
24
+
25
+ class EditImage(BaseTool):
26
+ """Edit existing images using Google's Gemini 2.5 Flash Image (Nano Banana) model.
27
+
28
+ Images are saved to: mnt/{product_name}/generated_images/
29
+ """
30
+
31
+ product_name: str = Field(
32
+ ...,
33
+ description="Name of the product this image is for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to organize files into product-specific folders.",
34
+ )
35
+ input_image_name: str = Field(
36
+ ...,
37
+ description="Name of the existing image file to edit (without extension).",
38
+ )
39
+ edit_prompt: str = Field(
40
+ ...,
41
+ description="Text prompt describing the edits to make to the image",
42
+ )
43
+ output_image_name: str = Field(
44
+ ...,
45
+ description="The name for the generated edited image file (without extension)",
46
+ )
47
+ num_variants: int = Field(
48
+ default=1,
49
+ description="Number of image variants to generate (1-4, default is 1)",
50
+ )
51
+ aspect_ratio: Literal["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"] = Field(
52
+ default="1:1",
53
+ description="The aspect ratio of the generated image (default is 1:1)",
54
+ )
55
+
56
+ @field_validator("input_image_name")
57
+ @classmethod
58
+ def _input_name_not_blank(cls, value: str) -> str:
59
+ if not value.strip():
60
+ raise ValueError("input_image_name must not be empty")
61
+ return value
62
+
63
+ @field_validator("edit_prompt")
64
+ @classmethod
65
+ def _prompt_not_blank(cls, value: str) -> str:
66
+ if not value.strip():
67
+ raise ValueError("edit_prompt must not be empty")
68
+ return value
69
+
70
+ @field_validator("output_image_name")
71
+ @classmethod
72
+ def _output_name_not_blank(cls, value: str) -> str:
73
+ if not value.strip():
74
+ raise ValueError("output_image_name must not be empty")
75
+ return value
76
+
77
+ @field_validator("num_variants")
78
+ @classmethod
79
+ def _validate_num_variants(cls, value: int) -> int:
80
+ if value < 1 or value > 4:
81
+ raise ValueError("num_variants must be between 1 and 4")
82
+ return value
83
+
84
+ async def run(self) -> list:
85
+ """Edit an image using the Gemini API."""
86
+ api_key = os.getenv("GOOGLE_API_KEY")
87
+ if not api_key:
88
+ raise ValueError("GOOGLE_API_KEY is not set. Add it to your .env to use image editing.")
89
+
90
+ client = genai.Client(api_key=api_key)
91
+ images_dir = get_images_dir(self.product_name)
92
+
93
+ image, _image_path, load_error = load_image_by_name(self.input_image_name, images_dir)
94
+ if load_error:
95
+ raise FileNotFoundError(load_error)
96
+
97
+ def edit_single_variant(variant_num: int):
98
+ try:
99
+ response = client.models.generate_content(
100
+ model=MODEL_NAME,
101
+ contents=[self.edit_prompt, image],
102
+ config=genai.types.GenerateContentConfig(
103
+ image_config=genai.types.ImageConfig(aspect_ratio=self.aspect_ratio),
104
+ ),
105
+ )
106
+ usage_metadata = extract_usage_metadata(response)
107
+ edited_image, _text = extract_image_from_response(response)
108
+ if edited_image is None:
109
+ return None
110
+ result = process_variant_result(
111
+ variant_num,
112
+ edited_image,
113
+ self.output_image_name,
114
+ self.num_variants,
115
+ compress_image_for_base64,
116
+ images_dir,
117
+ )
118
+ result["prompt_tokens"] = float(usage_metadata.get("prompt_token_count") or 0)
119
+ result["candidate_tokens"] = float(usage_metadata.get("candidates_token_count") or 0)
120
+ return result
121
+ except Exception:
122
+ return None
123
+
124
+ raw_results = await run_parallel_variants(edit_single_variant, self.num_variants)
125
+ if not raw_results:
126
+ raise RuntimeError("No variants were successfully generated")
127
+
128
+ results, _usage = split_results_and_usage(raw_results)
129
+ return results
130
+
131
+ if __name__ == "__main__":
132
+ # Example usage with Google Gemini 2.5 Flash Image
133
+ tool = EditImage(
134
+ product_name="Test_Product",
135
+ input_image_name="logo_image_variant_1",
136
+ edit_prompt="Change the logo color from red to blue",
137
+ output_image_name="logo_image_edited",
138
+ num_variants=1,
139
+ )
140
+ try:
141
+ result = asyncio.run(tool.run())
142
+ print(result)
143
+ except Exception as exc:
144
+ print(f"Image editing failed: {exc}")