ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
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.
- agentui/.prettierrc +15 -0
- agentui/QUICKSTART.md +272 -0
- agentui/README.md +59 -0
- agentui/env.example +16 -0
- agentui/jsconfig.json +14 -0
- agentui/package-lock.json +4242 -0
- agentui/package.json +34 -0
- agentui/scripts/postinstall/apply-patches.mjs +260 -0
- agentui/src/app.css +61 -0
- agentui/src/app.d.ts +13 -0
- agentui/src/app.html +12 -0
- agentui/src/components/LoadingSpinner.svelte +64 -0
- agentui/src/components/ThemeSwitcher.svelte +159 -0
- agentui/src/components/index.js +4 -0
- agentui/src/lib/api/bots.ts +60 -0
- agentui/src/lib/api/chat.ts +22 -0
- agentui/src/lib/api/http.ts +25 -0
- agentui/src/lib/components/BotCard.svelte +33 -0
- agentui/src/lib/components/ChatBubble.svelte +63 -0
- agentui/src/lib/components/Toast.svelte +21 -0
- agentui/src/lib/config.ts +20 -0
- agentui/src/lib/stores/auth.svelte.ts +73 -0
- agentui/src/lib/stores/theme.svelte.js +64 -0
- agentui/src/lib/stores/toast.svelte.ts +31 -0
- agentui/src/lib/utils/conversation.ts +39 -0
- agentui/src/routes/+layout.svelte +20 -0
- agentui/src/routes/+page.svelte +232 -0
- agentui/src/routes/login/+page.svelte +200 -0
- agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
- agentui/src/routes/talk/[agentId]/+page.ts +7 -0
- agentui/static/README.md +1 -0
- agentui/svelte.config.js +11 -0
- agentui/tailwind.config.ts +53 -0
- agentui/tsconfig.json +3 -0
- agentui/vite.config.ts +10 -0
- ai_parrot-0.17.2.dist-info/METADATA +472 -0
- ai_parrot-0.17.2.dist-info/RECORD +535 -0
- ai_parrot-0.17.2.dist-info/WHEEL +6 -0
- ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
- ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
- ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
- crew-builder/.prettierrc +15 -0
- crew-builder/QUICKSTART.md +259 -0
- crew-builder/README.md +113 -0
- crew-builder/env.example +17 -0
- crew-builder/jsconfig.json +14 -0
- crew-builder/package-lock.json +4182 -0
- crew-builder/package.json +37 -0
- crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
- crew-builder/src/app.css +62 -0
- crew-builder/src/app.d.ts +13 -0
- crew-builder/src/app.html +12 -0
- crew-builder/src/components/LoadingSpinner.svelte +64 -0
- crew-builder/src/components/ThemeSwitcher.svelte +149 -0
- crew-builder/src/components/index.js +9 -0
- crew-builder/src/lib/api/bots.ts +60 -0
- crew-builder/src/lib/api/chat.ts +80 -0
- crew-builder/src/lib/api/client.ts +56 -0
- crew-builder/src/lib/api/crew/crew.ts +136 -0
- crew-builder/src/lib/api/index.ts +5 -0
- crew-builder/src/lib/api/o365/auth.ts +65 -0
- crew-builder/src/lib/auth/auth.ts +54 -0
- crew-builder/src/lib/components/AgentNode.svelte +43 -0
- crew-builder/src/lib/components/BotCard.svelte +33 -0
- crew-builder/src/lib/components/ChatBubble.svelte +67 -0
- crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
- crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
- crew-builder/src/lib/components/JsonViewer.svelte +24 -0
- crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
- crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
- crew-builder/src/lib/components/Toast.svelte +67 -0
- crew-builder/src/lib/components/Toolbar.svelte +157 -0
- crew-builder/src/lib/components/index.ts +10 -0
- crew-builder/src/lib/config.ts +8 -0
- crew-builder/src/lib/stores/auth.svelte.ts +228 -0
- crew-builder/src/lib/stores/crewStore.ts +369 -0
- crew-builder/src/lib/stores/theme.svelte.js +145 -0
- crew-builder/src/lib/stores/toast.svelte.ts +69 -0
- crew-builder/src/lib/utils/conversation.ts +39 -0
- crew-builder/src/lib/utils/markdown.ts +122 -0
- crew-builder/src/lib/utils/talkHistory.ts +47 -0
- crew-builder/src/routes/+layout.svelte +20 -0
- crew-builder/src/routes/+page.svelte +539 -0
- crew-builder/src/routes/agents/+page.svelte +247 -0
- crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
- crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
- crew-builder/src/routes/builder/+page.svelte +204 -0
- crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
- crew-builder/src/routes/crew/ask/+page.ts +1 -0
- crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
- crew-builder/src/routes/login/+page.svelte +197 -0
- crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
- crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
- crew-builder/static/README.md +1 -0
- crew-builder/svelte.config.js +11 -0
- crew-builder/tailwind.config.ts +53 -0
- crew-builder/tsconfig.json +3 -0
- crew-builder/vite.config.ts +10 -0
- mcp_servers/calculator_server.py +309 -0
- parrot/__init__.py +27 -0
- parrot/__pycache__/__init__.cpython-310.pyc +0 -0
- parrot/__pycache__/version.cpython-310.pyc +0 -0
- parrot/_version.py +34 -0
- parrot/a2a/__init__.py +48 -0
- parrot/a2a/client.py +658 -0
- parrot/a2a/discovery.py +89 -0
- parrot/a2a/mixin.py +257 -0
- parrot/a2a/models.py +376 -0
- parrot/a2a/server.py +770 -0
- parrot/agents/__init__.py +29 -0
- parrot/bots/__init__.py +12 -0
- parrot/bots/a2a_agent.py +19 -0
- parrot/bots/abstract.py +3139 -0
- parrot/bots/agent.py +1129 -0
- parrot/bots/basic.py +9 -0
- parrot/bots/chatbot.py +669 -0
- parrot/bots/data.py +1618 -0
- parrot/bots/database/__init__.py +5 -0
- parrot/bots/database/abstract.py +3071 -0
- parrot/bots/database/cache.py +286 -0
- parrot/bots/database/models.py +468 -0
- parrot/bots/database/prompts.py +154 -0
- parrot/bots/database/retries.py +98 -0
- parrot/bots/database/router.py +269 -0
- parrot/bots/database/sql.py +41 -0
- parrot/bots/db/__init__.py +6 -0
- parrot/bots/db/abstract.py +556 -0
- parrot/bots/db/bigquery.py +602 -0
- parrot/bots/db/cache.py +85 -0
- parrot/bots/db/documentdb.py +668 -0
- parrot/bots/db/elastic.py +1014 -0
- parrot/bots/db/influx.py +898 -0
- parrot/bots/db/mock.py +96 -0
- parrot/bots/db/multi.py +783 -0
- parrot/bots/db/prompts.py +185 -0
- parrot/bots/db/sql.py +1255 -0
- parrot/bots/db/tools.py +212 -0
- parrot/bots/document.py +680 -0
- parrot/bots/hrbot.py +15 -0
- parrot/bots/kb.py +170 -0
- parrot/bots/mcp.py +36 -0
- parrot/bots/orchestration/README.md +463 -0
- parrot/bots/orchestration/__init__.py +1 -0
- parrot/bots/orchestration/agent.py +155 -0
- parrot/bots/orchestration/crew.py +3330 -0
- parrot/bots/orchestration/fsm.py +1179 -0
- parrot/bots/orchestration/hr.py +434 -0
- parrot/bots/orchestration/storage/__init__.py +4 -0
- parrot/bots/orchestration/storage/memory.py +100 -0
- parrot/bots/orchestration/storage/mixin.py +119 -0
- parrot/bots/orchestration/verify.py +202 -0
- parrot/bots/product.py +204 -0
- parrot/bots/prompts/__init__.py +96 -0
- parrot/bots/prompts/agents.py +155 -0
- parrot/bots/prompts/data.py +216 -0
- parrot/bots/prompts/output_generation.py +8 -0
- parrot/bots/scraper/__init__.py +3 -0
- parrot/bots/scraper/models.py +122 -0
- parrot/bots/scraper/scraper.py +1173 -0
- parrot/bots/scraper/templates.py +115 -0
- parrot/bots/stores/__init__.py +5 -0
- parrot/bots/stores/local.py +172 -0
- parrot/bots/webdev.py +81 -0
- parrot/cli.py +17 -0
- parrot/clients/__init__.py +16 -0
- parrot/clients/base.py +1491 -0
- parrot/clients/claude.py +1191 -0
- parrot/clients/factory.py +129 -0
- parrot/clients/google.py +4567 -0
- parrot/clients/gpt.py +1975 -0
- parrot/clients/grok.py +432 -0
- parrot/clients/groq.py +986 -0
- parrot/clients/hf.py +582 -0
- parrot/clients/models.py +18 -0
- parrot/conf.py +395 -0
- parrot/embeddings/__init__.py +9 -0
- parrot/embeddings/base.py +157 -0
- parrot/embeddings/google.py +98 -0
- parrot/embeddings/huggingface.py +74 -0
- parrot/embeddings/openai.py +84 -0
- parrot/embeddings/processor.py +88 -0
- parrot/exceptions.c +13868 -0
- parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/exceptions.pxd +22 -0
- parrot/exceptions.pxi +15 -0
- parrot/exceptions.pyx +44 -0
- parrot/generators/__init__.py +29 -0
- parrot/generators/base.py +200 -0
- parrot/generators/html.py +293 -0
- parrot/generators/react.py +205 -0
- parrot/generators/streamlit.py +203 -0
- parrot/generators/template.py +105 -0
- parrot/handlers/__init__.py +4 -0
- parrot/handlers/agent.py +861 -0
- parrot/handlers/agents/__init__.py +1 -0
- parrot/handlers/agents/abstract.py +900 -0
- parrot/handlers/bots.py +338 -0
- parrot/handlers/chat.py +915 -0
- parrot/handlers/creation.sql +192 -0
- parrot/handlers/crew/ARCHITECTURE.md +362 -0
- parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
- parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
- parrot/handlers/crew/__init__.py +0 -0
- parrot/handlers/crew/handler.py +801 -0
- parrot/handlers/crew/models.py +229 -0
- parrot/handlers/crew/redis_persistence.py +523 -0
- parrot/handlers/jobs/__init__.py +10 -0
- parrot/handlers/jobs/job.py +384 -0
- parrot/handlers/jobs/mixin.py +627 -0
- parrot/handlers/jobs/models.py +115 -0
- parrot/handlers/jobs/worker.py +31 -0
- parrot/handlers/models.py +596 -0
- parrot/handlers/o365_auth.py +105 -0
- parrot/handlers/stream.py +337 -0
- parrot/interfaces/__init__.py +6 -0
- parrot/interfaces/aws.py +143 -0
- parrot/interfaces/credentials.py +113 -0
- parrot/interfaces/database.py +27 -0
- parrot/interfaces/google.py +1123 -0
- parrot/interfaces/hierarchy.py +1227 -0
- parrot/interfaces/http.py +651 -0
- parrot/interfaces/images/__init__.py +0 -0
- parrot/interfaces/images/plugins/__init__.py +24 -0
- parrot/interfaces/images/plugins/abstract.py +58 -0
- parrot/interfaces/images/plugins/analisys.py +148 -0
- parrot/interfaces/images/plugins/classify.py +150 -0
- parrot/interfaces/images/plugins/classifybase.py +182 -0
- parrot/interfaces/images/plugins/detect.py +150 -0
- parrot/interfaces/images/plugins/exif.py +1103 -0
- parrot/interfaces/images/plugins/hash.py +52 -0
- parrot/interfaces/images/plugins/vision.py +104 -0
- parrot/interfaces/images/plugins/yolo.py +66 -0
- parrot/interfaces/images/plugins/zerodetect.py +197 -0
- parrot/interfaces/o365.py +978 -0
- parrot/interfaces/onedrive.py +822 -0
- parrot/interfaces/sharepoint.py +1435 -0
- parrot/interfaces/soap.py +257 -0
- parrot/loaders/__init__.py +8 -0
- parrot/loaders/abstract.py +1131 -0
- parrot/loaders/audio.py +199 -0
- parrot/loaders/basepdf.py +53 -0
- parrot/loaders/basevideo.py +1568 -0
- parrot/loaders/csv.py +409 -0
- parrot/loaders/docx.py +116 -0
- parrot/loaders/epubloader.py +316 -0
- parrot/loaders/excel.py +199 -0
- parrot/loaders/factory.py +55 -0
- parrot/loaders/files/__init__.py +0 -0
- parrot/loaders/files/abstract.py +39 -0
- parrot/loaders/files/html.py +26 -0
- parrot/loaders/files/text.py +63 -0
- parrot/loaders/html.py +152 -0
- parrot/loaders/markdown.py +442 -0
- parrot/loaders/pdf.py +373 -0
- parrot/loaders/pdfmark.py +320 -0
- parrot/loaders/pdftables.py +506 -0
- parrot/loaders/ppt.py +476 -0
- parrot/loaders/qa.py +63 -0
- parrot/loaders/splitters/__init__.py +10 -0
- parrot/loaders/splitters/base.py +138 -0
- parrot/loaders/splitters/md.py +228 -0
- parrot/loaders/splitters/token.py +143 -0
- parrot/loaders/txt.py +26 -0
- parrot/loaders/video.py +89 -0
- parrot/loaders/videolocal.py +218 -0
- parrot/loaders/videounderstanding.py +377 -0
- parrot/loaders/vimeo.py +167 -0
- parrot/loaders/web.py +599 -0
- parrot/loaders/youtube.py +504 -0
- parrot/manager/__init__.py +5 -0
- parrot/manager/manager.py +1030 -0
- parrot/mcp/__init__.py +28 -0
- parrot/mcp/adapter.py +105 -0
- parrot/mcp/cli.py +174 -0
- parrot/mcp/client.py +119 -0
- parrot/mcp/config.py +75 -0
- parrot/mcp/integration.py +842 -0
- parrot/mcp/oauth.py +933 -0
- parrot/mcp/server.py +225 -0
- parrot/mcp/transports/__init__.py +3 -0
- parrot/mcp/transports/base.py +279 -0
- parrot/mcp/transports/grpc_session.py +163 -0
- parrot/mcp/transports/http.py +312 -0
- parrot/mcp/transports/mcp.proto +108 -0
- parrot/mcp/transports/quic.py +1082 -0
- parrot/mcp/transports/sse.py +330 -0
- parrot/mcp/transports/stdio.py +309 -0
- parrot/mcp/transports/unix.py +395 -0
- parrot/mcp/transports/websocket.py +547 -0
- parrot/memory/__init__.py +16 -0
- parrot/memory/abstract.py +209 -0
- parrot/memory/agent.py +32 -0
- parrot/memory/cache.py +175 -0
- parrot/memory/core.py +555 -0
- parrot/memory/file.py +153 -0
- parrot/memory/mem.py +131 -0
- parrot/memory/redis.py +613 -0
- parrot/models/__init__.py +46 -0
- parrot/models/basic.py +118 -0
- parrot/models/compliance.py +208 -0
- parrot/models/crew.py +395 -0
- parrot/models/detections.py +654 -0
- parrot/models/generation.py +85 -0
- parrot/models/google.py +223 -0
- parrot/models/groq.py +23 -0
- parrot/models/openai.py +30 -0
- parrot/models/outputs.py +285 -0
- parrot/models/responses.py +938 -0
- parrot/notifications/__init__.py +743 -0
- parrot/openapi/__init__.py +3 -0
- parrot/openapi/components.yaml +641 -0
- parrot/openapi/config.py +322 -0
- parrot/outputs/__init__.py +32 -0
- parrot/outputs/formats/__init__.py +108 -0
- parrot/outputs/formats/altair.py +359 -0
- parrot/outputs/formats/application.py +122 -0
- parrot/outputs/formats/base.py +351 -0
- parrot/outputs/formats/bokeh.py +356 -0
- parrot/outputs/formats/card.py +424 -0
- parrot/outputs/formats/chart.py +436 -0
- parrot/outputs/formats/d3.py +255 -0
- parrot/outputs/formats/echarts.py +310 -0
- parrot/outputs/formats/generators/__init__.py +0 -0
- parrot/outputs/formats/generators/abstract.py +61 -0
- parrot/outputs/formats/generators/panel.py +145 -0
- parrot/outputs/formats/generators/streamlit.py +86 -0
- parrot/outputs/formats/generators/terminal.py +63 -0
- parrot/outputs/formats/holoviews.py +310 -0
- parrot/outputs/formats/html.py +147 -0
- parrot/outputs/formats/jinja2.py +46 -0
- parrot/outputs/formats/json.py +87 -0
- parrot/outputs/formats/map.py +933 -0
- parrot/outputs/formats/markdown.py +172 -0
- parrot/outputs/formats/matplotlib.py +237 -0
- parrot/outputs/formats/mixins/__init__.py +0 -0
- parrot/outputs/formats/mixins/emaps.py +855 -0
- parrot/outputs/formats/plotly.py +341 -0
- parrot/outputs/formats/seaborn.py +310 -0
- parrot/outputs/formats/table.py +397 -0
- parrot/outputs/formats/template_report.py +138 -0
- parrot/outputs/formats/yaml.py +125 -0
- parrot/outputs/formatter.py +152 -0
- parrot/outputs/templates/__init__.py +95 -0
- parrot/pipelines/__init__.py +0 -0
- parrot/pipelines/abstract.py +210 -0
- parrot/pipelines/detector.py +124 -0
- parrot/pipelines/models.py +90 -0
- parrot/pipelines/planogram.py +3002 -0
- parrot/pipelines/table.sql +97 -0
- parrot/plugins/__init__.py +106 -0
- parrot/plugins/importer.py +80 -0
- parrot/py.typed +0 -0
- parrot/registry/__init__.py +18 -0
- parrot/registry/registry.py +594 -0
- parrot/scheduler/__init__.py +1189 -0
- parrot/scheduler/models.py +60 -0
- parrot/security/__init__.py +16 -0
- parrot/security/prompt_injection.py +268 -0
- parrot/security/security_events.sql +25 -0
- parrot/services/__init__.py +1 -0
- parrot/services/mcp/__init__.py +8 -0
- parrot/services/mcp/config.py +13 -0
- parrot/services/mcp/server.py +295 -0
- parrot/services/o365_remote_auth.py +235 -0
- parrot/stores/__init__.py +7 -0
- parrot/stores/abstract.py +352 -0
- parrot/stores/arango.py +1090 -0
- parrot/stores/bigquery.py +1377 -0
- parrot/stores/cache.py +106 -0
- parrot/stores/empty.py +10 -0
- parrot/stores/faiss_store.py +1157 -0
- parrot/stores/kb/__init__.py +9 -0
- parrot/stores/kb/abstract.py +68 -0
- parrot/stores/kb/cache.py +165 -0
- parrot/stores/kb/doc.py +325 -0
- parrot/stores/kb/hierarchy.py +346 -0
- parrot/stores/kb/local.py +457 -0
- parrot/stores/kb/prompt.py +28 -0
- parrot/stores/kb/redis.py +659 -0
- parrot/stores/kb/store.py +115 -0
- parrot/stores/kb/user.py +374 -0
- parrot/stores/models.py +59 -0
- parrot/stores/pgvector.py +3 -0
- parrot/stores/postgres.py +2853 -0
- parrot/stores/utils/__init__.py +0 -0
- parrot/stores/utils/chunking.py +197 -0
- parrot/telemetry/__init__.py +3 -0
- parrot/telemetry/mixin.py +111 -0
- parrot/template/__init__.py +3 -0
- parrot/template/engine.py +259 -0
- parrot/tools/__init__.py +23 -0
- parrot/tools/abstract.py +644 -0
- parrot/tools/agent.py +363 -0
- parrot/tools/arangodbsearch.py +537 -0
- parrot/tools/arxiv_tool.py +188 -0
- parrot/tools/calculator/__init__.py +3 -0
- parrot/tools/calculator/operations/__init__.py +38 -0
- parrot/tools/calculator/operations/calculus.py +80 -0
- parrot/tools/calculator/operations/statistics.py +76 -0
- parrot/tools/calculator/tool.py +150 -0
- parrot/tools/cloudwatch.py +988 -0
- parrot/tools/codeinterpreter/__init__.py +127 -0
- parrot/tools/codeinterpreter/executor.py +371 -0
- parrot/tools/codeinterpreter/internals.py +473 -0
- parrot/tools/codeinterpreter/models.py +643 -0
- parrot/tools/codeinterpreter/prompts.py +224 -0
- parrot/tools/codeinterpreter/tool.py +664 -0
- parrot/tools/company_info/__init__.py +6 -0
- parrot/tools/company_info/tool.py +1138 -0
- parrot/tools/correlationanalysis.py +437 -0
- parrot/tools/database/abstract.py +286 -0
- parrot/tools/database/bq.py +115 -0
- parrot/tools/database/cache.py +284 -0
- parrot/tools/database/models.py +95 -0
- parrot/tools/database/pg.py +343 -0
- parrot/tools/databasequery.py +1159 -0
- parrot/tools/db.py +1800 -0
- parrot/tools/ddgo.py +370 -0
- parrot/tools/decorators.py +271 -0
- parrot/tools/dftohtml.py +282 -0
- parrot/tools/document.py +549 -0
- parrot/tools/ecs.py +819 -0
- parrot/tools/edareport.py +368 -0
- parrot/tools/elasticsearch.py +1049 -0
- parrot/tools/employees.py +462 -0
- parrot/tools/epson/__init__.py +96 -0
- parrot/tools/excel.py +683 -0
- parrot/tools/file/__init__.py +13 -0
- parrot/tools/file/abstract.py +76 -0
- parrot/tools/file/gcs.py +378 -0
- parrot/tools/file/local.py +284 -0
- parrot/tools/file/s3.py +511 -0
- parrot/tools/file/tmp.py +309 -0
- parrot/tools/file/tool.py +501 -0
- parrot/tools/file_reader.py +129 -0
- parrot/tools/flowtask/__init__.py +19 -0
- parrot/tools/flowtask/tool.py +761 -0
- parrot/tools/gittoolkit.py +508 -0
- parrot/tools/google/__init__.py +18 -0
- parrot/tools/google/base.py +169 -0
- parrot/tools/google/tools.py +1251 -0
- parrot/tools/googlelocation.py +5 -0
- parrot/tools/googleroutes.py +5 -0
- parrot/tools/googlesearch.py +5 -0
- parrot/tools/googlesitesearch.py +5 -0
- parrot/tools/googlevoice.py +2 -0
- parrot/tools/gvoice.py +695 -0
- parrot/tools/ibisworld/README.md +225 -0
- parrot/tools/ibisworld/__init__.py +11 -0
- parrot/tools/ibisworld/tool.py +366 -0
- parrot/tools/jiratoolkit.py +1718 -0
- parrot/tools/manager.py +1098 -0
- parrot/tools/math.py +152 -0
- parrot/tools/metadata.py +476 -0
- parrot/tools/msteams.py +1621 -0
- parrot/tools/msword.py +635 -0
- parrot/tools/multidb.py +580 -0
- parrot/tools/multistoresearch.py +369 -0
- parrot/tools/networkninja.py +167 -0
- parrot/tools/nextstop/__init__.py +4 -0
- parrot/tools/nextstop/base.py +286 -0
- parrot/tools/nextstop/employee.py +733 -0
- parrot/tools/nextstop/store.py +462 -0
- parrot/tools/notification.py +435 -0
- parrot/tools/o365/__init__.py +42 -0
- parrot/tools/o365/base.py +295 -0
- parrot/tools/o365/bundle.py +522 -0
- parrot/tools/o365/events.py +554 -0
- parrot/tools/o365/mail.py +992 -0
- parrot/tools/o365/onedrive.py +497 -0
- parrot/tools/o365/sharepoint.py +641 -0
- parrot/tools/openapi_toolkit.py +904 -0
- parrot/tools/openweather.py +527 -0
- parrot/tools/pdfprint.py +1001 -0
- parrot/tools/powerbi.py +518 -0
- parrot/tools/powerpoint.py +1113 -0
- parrot/tools/pricestool.py +146 -0
- parrot/tools/products/__init__.py +246 -0
- parrot/tools/prophet_tool.py +171 -0
- parrot/tools/pythonpandas.py +630 -0
- parrot/tools/pythonrepl.py +910 -0
- parrot/tools/qsource.py +436 -0
- parrot/tools/querytoolkit.py +395 -0
- parrot/tools/quickeda.py +827 -0
- parrot/tools/resttool.py +553 -0
- parrot/tools/retail/__init__.py +0 -0
- parrot/tools/retail/bby.py +528 -0
- parrot/tools/sandboxtool.py +703 -0
- parrot/tools/sassie/__init__.py +352 -0
- parrot/tools/scraping/__init__.py +7 -0
- parrot/tools/scraping/docs/select.md +466 -0
- parrot/tools/scraping/documentation.md +1278 -0
- parrot/tools/scraping/driver.py +436 -0
- parrot/tools/scraping/models.py +576 -0
- parrot/tools/scraping/options.py +85 -0
- parrot/tools/scraping/orchestrator.py +517 -0
- parrot/tools/scraping/readme.md +740 -0
- parrot/tools/scraping/tool.py +3115 -0
- parrot/tools/seasonaldetection.py +642 -0
- parrot/tools/shell_tool/__init__.py +5 -0
- parrot/tools/shell_tool/actions.py +408 -0
- parrot/tools/shell_tool/engine.py +155 -0
- parrot/tools/shell_tool/models.py +322 -0
- parrot/tools/shell_tool/tool.py +442 -0
- parrot/tools/site_search.py +214 -0
- parrot/tools/textfile.py +418 -0
- parrot/tools/think.py +378 -0
- parrot/tools/toolkit.py +298 -0
- parrot/tools/webapp_tool.py +187 -0
- parrot/tools/whatif.py +1279 -0
- parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
- parrot/tools/workday/__init__.py +6 -0
- parrot/tools/workday/models.py +1389 -0
- parrot/tools/workday/tool.py +1293 -0
- parrot/tools/yfinance_tool.py +306 -0
- parrot/tools/zipcode.py +217 -0
- parrot/utils/__init__.py +2 -0
- parrot/utils/helpers.py +73 -0
- parrot/utils/parsers/__init__.py +5 -0
- parrot/utils/parsers/toml.c +12078 -0
- parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/parsers/toml.pyx +21 -0
- parrot/utils/toml.py +11 -0
- parrot/utils/types.cpp +20936 -0
- parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/types.pyx +213 -0
- parrot/utils/uv.py +11 -0
- parrot/version.py +10 -0
- parrot/yaml-rs/Cargo.lock +350 -0
- parrot/yaml-rs/Cargo.toml +19 -0
- parrot/yaml-rs/pyproject.toml +19 -0
- parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
- parrot/yaml-rs/src/lib.rs +222 -0
- requirements/docker-compose.yml +24 -0
- requirements/requirements-dev.txt +21 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PowerPoint Tool migrated to use AbstractDocumentTool framework.
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import Dict, List, Optional, Any, Union
|
|
8
|
+
import io
|
|
9
|
+
import traceback
|
|
10
|
+
import requests
|
|
11
|
+
from pptx import Presentation
|
|
12
|
+
from pptx.util import Pt, Inches, Cm
|
|
13
|
+
from pptx.dml.color import RGBColor
|
|
14
|
+
from pptx.enum.text import PP_ALIGN
|
|
15
|
+
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
|
16
|
+
from jinja2 import Environment, FileSystemLoader
|
|
17
|
+
from pydantic import Field, field_validator
|
|
18
|
+
import markdown
|
|
19
|
+
from bs4 import BeautifulSoup, NavigableString
|
|
20
|
+
from navconfig import BASE_DIR
|
|
21
|
+
from .document import (
|
|
22
|
+
AbstractDocumentTool,
|
|
23
|
+
DocumentGenerationArgs
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
class PowerPointArgs(DocumentGenerationArgs):
|
|
27
|
+
"""Arguments schema for PowerPoint presentation generation."""
|
|
28
|
+
|
|
29
|
+
template_name: Optional[str] = Field(
|
|
30
|
+
None,
|
|
31
|
+
description="Name of the HTML template (e.g., 'presentation.html') to render before conversion"
|
|
32
|
+
)
|
|
33
|
+
template_vars: Optional[Dict[str, Any]] = Field(
|
|
34
|
+
None,
|
|
35
|
+
description="Variables to pass to the HTML template (e.g., title, author, date)"
|
|
36
|
+
)
|
|
37
|
+
pptx_template: Optional[str] = Field(
|
|
38
|
+
None,
|
|
39
|
+
description="Filename of PowerPoint template file (.pptx or .potx) to use as base"
|
|
40
|
+
)
|
|
41
|
+
pptx_template_path: Optional[Path] = Field(
|
|
42
|
+
None,
|
|
43
|
+
description="Path where the PowerPoint template file is located"
|
|
44
|
+
)
|
|
45
|
+
slide_layout: int = Field(
|
|
46
|
+
1,
|
|
47
|
+
description="Default slide layout index (0=Title Slide, 1=Title and Content, etc.)",
|
|
48
|
+
ge=0,
|
|
49
|
+
le=15
|
|
50
|
+
)
|
|
51
|
+
title_styles: Optional[Dict[str, Any]] = Field(
|
|
52
|
+
None,
|
|
53
|
+
description="Styles to apply to slide titles (font_name, font_size, bold, italic, font_color, alignment)"
|
|
54
|
+
)
|
|
55
|
+
content_styles: Optional[Dict[str, Any]] = Field(
|
|
56
|
+
None,
|
|
57
|
+
description="Styles to apply to slide content (font_name, font_size, bold, italic, font_color, alignment)"
|
|
58
|
+
)
|
|
59
|
+
max_slides: int = Field(
|
|
60
|
+
50,
|
|
61
|
+
description="Maximum number of slides to generate",
|
|
62
|
+
ge=1,
|
|
63
|
+
le=100
|
|
64
|
+
)
|
|
65
|
+
split_by_headings: bool = Field(
|
|
66
|
+
True,
|
|
67
|
+
description="Whether to split content into slides based on headings (H1, H2, etc.)"
|
|
68
|
+
)
|
|
69
|
+
enable_images: bool = Field(
|
|
70
|
+
True,
|
|
71
|
+
description="Whether to download and include images from URLs in markdown"
|
|
72
|
+
)
|
|
73
|
+
image_width: Optional[float] = Field(
|
|
74
|
+
None,
|
|
75
|
+
description="Default image width in inches (None for auto-sizing)"
|
|
76
|
+
)
|
|
77
|
+
image_height: Optional[float] = Field(
|
|
78
|
+
None,
|
|
79
|
+
description="Default image height in inches (None for auto-sizing)"
|
|
80
|
+
)
|
|
81
|
+
max_image_size: float = Field(
|
|
82
|
+
6.0,
|
|
83
|
+
description="Maximum image size in inches (width or height)"
|
|
84
|
+
)
|
|
85
|
+
image_quality: str = Field(
|
|
86
|
+
"high",
|
|
87
|
+
description="Image quality: 'high', 'medium', 'low'"
|
|
88
|
+
)
|
|
89
|
+
image_timeout: int = Field(
|
|
90
|
+
30,
|
|
91
|
+
description="Timeout for image downloads in seconds"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@field_validator('template_name')
|
|
95
|
+
@classmethod
|
|
96
|
+
def validate_template_name(cls, v):
|
|
97
|
+
if v and not v.endswith('.html'):
|
|
98
|
+
v = f"{v}.html"
|
|
99
|
+
return v
|
|
100
|
+
|
|
101
|
+
class PowerPointTool(AbstractDocumentTool):
|
|
102
|
+
"""
|
|
103
|
+
PowerPoint Presentation Generator Tool.
|
|
104
|
+
|
|
105
|
+
This tool converts text content (including Markdown and HTML) into professionally
|
|
106
|
+
formatted PowerPoint presentations. It automatically splits content into slides
|
|
107
|
+
based on headings and supports custom templates, styling, and layout options.
|
|
108
|
+
|
|
109
|
+
Features:
|
|
110
|
+
- Automatic slide splitting based on headings (H1, H2, H3, etc.)
|
|
111
|
+
- Markdown to PowerPoint conversion with proper formatting
|
|
112
|
+
- HTML to PowerPoint conversion support
|
|
113
|
+
- Custom PowerPoint template support
|
|
114
|
+
- Jinja2 HTML template processing
|
|
115
|
+
- Configurable slide layouts and styling
|
|
116
|
+
- Table, list, and content formatting
|
|
117
|
+
- Professional presentation generation
|
|
118
|
+
|
|
119
|
+
Slide Splitting Logic:
|
|
120
|
+
- H1 (# Title) → Title slide (layout 0)
|
|
121
|
+
- H2 (## Section) → Content slide (layout 1)
|
|
122
|
+
- H3 (### Subsection) → Content slide (layout 1)
|
|
123
|
+
- Content between headings → Added to the slide
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
name = "powerpoint_generator"
|
|
127
|
+
description = (
|
|
128
|
+
"Generate PowerPoint presentations from text, Markdown, or HTML content. "
|
|
129
|
+
"Automatically splits content into slides based on headings. "
|
|
130
|
+
"Supports custom templates, styling, and professional presentation formatting."
|
|
131
|
+
)
|
|
132
|
+
args_schema = PowerPointArgs
|
|
133
|
+
|
|
134
|
+
# Document type configuration
|
|
135
|
+
document_type = "presentation"
|
|
136
|
+
default_extension = "pptx"
|
|
137
|
+
supported_extensions = [".pptx", ".potx"]
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
templates_dir: Optional[Path] = None,
|
|
142
|
+
output_dir: Optional[Union[str, Path]] = None,
|
|
143
|
+
pptx_template_path: Optional[Path] = None,
|
|
144
|
+
default_html_template: Optional[str] = None,
|
|
145
|
+
**kwargs
|
|
146
|
+
):
|
|
147
|
+
"""
|
|
148
|
+
Initialize the PowerPoint Tool.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
templates_dir: Directory containing HTML and PowerPoint templates
|
|
152
|
+
output_dir: Directory where generated presentations will be saved
|
|
153
|
+
default_html_template: Default HTML template for content processing
|
|
154
|
+
**kwargs: Additional arguments for AbstractDocumentTool
|
|
155
|
+
"""
|
|
156
|
+
# Set up output directory before calling super().__init__
|
|
157
|
+
if output_dir:
|
|
158
|
+
kwargs['output_dir'] = Path(output_dir)
|
|
159
|
+
|
|
160
|
+
super().__init__(templates_dir=templates_dir, **kwargs)
|
|
161
|
+
|
|
162
|
+
self.default_html_template = default_html_template
|
|
163
|
+
self.pptx_template_path = pptx_template_path or BASE_DIR.joinpath('presentations')
|
|
164
|
+
|
|
165
|
+
# Initialize Jinja2 environment for HTML templates
|
|
166
|
+
if self.templates_dir:
|
|
167
|
+
self.html_env = Environment(
|
|
168
|
+
loader=FileSystemLoader(str(self.templates_dir)),
|
|
169
|
+
autoescape=True
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
self.html_env = None
|
|
173
|
+
|
|
174
|
+
def _render_html_template(self, content: str, template_name: Optional[str], template_vars: Optional[Dict[str, Any]]) -> str:
|
|
175
|
+
"""Render content through Jinja2 HTML template if provided."""
|
|
176
|
+
if not template_name or not self.html_env:
|
|
177
|
+
return content
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
template = self.html_env.get_template(template_name)
|
|
181
|
+
vars_dict = template_vars or {}
|
|
182
|
+
|
|
183
|
+
# Add default variables
|
|
184
|
+
vars_dict.setdefault('content', content)
|
|
185
|
+
vars_dict.setdefault('date', self._get_current_date())
|
|
186
|
+
vars_dict.setdefault('timestamp', self._get_current_timestamp())
|
|
187
|
+
|
|
188
|
+
rendered = template.render(**vars_dict)
|
|
189
|
+
self.logger.info(f"Rendered content through HTML template: {template_name}")
|
|
190
|
+
return rendered
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self.logger.error(f"HTML template rendering failed: {e}")
|
|
194
|
+
return content
|
|
195
|
+
|
|
196
|
+
def _preprocess_markdown(self, text: str) -> str:
|
|
197
|
+
"""Preprocess markdown to handle common issues."""
|
|
198
|
+
# Replace placeholder variables with empty strings
|
|
199
|
+
text = re.sub(r'\{[a-zA-Z0-9_]+\}', '', text)
|
|
200
|
+
|
|
201
|
+
# Handle f-strings that weren't evaluated
|
|
202
|
+
text = re.sub(r'f"""(.*?)"""', r'\1', text, flags=re.DOTALL)
|
|
203
|
+
text = re.sub(r"f'''(.*?)'''", r'\1', text, flags=re.DOTALL)
|
|
204
|
+
|
|
205
|
+
# Remove triple backticks and language indicators
|
|
206
|
+
text = re.sub(r'```[a-zA-Z]*\n', '', text)
|
|
207
|
+
text = re.sub(r'```', '', text)
|
|
208
|
+
|
|
209
|
+
# Fix heading issues (ensure space after #) - this should work correctly
|
|
210
|
+
text = re.sub(r'(#+)([^ \n])', r'\1 \2', text)
|
|
211
|
+
|
|
212
|
+
# Fix escaped newlines if any
|
|
213
|
+
text = text.replace('\\n', '\n')
|
|
214
|
+
|
|
215
|
+
# Clean up extra whitespace but preserve line structure
|
|
216
|
+
lines = text.split('\n')
|
|
217
|
+
cleaned_lines = []
|
|
218
|
+
for line in lines:
|
|
219
|
+
# Strip leading/trailing whitespace but preserve the line
|
|
220
|
+
cleaned_line = line.strip()
|
|
221
|
+
cleaned_lines.append(cleaned_line)
|
|
222
|
+
|
|
223
|
+
return '\n'.join(cleaned_lines)
|
|
224
|
+
|
|
225
|
+
def _markdown_to_html(self, markdown_text: str) -> str:
|
|
226
|
+
"""Convert markdown to HTML."""
|
|
227
|
+
try:
|
|
228
|
+
self.logger.debug(f"Converting markdown to HTML. Input preview: {markdown_text[:100]}...")
|
|
229
|
+
|
|
230
|
+
html = markdown.markdown(
|
|
231
|
+
markdown_text,
|
|
232
|
+
extensions=['extra', 'codehilite', 'tables'] # Removed 'toc' to avoid issues
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
self.logger.debug(f"HTML conversion result preview: {html[:200]}...")
|
|
236
|
+
return html
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.logger.error(f"Markdown conversion failed: {e}")
|
|
240
|
+
# Fallback: wrap in paragraphs
|
|
241
|
+
paragraphs = markdown_text.split('\n\n')
|
|
242
|
+
html_paragraphs = [f'<p>{p.replace(chr(10), "<br>")}</p>' for p in paragraphs if p.strip()]
|
|
243
|
+
fallback_html = '\n'.join(html_paragraphs)
|
|
244
|
+
self.logger.debug(f"Using fallback HTML: {fallback_html[:200]}...")
|
|
245
|
+
return fallback_html
|
|
246
|
+
|
|
247
|
+
def _extract_slides_from_html(self, html_content: str, max_slides: int) -> List[Dict[str, Any]]:
|
|
248
|
+
"""Extract slides from HTML content based on headings."""
|
|
249
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
250
|
+
slides = []
|
|
251
|
+
|
|
252
|
+
# Find all heading elements
|
|
253
|
+
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
|
254
|
+
|
|
255
|
+
if not headings:
|
|
256
|
+
# If no headings, create a single slide with all content
|
|
257
|
+
slides.append({
|
|
258
|
+
'title': 'Presentation',
|
|
259
|
+
'content': self._extract_content_elements(soup),
|
|
260
|
+
'level': 1,
|
|
261
|
+
'layout': None
|
|
262
|
+
})
|
|
263
|
+
return slides
|
|
264
|
+
|
|
265
|
+
# Get all elements in the document to process sequentially
|
|
266
|
+
all_elements = soup.find_all(
|
|
267
|
+
['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul', 'ol', 'table', 'blockquote', 'div', 'img']
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
current_slide = None
|
|
271
|
+
|
|
272
|
+
for element in all_elements:
|
|
273
|
+
if len(slides) >= max_slides:
|
|
274
|
+
self.logger.warning(
|
|
275
|
+
f"Reached maximum slides limit ({max_slides})"
|
|
276
|
+
)
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
# If this is a heading, start a new slide
|
|
280
|
+
if element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
|
|
281
|
+
if current_slide is not None and current_slide['content']:
|
|
282
|
+
slides.append(current_slide)
|
|
283
|
+
|
|
284
|
+
heading_level = int(element.name[1])
|
|
285
|
+
heading_text = self._get_text_content(element)
|
|
286
|
+
heading_text = heading_text.strip()
|
|
287
|
+
|
|
288
|
+
if not heading_text:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
current_slide = {
|
|
292
|
+
'title': heading_text,
|
|
293
|
+
'level': heading_level,
|
|
294
|
+
'content': [],
|
|
295
|
+
# 'layout': 0 if (len(slides) == 0 and heading_level == 1) else None
|
|
296
|
+
'layout': None
|
|
297
|
+
}
|
|
298
|
+
# If this is content and we have a current slide, add it
|
|
299
|
+
elif element.name in ['p', 'ul', 'ol', 'table', 'blockquote', 'div', 'img'] and current_slide is not None:
|
|
300
|
+
if element.name == 'img':
|
|
301
|
+
# Always add images
|
|
302
|
+
current_slide['content'].append(element)
|
|
303
|
+
img_alt = element.get('alt', 'Image')
|
|
304
|
+
self.logger.debug(f"Added image to slide '{current_slide['title']}': {img_alt}")
|
|
305
|
+
else:
|
|
306
|
+
# Add other content if not empty
|
|
307
|
+
content_text = self._get_text_content(element).strip()
|
|
308
|
+
if content_text:
|
|
309
|
+
current_slide['content'].append(element)
|
|
310
|
+
|
|
311
|
+
# Don't forget the last slide
|
|
312
|
+
if current_slide is not None and current_slide['content']:
|
|
313
|
+
slides.append(current_slide)
|
|
314
|
+
|
|
315
|
+
self.logger.info(
|
|
316
|
+
f"Extracted {len(slides)} slides from HTML content"
|
|
317
|
+
)
|
|
318
|
+
return slides
|
|
319
|
+
|
|
320
|
+
def _extract_content_elements(self, soup) -> List:
|
|
321
|
+
"""Extract content elements from soup."""
|
|
322
|
+
content_elements = []
|
|
323
|
+
# Get all content elements, including images
|
|
324
|
+
for element in soup.find_all(['p', 'ul', 'ol', 'table', 'blockquote', 'div', 'img']):
|
|
325
|
+
# Skip if this div only contains headings
|
|
326
|
+
if element.name == 'div':
|
|
327
|
+
child_tags = [child.name for child in element.find_all() if hasattr(child, 'name')]
|
|
328
|
+
if all(tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] for tag in child_tags):
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
# Special handling for paragraphs that contain images
|
|
332
|
+
if element.name == 'p':
|
|
333
|
+
# Check if paragraph contains images
|
|
334
|
+
images = element.find_all('img')
|
|
335
|
+
if images:
|
|
336
|
+
# Add text content if any
|
|
337
|
+
text_content = self._get_text_content(element).strip()
|
|
338
|
+
if text_content:
|
|
339
|
+
content_elements.append(element)
|
|
340
|
+
# Add images separately
|
|
341
|
+
for img in images:
|
|
342
|
+
content_elements.append(img)
|
|
343
|
+
else:
|
|
344
|
+
content_elements.append(element)
|
|
345
|
+
else:
|
|
346
|
+
content_elements.append(element)
|
|
347
|
+
|
|
348
|
+
return content_elements
|
|
349
|
+
|
|
350
|
+
def _create_presentation(self, template_path: Optional[str] = None) -> Presentation:
|
|
351
|
+
"""Create or load PowerPoint presentation."""
|
|
352
|
+
if template_path:
|
|
353
|
+
pptx_template = self._get_template_path(template_path)
|
|
354
|
+
if pptx_template and pptx_template.exists():
|
|
355
|
+
self.logger.info(f"Loading PowerPoint template: {pptx_template}")
|
|
356
|
+
return Presentation(str(pptx_template))
|
|
357
|
+
|
|
358
|
+
# Create new presentation
|
|
359
|
+
return Presentation()
|
|
360
|
+
|
|
361
|
+
def _create_slides(
|
|
362
|
+
self,
|
|
363
|
+
prs: Presentation,
|
|
364
|
+
slides_data: List[Dict[str, Any]],
|
|
365
|
+
slide_layout: int,
|
|
366
|
+
title_styles: Optional[Dict[str, Any]],
|
|
367
|
+
content_styles: Optional[Dict[str, Any]],
|
|
368
|
+
**kwargs: Any
|
|
369
|
+
) -> None:
|
|
370
|
+
"""Create slides from extracted data with dynamic layout detection."""
|
|
371
|
+
|
|
372
|
+
# Debug available layouts
|
|
373
|
+
self.logger.info(
|
|
374
|
+
f"Available slide layouts: {len(prs.slide_layouts)}"
|
|
375
|
+
)
|
|
376
|
+
for i, layout in enumerate(prs.slide_layouts):
|
|
377
|
+
self.logger.debug(
|
|
378
|
+
f"Layout {i}: {layout.name}"
|
|
379
|
+
)
|
|
380
|
+
if len(prs.slides) > 0:
|
|
381
|
+
# Remove the empty slide if it exists
|
|
382
|
+
# slide_to_remove = prs.slides[0]
|
|
383
|
+
# slide_id = slide_to_remove.slide_id
|
|
384
|
+
prs.part.drop_rel(prs.slides._sldIdLst[0].rId)
|
|
385
|
+
del prs.slides._sldIdLst[0]
|
|
386
|
+
|
|
387
|
+
for i, slide_data in enumerate(slides_data):
|
|
388
|
+
try:
|
|
389
|
+
if len(prs.slide_layouts) == 1:
|
|
390
|
+
# Template has only one layout - use it for everything
|
|
391
|
+
layout_idx = 0
|
|
392
|
+
elif len(prs.slide_layouts) >= 2:
|
|
393
|
+
# Template has multiple layouts - use layout 1 for content, 0 for title-only
|
|
394
|
+
if slide_data['content']:
|
|
395
|
+
layout_idx = 1 # Content slide
|
|
396
|
+
else:
|
|
397
|
+
layout_idx = 0 # Title-only slide
|
|
398
|
+
self.logger.debug(
|
|
399
|
+
f"Using layout {layout_idx} for slide with content: {bool(slide_data['content'])}"
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
# Fallback - shouldn't happen but just in case
|
|
403
|
+
layout_idx = 0
|
|
404
|
+
# Additional safety check
|
|
405
|
+
if layout_idx >= len(prs.slide_layouts):
|
|
406
|
+
layout_idx = 0
|
|
407
|
+
self.logger.warning(
|
|
408
|
+
f"Layout index {layout_idx} out of range, using layout 0"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Add slide
|
|
412
|
+
slide_layout_obj = prs.slide_layouts[layout_idx]
|
|
413
|
+
slide = prs.slides.add_slide(slide_layout_obj)
|
|
414
|
+
|
|
415
|
+
# FIXED: Better title cleaning
|
|
416
|
+
if slide.shapes.title and slide_data['title']:
|
|
417
|
+
# Remove markdown symbols more thoroughly
|
|
418
|
+
_title = slide_data['title']
|
|
419
|
+
_title = re.sub(r'^#+\s*', '', _title) # Remove leading # symbols
|
|
420
|
+
_title = _title.lstrip('#').strip() # Remove any remaining # symbols
|
|
421
|
+
_title = _title.strip() # Final cleanup
|
|
422
|
+
|
|
423
|
+
slide.shapes.title.text = _title
|
|
424
|
+
self.logger.debug(f"Set slide title: '{_title}'")
|
|
425
|
+
|
|
426
|
+
if title_styles:
|
|
427
|
+
self._apply_text_styles(slide.shapes.title, title_styles)
|
|
428
|
+
|
|
429
|
+
# Add content only if there is content
|
|
430
|
+
if slide_data['content']:
|
|
431
|
+
self._add_slide_content(
|
|
432
|
+
slide, slide_data['content'], content_styles, **kwargs
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
self.logger.debug(
|
|
436
|
+
f"Created slide: '{_title}' using layout {layout_idx}"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
self.logger.error(
|
|
441
|
+
f"Error creating slide '{slide_data.get('title', 'Unknown')}': {e}"
|
|
442
|
+
)
|
|
443
|
+
# Continue to next slide instead of failing completely
|
|
444
|
+
continue
|
|
445
|
+
|
|
446
|
+
def _find_content_placeholder(self, slide):
|
|
447
|
+
"""Find the appropriate content placeholder on a slide."""
|
|
448
|
+
try:
|
|
449
|
+
# Debug: Log all available placeholders
|
|
450
|
+
self.logger.debug(
|
|
451
|
+
f"Available placeholders: {len(slide.shapes.placeholders)}"
|
|
452
|
+
)
|
|
453
|
+
for i, placeholder in enumerate(slide.shapes.placeholders):
|
|
454
|
+
self.logger.debug(
|
|
455
|
+
f"Placeholder {i}: {placeholder.placeholder_format.type}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Try to find content placeholder
|
|
459
|
+
# Common content placeholder types: BODY (2), OBJECT (7), CONTENT (13)
|
|
460
|
+
content_placeholder_types = [2, 7, 13, 14, 15, 16, 17, 18]
|
|
461
|
+
|
|
462
|
+
for placeholder in slide.shapes.placeholders:
|
|
463
|
+
try:
|
|
464
|
+
if hasattr(placeholder.placeholder_format, 'type'):
|
|
465
|
+
if placeholder.placeholder_format.type in content_placeholder_types:
|
|
466
|
+
if hasattr(placeholder, 'text_frame'):
|
|
467
|
+
self.logger.debug(
|
|
468
|
+
f"Found content placeholder: {placeholder.placeholder_format.type}"
|
|
469
|
+
)
|
|
470
|
+
return placeholder
|
|
471
|
+
except Exception as e:
|
|
472
|
+
self.logger.error(
|
|
473
|
+
f"Error finding content placeholder: {e}"
|
|
474
|
+
)
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
# Fallback: Try to find any placeholder that's not the title (idx 0)
|
|
478
|
+
if len(slide.shapes.placeholders) > 1:
|
|
479
|
+
# Skip title placeholder (usually index 0) and try others
|
|
480
|
+
for i in range(1, len(slide.shapes.placeholders)):
|
|
481
|
+
placeholder = slide.shapes.placeholders[i]
|
|
482
|
+
if hasattr(placeholder, 'text_frame'):
|
|
483
|
+
self.logger.debug(f"Using placeholder at index {i} as content placeholder")
|
|
484
|
+
return placeholder
|
|
485
|
+
|
|
486
|
+
self.logger.warning(
|
|
487
|
+
"No suitable content placeholder found"
|
|
488
|
+
)
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
self.logger.error(f"Error finding content placeholder: {e}")
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
def _add_text_as_textbox(self, slide, content_elements, content_styles):
|
|
496
|
+
"""Add text content as a text box when no content placeholder is available."""
|
|
497
|
+
try:
|
|
498
|
+
# FIXED: Better positioning that allows movement
|
|
499
|
+
left = Inches(0.5)
|
|
500
|
+
top = Inches(1.8) # Start below title area
|
|
501
|
+
width = Inches(9) # Full width minus margins
|
|
502
|
+
height = Inches(5) # Reasonable height
|
|
503
|
+
|
|
504
|
+
textbox = slide.shapes.add_textbox(left, top, width, height)
|
|
505
|
+
text_frame = textbox.text_frame
|
|
506
|
+
|
|
507
|
+
# FIXED: Better text frame settings
|
|
508
|
+
text_frame.clear()
|
|
509
|
+
text_frame.margin_left = Inches(0.1)
|
|
510
|
+
text_frame.margin_right = Inches(0.1)
|
|
511
|
+
text_frame.margin_top = Inches(0.1)
|
|
512
|
+
text_frame.margin_bottom = Inches(0.1)
|
|
513
|
+
text_frame.word_wrap = True
|
|
514
|
+
|
|
515
|
+
# Add content to text box with better formatting
|
|
516
|
+
first_paragraph = True
|
|
517
|
+
for element in content_elements:
|
|
518
|
+
if element.name == 'p':
|
|
519
|
+
paragraph_text = self._get_text_content(element)
|
|
520
|
+
|
|
521
|
+
# Split by line breaks for better formatting
|
|
522
|
+
lines = [line.strip() for line in paragraph_text.split('\n') if line.strip()]
|
|
523
|
+
|
|
524
|
+
for line in lines:
|
|
525
|
+
if first_paragraph and not text_frame.paragraphs[0].text:
|
|
526
|
+
p = text_frame.paragraphs[0]
|
|
527
|
+
first_paragraph = False
|
|
528
|
+
else:
|
|
529
|
+
p = text_frame.add_paragraph()
|
|
530
|
+
|
|
531
|
+
p.text = line
|
|
532
|
+
p.space_after = Pt(6) # Add some spacing between lines
|
|
533
|
+
|
|
534
|
+
if content_styles:
|
|
535
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
536
|
+
|
|
537
|
+
elif element.name in ['ul', 'ol']:
|
|
538
|
+
for li in element.find_all('li', recursive=False):
|
|
539
|
+
if first_paragraph and not text_frame.paragraphs[0].text:
|
|
540
|
+
p = text_frame.paragraphs[0]
|
|
541
|
+
first_paragraph = False
|
|
542
|
+
else:
|
|
543
|
+
p = text_frame.add_paragraph()
|
|
544
|
+
|
|
545
|
+
p.text = f"• {self._get_text_content(li)}"
|
|
546
|
+
p.space_after = Pt(3)
|
|
547
|
+
|
|
548
|
+
if content_styles:
|
|
549
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
550
|
+
|
|
551
|
+
elif element.name == 'table':
|
|
552
|
+
table_text = self._extract_table_text(element)
|
|
553
|
+
if first_paragraph and not text_frame.paragraphs[0].text:
|
|
554
|
+
p = text_frame.paragraphs[0]
|
|
555
|
+
first_paragraph = False
|
|
556
|
+
else:
|
|
557
|
+
p = text_frame.add_paragraph()
|
|
558
|
+
|
|
559
|
+
p.text = table_text
|
|
560
|
+
p.space_after = Pt(6)
|
|
561
|
+
|
|
562
|
+
if content_styles:
|
|
563
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
564
|
+
|
|
565
|
+
elif element.name == 'blockquote':
|
|
566
|
+
if first_paragraph and not text_frame.paragraphs[0].text:
|
|
567
|
+
p = text_frame.paragraphs[0]
|
|
568
|
+
first_paragraph = False
|
|
569
|
+
else:
|
|
570
|
+
p = text_frame.add_paragraph()
|
|
571
|
+
|
|
572
|
+
p.text = f'"{self._get_text_content(element)}"'
|
|
573
|
+
p.space_after = Pt(6)
|
|
574
|
+
|
|
575
|
+
if content_styles:
|
|
576
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
577
|
+
|
|
578
|
+
self.logger.debug("Added content as movable textbox")
|
|
579
|
+
|
|
580
|
+
except Exception as e:
|
|
581
|
+
self.logger.error(f"Error adding text as textbox: {e}")
|
|
582
|
+
|
|
583
|
+
def _add_slide_content(
|
|
584
|
+
self,
|
|
585
|
+
slide: Any,
|
|
586
|
+
content_elements: List,
|
|
587
|
+
content_styles: Optional[Dict[str, Any]],
|
|
588
|
+
**kwargs
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Add content to a slide placeholder."""
|
|
591
|
+
|
|
592
|
+
enable_images = kwargs.get('enable_images', True)
|
|
593
|
+
image_width = kwargs.get('image_width')
|
|
594
|
+
image_height = kwargs.get('image_height')
|
|
595
|
+
max_image_size = kwargs.get('max_image_size', 6.0)
|
|
596
|
+
|
|
597
|
+
# Separate images from other content
|
|
598
|
+
images = [elem for elem in content_elements if elem.name == 'img']
|
|
599
|
+
other_content = [elem for elem in content_elements if elem.name != 'img']
|
|
600
|
+
|
|
601
|
+
# Handle text content first
|
|
602
|
+
if other_content:
|
|
603
|
+
content_placeholder = self._find_content_placeholder(slide)
|
|
604
|
+
if content_placeholder:
|
|
605
|
+
try:
|
|
606
|
+
text_frame = content_placeholder.text_frame
|
|
607
|
+
text_frame.clear()
|
|
608
|
+
|
|
609
|
+
for element in other_content:
|
|
610
|
+
if element.name == 'p':
|
|
611
|
+
if len(text_frame.paragraphs) == 1 and not text_frame.paragraphs[0].text:
|
|
612
|
+
p = text_frame.paragraphs[0]
|
|
613
|
+
else:
|
|
614
|
+
p = text_frame.add_paragraph()
|
|
615
|
+
p.text = self._get_text_content(element)
|
|
616
|
+
if content_styles:
|
|
617
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
618
|
+
|
|
619
|
+
elif element.name in ['ul', 'ol']:
|
|
620
|
+
for li in element.find_all('li', recursive=False):
|
|
621
|
+
p = text_frame.add_paragraph()
|
|
622
|
+
p.text = self._get_text_content(li)
|
|
623
|
+
p.level = 1
|
|
624
|
+
if content_styles:
|
|
625
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
626
|
+
|
|
627
|
+
elif element.name == 'table':
|
|
628
|
+
table_text = self._extract_table_text(element)
|
|
629
|
+
p = text_frame.add_paragraph()
|
|
630
|
+
p.text = table_text
|
|
631
|
+
if content_styles:
|
|
632
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
633
|
+
|
|
634
|
+
elif element.name == 'blockquote':
|
|
635
|
+
p = text_frame.add_paragraph()
|
|
636
|
+
p.text = f'"{self._get_text_content(element)}"'
|
|
637
|
+
if content_styles:
|
|
638
|
+
self._apply_paragraph_styles(p, content_styles)
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
self.logger.error(f"Error adding text content to placeholder: {e}")
|
|
642
|
+
# Fallback: Add text as a text box
|
|
643
|
+
self._add_text_as_textbox(slide, other_content, content_styles)
|
|
644
|
+
else:
|
|
645
|
+
# No content placeholder found, add text as a text box
|
|
646
|
+
self.logger.info("No content placeholder found, adding text as textbox")
|
|
647
|
+
self._add_text_as_textbox(slide, other_content, content_styles)
|
|
648
|
+
|
|
649
|
+
# Handle images
|
|
650
|
+
if images and enable_images:
|
|
651
|
+
self._add_images_to_slide(
|
|
652
|
+
slide,
|
|
653
|
+
images,
|
|
654
|
+
image_width,
|
|
655
|
+
image_height,
|
|
656
|
+
max_image_size
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def _download_image(self, img_url: str, timeout: int = 30) -> Optional[bytes]:
|
|
660
|
+
"""Download image from URL."""
|
|
661
|
+
try:
|
|
662
|
+
self.logger.debug(f"Downloading image: {img_url}")
|
|
663
|
+
|
|
664
|
+
headers = {
|
|
665
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
response = requests.get(img_url, headers=headers, timeout=timeout, stream=True)
|
|
669
|
+
response.raise_for_status()
|
|
670
|
+
|
|
671
|
+
# Check content type
|
|
672
|
+
content_type = response.headers.get('content-type', '').lower()
|
|
673
|
+
if not any(img_type in content_type for img_type in ['image/', 'jpeg', 'jpg', 'png', 'gif', 'bmp']):
|
|
674
|
+
self.logger.warning(f"URL does not appear to be an image: {content_type}")
|
|
675
|
+
|
|
676
|
+
# Read image data
|
|
677
|
+
image_data = response.content
|
|
678
|
+
|
|
679
|
+
if len(image_data) == 0:
|
|
680
|
+
self.logger.error(f"Downloaded image is empty: {img_url}")
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
self.logger.debug(f"Successfully downloaded image: {len(image_data)} bytes")
|
|
684
|
+
return image_data
|
|
685
|
+
|
|
686
|
+
except requests.exceptions.RequestException as e:
|
|
687
|
+
self.logger.error(f"Failed to download image {img_url}: {e}")
|
|
688
|
+
return None
|
|
689
|
+
except Exception as e:
|
|
690
|
+
self.logger.error(f"Unexpected error downloading image {img_url}: {e}")
|
|
691
|
+
return None
|
|
692
|
+
|
|
693
|
+
def _calculate_image_position(
|
|
694
|
+
self,
|
|
695
|
+
image_index: int,
|
|
696
|
+
total_images: int,
|
|
697
|
+
image_width: Optional[float],
|
|
698
|
+
image_height: Optional[float],
|
|
699
|
+
max_image_size: float
|
|
700
|
+
) -> tuple:
|
|
701
|
+
"""Calculate position and size for image on slide."""
|
|
702
|
+
|
|
703
|
+
# Default dimensions
|
|
704
|
+
slide_width = Inches(10) # Standard slide width
|
|
705
|
+
slide_height = Inches(7.5) # Standard slide height
|
|
706
|
+
|
|
707
|
+
# Calculate default size
|
|
708
|
+
if image_width and image_height:
|
|
709
|
+
width = Inches(image_width)
|
|
710
|
+
height = Inches(image_height)
|
|
711
|
+
else:
|
|
712
|
+
# Auto-size with max constraints
|
|
713
|
+
max_width = Inches(max_image_size)
|
|
714
|
+
max_height = Inches(max_image_size * 0.75) # Maintain aspect ratio
|
|
715
|
+
|
|
716
|
+
# For multiple images, make them smaller
|
|
717
|
+
if total_images > 1:
|
|
718
|
+
max_width = Inches(max_image_size * 0.7)
|
|
719
|
+
max_height = Inches(max_image_size * 0.5)
|
|
720
|
+
|
|
721
|
+
width = max_width
|
|
722
|
+
height = max_height
|
|
723
|
+
|
|
724
|
+
# Calculate position
|
|
725
|
+
if total_images == 1:
|
|
726
|
+
# Center single image
|
|
727
|
+
left = (slide_width - width) / 2
|
|
728
|
+
top = Inches(2) # Below title
|
|
729
|
+
else:
|
|
730
|
+
# Arrange multiple images
|
|
731
|
+
images_per_row = 2 if total_images <= 4 else 3
|
|
732
|
+
row = image_index // images_per_row
|
|
733
|
+
col = image_index % images_per_row
|
|
734
|
+
|
|
735
|
+
margin = Inches(0.5)
|
|
736
|
+
available_width = slide_width - (2 * margin)
|
|
737
|
+
available_height = slide_height - Inches(3) # Account for title
|
|
738
|
+
|
|
739
|
+
img_width = (available_width - (images_per_row - 1) * Inches(0.2)) / images_per_row
|
|
740
|
+
img_height = min(height, available_height / ((total_images - 1) // images_per_row + 1))
|
|
741
|
+
|
|
742
|
+
left = margin + col * (img_width + Inches(0.2))
|
|
743
|
+
top = Inches(2) + row * (img_height + Inches(0.2))
|
|
744
|
+
|
|
745
|
+
width = img_width
|
|
746
|
+
height = img_height
|
|
747
|
+
|
|
748
|
+
return left, top, width, height
|
|
749
|
+
|
|
750
|
+
def _add_images_to_slide(
|
|
751
|
+
self,
|
|
752
|
+
slide,
|
|
753
|
+
images: List,
|
|
754
|
+
image_width: Optional[float],
|
|
755
|
+
image_height: Optional[float],
|
|
756
|
+
max_image_size: float
|
|
757
|
+
) -> None:
|
|
758
|
+
"""Add images to a PowerPoint slide."""
|
|
759
|
+
|
|
760
|
+
for i, img_element in enumerate(images):
|
|
761
|
+
try:
|
|
762
|
+
img_src = img_element.get('src')
|
|
763
|
+
img_alt = img_element.get('alt', f'Image {i+1}')
|
|
764
|
+
|
|
765
|
+
if not img_src:
|
|
766
|
+
self.logger.warning(f"Image has no src attribute: {img_alt}")
|
|
767
|
+
continue
|
|
768
|
+
|
|
769
|
+
# Download image
|
|
770
|
+
image_data = self._download_image(img_src)
|
|
771
|
+
if not image_data:
|
|
772
|
+
self.logger.error(f"Failed to download image: {img_src}")
|
|
773
|
+
continue
|
|
774
|
+
|
|
775
|
+
# Calculate position and size
|
|
776
|
+
left, top, width, height = self._calculate_image_position(
|
|
777
|
+
i, len(images), image_width, image_height, max_image_size
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Add image to slide
|
|
781
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file:
|
|
782
|
+
tmp_file.write(image_data)
|
|
783
|
+
tmp_file_path = tmp_file.name
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
picture = slide.shapes.add_picture(tmp_file_path, left, top, width, height)
|
|
787
|
+
self.logger.debug(f"Added image to slide: {img_alt}")
|
|
788
|
+
finally:
|
|
789
|
+
# Clean up temporary file
|
|
790
|
+
Path(tmp_file_path).unlink(missing_ok=True)
|
|
791
|
+
|
|
792
|
+
except Exception as e:
|
|
793
|
+
self.logger.error(f"Error adding image '{img_alt}': {e}")
|
|
794
|
+
|
|
795
|
+
def _extract_table_text(self, table_element) -> str:
|
|
796
|
+
"""Extract text from table element."""
|
|
797
|
+
rows = table_element.find_all('tr')
|
|
798
|
+
table_lines = []
|
|
799
|
+
|
|
800
|
+
for row in rows:
|
|
801
|
+
cells = row.find_all(['td', 'th'])
|
|
802
|
+
row_text = ' | '.join([self._get_text_content(cell) for cell in cells])
|
|
803
|
+
table_lines.append(row_text)
|
|
804
|
+
|
|
805
|
+
return '\n'.join(table_lines)
|
|
806
|
+
|
|
807
|
+
def _get_text_content(self, element) -> str:
|
|
808
|
+
"""Extract clean text content from HTML element with preserved line breaks."""
|
|
809
|
+
if isinstance(element, NavigableString):
|
|
810
|
+
return str(element).strip()
|
|
811
|
+
|
|
812
|
+
# For HTML elements, get the text content with line break preservation
|
|
813
|
+
if hasattr(element, 'get_text'):
|
|
814
|
+
# Use separator to preserve line breaks between elements
|
|
815
|
+
text = element.get_text(separator='\n', strip=True)
|
|
816
|
+
|
|
817
|
+
# Clean up excessive newlines but preserve intentional line breaks
|
|
818
|
+
# Replace multiple consecutive newlines with single newlines
|
|
819
|
+
text = re.sub(r'\n{3,}', '\n\n', text)
|
|
820
|
+
|
|
821
|
+
# Handle specific markdown formatting that should have line breaks
|
|
822
|
+
# Convert patterns like "**Key:** value" to have proper line breaks
|
|
823
|
+
text = re.sub(r'(\*\*[^*]+\*\*[^*\n]*?)(\*\*[^*]+\*\*)', r'\1\n\2', text)
|
|
824
|
+
|
|
825
|
+
return text
|
|
826
|
+
|
|
827
|
+
# Fallback method for manual text extraction
|
|
828
|
+
text_parts = []
|
|
829
|
+
for content in element.contents:
|
|
830
|
+
if isinstance(content, NavigableString):
|
|
831
|
+
text_parts.append(str(content).strip())
|
|
832
|
+
else:
|
|
833
|
+
text_parts.append(self._get_text_content(content))
|
|
834
|
+
|
|
835
|
+
result = '\n'.join([part for part in text_parts if part.strip()])
|
|
836
|
+
|
|
837
|
+
# Additional cleanup: remove any remaining markdown symbols
|
|
838
|
+
result = re.sub(r'^#+\s*', '', result) # Remove leading hashtags
|
|
839
|
+
result = re.sub(r'\*\*([^*]+)\*\*', r'\1', result) # Remove bold markers
|
|
840
|
+
result = re.sub(r'\*([^*]+)\*', r'\1', result) # Remove italic markers
|
|
841
|
+
|
|
842
|
+
return result
|
|
843
|
+
|
|
844
|
+
def _apply_text_styles(self, shape, styles: Dict[str, Any]) -> None:
|
|
845
|
+
"""Apply styles to a text shape."""
|
|
846
|
+
if not shape.has_text_frame:
|
|
847
|
+
return
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
text_frame = shape.text_frame
|
|
851
|
+
for paragraph in text_frame.paragraphs:
|
|
852
|
+
self._apply_paragraph_styles(paragraph, styles)
|
|
853
|
+
except Exception as e:
|
|
854
|
+
self.logger.error(f"Error applying text styles: {e}")
|
|
855
|
+
|
|
856
|
+
def _apply_paragraph_styles(self, paragraph, styles: Dict[str, Any]) -> None:
|
|
857
|
+
"""Apply styles to a paragraph."""
|
|
858
|
+
try:
|
|
859
|
+
# Font styling
|
|
860
|
+
if 'font_name' in styles:
|
|
861
|
+
paragraph.font.name = styles['font_name']
|
|
862
|
+
if 'font_size' in styles:
|
|
863
|
+
paragraph.font.size = Pt(styles['font_size'])
|
|
864
|
+
if 'bold' in styles:
|
|
865
|
+
paragraph.font.bold = styles['bold']
|
|
866
|
+
if 'italic' in styles:
|
|
867
|
+
paragraph.font.italic = styles['italic']
|
|
868
|
+
if 'font_color' in styles:
|
|
869
|
+
# Convert hex color to RGB
|
|
870
|
+
color_hex = styles['font_color'].lstrip('#')
|
|
871
|
+
r, g, b = tuple(int(color_hex[i:i+2], 16) for i in (0, 2, 4))
|
|
872
|
+
paragraph.font.color.rgb = RGBColor(r, g, b)
|
|
873
|
+
|
|
874
|
+
# Alignment
|
|
875
|
+
if 'alignment' in styles:
|
|
876
|
+
alignment_map = {
|
|
877
|
+
'left': PP_ALIGN.LEFT,
|
|
878
|
+
'center': PP_ALIGN.CENTER,
|
|
879
|
+
'right': PP_ALIGN.RIGHT,
|
|
880
|
+
'justify': PP_ALIGN.JUSTIFY
|
|
881
|
+
}
|
|
882
|
+
paragraph.alignment = alignment_map.get(styles['alignment'], PP_ALIGN.LEFT)
|
|
883
|
+
|
|
884
|
+
except Exception as e:
|
|
885
|
+
self.logger.error(f"Error applying paragraph styles: {e}")
|
|
886
|
+
|
|
887
|
+
def debug_content_parsing(self, content: str) -> Dict[str, Any]:
|
|
888
|
+
"""
|
|
889
|
+
Debug method to see how content is being parsed.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
content: Input content to debug
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
Dictionary with debug information
|
|
896
|
+
"""
|
|
897
|
+
try:
|
|
898
|
+
# Process the content the same way as in generation
|
|
899
|
+
processed_content = self._preprocess_markdown(content)
|
|
900
|
+
html_content = self._markdown_to_html(processed_content)
|
|
901
|
+
|
|
902
|
+
# Parse with BeautifulSoup
|
|
903
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
904
|
+
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
|
905
|
+
|
|
906
|
+
# Extract slide information
|
|
907
|
+
slides_data = self._extract_slides_from_html(html_content, 50)
|
|
908
|
+
|
|
909
|
+
debug_info = {
|
|
910
|
+
"original_content_length": len(content),
|
|
911
|
+
"original_content_preview": content[:300] + "..." if len(content) > 300 else content,
|
|
912
|
+
"processed_content_preview": processed_content[:300] + "..." if len(processed_content) > 300 else processed_content,
|
|
913
|
+
"html_content": html_content, # Show full HTML to debug
|
|
914
|
+
"headings_found": [
|
|
915
|
+
{
|
|
916
|
+
"tag": h.name,
|
|
917
|
+
"level": int(h.name[1]),
|
|
918
|
+
"raw_html": str(h),
|
|
919
|
+
"extracted_text": self._get_text_content(h),
|
|
920
|
+
"inner_text": h.get_text() if hasattr(h, 'get_text') else "N/A"
|
|
921
|
+
} for h in headings
|
|
922
|
+
],
|
|
923
|
+
"slides_extracted": [
|
|
924
|
+
{
|
|
925
|
+
"title": slide['title'],
|
|
926
|
+
"level": slide['level'],
|
|
927
|
+
"layout": slide['layout'],
|
|
928
|
+
"content_count": len(slide['content']),
|
|
929
|
+
"content_preview": [
|
|
930
|
+
{
|
|
931
|
+
"tag": elem.name,
|
|
932
|
+
"text": self._get_text_content(elem)[:100]
|
|
933
|
+
} for elem in slide['content'][:3]
|
|
934
|
+
]
|
|
935
|
+
} for slide in slides_data
|
|
936
|
+
],
|
|
937
|
+
"total_slides": len(slides_data)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return debug_info
|
|
941
|
+
|
|
942
|
+
except Exception as e:
|
|
943
|
+
return {
|
|
944
|
+
"error": str(e),
|
|
945
|
+
"traceback": traceback.format_exc(),
|
|
946
|
+
"message": "Debug parsing failed"
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
async def _generate_document_content(self, content: str, **kwargs) -> bytes:
|
|
950
|
+
"""
|
|
951
|
+
Generate PowerPoint presentation content from input.
|
|
952
|
+
|
|
953
|
+
Args:
|
|
954
|
+
content: Input content (text, markdown, or HTML)
|
|
955
|
+
**kwargs: Additional arguments from PowerPointArgs
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
PowerPoint presentation as bytes
|
|
959
|
+
"""
|
|
960
|
+
try:
|
|
961
|
+
# Extract arguments
|
|
962
|
+
template_name = kwargs.get('template_name')
|
|
963
|
+
template_vars = kwargs.get('template_vars')
|
|
964
|
+
pptx_template = kwargs.get('pptx_template')
|
|
965
|
+
slide_layout = kwargs.pop('slide_layout', 1)
|
|
966
|
+
title_styles = kwargs.pop('title_styles', None)
|
|
967
|
+
content_styles = kwargs.pop('content_styles', None)
|
|
968
|
+
max_slides = kwargs.pop('max_slides', 50)
|
|
969
|
+
split_by_headings = kwargs.pop('split_by_headings', True)
|
|
970
|
+
|
|
971
|
+
# Process content through HTML template if provided
|
|
972
|
+
processed_content = self._render_html_template(content, template_name, template_vars)
|
|
973
|
+
|
|
974
|
+
if pptx_template:
|
|
975
|
+
pptx_template = self.pptx_template_path.joinpath(pptx_template)
|
|
976
|
+
|
|
977
|
+
# Preprocess markdown
|
|
978
|
+
cleaned_content = self._preprocess_markdown(processed_content)
|
|
979
|
+
|
|
980
|
+
# Convert to HTML
|
|
981
|
+
html_content = self._markdown_to_html(cleaned_content)
|
|
982
|
+
|
|
983
|
+
# Extract slides from HTML
|
|
984
|
+
if split_by_headings:
|
|
985
|
+
slides_data = self._extract_slides_from_html(
|
|
986
|
+
html_content,
|
|
987
|
+
max_slides
|
|
988
|
+
)
|
|
989
|
+
else:
|
|
990
|
+
# Create single slide with all content
|
|
991
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
992
|
+
slides_data = [{
|
|
993
|
+
'title': 'Presentation',
|
|
994
|
+
'content': self._extract_content_elements(soup),
|
|
995
|
+
'level': 1,
|
|
996
|
+
'layout': 0
|
|
997
|
+
}]
|
|
998
|
+
|
|
999
|
+
self.logger.info(
|
|
1000
|
+
f"Generated {len(slides_data)} slides from content"
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
# Create PowerPoint presentation
|
|
1004
|
+
prs = self._create_presentation(
|
|
1005
|
+
pptx_template
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
# Create slides
|
|
1009
|
+
create_slides_kwargs = {
|
|
1010
|
+
k: v for k, v in kwargs.items()
|
|
1011
|
+
if k not in [
|
|
1012
|
+
'template_name', 'template_vars', 'pptx_template',
|
|
1013
|
+
'slide_layout', 'title_styles', 'content_styles',
|
|
1014
|
+
'max_slides', 'split_by_headings'
|
|
1015
|
+
]
|
|
1016
|
+
}
|
|
1017
|
+
self._create_slides(
|
|
1018
|
+
prs=prs,
|
|
1019
|
+
slides_data=slides_data,
|
|
1020
|
+
slide_layout=slide_layout,
|
|
1021
|
+
title_styles=title_styles,
|
|
1022
|
+
content_styles=content_styles,
|
|
1023
|
+
**create_slides_kwargs
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
# Save presentation to bytes
|
|
1027
|
+
ppt_bytes = io.BytesIO()
|
|
1028
|
+
prs.save(ppt_bytes)
|
|
1029
|
+
ppt_bytes.seek(0)
|
|
1030
|
+
|
|
1031
|
+
return ppt_bytes.getvalue()
|
|
1032
|
+
|
|
1033
|
+
except Exception as e:
|
|
1034
|
+
self.logger.error(f"Error generating PowerPoint presentation: {e}")
|
|
1035
|
+
raise
|
|
1036
|
+
|
|
1037
|
+
async def _execute(
|
|
1038
|
+
self,
|
|
1039
|
+
content: str,
|
|
1040
|
+
output_filename: Optional[str] = None,
|
|
1041
|
+
file_prefix: str = "presentation",
|
|
1042
|
+
output_dir: Optional[str] = None,
|
|
1043
|
+
overwrite_existing: bool = False,
|
|
1044
|
+
template_name: Optional[str] = None,
|
|
1045
|
+
template_vars: Optional[Dict[str, Any]] = None,
|
|
1046
|
+
pptx_template: Optional[str] = None,
|
|
1047
|
+
slide_layout: int = 1,
|
|
1048
|
+
title_styles: Optional[Dict[str, Any]] = None,
|
|
1049
|
+
content_styles: Optional[Dict[str, Any]] = None,
|
|
1050
|
+
max_slides: int = 50,
|
|
1051
|
+
split_by_headings: bool = True,
|
|
1052
|
+
**kwargs
|
|
1053
|
+
) -> Dict[str, Any]:
|
|
1054
|
+
"""
|
|
1055
|
+
Execute PowerPoint presentation generation (AbstractTool interface).
|
|
1056
|
+
|
|
1057
|
+
Args:
|
|
1058
|
+
content: Content to convert to PowerPoint presentation
|
|
1059
|
+
output_filename: Custom filename (without extension)
|
|
1060
|
+
file_prefix: Prefix for auto-generated filenames
|
|
1061
|
+
output_dir: Custom output directory
|
|
1062
|
+
overwrite_existing: Whether to overwrite existing files
|
|
1063
|
+
template_name: HTML template name for content processing
|
|
1064
|
+
template_vars: Variables for HTML template
|
|
1065
|
+
pptx_template: PowerPoint template file path
|
|
1066
|
+
slide_layout: Default slide layout index
|
|
1067
|
+
title_styles: Styles for slide titles
|
|
1068
|
+
content_styles: Styles for slide content
|
|
1069
|
+
max_slides: Maximum number of slides to generate
|
|
1070
|
+
split_by_headings: Whether to split by headings
|
|
1071
|
+
**kwargs: Additional arguments
|
|
1072
|
+
|
|
1073
|
+
Returns:
|
|
1074
|
+
Dictionary with presentation generation results
|
|
1075
|
+
"""
|
|
1076
|
+
try:
|
|
1077
|
+
self.logger.info(f"Starting PowerPoint generation with {len(content)} characters of content")
|
|
1078
|
+
|
|
1079
|
+
# Use the safe document creation workflow
|
|
1080
|
+
result = await self._create_document_safely(
|
|
1081
|
+
content=content,
|
|
1082
|
+
output_filename=output_filename,
|
|
1083
|
+
file_prefix=file_prefix,
|
|
1084
|
+
output_dir=output_dir,
|
|
1085
|
+
overwrite_existing=overwrite_existing,
|
|
1086
|
+
extension="pptx",
|
|
1087
|
+
template_name=template_name,
|
|
1088
|
+
template_vars=template_vars,
|
|
1089
|
+
pptx_template=pptx_template,
|
|
1090
|
+
slide_layout=slide_layout,
|
|
1091
|
+
title_styles=title_styles,
|
|
1092
|
+
content_styles=content_styles,
|
|
1093
|
+
max_slides=max_slides,
|
|
1094
|
+
split_by_headings=split_by_headings
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
if result['status'] == 'success':
|
|
1098
|
+
# Add presentation-specific metadata
|
|
1099
|
+
result['presentation_info'] = {
|
|
1100
|
+
'max_slides_limit': max_slides,
|
|
1101
|
+
'split_by_headings': split_by_headings,
|
|
1102
|
+
'slide_layout_used': slide_layout
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
self.logger.debug(
|
|
1106
|
+
f"PowerPoint presentation created successfully: {result['metadata']['filename']}"
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
return result
|
|
1110
|
+
|
|
1111
|
+
except Exception as e:
|
|
1112
|
+
self.logger.error(f"Error in PowerPoint generation: {e}")
|
|
1113
|
+
raise
|