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