@_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,942 @@
1
+ from typing import Dict, List, Optional, Tuple
2
+
3
+ from bs4.element import Tag
4
+ from docx.enum.table import WD_TABLE_ALIGNMENT
5
+ from docx.oxml import OxmlElement
6
+ from docx.oxml.ns import qn
7
+
8
+ from .html_docx_constants import _INHERITABLE_STYLES, _NAMED_COLORS, _PADDING_SCALE
9
+ from .html_docx_css import (
10
+ _border_sz,
11
+ _normalize_padding,
12
+ _parse_background_color,
13
+ _parse_border,
14
+ _parse_border_left,
15
+ _parse_percentage,
16
+ _parse_px_to_pt,
17
+ _resolve_padding,
18
+ )
19
+ from .html_docx_paragraphs import (
20
+ _add_list_indent_padding,
21
+ _add_paragraph_indent,
22
+ _add_paragraph_spacing,
23
+ )
24
+ from .html_docx_playwright import _extract_auto_widths
25
+ from .html_docx_selectors import _compute_style_map
26
+ from .html_docx_shared import _remove_trailing_empty_paragraph
27
+
28
+
29
+ def _handle_table(
30
+ table_node: Tag, target_container, css_rules, parent_style: Dict[str, str], table_auto_widths
31
+ ) -> None:
32
+ table_style = _merge_styles(parent_style, _compute_style_map(table_node, css_rules))
33
+ parent_padding = _resolve_padding(parent_style)
34
+ parent_padding_right = None
35
+ if parent_padding:
36
+ _top, right, _bottom, _left = _normalize_padding(parent_padding)
37
+ parent_padding_right = right if right else None
38
+ rows = table_node.find_all("tr", recursive=False)
39
+ if not rows:
40
+ for section in table_node.find_all(["thead", "tbody", "tfoot"], recursive=False):
41
+ rows.extend(section.find_all("tr", recursive=False))
42
+ if not rows:
43
+ return
44
+
45
+ max_cols = 0
46
+ row_cells: list[list[Tag]] = []
47
+ row_colspans: list[list[int]] = []
48
+ for row in rows:
49
+ cells = [cell for cell in row.find_all(["td", "th"], recursive=False)]
50
+ if not cells:
51
+ continue
52
+ colspans = []
53
+ col_count = 0
54
+ for cell in cells:
55
+ colspan = cell.get("colspan")
56
+ try:
57
+ span_value = int(colspan) if colspan else 1
58
+ except ValueError:
59
+ span_value = 1
60
+ span_value = max(span_value, 1)
61
+ colspans.append(span_value)
62
+ col_count += span_value
63
+ max_cols = max(max_cols, col_count)
64
+ row_cells.append(cells)
65
+ row_colspans.append(colspans)
66
+
67
+ if max_cols == 0:
68
+ return
69
+
70
+ _remove_trailing_empty_paragraph(target_container)
71
+ docx_table = target_container.add_table(rows=len(rows), cols=max_cols)
72
+ _apply_table_styles(docx_table, table_style)
73
+ _apply_table_parent_padding(docx_table, parent_style)
74
+ if _should_prevent_row_split(table_node, table_style):
75
+ _set_table_cant_split(docx_table)
76
+ column_widths_pt = _extract_table_column_widths(table_node, table_style, max_cols, table_auto_widths)
77
+ if not column_widths_pt:
78
+ column_widths_pt = _extract_auto_widths(table_node, table_auto_widths, max_cols)
79
+ if column_widths_pt:
80
+ _apply_table_column_widths(docx_table, column_widths_pt)
81
+
82
+ is_collapsed = table_style.get("border-collapse", "").strip().lower() == "collapse"
83
+ collapsed_borders: dict = {}
84
+ if is_collapsed:
85
+ collapsed_borders = _collect_collapsed_borders(
86
+ row_cells, row_colspans, max_cols, css_rules
87
+ )
88
+
89
+ total_rows = len(row_cells)
90
+ for row_idx, cells in enumerate(row_cells):
91
+ col_idx = 0
92
+ for cell_idx, cell_node in enumerate(cells):
93
+ if col_idx >= max_cols:
94
+ break
95
+ docx_cell = docx_table.cell(row_idx, col_idx)
96
+ colspan = (
97
+ row_colspans[row_idx][cell_idx] if cell_idx < len(row_colspans[row_idx]) else 1
98
+ )
99
+ if colspan > 1 and col_idx + colspan - 1 < max_cols:
100
+ docx_cell = docx_cell.merge(docx_table.cell(row_idx, col_idx + colspan - 1))
101
+ if column_widths_pt and colspan == max_cols and len(cells) == 1:
102
+ span_width = sum(column_widths_pt)
103
+ if span_width > 0:
104
+ _set_cell_width(docx_cell, span_width)
105
+
106
+ cell_own_style = _compute_style_map(cell_node, css_rules)
107
+ cell_style = _merge_styles(table_style, cell_own_style)
108
+ # CSS `background` is not inherited, but it shows through transparent cells
109
+ # visually. In DOCX every cell is opaque by default (white), so we must
110
+ # explicitly forward the background to any cell that doesn't declare its own.
111
+ # We also fall back to parent_style so that cells inside a nested table that
112
+ # sits inside a coloured cell also receive the correct background.
113
+ effective_bg = _parse_background_color(table_style) or _parse_background_color(
114
+ parent_style
115
+ )
116
+ if effective_bg and not _parse_background_color(cell_own_style):
117
+ # Store with '#' prefix so _parse_background_color can re-read it
118
+ # when this cell_style is passed as parent_style to nested elements.
119
+ cell_style = {**cell_style, "background-color": f"#{effective_bg}"}
120
+ if cell_node.name == "th":
121
+ cell_style = {**cell_style, "font-weight": "bold"}
122
+ if (
123
+ parent_padding_right is not None
124
+ and cell_style.get("text-align", "").strip().lower() == "right"
125
+ and not cell_style.get("padding-right")
126
+ and not cell_style.get("padding")
127
+ ):
128
+ cell_style = {**cell_style, "padding-right": f"{parent_padding_right}pt"}
129
+
130
+ # For border-collapse:collapse tables, interior borders are handled by
131
+ # tblBorders/insideH+insideV (applied once after the loop). Suppress cell-level
132
+ # borders on interior edges so they don't double up with the table-level lines.
133
+ # Outer-edge cells keep their cell-level borders (one side each).
134
+ suppress_borders: Optional[set] = None
135
+ if is_collapsed:
136
+ is_first_row = row_idx == 0
137
+ is_last_row = row_idx == total_rows - 1
138
+ is_first_col = col_idx == 0
139
+ is_last_col = col_idx + max(colspan, 1) - 1 >= max_cols - 1
140
+ suppress_borders = set()
141
+ if not is_first_row:
142
+ suppress_borders.add("top")
143
+ if not is_last_row:
144
+ suppress_borders.add("bottom")
145
+ if not is_first_col:
146
+ suppress_borders.add("left")
147
+ if not is_last_col:
148
+ suppress_borders.add("right")
149
+
150
+ _apply_cell_styles(docx_cell, cell_style, suppress_borders=suppress_borders)
151
+ if (
152
+ len(cells) == 2
153
+ and cell_style.get("text-align", "").strip().lower() == "right"
154
+ ):
155
+ _set_cell_no_wrap(docx_cell)
156
+ _set_cell_width(docx_cell, _estimate_right_column_width_pt(column_widths_pt))
157
+ if column_widths_pt and col_idx < len(column_widths_pt):
158
+ col_width_pt = column_widths_pt[col_idx]
159
+ _set_cell_width(docx_cell, col_width_pt)
160
+ # Inject absolute cell width so nested image sizing can cap to it.
161
+ cell_style = {**cell_style, "_cell_width_pt": str(col_width_pt)}
162
+
163
+ from .html_docx_blocks import (
164
+ _add_inline_runs,
165
+ _handle_block,
166
+ _has_block_children,
167
+ )
168
+ from .html_docx_paragraphs import _apply_paragraph_style
169
+ from .html_docx_blocks import _ensure_paragraph
170
+
171
+ # Cell borders are applied at the cell level (w:tcBorders). Strip
172
+ # border-* from the style passed to paragraph content so it doesn't
173
+ # also produce paragraph-level borders (w:pBdr), which would render
174
+ # as a second visible line on top of the cell border.
175
+ content_style = {k: v for k, v in cell_style.items() if not k.startswith("border")}
176
+
177
+ if _has_block_children(cell_node):
178
+ for child in cell_node.children:
179
+ _handle_block(child, docx_cell, css_rules, content_style, table_auto_widths)
180
+ else:
181
+ # Cell contains only inline content (text, spans, <br>) —
182
+ # render as one paragraph so <br> becomes a soft line break
183
+ # rather than each text node becoming a separate paragraph.
184
+ paragraph = _ensure_paragraph(docx_cell)
185
+ _apply_paragraph_style(paragraph, content_style)
186
+ _add_inline_runs(cell_node, paragraph, css_rules, content_style)
187
+
188
+ col_idx += max(colspan, 1)
189
+
190
+ if is_collapsed:
191
+ inside_h = collapsed_borders.get("inside_h")
192
+ inside_v = collapsed_borders.get("inside_v")
193
+ if inside_h or inside_v:
194
+ _apply_collapsed_table_borders(
195
+ docx_table,
196
+ {"inside_h": inside_h, "inside_v": inside_v},
197
+ )
198
+
199
+
200
+ def _extract_table_column_widths(
201
+ table_node: Tag,
202
+ table_style: Dict[str, str],
203
+ column_count: int,
204
+ table_auto_widths=None,
205
+ ) -> Optional[List[float]]:
206
+ if column_count <= 0:
207
+ return None
208
+ rows = table_node.find_all("tr", recursive=False)
209
+ if not rows:
210
+ for section in table_node.find_all(["thead", "tbody", "tfoot"], recursive=False):
211
+ rows.extend(section.find_all("tr", recursive=False))
212
+ if not rows:
213
+ return None
214
+
215
+ widths: List[Optional[float]] | None = None
216
+ for row in rows:
217
+ cells = [cell for cell in row.find_all(["td", "th"], recursive=False)]
218
+ if not cells:
219
+ continue
220
+ candidate_widths: List[Optional[float]] = []
221
+ for cell in cells:
222
+ style_map = _compute_style_map(cell, [])
223
+ width_value = style_map.get("width", "") or cell.get("width", "")
224
+ if isinstance(width_value, str) and width_value.endswith("%"):
225
+ candidate_widths.append(_parse_percentage(width_value))
226
+ else:
227
+ candidate_widths.append(_parse_px_to_pt(str(width_value)) if width_value else None)
228
+ if any(candidate_widths) and len(candidate_widths) >= column_count:
229
+ widths = candidate_widths
230
+ break
231
+
232
+ if not widths or not any(widths):
233
+ return None
234
+
235
+ total_width_pt = _parse_px_to_pt(table_style.get("width", ""))
236
+ if total_width_pt is None:
237
+ # width:100% or a CSS percentage — look up the Playwright-rendered width.
238
+ # Without the actual rendered width, percentage column values can't be
239
+ # resolved correctly (the fallback of 547pt is wrong for nested tables).
240
+ auto_id = table_node.get("data-table-id")
241
+ if auto_id and table_auto_widths and auto_id in table_auto_widths:
242
+ auto = table_auto_widths[auto_id]
243
+ if auto:
244
+ total_width_pt = sum(auto)
245
+ if total_width_pt is None:
246
+ # No context available — let _extract_auto_widths handle this table.
247
+ return None
248
+
249
+ resolved: List[float | None] = []
250
+ fixed_total = sum(w for w in widths if isinstance(w, float) and w > 1.0)
251
+ remaining = max(total_width_pt - fixed_total, 0)
252
+
253
+ for width in widths:
254
+ if width is None:
255
+ resolved.append(None)
256
+ elif width <= 1.0:
257
+ # Treat as an absolute fraction of total_width_pt, not normalised
258
+ # against other percentage columns. This correctly handles mixed
259
+ # rows where some columns specify a % and others have no width.
260
+ resolved.append(total_width_pt * width)
261
+ else:
262
+ resolved.append(width)
263
+
264
+ none_count = sum(1 for value in resolved if value is None)
265
+ if none_count:
266
+ filled_total = sum(value for value in resolved if isinstance(value, float))
267
+ filler = max(total_width_pt - filled_total, 0) / none_count
268
+ resolved = [value if value is not None else filler for value in resolved]
269
+
270
+ if len(resolved) < column_count:
271
+ missing = column_count - len(resolved)
272
+ filler = (total_width_pt - sum(resolved)) / max(missing, 1)
273
+ resolved.extend([filler] * missing)
274
+ return [float(value) for value in resolved[:column_count]]
275
+
276
+
277
+ def _apply_table_column_widths(table, widths_pt: List[float]) -> None:
278
+ widths_pt = _adjust_column_widths_for_parent_padding(table, widths_pt)
279
+ widths_pt = _adjust_column_widths_for_outer_borders(table, widths_pt)
280
+ _update_table_grid(table, widths_pt)
281
+ for row in table.rows:
282
+ for idx, cell in enumerate(row.cells):
283
+ if idx < len(widths_pt) and widths_pt[idx] > 0:
284
+ _set_cell_width(cell, widths_pt[idx])
285
+
286
+
287
+ def _update_table_grid(table, widths_pt: List[float]) -> None:
288
+ """Replace w:tblGrid with column definitions matching widths_pt.
289
+
290
+ python-docx creates w:tblGrid with equal-width columns when add_table() is
291
+ called. Without updating it, Word ignores the per-cell w:tcW values and
292
+ renders equal columns. This must be called before setting cell widths.
293
+ """
294
+ tbl = table._tbl
295
+ existing_grid = tbl.find(qn("w:tblGrid"))
296
+ if existing_grid is not None:
297
+ tbl.remove(existing_grid)
298
+ grid = OxmlElement("w:tblGrid")
299
+ for width in widths_pt:
300
+ col = OxmlElement("w:gridCol")
301
+ col.set(qn("w:w"), str(int(width * 20)))
302
+ grid.append(col)
303
+ tbl_pr = tbl.find(qn("w:tblPr"))
304
+ if tbl_pr is not None:
305
+ tbl_pr.addnext(grid)
306
+ else:
307
+ tbl.insert(0, grid)
308
+
309
+
310
+ def _set_cell_width(cell, width_pt: float) -> None:
311
+ tc_pr = cell._tc.get_or_add_tcPr()
312
+ tc_w = tc_pr.find(qn("w:tcW"))
313
+ if tc_w is None:
314
+ tc_w = OxmlElement("w:tcW")
315
+ tc_pr.append(tc_w)
316
+ tc_w.set(qn("w:type"), "dxa")
317
+ tc_w.set(qn("w:w"), str(int(width_pt * 20)))
318
+
319
+
320
+ def _set_cell_no_wrap(cell) -> None:
321
+ tc_pr = cell._tc.get_or_add_tcPr()
322
+ no_wrap = tc_pr.find(qn("w:noWrap"))
323
+ if no_wrap is None:
324
+ no_wrap = OxmlElement("w:noWrap")
325
+ tc_pr.append(no_wrap)
326
+
327
+
328
+ def _estimate_right_column_width_pt(column_widths_pt: Optional[List[float]]) -> float:
329
+ if column_widths_pt and len(column_widths_pt) >= 2:
330
+ return max(column_widths_pt[1], 160.0)
331
+ return 180.0
332
+
333
+
334
+ def _merge_styles(parent_style: Dict[str, str], own_style: Dict[str, str]) -> Dict[str, str]:
335
+ merged: Dict[str, str] = {}
336
+ for key in _INHERITABLE_STYLES:
337
+ if key in parent_style:
338
+ merged[key] = parent_style[key]
339
+ for key, value in own_style.items():
340
+ merged[key] = value
341
+ return merged
342
+
343
+
344
+ def _apply_table_styles(table, style_map: Dict[str, str]) -> None:
345
+ width_value = style_map.get("width", "") or style_map.get("max-width", "")
346
+ width_pt = _parse_px_to_pt(width_value)
347
+ if width_pt:
348
+ _set_table_width(table, width_pt)
349
+
350
+ if _should_center_table(style_map):
351
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
352
+
353
+ _set_table_normal_style(table)
354
+ _clear_table_look(table)
355
+ _set_table_default_cell_margins(table, 0, 0, 0, 0)
356
+ _set_table_cell_spacing(table, 0)
357
+ _apply_table_border(table, style_map)
358
+
359
+
360
+ def _set_table_width(table, width_pt: float) -> None:
361
+ tbl_pr = table._tbl.tblPr
362
+ if tbl_pr is None:
363
+ tbl_pr = OxmlElement("w:tblPr")
364
+ table._tbl.append(tbl_pr)
365
+ tbl_w = tbl_pr.find(qn("w:tblW"))
366
+ if tbl_w is None:
367
+ tbl_w = OxmlElement("w:tblW")
368
+ tbl_pr.append(tbl_w)
369
+ tbl_w.set(qn("w:type"), "dxa")
370
+ tbl_w.set(qn("w:w"), str(int(width_pt * 20)))
371
+
372
+
373
+ def _apply_table_border(table, style_map: Dict[str, str]) -> None:
374
+ border = style_map.get("border", "")
375
+ if not border:
376
+ return
377
+ parts = border.split()
378
+ width_pt = _parse_px_to_pt(parts[0]) if parts else None
379
+ color = None
380
+ for part in parts:
381
+ if part.startswith("#"):
382
+ color = part[1:].upper()
383
+ break
384
+ if part.lower() in _NAMED_COLORS:
385
+ r, g, b = _NAMED_COLORS[part.lower()]
386
+ color = f"{r:02X}{g:02X}{b:02X}"
387
+ break
388
+ if not width_pt or not color:
389
+ return
390
+ _apply_table_outer_borders(table, width_pt, color)
391
+ table._docs_border_width_pt = width_pt
392
+
393
+
394
+ def _set_table_normal_style(table) -> None:
395
+ tbl_pr = table._tbl.tblPr
396
+ if tbl_pr is None:
397
+ tbl_pr = OxmlElement("w:tblPr")
398
+ table._tbl.append(tbl_pr)
399
+ tbl_style = tbl_pr.find(qn("w:tblStyle"))
400
+ if tbl_style is None:
401
+ tbl_style = OxmlElement("w:tblStyle")
402
+ tbl_pr.insert(0, tbl_style)
403
+ tbl_style.set(qn("w:val"), "TableNormal")
404
+
405
+
406
+ def _clear_table_look(table) -> None:
407
+ tbl_pr = table._tbl.tblPr
408
+ if tbl_pr is None:
409
+ return
410
+ tbl_look = tbl_pr.find(qn("w:tblLook"))
411
+ if tbl_look is not None:
412
+ tbl_pr.remove(tbl_look)
413
+
414
+
415
+ def _set_table_default_cell_margins(
416
+ table, top: float, right: float, bottom: float, left: float
417
+ ) -> None:
418
+ tbl_pr = table._tbl.tblPr
419
+ if tbl_pr is None:
420
+ tbl_pr = OxmlElement("w:tblPr")
421
+ table._tbl.append(tbl_pr)
422
+ tbl_cell_mar = tbl_pr.find(qn("w:tblCellMar"))
423
+ if tbl_cell_mar is None:
424
+ tbl_cell_mar = OxmlElement("w:tblCellMar")
425
+ tbl_pr.append(tbl_cell_mar)
426
+ for side, value in [("top", top), ("right", right), ("bottom", bottom), ("left", left)]:
427
+ elem = tbl_cell_mar.find(qn(f"w:{side}"))
428
+ if elem is None:
429
+ elem = OxmlElement(f"w:{side}")
430
+ tbl_cell_mar.append(elem)
431
+ elem.set(qn("w:w"), str(int(value * 20)))
432
+ elem.set(qn("w:type"), "dxa")
433
+
434
+
435
+ def _set_table_cell_spacing(table, spacing_pt: float) -> None:
436
+ tbl_pr = table._tbl.tblPr
437
+ if tbl_pr is None:
438
+ tbl_pr = OxmlElement("w:tblPr")
439
+ table._tbl.append(tbl_pr)
440
+ tbl_cell_spacing = tbl_pr.find(qn("w:tblCellSpacing"))
441
+ if spacing_pt <= 0:
442
+ if tbl_cell_spacing is not None:
443
+ tbl_pr.remove(tbl_cell_spacing)
444
+ return
445
+ if tbl_cell_spacing is None:
446
+ tbl_cell_spacing = OxmlElement("w:tblCellSpacing")
447
+ tbl_pr.append(tbl_cell_spacing)
448
+ tbl_cell_spacing.set(qn("w:w"), str(int(spacing_pt * 20)))
449
+ tbl_cell_spacing.set(qn("w:type"), "dxa")
450
+
451
+
452
+ def _should_center_table(style_map: Dict[str, str]) -> bool:
453
+ margin = style_map.get("margin", "")
454
+ if margin and "auto" in margin:
455
+ return True
456
+ left = style_map.get("margin-left", "").strip().lower()
457
+ right = style_map.get("margin-right", "").strip().lower()
458
+ if left == "auto" and right == "auto":
459
+ return True
460
+ return False
461
+
462
+
463
+ def _apply_cell_styles(
464
+ cell,
465
+ style_map: Dict[str, str],
466
+ apply_padding: bool = True,
467
+ suppress_borders: Optional[set] = None,
468
+ ) -> None:
469
+ bg_color = _parse_background_color(style_map)
470
+ if bg_color:
471
+ _set_cell_shading(cell, bg_color)
472
+
473
+ _apply_cell_vertical_alignment(cell, style_map)
474
+
475
+ suppress = suppress_borders or set()
476
+ border = _parse_border(style_map.get("border", ""))
477
+ top_border = _parse_border(style_map.get("border-top", ""))
478
+ right_border = _parse_border(style_map.get("border-right", ""))
479
+ bottom_border = _parse_border(style_map.get("border-bottom", ""))
480
+ left_border = _parse_border(style_map.get("border-left", ""))
481
+
482
+ effective_top = (top_border or border) if "top" not in suppress else None
483
+ effective_right = (right_border or border) if "right" not in suppress else None
484
+ effective_bottom = (bottom_border or border) if "bottom" not in suppress else None
485
+ effective_left = (left_border or border) if "left" not in suppress else None
486
+
487
+ if any([effective_top, effective_right, effective_bottom, effective_left]):
488
+ _set_cell_border(
489
+ cell,
490
+ top=effective_top,
491
+ right=effective_right,
492
+ bottom=effective_bottom,
493
+ left=effective_left,
494
+ )
495
+
496
+ if "left" not in suppress:
497
+ border_left = _parse_border_left(style_map)
498
+ if border_left:
499
+ width_pt, color = border_left
500
+ _set_cell_border(cell, left=(width_pt, color))
501
+
502
+ padding = _resolve_padding(style_map)
503
+ if padding and apply_padding:
504
+ top, right, bottom, left = _normalize_padding(padding)
505
+ _set_cell_margins(
506
+ cell,
507
+ top * _PADDING_SCALE,
508
+ right * _PADDING_SCALE,
509
+ bottom * _PADDING_SCALE,
510
+ left * _PADDING_SCALE,
511
+ )
512
+ else:
513
+ _set_cell_margins(cell, 0, 0, 0, 0)
514
+
515
+
516
+ def _apply_cell_vertical_alignment(cell, style_map: Dict[str, str]) -> None:
517
+ vertical_align = style_map.get("vertical-align", "").strip().lower()
518
+ if not vertical_align:
519
+ return
520
+ if vertical_align in {"middle", "center"}:
521
+ val = "center"
522
+ elif vertical_align in {"top", "bottom"}:
523
+ val = vertical_align
524
+ else:
525
+ return
526
+ tc_pr = cell._tc.get_or_add_tcPr()
527
+ v_align = tc_pr.find(qn("w:vAlign"))
528
+ if v_align is None:
529
+ v_align = OxmlElement("w:vAlign")
530
+ tc_pr.append(v_align)
531
+ v_align.set(qn("w:val"), val)
532
+
533
+
534
+ def _apply_cell_padding_spacing(cell, padding: Tuple[float, float, float, float]) -> None:
535
+ paragraphs = [p for p in cell.paragraphs if _is_direct_cell_paragraph(cell, p)]
536
+ if not paragraphs:
537
+ return
538
+ top, right, bottom, left = _normalize_padding(padding)
539
+ if top:
540
+ _add_paragraph_spacing(paragraphs[0], before_pt=top * _PADDING_SCALE)
541
+ if bottom:
542
+ _add_paragraph_spacing(paragraphs[-1], after_pt=bottom * _PADDING_SCALE)
543
+ if left or right:
544
+ for paragraph in paragraphs:
545
+ if paragraph.style and paragraph.style.name == "List Bullet":
546
+ # "List Bullet" indentation is controlled by the numbering definition;
547
+ # adding paragraph-level w:ind here overrides it and misaligns bullets.
548
+ pass
549
+ else:
550
+ _add_paragraph_indent(
551
+ paragraph,
552
+ left_pt=left * _PADDING_SCALE,
553
+ right_pt=right * _PADDING_SCALE,
554
+ )
555
+
556
+
557
+ def _cell_has_only_tables(cell) -> bool:
558
+ try:
559
+ if not cell.tables:
560
+ return False
561
+ for paragraph in cell.paragraphs:
562
+ if paragraph.text.strip():
563
+ return False
564
+ return True
565
+ except Exception:
566
+ return "<w:tbl>" in cell._tc.xml
567
+
568
+
569
+ def _is_direct_cell_paragraph(cell, paragraph) -> bool:
570
+ try:
571
+ return paragraph._p.getparent() is cell._tc
572
+ except Exception:
573
+ return True
574
+
575
+
576
+ def _apply_nested_table_vertical_padding(
577
+ cell, padding: Tuple[float, float, float, float]
578
+ ) -> None:
579
+ if not cell.tables:
580
+ return
581
+ top, _right, bottom, _left = _normalize_padding(padding)
582
+ top_pt = top * _PADDING_SCALE if top else 0
583
+ bottom_pt = bottom * _PADDING_SCALE if bottom else 0
584
+ tables = cell.tables
585
+ if top_pt and tables:
586
+ first_table = tables[0]
587
+ if first_table.rows:
588
+ for tcell in first_table.rows[0].cells:
589
+ _add_cell_margins(tcell, top=top_pt)
590
+ if bottom_pt and tables:
591
+ last_table = tables[-1]
592
+ if last_table.rows:
593
+ last_row = last_table.rows[-1]
594
+ for tcell in last_row.cells:
595
+ _add_cell_margins(tcell, bottom=bottom_pt)
596
+
597
+
598
+ def _set_cell_shading(cell, color_hex: str) -> None:
599
+ tc_pr = cell._tc.get_or_add_tcPr()
600
+ shd = OxmlElement("w:shd")
601
+ shd.set(qn("w:val"), "clear")
602
+ shd.set(qn("w:color"), "auto")
603
+ shd.set(qn("w:fill"), color_hex)
604
+ tc_pr.append(shd)
605
+
606
+
607
+ def _set_cell_border(
608
+ cell,
609
+ top: Optional[Tuple[float, str]] = None,
610
+ right: Optional[Tuple[float, str]] = None,
611
+ bottom: Optional[Tuple[float, str]] = None,
612
+ left: Optional[Tuple[float, str]] = None,
613
+ ) -> None:
614
+ tc_pr = cell._tc.get_or_add_tcPr()
615
+ borders = tc_pr.find(qn("w:tcBorders"))
616
+ if borders is None:
617
+ borders = OxmlElement("w:tcBorders")
618
+ tc_pr.append(borders)
619
+
620
+ for side_name, val in [
621
+ ("w:top", top),
622
+ ("w:right", right),
623
+ ("w:bottom", bottom),
624
+ ("w:left", left),
625
+ ]:
626
+ if not val:
627
+ continue
628
+ width_pt, color = val
629
+ # Remove any existing element for this side to prevent duplicates when
630
+ # _set_cell_border is called more than once on the same cell.
631
+ for existing in borders.findall(qn(side_name)):
632
+ borders.remove(existing)
633
+ el = OxmlElement(side_name)
634
+ el.set(qn("w:val"), "single")
635
+ el.set(qn("w:sz"), _border_sz(width_pt))
636
+ el.set(qn("w:color"), color)
637
+ borders.append(el)
638
+
639
+
640
+ def _apply_table_outer_borders(table, width_pt: float, color: str) -> None:
641
+ if not table.rows:
642
+ return
643
+ last_row_idx = len(table.rows) - 1
644
+ last_col_idx = len(table.rows[0].cells) - 1
645
+ for row_idx, row in enumerate(table.rows):
646
+ for col_idx, cell in enumerate(row.cells):
647
+ top = (width_pt, color) if row_idx == 0 else None
648
+ bottom = (width_pt, color) if row_idx == last_row_idx else None
649
+ left = (width_pt, color) if col_idx == 0 else None
650
+ right = (width_pt, color) if col_idx == last_col_idx else None
651
+ if top or right or bottom or left:
652
+ _set_cell_border(
653
+ cell,
654
+ top=top,
655
+ right=right,
656
+ bottom=bottom,
657
+ left=left,
658
+ )
659
+
660
+
661
+ def _adjust_column_widths_for_outer_borders(
662
+ table, widths_pt: List[float]
663
+ ) -> List[float]:
664
+ border_width = getattr(table, "_docs_border_width_pt", 0)
665
+ if border_width <= 0 or not widths_pt:
666
+ return widths_pt
667
+ adjusted = list(widths_pt)
668
+ if len(adjusted) == 1:
669
+ adjusted[0] = max(adjusted[0] - (border_width * 2), 0)
670
+ return adjusted
671
+ adjusted[0] = max(adjusted[0] - border_width, 0)
672
+ adjusted[-1] = max(adjusted[-1] - border_width, 0)
673
+ return adjusted
674
+
675
+
676
+ def _apply_table_parent_padding(table, parent_style: Dict[str, str]) -> None:
677
+ padding = _resolve_padding(parent_style)
678
+ if not padding:
679
+ return
680
+ top, right, bottom, left = _normalize_padding(padding)
681
+ left_pt = left * _PADDING_SCALE
682
+ right_pt = right * _PADDING_SCALE
683
+ if left_pt:
684
+ _set_table_indent(table, left_pt)
685
+ table._docs_parent_padding_pt = (
686
+ left_pt,
687
+ right_pt,
688
+ top * _PADDING_SCALE,
689
+ bottom * _PADDING_SCALE,
690
+ )
691
+
692
+
693
+ def _set_table_indent(table, indent_pt: float) -> None:
694
+ if indent_pt <= 0:
695
+ return
696
+ tbl_pr = table._tbl.tblPr
697
+ if tbl_pr is None:
698
+ tbl_pr = OxmlElement("w:tblPr")
699
+ table._tbl.append(tbl_pr)
700
+ tbl_ind = tbl_pr.find(qn("w:tblInd"))
701
+ if tbl_ind is None:
702
+ tbl_ind = OxmlElement("w:tblInd")
703
+ tbl_pr.append(tbl_ind)
704
+ tbl_ind.set(qn("w:w"), str(int(indent_pt * 20)))
705
+ tbl_ind.set(qn("w:type"), "dxa")
706
+
707
+
708
+ def _adjust_column_widths_for_parent_padding(
709
+ table, widths_pt: List[float]
710
+ ) -> List[float]:
711
+ padding = getattr(table, "_docs_parent_padding_pt", None)
712
+ if not padding:
713
+ return widths_pt
714
+ left_pt, right_pt, _top_pt, _bottom_pt = padding
715
+ shrink = max(left_pt + right_pt, 0)
716
+ if shrink <= 0:
717
+ return widths_pt
718
+ total = sum(widths_pt)
719
+ if total <= 0 or total <= shrink:
720
+ return widths_pt
721
+ scale = (total - shrink) / total
722
+ return [max(width * scale, 0) for width in widths_pt]
723
+
724
+
725
+ def _set_cell_margins(cell, top: float, right: float, bottom: float, left: float) -> None:
726
+ tc_pr = cell._tc.get_or_add_tcPr()
727
+ tc_mar = tc_pr.find(qn("w:tcMar"))
728
+ if tc_mar is None:
729
+ tc_mar = OxmlElement("w:tcMar")
730
+ tc_pr.append(tc_mar)
731
+
732
+ for side, value in [("top", top), ("right", right), ("bottom", bottom), ("left", left)]:
733
+ elem = tc_mar.find(qn(f"w:{side}"))
734
+ if elem is None:
735
+ elem = OxmlElement(f"w:{side}")
736
+ tc_mar.append(elem)
737
+ elem.set(qn("w:w"), str(int(value * 20)))
738
+ elem.set(qn("w:type"), "dxa")
739
+
740
+
741
+ def _should_prevent_row_split(table_node: Tag, style_map: Dict[str, str]) -> bool:
742
+ """Return True if w:cantSplit should be applied to this table's rows.
743
+
744
+ Triggers when:
745
+ - auto_page_breaks marked the table as fitting within one page
746
+ (data-docx-cant-split attribute), OR
747
+ - the agent explicitly set page-break-inside:avoid on the table.
748
+ """
749
+ if table_node.get("data-docx-cant-split"):
750
+ return True
751
+ for key in ("page-break-inside", "break-inside"):
752
+ if style_map.get(key, "").strip().lower() == "avoid":
753
+ return True
754
+ return False
755
+
756
+
757
+ def _set_table_cant_split(table) -> None:
758
+ """Add w:cantSplit to every row so Word won't split a row across pages."""
759
+ for row in table.rows:
760
+ tr_pr = row._tr.get_or_add_trPr()
761
+ if tr_pr.find(qn("w:cantSplit")) is None:
762
+ cant_split = OxmlElement("w:cantSplit")
763
+ tr_pr.append(cant_split)
764
+
765
+
766
+ def _collect_collapsed_borders(
767
+ row_cells: list[list],
768
+ row_colspans: list[list[int]],
769
+ max_cols: int,
770
+ css_rules,
771
+ ) -> dict[str, Optional[tuple[float, str]]]:
772
+ """Collect all six border positions for a border-collapse:collapse table.
773
+
774
+ Returns a dict with keys: inside_h, inside_v, top, bottom, left, right.
775
+ Each value is (width_pt, color_hex) or None.
776
+
777
+ The complete set is needed so _apply_collapsed_table_borders can write all six
778
+ directions into w:tblBorders without mixing in any cell-level tcBorders.
779
+ """
780
+
781
+ def first_cell_border(cells, *props) -> Optional[tuple[float, str]]:
782
+ for cell_node in cells:
783
+ style = _compute_style_map(cell_node, css_rules)
784
+ for prop in props:
785
+ val = _parse_border(style.get(prop, ""))
786
+ if val:
787
+ return val
788
+ # Fall back to shorthand border
789
+ val = _parse_border(style.get("border", ""))
790
+ if val:
791
+ return val
792
+ return None
793
+
794
+ inside_h: Optional[tuple[float, str]] = None
795
+ inside_v: Optional[tuple[float, str]] = None
796
+
797
+ # interior-H: border-bottom on non-last rows, then border-top on non-first rows
798
+ for cells in row_cells[:-1]:
799
+ inside_h = first_cell_border(cells, "border-bottom")
800
+ if inside_h:
801
+ break
802
+ if not inside_h and len(row_cells) > 1:
803
+ for cells in row_cells[1:]:
804
+ inside_h = first_cell_border(cells, "border-top")
805
+ if inside_h:
806
+ break
807
+
808
+ # interior-V: border-right on non-last cols, then border-left on non-first cols
809
+ if max_cols > 1:
810
+ for row_idx, cells in enumerate(row_cells):
811
+ colspans = row_colspans[row_idx]
812
+ col_pos = 0
813
+ for cell_idx, cell_node in enumerate(cells):
814
+ span = colspans[cell_idx] if cell_idx < len(colspans) else 1
815
+ if col_pos + span < max_cols:
816
+ style = _compute_style_map(cell_node, css_rules)
817
+ inside_v = _parse_border(style.get("border-right", "")) or _parse_border(
818
+ style.get("border", "")
819
+ )
820
+ if inside_v:
821
+ break
822
+ col_pos += span
823
+ if inside_v:
824
+ break
825
+
826
+ if not inside_v:
827
+ for row_idx, cells in enumerate(row_cells):
828
+ colspans = row_colspans[row_idx]
829
+ col_pos = 0
830
+ for cell_idx, cell_node in enumerate(cells):
831
+ span = colspans[cell_idx] if cell_idx < len(colspans) else 1
832
+ if col_pos > 0:
833
+ style = _compute_style_map(cell_node, css_rules)
834
+ inside_v = _parse_border(style.get("border-left", ""))
835
+ if inside_v:
836
+ break
837
+ col_pos += span
838
+ if inside_v:
839
+ break
840
+
841
+ # outer edges — collect from the boundary cells
842
+ outer_top = first_cell_border(row_cells[0], "border-top") if row_cells else None
843
+ outer_bottom = first_cell_border(row_cells[-1], "border-bottom") if row_cells else None
844
+
845
+ first_col_cells = [cells[0] for cells in row_cells if cells]
846
+ outer_left = first_cell_border(first_col_cells, "border-left") if first_col_cells else None
847
+
848
+ def last_col_cell(cells, colspans):
849
+ col_pos = 0
850
+ last = None
851
+ for idx, cell_node in enumerate(cells):
852
+ span = colspans[idx] if idx < len(colspans) else 1
853
+ last = cell_node
854
+ col_pos += span
855
+ return last
856
+
857
+ last_col_cells = [
858
+ last_col_cell(cells, row_colspans[ri]) for ri, cells in enumerate(row_cells) if cells
859
+ ]
860
+ last_col_cells = [c for c in last_col_cells if c is not None]
861
+ outer_right = first_cell_border(last_col_cells, "border-right") if last_col_cells else None
862
+
863
+ return {
864
+ "inside_h": inside_h,
865
+ "inside_v": inside_v,
866
+ "top": outer_top,
867
+ "bottom": outer_bottom,
868
+ "left": outer_left,
869
+ "right": outer_right,
870
+ }
871
+
872
+
873
+ def _apply_collapsed_table_borders(
874
+ table,
875
+ collapsed_borders: dict[str, Optional[tuple[float, str]]],
876
+ ) -> None:
877
+ """Write all six border positions into w:tblBorders for a border-collapse:collapse table.
878
+
879
+ Using tblBorders exclusively (no tcBorders on any cell) guarantees that Word
880
+ draws exactly one line per edge and never mixes table-level and cell-level borders,
881
+ which is the root cause of double-line rendering.
882
+ """
883
+ tbl_pr = table._tbl.tblPr
884
+ if tbl_pr is None:
885
+ tbl_pr = OxmlElement("w:tblPr")
886
+ table._tbl.append(tbl_pr)
887
+ borders = tbl_pr.find(qn("w:tblBorders"))
888
+ if borders is None:
889
+ borders = OxmlElement("w:tblBorders")
890
+ tbl_pr.append(borders)
891
+
892
+ # OOXML requires a specific child order inside w:tblBorders.
893
+ ordered_sides = [
894
+ ("w:top", collapsed_borders.get("top")),
895
+ ("w:left", collapsed_borders.get("left")),
896
+ ("w:bottom", collapsed_borders.get("bottom")),
897
+ ("w:right", collapsed_borders.get("right")),
898
+ ("w:insideH", collapsed_borders.get("inside_h")),
899
+ ("w:insideV", collapsed_borders.get("inside_v")),
900
+ ]
901
+ # Clear existing children to guarantee order and avoid stale entries.
902
+ for child in list(borders):
903
+ borders.remove(child)
904
+
905
+ for tag_name, border_val in ordered_sides:
906
+ if not border_val:
907
+ continue
908
+ width_pt, color = border_val
909
+ el = OxmlElement(tag_name)
910
+ el.set(qn("w:val"), "single")
911
+ el.set(qn("w:sz"), _border_sz(width_pt))
912
+ el.set(qn("w:color"), color)
913
+ borders.append(el)
914
+
915
+
916
+ def _add_cell_margins(
917
+ cell,
918
+ top: Optional[float] = None,
919
+ right: Optional[float] = None,
920
+ bottom: Optional[float] = None,
921
+ left: Optional[float] = None,
922
+ ) -> None:
923
+ tc_pr = cell._tc.get_or_add_tcPr()
924
+ tc_mar = tc_pr.find(qn("w:tcMar"))
925
+ if tc_mar is None:
926
+ tc_mar = OxmlElement("w:tcMar")
927
+ tc_pr.append(tc_mar)
928
+
929
+ for side, value in [("top", top), ("right", right), ("bottom", bottom), ("left", left)]:
930
+ if value is None:
931
+ continue
932
+ elem = tc_mar.find(qn(f"w:{side}"))
933
+ current_val = 0
934
+ if elem is not None:
935
+ current = elem.get(qn("w:w"))
936
+ if current and current.isdigit():
937
+ current_val = int(current)
938
+ else:
939
+ elem = OxmlElement(f"w:{side}")
940
+ tc_mar.append(elem)
941
+ elem.set(qn("w:w"), str(current_val + int(value * 20)))
942
+ elem.set(qn("w:type"), "dxa")