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,992 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Office365 Mails Tools.
|
|
3
|
+
|
|
4
|
+
Specific tools for interacting with Office365 services:
|
|
5
|
+
- CreateDraftMessage: Create email drafts
|
|
6
|
+
- SearchEmail: Search through emails
|
|
7
|
+
- SendEmail: Send emails directly
|
|
8
|
+
"""
|
|
9
|
+
from typing import Dict, Any, Optional, List, Type
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import base64
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
import aiofiles
|
|
15
|
+
from msgraph.generated.models.message import Message
|
|
16
|
+
from msgraph.generated.models.recipient import Recipient
|
|
17
|
+
from msgraph.generated.models.email_address import EmailAddress
|
|
18
|
+
from msgraph.generated.models.item_body import ItemBody
|
|
19
|
+
from msgraph.generated.models.body_type import BodyType
|
|
20
|
+
from msgraph.generated.models.importance import Importance
|
|
21
|
+
from msgraph.generated.users.item.mail_folders.item.messages.messages_request_builder import (
|
|
22
|
+
MessagesRequestBuilder
|
|
23
|
+
)
|
|
24
|
+
from msgraph.generated.users.item.send_mail.send_mail_post_request_body import (
|
|
25
|
+
SendMailPostRequestBody
|
|
26
|
+
)
|
|
27
|
+
from kiota_abstractions.base_request_configuration import RequestConfiguration
|
|
28
|
+
|
|
29
|
+
from .base import O365Tool, O365ToolArgsSchema, O365Client
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ============================================================================
|
|
33
|
+
# CREATE DRAFT MESSAGE TOOL
|
|
34
|
+
# ============================================================================
|
|
35
|
+
|
|
36
|
+
class CreateDraftMessageArgs(O365ToolArgsSchema):
|
|
37
|
+
"""Arguments for creating a draft email message."""
|
|
38
|
+
subject: str = Field(
|
|
39
|
+
description="Email subject line"
|
|
40
|
+
)
|
|
41
|
+
body: str = Field(
|
|
42
|
+
description="Email body content (can be HTML or plain text)"
|
|
43
|
+
)
|
|
44
|
+
to_recipients: List[str] = Field(
|
|
45
|
+
description="List of recipient email addresses"
|
|
46
|
+
)
|
|
47
|
+
cc_recipients: Optional[List[str]] = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
description="List of CC recipient email addresses"
|
|
50
|
+
)
|
|
51
|
+
bcc_recipients: Optional[List[str]] = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description="List of BCC recipient email addresses"
|
|
54
|
+
)
|
|
55
|
+
importance: Optional[str] = Field(
|
|
56
|
+
default="normal",
|
|
57
|
+
description="Email importance: 'low', 'normal', or 'high'"
|
|
58
|
+
)
|
|
59
|
+
is_html: bool = Field(
|
|
60
|
+
default=False,
|
|
61
|
+
description="Whether the body is HTML (True) or plain text (False)"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
class CreateDraftMessageTool(O365Tool):
|
|
65
|
+
"""
|
|
66
|
+
Tool for creating draft email messages in Office365.
|
|
67
|
+
|
|
68
|
+
This tool creates a draft email message that can be reviewed and sent later.
|
|
69
|
+
The draft is saved in the user's Drafts folder.
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
# Create a simple draft
|
|
73
|
+
result = await tool.run(
|
|
74
|
+
subject="Project Update",
|
|
75
|
+
body="Here's the latest update on the project...",
|
|
76
|
+
to_recipients=["colleague@company.com"]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Create an HTML draft with CC
|
|
80
|
+
result = await tool.run(
|
|
81
|
+
subject="Monthly Report",
|
|
82
|
+
body="<h1>Report</h1><p>Details here...</p>",
|
|
83
|
+
to_recipients=["boss@company.com"],
|
|
84
|
+
cc_recipients=["team@company.com"],
|
|
85
|
+
importance="high",
|
|
86
|
+
is_html=True
|
|
87
|
+
)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
name: str = "create_draft_message"
|
|
91
|
+
description: str = (
|
|
92
|
+
"Create a draft email message in Office365. "
|
|
93
|
+
"The draft is saved in the Drafts folder and can be sent later."
|
|
94
|
+
)
|
|
95
|
+
args_schema: Type[BaseModel] = CreateDraftMessageArgs
|
|
96
|
+
|
|
97
|
+
async def _execute_graph_operation(
|
|
98
|
+
self,
|
|
99
|
+
client: O365Client,
|
|
100
|
+
**kwargs
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Create a draft email using Microsoft Graph API.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
client: Authenticated O365Client
|
|
107
|
+
**kwargs: Draft parameters
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dict with draft details
|
|
111
|
+
"""
|
|
112
|
+
# Extract parameters
|
|
113
|
+
subject = kwargs.get('subject')
|
|
114
|
+
body_content = kwargs.get('body')
|
|
115
|
+
to_recipients = kwargs.get('to_recipients', [])
|
|
116
|
+
cc_recipients = kwargs.get('cc_recipients')
|
|
117
|
+
bcc_recipients = kwargs.get('bcc_recipients')
|
|
118
|
+
importance_str = kwargs.get('importance', 'normal')
|
|
119
|
+
is_html = kwargs.get('is_html', False)
|
|
120
|
+
user_id = kwargs.get('user_id')
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Get user context
|
|
124
|
+
mailbox = client.get_user_context(user_id=user_id)
|
|
125
|
+
|
|
126
|
+
# Build message object
|
|
127
|
+
message = Message()
|
|
128
|
+
message.subject = subject
|
|
129
|
+
|
|
130
|
+
# Set body
|
|
131
|
+
message.body = ItemBody()
|
|
132
|
+
message.body.content = body_content
|
|
133
|
+
message.body.content_type = BodyType.Html if is_html else BodyType.Text
|
|
134
|
+
|
|
135
|
+
# Helper function to create recipient
|
|
136
|
+
def create_recipient(email: str) -> Recipient:
|
|
137
|
+
recipient = Recipient()
|
|
138
|
+
recipient.email_address = EmailAddress()
|
|
139
|
+
recipient.email_address.address = email
|
|
140
|
+
return recipient
|
|
141
|
+
|
|
142
|
+
# Set recipients
|
|
143
|
+
message.to_recipients = [create_recipient(email) for email in to_recipients]
|
|
144
|
+
|
|
145
|
+
if cc_recipients:
|
|
146
|
+
message.cc_recipients = [create_recipient(email) for email in cc_recipients]
|
|
147
|
+
|
|
148
|
+
if bcc_recipients:
|
|
149
|
+
message.bcc_recipients = [create_recipient(email) for email in bcc_recipients]
|
|
150
|
+
|
|
151
|
+
# Set importance
|
|
152
|
+
importance_map = {
|
|
153
|
+
'low': Importance.Low,
|
|
154
|
+
'normal': Importance.Normal,
|
|
155
|
+
'high': Importance.High
|
|
156
|
+
}
|
|
157
|
+
message.importance = importance_map.get(importance_str.lower(), Importance.Normal)
|
|
158
|
+
|
|
159
|
+
# Create the draft
|
|
160
|
+
self.logger.info(f"Creating draft message: {subject}")
|
|
161
|
+
draft = await mailbox.messages.post(message)
|
|
162
|
+
|
|
163
|
+
self.logger.info(f"Created draft with ID: {draft.id}")
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"status": "created",
|
|
167
|
+
"id": draft.id,
|
|
168
|
+
"subject": draft.subject,
|
|
169
|
+
"to_recipients": to_recipients,
|
|
170
|
+
"cc_recipients": cc_recipients or [],
|
|
171
|
+
"bcc_recipients": bcc_recipients or [],
|
|
172
|
+
"importance": importance_str,
|
|
173
|
+
"is_html": is_html,
|
|
174
|
+
"web_link": draft.web_link,
|
|
175
|
+
"created_datetime": draft.created_date_time.isoformat() if draft.created_date_time else None
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
self.logger.error(f"Failed to create draft message: {e}", exc_info=True)
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
# ============================================================================
|
|
183
|
+
# SEARCH EMAIL TOOL
|
|
184
|
+
# ============================================================================
|
|
185
|
+
|
|
186
|
+
class SearchEmailArgs(O365ToolArgsSchema):
|
|
187
|
+
"""Arguments for searching emails."""
|
|
188
|
+
query: str = Field(
|
|
189
|
+
description="Search query string (supports keywords, from:, to:, subject:, etc.)"
|
|
190
|
+
)
|
|
191
|
+
folder: Optional[str] = Field(
|
|
192
|
+
default="inbox",
|
|
193
|
+
description="Folder to search in: 'inbox', 'sentitems', 'drafts', 'deleteditems', or folder ID"
|
|
194
|
+
)
|
|
195
|
+
max_results: int = Field(
|
|
196
|
+
default=10,
|
|
197
|
+
description="Maximum number of results to return (1-50)"
|
|
198
|
+
)
|
|
199
|
+
include_attachments: bool = Field(
|
|
200
|
+
default=False,
|
|
201
|
+
description="Whether to include attachment information in results"
|
|
202
|
+
)
|
|
203
|
+
order_by: str = Field(
|
|
204
|
+
default="receivedDateTime desc",
|
|
205
|
+
description=(
|
|
206
|
+
"Sort order (e.g., 'receivedDateTime desc', 'subject asc'). "
|
|
207
|
+
"Note: When using search queries, sorting is done client-side after retrieval."
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
class SearchEmailTool(O365Tool):
|
|
212
|
+
"""
|
|
213
|
+
Tool for searching emails in Office365.
|
|
214
|
+
|
|
215
|
+
This tool searches through emails with support for:
|
|
216
|
+
- Advanced search queries
|
|
217
|
+
- Folder-specific searches
|
|
218
|
+
- Sorting and limiting results
|
|
219
|
+
- Attachment information
|
|
220
|
+
|
|
221
|
+
Search query examples:
|
|
222
|
+
- "project update" - Keywords in subject or body
|
|
223
|
+
- "from:john@company.com" - Emails from specific sender
|
|
224
|
+
- "subject:invoice" - Search in subject only
|
|
225
|
+
- "hasAttachments:true" - Only emails with attachments
|
|
226
|
+
- "received>=2025-01-01" - Emails received after date
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
# Search for recent emails
|
|
230
|
+
result = await tool.run(
|
|
231
|
+
query="project deadline",
|
|
232
|
+
max_results=5
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Search sent items
|
|
236
|
+
result = await tool.run(
|
|
237
|
+
query="from:me to:client@company.com",
|
|
238
|
+
folder="sentitems",
|
|
239
|
+
max_results=10
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Search with attachments
|
|
243
|
+
result = await tool.run(
|
|
244
|
+
query="invoice hasAttachments:true",
|
|
245
|
+
include_attachments=True
|
|
246
|
+
)
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
name: str = "search_email"
|
|
250
|
+
description: str = (
|
|
251
|
+
"Search through emails in Office365. "
|
|
252
|
+
"Supports advanced queries, folder filtering, and sorting."
|
|
253
|
+
)
|
|
254
|
+
args_schema: Type[BaseModel] = SearchEmailArgs
|
|
255
|
+
|
|
256
|
+
async def _execute_graph_operation(
|
|
257
|
+
self,
|
|
258
|
+
client: O365Client,
|
|
259
|
+
**kwargs
|
|
260
|
+
) -> Dict[str, Any]:
|
|
261
|
+
"""
|
|
262
|
+
Search emails using Microsoft Graph API.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
client: Authenticated O365Client
|
|
266
|
+
**kwargs: Search parameters
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dict with search results
|
|
270
|
+
"""
|
|
271
|
+
query = kwargs.get('query')
|
|
272
|
+
folder = kwargs.get('folder', 'inbox')
|
|
273
|
+
max_results = min(kwargs.get('max_results', 10), 50) # Cap at 50
|
|
274
|
+
include_attachments = kwargs.get('include_attachments', False)
|
|
275
|
+
order_by = kwargs.get('order_by', 'receivedDateTime desc')
|
|
276
|
+
user_id = kwargs.get('user_id')
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
# Build the request
|
|
280
|
+
mailbox = client.get_user_context(user_id=user_id)
|
|
281
|
+
|
|
282
|
+
# Select fields to return
|
|
283
|
+
select_fields = [
|
|
284
|
+
'id', 'subject', 'from', 'toRecipients', 'receivedDateTime',
|
|
285
|
+
'bodyPreview', 'isRead', 'hasAttachments', 'importance',
|
|
286
|
+
'conversationId', 'webLink'
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
if include_attachments:
|
|
290
|
+
select_fields.append('attachments')
|
|
291
|
+
|
|
292
|
+
# Apply folder filter and get appropriate request builder
|
|
293
|
+
folder_map = {
|
|
294
|
+
'inbox': 'inbox',
|
|
295
|
+
'sentitems': 'sentitems',
|
|
296
|
+
'drafts': 'drafts',
|
|
297
|
+
'deleteditems': 'deleteditems'
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if folder.lower() in folder_map:
|
|
301
|
+
folder_name = folder_map[folder.lower()]
|
|
302
|
+
request_builder = mailbox.mail_folders.by_mail_folder_id(folder_name).messages
|
|
303
|
+
|
|
304
|
+
else:
|
|
305
|
+
# Direct messages (inbox shortcut)
|
|
306
|
+
request_builder = mailbox.messages
|
|
307
|
+
|
|
308
|
+
query_params_obj = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(
|
|
309
|
+
top=max_results,
|
|
310
|
+
select=select_fields
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# CRITICAL: Only add orderby if NOT using search
|
|
314
|
+
if not query:
|
|
315
|
+
query_params_obj.orderby = [order_by]
|
|
316
|
+
|
|
317
|
+
# Add search filter if query provided
|
|
318
|
+
if query:
|
|
319
|
+
query_params_obj.search = f'"{query}"'
|
|
320
|
+
self.logger.info(f"Using search (orderby disabled): '{query}'")
|
|
321
|
+
else:
|
|
322
|
+
self.logger.info(f"Listing messages with orderby: {order_by}")
|
|
323
|
+
|
|
324
|
+
# Wrap in RequestConfiguration
|
|
325
|
+
request_config = RequestConfiguration(
|
|
326
|
+
query_parameters=query_params_obj
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Execute search
|
|
330
|
+
self.logger.info(f"Searching emails: query='{query}', folder='{folder}'")
|
|
331
|
+
messages = await request_builder.get(request_configuration=request_config)
|
|
332
|
+
|
|
333
|
+
# Format results
|
|
334
|
+
results = []
|
|
335
|
+
if messages and messages.value:
|
|
336
|
+
for msg in messages.value:
|
|
337
|
+
result_item = {
|
|
338
|
+
"id": msg.id,
|
|
339
|
+
"subject": msg.subject or "(No subject)",
|
|
340
|
+
# Fix: Use from_ instead of from_prop
|
|
341
|
+
"from": msg.from_.email_address.address if msg.from_ and msg.from_.email_address else None,
|
|
342
|
+
"from_name": msg.from_.email_address.name if msg.from_ and msg.from_.email_address else None,
|
|
343
|
+
"to": [
|
|
344
|
+
r.email_address.address
|
|
345
|
+
for r in (msg.to_recipients or [])
|
|
346
|
+
if r and r.email_address
|
|
347
|
+
],
|
|
348
|
+
"received_datetime": msg.received_date_time.isoformat() if msg.received_date_time else None,
|
|
349
|
+
"body_preview": msg.body_preview,
|
|
350
|
+
"is_read": msg.is_read,
|
|
351
|
+
"has_attachments": msg.has_attachments,
|
|
352
|
+
"importance": str(msg.importance) if msg.importance else None,
|
|
353
|
+
"web_link": msg.web_link
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Add attachment info if requested
|
|
357
|
+
if include_attachments and msg.has_attachments and hasattr(msg, 'attachments'):
|
|
358
|
+
result_item["attachments"] = [
|
|
359
|
+
{
|
|
360
|
+
"name": att.name,
|
|
361
|
+
"size": att.size,
|
|
362
|
+
"content_type": att.content_type
|
|
363
|
+
}
|
|
364
|
+
for att in (msg.attachments or [])
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
results.append(result_item)
|
|
368
|
+
|
|
369
|
+
self.logger.info(f"Found {len(results)} emails matching query: {query}")
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"query": query,
|
|
373
|
+
"folder": folder,
|
|
374
|
+
"total_results": len(results),
|
|
375
|
+
"messages": results
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
self.logger.error(f"Failed to search emails: {e}", exc_info=True)
|
|
380
|
+
raise
|
|
381
|
+
|
|
382
|
+
# ============================================================================
|
|
383
|
+
# SEND EMAIL TOOL
|
|
384
|
+
# ============================================================================
|
|
385
|
+
|
|
386
|
+
class SendEmailArgs(O365ToolArgsSchema):
|
|
387
|
+
"""Arguments for sending an email."""
|
|
388
|
+
subject: str = Field(
|
|
389
|
+
description="Email subject line"
|
|
390
|
+
)
|
|
391
|
+
body: str = Field(
|
|
392
|
+
description="Email body content (can be HTML or plain text)"
|
|
393
|
+
)
|
|
394
|
+
to_recipients: List[str] = Field(
|
|
395
|
+
description="List of recipient email addresses"
|
|
396
|
+
)
|
|
397
|
+
cc_recipients: Optional[List[str]] = Field(
|
|
398
|
+
default=None,
|
|
399
|
+
description="List of CC recipient email addresses"
|
|
400
|
+
)
|
|
401
|
+
bcc_recipients: Optional[List[str]] = Field(
|
|
402
|
+
default=None,
|
|
403
|
+
description="List of BCC recipient email addresses"
|
|
404
|
+
)
|
|
405
|
+
importance: Optional[str] = Field(
|
|
406
|
+
default="normal",
|
|
407
|
+
description="Email importance: 'low', 'normal', or 'high'"
|
|
408
|
+
)
|
|
409
|
+
is_html: bool = Field(
|
|
410
|
+
default=False,
|
|
411
|
+
description="Whether the body is HTML (True) or plain text (False)"
|
|
412
|
+
)
|
|
413
|
+
save_to_sent_items: bool = Field(
|
|
414
|
+
default=True,
|
|
415
|
+
description="Whether to save a copy in Sent Items folder"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class SendEmailTool(O365Tool):
|
|
420
|
+
"""
|
|
421
|
+
Tool for sending emails directly in Office365.
|
|
422
|
+
|
|
423
|
+
This tool sends an email immediately without creating a draft.
|
|
424
|
+
The email is sent and optionally saved to the Sent Items folder.
|
|
425
|
+
|
|
426
|
+
Examples:
|
|
427
|
+
# Send a simple email
|
|
428
|
+
result = await tool.run(
|
|
429
|
+
subject="Quick Update",
|
|
430
|
+
body="Just wanted to let you know...",
|
|
431
|
+
to_recipients=["colleague@company.com"]
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Send HTML email with CC
|
|
435
|
+
result = await tool.run(
|
|
436
|
+
subject="Newsletter",
|
|
437
|
+
body="<h2>This Month's Updates</h2><p>Content here...</p>",
|
|
438
|
+
to_recipients=["subscriber@email.com"],
|
|
439
|
+
cc_recipients=["team@company.com"],
|
|
440
|
+
importance="high",
|
|
441
|
+
is_html=True
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Send without saving to Sent Items
|
|
445
|
+
result = await tool.run(
|
|
446
|
+
subject="Temporary Message",
|
|
447
|
+
body="This won't be saved in Sent Items",
|
|
448
|
+
to_recipients=["user@company.com"],
|
|
449
|
+
save_to_sent_items=False
|
|
450
|
+
)
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
name: str = "send_email"
|
|
454
|
+
description: str = (
|
|
455
|
+
"Send an email directly through Office365. "
|
|
456
|
+
"The email is sent immediately and optionally saved to Sent Items."
|
|
457
|
+
)
|
|
458
|
+
args_schema: Type[BaseModel] = SendEmailArgs
|
|
459
|
+
|
|
460
|
+
async def _execute_graph_operation(
|
|
461
|
+
self,
|
|
462
|
+
client: O365Client,
|
|
463
|
+
**kwargs
|
|
464
|
+
) -> Dict[str, Any]:
|
|
465
|
+
"""
|
|
466
|
+
Send an email using Microsoft Graph API.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
client: Authenticated O365Client
|
|
470
|
+
**kwargs: Email parameters
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Dict with send confirmation
|
|
474
|
+
"""
|
|
475
|
+
# Extract parameters
|
|
476
|
+
subject = kwargs.get('subject')
|
|
477
|
+
body_content = kwargs.get('body')
|
|
478
|
+
to_recipients = kwargs.get('to_recipients', [])
|
|
479
|
+
cc_recipients = kwargs.get('cc_recipients')
|
|
480
|
+
bcc_recipients = kwargs.get('bcc_recipients')
|
|
481
|
+
importance_str = kwargs.get('importance', 'normal')
|
|
482
|
+
is_html = kwargs.get('is_html', False)
|
|
483
|
+
save_to_sent = kwargs.get('save_to_sent_items', True)
|
|
484
|
+
user_id = kwargs.get('user_id')
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
# Get user context
|
|
488
|
+
mailbox = client.get_user_context(user_id=user_id)
|
|
489
|
+
|
|
490
|
+
# Build message object
|
|
491
|
+
message = Message()
|
|
492
|
+
message.subject = subject
|
|
493
|
+
|
|
494
|
+
# Set body
|
|
495
|
+
message.body = ItemBody()
|
|
496
|
+
message.body.content = body_content
|
|
497
|
+
message.body.content_type = BodyType.Html if is_html else BodyType.Text
|
|
498
|
+
|
|
499
|
+
# Helper function to create recipient
|
|
500
|
+
def create_recipient(email: str) -> Recipient:
|
|
501
|
+
recipient = Recipient()
|
|
502
|
+
recipient.email_address = EmailAddress()
|
|
503
|
+
recipient.email_address.address = email
|
|
504
|
+
return recipient
|
|
505
|
+
|
|
506
|
+
# Set recipients
|
|
507
|
+
message.to_recipients = [create_recipient(email) for email in to_recipients]
|
|
508
|
+
|
|
509
|
+
if cc_recipients:
|
|
510
|
+
message.cc_recipients = [create_recipient(email) for email in cc_recipients]
|
|
511
|
+
|
|
512
|
+
if bcc_recipients:
|
|
513
|
+
message.bcc_recipients = [create_recipient(email) for email in bcc_recipients]
|
|
514
|
+
|
|
515
|
+
# Set importance
|
|
516
|
+
importance_map = {
|
|
517
|
+
'low': Importance.Low,
|
|
518
|
+
'normal': Importance.Normal,
|
|
519
|
+
'high': Importance.High
|
|
520
|
+
}
|
|
521
|
+
message.importance = importance_map.get(importance_str.lower(), Importance.Normal)
|
|
522
|
+
|
|
523
|
+
# Create the request body for send_mail
|
|
524
|
+
request_body = SendMailPostRequestBody()
|
|
525
|
+
request_body.message = message
|
|
526
|
+
request_body.save_to_sent_items = save_to_sent
|
|
527
|
+
|
|
528
|
+
# Send the email
|
|
529
|
+
self.logger.info(f"Sending email to {to_recipients}")
|
|
530
|
+
await mailbox.send_mail.post(body=request_body)
|
|
531
|
+
|
|
532
|
+
self.logger.info(f"Successfully sent email: {subject}")
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
"status": "sent",
|
|
536
|
+
"subject": subject,
|
|
537
|
+
"to_recipients": to_recipients,
|
|
538
|
+
"cc_recipients": cc_recipients or [],
|
|
539
|
+
"bcc_recipients": bcc_recipients or [],
|
|
540
|
+
"importance": importance_str,
|
|
541
|
+
"is_html": is_html,
|
|
542
|
+
"sent_datetime": datetime.now().isoformat(),
|
|
543
|
+
"saved_to_sent_items": save_to_sent
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
except Exception as e:
|
|
547
|
+
self.logger.error(f"Failed to send email: {e}", exc_info=True)
|
|
548
|
+
raise
|
|
549
|
+
|
|
550
|
+
# ============================================================================
|
|
551
|
+
# LIST MESSAGES TOOL
|
|
552
|
+
# ============================================================================
|
|
553
|
+
|
|
554
|
+
class ListMessagesArgs(O365ToolArgsSchema):
|
|
555
|
+
"""Arguments for listing email messages."""
|
|
556
|
+
folder: str = Field(
|
|
557
|
+
default="inbox",
|
|
558
|
+
description="Folder to list messages from: 'inbox', 'sentitems', 'drafts', 'deleteditems', or folder ID"
|
|
559
|
+
)
|
|
560
|
+
top: int = Field(
|
|
561
|
+
default=10,
|
|
562
|
+
description="Maximum number of messages to return (1-1000)"
|
|
563
|
+
)
|
|
564
|
+
filter_query: Optional[str] = Field(
|
|
565
|
+
default=None,
|
|
566
|
+
description="OData filter query (e.g., 'isRead eq false', 'hasAttachments eq true')"
|
|
567
|
+
)
|
|
568
|
+
order_by: str = Field(
|
|
569
|
+
default="receivedDateTime desc",
|
|
570
|
+
description="Sort order (e.g., 'receivedDateTime desc', 'subject asc')"
|
|
571
|
+
)
|
|
572
|
+
select_fields: Optional[List[str]] = Field(
|
|
573
|
+
default=None,
|
|
574
|
+
description="Specific fields to retrieve. If None, returns default fields."
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class ListMessagesTool(O365Tool):
|
|
579
|
+
"""
|
|
580
|
+
Tool for listing email messages from a specified folder.
|
|
581
|
+
|
|
582
|
+
This tool allows you to:
|
|
583
|
+
- List messages from any mail folder (Inbox, Sent Items, etc.)
|
|
584
|
+
- Filter messages by various criteria (read status, sender, date, etc.)
|
|
585
|
+
- Limit the number of results
|
|
586
|
+
- Order results by different fields
|
|
587
|
+
- Select specific fields to retrieve
|
|
588
|
+
|
|
589
|
+
Filter query examples:
|
|
590
|
+
- "isRead eq false" - Unread messages
|
|
591
|
+
- "hasAttachments eq true" - Messages with attachments
|
|
592
|
+
- "from/emailAddress/address eq 'user@example.com'" - From specific sender
|
|
593
|
+
- "receivedDateTime ge 2025-10-16T00:00:00Z" - Received after date
|
|
594
|
+
- "importance eq 'high'" - High importance messages
|
|
595
|
+
|
|
596
|
+
Examples:
|
|
597
|
+
# List recent messages
|
|
598
|
+
result = await tool.run(
|
|
599
|
+
folder="inbox",
|
|
600
|
+
top=20
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# List unread messages
|
|
604
|
+
result = await tool.run(
|
|
605
|
+
folder="inbox",
|
|
606
|
+
filter_query="isRead eq false"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# List messages from specific sender
|
|
610
|
+
result = await tool.run(
|
|
611
|
+
folder="inbox",
|
|
612
|
+
filter_query="from/emailAddress/address eq 'boss@company.com'",
|
|
613
|
+
top=10
|
|
614
|
+
)
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
name: str = "list_messages"
|
|
618
|
+
description: str = (
|
|
619
|
+
"List email messages from a specified folder with optional filtering and sorting. "
|
|
620
|
+
"Supports OData filters for advanced queries."
|
|
621
|
+
)
|
|
622
|
+
args_schema: Type[BaseModel] = ListMessagesArgs
|
|
623
|
+
|
|
624
|
+
async def _execute_graph_operation(
|
|
625
|
+
self,
|
|
626
|
+
client: O365Client,
|
|
627
|
+
**kwargs
|
|
628
|
+
) -> Dict[str, Any]:
|
|
629
|
+
"""
|
|
630
|
+
List messages using Microsoft Graph API.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
client: Authenticated O365Client
|
|
634
|
+
**kwargs: List parameters
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
Dict with list of messages
|
|
638
|
+
"""
|
|
639
|
+
folder = kwargs.get('folder', 'inbox')
|
|
640
|
+
top = min(kwargs.get('top', 10), 1000) # Cap at 1000
|
|
641
|
+
filter_query = kwargs.get('filter_query')
|
|
642
|
+
order_by = kwargs.get('order_by', 'receivedDateTime desc')
|
|
643
|
+
select_fields = kwargs.get('select_fields')
|
|
644
|
+
user_id = kwargs.get('user_id')
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
# Get user context
|
|
648
|
+
mailbox = client.get_user_context(user_id=user_id)
|
|
649
|
+
|
|
650
|
+
# Default fields to select
|
|
651
|
+
default_fields = [
|
|
652
|
+
'id', 'subject', 'from', 'toRecipients', 'receivedDateTime',
|
|
653
|
+
'sentDateTime', 'hasAttachments', 'importance', 'isRead',
|
|
654
|
+
'bodyPreview', 'internetMessageId', 'webLink'
|
|
655
|
+
]
|
|
656
|
+
|
|
657
|
+
fields = select_fields or default_fields
|
|
658
|
+
|
|
659
|
+
# Apply folder filter and get appropriate request builder
|
|
660
|
+
folder_map = {
|
|
661
|
+
'inbox': 'inbox',
|
|
662
|
+
'sentitems': 'sentitems',
|
|
663
|
+
'drafts': 'drafts',
|
|
664
|
+
'deleteditems': 'deleteditems'
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if folder.lower() in folder_map:
|
|
668
|
+
folder_name = folder_map[folder.lower()]
|
|
669
|
+
request_builder = mailbox.mail_folders.by_mail_folder_id(folder_name).messages
|
|
670
|
+
else:
|
|
671
|
+
# Try as direct folder ID or use inbox as fallback
|
|
672
|
+
request_builder = mailbox.messages
|
|
673
|
+
|
|
674
|
+
# Build query parameters
|
|
675
|
+
query_params_obj = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(
|
|
676
|
+
top=top,
|
|
677
|
+
select=fields,
|
|
678
|
+
orderby=[order_by]
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Add filter if provided
|
|
682
|
+
if filter_query:
|
|
683
|
+
query_params_obj.filter = filter_query
|
|
684
|
+
|
|
685
|
+
# Wrap in RequestConfiguration
|
|
686
|
+
request_config = RequestConfiguration(
|
|
687
|
+
query_parameters=query_params_obj
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
# Execute request
|
|
691
|
+
self.logger.info(f"Listing messages: folder='{folder}', top={top}, filter='{filter_query}'")
|
|
692
|
+
messages = await request_builder.get(request_configuration=request_config)
|
|
693
|
+
|
|
694
|
+
# Format results
|
|
695
|
+
results = []
|
|
696
|
+
if messages and messages.value:
|
|
697
|
+
for msg in messages.value:
|
|
698
|
+
result_item = {
|
|
699
|
+
"id": msg.id,
|
|
700
|
+
"subject": msg.subject or "(No subject)",
|
|
701
|
+
"from": msg.from_.email_address.address if msg.from_ and msg.from_.email_address else None,
|
|
702
|
+
"from_name": msg.from_.email_address.name if msg.from_ and msg.from_.email_address else None,
|
|
703
|
+
"to": [
|
|
704
|
+
r.email_address.address
|
|
705
|
+
for r in (msg.to_recipients or [])
|
|
706
|
+
if r and r.email_address
|
|
707
|
+
],
|
|
708
|
+
"received_datetime": msg.received_date_time.isoformat() if msg.received_date_time else None,
|
|
709
|
+
"sent_datetime": msg.sent_date_time.isoformat() if msg.sent_date_time else None,
|
|
710
|
+
"has_attachments": msg.has_attachments,
|
|
711
|
+
"importance": str(msg.importance) if msg.importance else None,
|
|
712
|
+
"is_read": msg.is_read,
|
|
713
|
+
"body_preview": msg.body_preview,
|
|
714
|
+
"internet_message_id": msg.internet_message_id,
|
|
715
|
+
"web_link": msg.web_link
|
|
716
|
+
}
|
|
717
|
+
results.append(result_item)
|
|
718
|
+
|
|
719
|
+
self.logger.info(f"Found {len(results)} messages in folder '{folder}'")
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
"folder": folder,
|
|
723
|
+
"total_results": len(results),
|
|
724
|
+
"messages": results
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
except Exception as e:
|
|
728
|
+
self.logger.error(f"Failed to list messages: {e}", exc_info=True)
|
|
729
|
+
raise
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
# ============================================================================
|
|
733
|
+
# GET MESSAGE TOOL
|
|
734
|
+
# ============================================================================
|
|
735
|
+
|
|
736
|
+
class GetMessageArgs(O365ToolArgsSchema):
|
|
737
|
+
"""Arguments for retrieving a specific message."""
|
|
738
|
+
message_id: str = Field(
|
|
739
|
+
description="The ID of the message to retrieve"
|
|
740
|
+
)
|
|
741
|
+
include_body: bool = Field(
|
|
742
|
+
default=True,
|
|
743
|
+
description="Whether to include the full message body content"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class GetMessageTool(O365Tool):
|
|
748
|
+
"""
|
|
749
|
+
Tool for retrieving a specific email message by its ID.
|
|
750
|
+
|
|
751
|
+
This tool retrieves complete information about a single message, including:
|
|
752
|
+
- Full message headers (subject, sender, recipients, dates)
|
|
753
|
+
- Message body content (if include_body=True)
|
|
754
|
+
- Attachment information
|
|
755
|
+
- Message metadata (read status, importance, conversation ID)
|
|
756
|
+
|
|
757
|
+
Use this tool when you need detailed information about a specific message,
|
|
758
|
+
such as reading the full content or checking for attachments.
|
|
759
|
+
|
|
760
|
+
Examples:
|
|
761
|
+
# Get message with body
|
|
762
|
+
result = await tool.run(
|
|
763
|
+
message_id="AAMkAGI...",
|
|
764
|
+
include_body=True
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# Get message metadata only (faster)
|
|
768
|
+
result = await tool.run(
|
|
769
|
+
message_id="AAMkAGI...",
|
|
770
|
+
include_body=False
|
|
771
|
+
)
|
|
772
|
+
"""
|
|
773
|
+
|
|
774
|
+
name: str = "get_message"
|
|
775
|
+
description: str = (
|
|
776
|
+
"Retrieve a specific email message by its ID with complete details. "
|
|
777
|
+
"Can include full body content and attachment information."
|
|
778
|
+
)
|
|
779
|
+
args_schema: Type[BaseModel] = GetMessageArgs
|
|
780
|
+
|
|
781
|
+
async def _execute_graph_operation(
|
|
782
|
+
self,
|
|
783
|
+
client: O365Client,
|
|
784
|
+
**kwargs
|
|
785
|
+
) -> Dict[str, Any]:
|
|
786
|
+
"""
|
|
787
|
+
Get a specific message using Microsoft Graph API.
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
client: Authenticated O365Client
|
|
791
|
+
**kwargs: Get parameters
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
Dict with message details
|
|
795
|
+
"""
|
|
796
|
+
message_id = kwargs.get('message_id')
|
|
797
|
+
include_body = kwargs.get('include_body', True)
|
|
798
|
+
user_id = kwargs.get('user_id')
|
|
799
|
+
|
|
800
|
+
try:
|
|
801
|
+
# Get user context
|
|
802
|
+
mailbox = client.get_user_context(user_id=user_id)
|
|
803
|
+
|
|
804
|
+
# Select fields based on whether body is needed
|
|
805
|
+
if include_body:
|
|
806
|
+
select_fields = [
|
|
807
|
+
'id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'bccRecipients',
|
|
808
|
+
'receivedDateTime', 'sentDateTime', 'hasAttachments', 'importance', 'isRead',
|
|
809
|
+
'body', 'bodyPreview', 'internetMessageId', 'conversationId', 'webLink'
|
|
810
|
+
]
|
|
811
|
+
else:
|
|
812
|
+
select_fields = [
|
|
813
|
+
'id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'bccRecipients',
|
|
814
|
+
'receivedDateTime', 'sentDateTime', 'hasAttachments', 'importance', 'isRead',
|
|
815
|
+
'bodyPreview', 'internetMessageId', 'conversationId', 'webLink'
|
|
816
|
+
]
|
|
817
|
+
|
|
818
|
+
# Build query parameters
|
|
819
|
+
query_params_obj = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(
|
|
820
|
+
select=select_fields
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
request_config = RequestConfiguration(
|
|
824
|
+
query_parameters=query_params_obj
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# Get the message
|
|
828
|
+
self.logger.info(f"Getting message: {message_id}")
|
|
829
|
+
message = await mailbox.messages.by_message_id(message_id).get(
|
|
830
|
+
request_configuration=request_config
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
if not message:
|
|
834
|
+
raise ValueError(f"Message {message_id} not found")
|
|
835
|
+
|
|
836
|
+
# Format result
|
|
837
|
+
result = {
|
|
838
|
+
"id": message.id,
|
|
839
|
+
"subject": message.subject or "(No subject)",
|
|
840
|
+
"from": message.from_.email_address.address if message.from_ and message.from_.email_address else None,
|
|
841
|
+
"from_name": message.from_.email_address.name if message.from_ and message.from_.email_address else None,
|
|
842
|
+
"to_recipients": [
|
|
843
|
+
r.email_address.address
|
|
844
|
+
for r in (message.to_recipients or [])
|
|
845
|
+
if r and r.email_address
|
|
846
|
+
],
|
|
847
|
+
"cc_recipients": [
|
|
848
|
+
r.email_address.address
|
|
849
|
+
for r in (message.cc_recipients or [])
|
|
850
|
+
if r and r.email_address
|
|
851
|
+
],
|
|
852
|
+
"bcc_recipients": [
|
|
853
|
+
r.email_address.address
|
|
854
|
+
for r in (message.bcc_recipients or [])
|
|
855
|
+
if r and r.email_address
|
|
856
|
+
],
|
|
857
|
+
"received_datetime": message.received_date_time.isoformat() if message.received_date_time else None,
|
|
858
|
+
"sent_datetime": message.sent_date_time.isoformat() if message.sent_date_time else None,
|
|
859
|
+
"has_attachments": message.has_attachments,
|
|
860
|
+
"importance": str(message.importance) if message.importance else None,
|
|
861
|
+
"is_read": message.is_read,
|
|
862
|
+
"body_preview": message.body_preview,
|
|
863
|
+
"internet_message_id": message.internet_message_id,
|
|
864
|
+
"conversation_id": message.conversation_id,
|
|
865
|
+
"web_link": message.web_link
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
# Add body if requested
|
|
869
|
+
if include_body and message.body:
|
|
870
|
+
result["body"] = {
|
|
871
|
+
"content_type": str(message.body.content_type) if message.body.content_type else "text",
|
|
872
|
+
"content": message.body.content or ""
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
self.logger.info(f"Retrieved message: {message.subject}")
|
|
876
|
+
|
|
877
|
+
return result
|
|
878
|
+
|
|
879
|
+
except Exception as e:
|
|
880
|
+
self.logger.error(f"Failed to get message {message_id}: {e}", exc_info=True)
|
|
881
|
+
raise
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
# ============================================================================
|
|
885
|
+
# DOWNLOAD ATTACHMENT TOOL
|
|
886
|
+
# ============================================================================
|
|
887
|
+
|
|
888
|
+
class DownloadAttachmentArgs(O365ToolArgsSchema):
|
|
889
|
+
"""Arguments for downloading an email attachment."""
|
|
890
|
+
message_id: str = Field(
|
|
891
|
+
description="The ID of the message containing the attachment"
|
|
892
|
+
)
|
|
893
|
+
attachment_id: str = Field(
|
|
894
|
+
description="The ID of the attachment to download"
|
|
895
|
+
)
|
|
896
|
+
destination: str = Field(
|
|
897
|
+
description="Local path where the attachment should be saved"
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
class DownloadAttachmentTool(O365Tool):
|
|
902
|
+
"""
|
|
903
|
+
Tool for downloading email attachments to local storage.
|
|
904
|
+
|
|
905
|
+
This tool downloads a specific attachment from an email message and saves it
|
|
906
|
+
to a specified location on the local filesystem.
|
|
907
|
+
|
|
908
|
+
Before downloading, you should:
|
|
909
|
+
1. Use GetMessageTool to retrieve the message and check hasAttachments
|
|
910
|
+
2. List the attachments to get their IDs and names
|
|
911
|
+
3. Use this tool to download specific attachments
|
|
912
|
+
|
|
913
|
+
The tool will:
|
|
914
|
+
- Create parent directories if they don't exist
|
|
915
|
+
- Decode and save the attachment content
|
|
916
|
+
- Return the path where the file was saved
|
|
917
|
+
|
|
918
|
+
Examples:
|
|
919
|
+
# Download attachment
|
|
920
|
+
result = await tool.run(
|
|
921
|
+
message_id="AAMkAGI...",
|
|
922
|
+
attachment_id="AAMkAGI...Attach...",
|
|
923
|
+
destination="/tmp/documents/report.pdf"
|
|
924
|
+
)
|
|
925
|
+
"""
|
|
926
|
+
|
|
927
|
+
name: str = "download_attachment"
|
|
928
|
+
description: str = (
|
|
929
|
+
"Download an email attachment to local storage. "
|
|
930
|
+
"Saves the attachment file to the specified destination path."
|
|
931
|
+
)
|
|
932
|
+
args_schema: Type[BaseModel] = DownloadAttachmentArgs
|
|
933
|
+
|
|
934
|
+
async def _execute_graph_operation(
|
|
935
|
+
self,
|
|
936
|
+
client: O365Client,
|
|
937
|
+
**kwargs
|
|
938
|
+
) -> Dict[str, Any]:
|
|
939
|
+
"""
|
|
940
|
+
Download an attachment using Microsoft Graph API.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
client: Authenticated O365Client
|
|
944
|
+
**kwargs: Download parameters
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
Dict with download confirmation and path
|
|
948
|
+
"""
|
|
949
|
+
message_id = kwargs.get('message_id')
|
|
950
|
+
attachment_id = kwargs.get('attachment_id')
|
|
951
|
+
destination = kwargs.get('destination')
|
|
952
|
+
user_id = kwargs.get('user_id')
|
|
953
|
+
|
|
954
|
+
try:
|
|
955
|
+
# Get user context
|
|
956
|
+
mailbox = client.get_user_context(user_id=user_id)
|
|
957
|
+
|
|
958
|
+
# Get attachment info
|
|
959
|
+
self.logger.info(f"Getting attachment {attachment_id} from message {message_id}")
|
|
960
|
+
attachment = await mailbox.messages.by_message_id(message_id)\
|
|
961
|
+
.attachments.by_attachment_id(attachment_id).get()
|
|
962
|
+
|
|
963
|
+
if not attachment:
|
|
964
|
+
raise ValueError(f"Attachment {attachment_id} not found")
|
|
965
|
+
|
|
966
|
+
# Prepare destination path
|
|
967
|
+
destination_path = Path(destination)
|
|
968
|
+
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
|
969
|
+
|
|
970
|
+
# Handle different attachment types
|
|
971
|
+
if hasattr(attachment, 'content_bytes') and attachment.content_bytes:
|
|
972
|
+
# File attachment with content
|
|
973
|
+
content = base64.b64decode(attachment.content_bytes)
|
|
974
|
+
async with aiofiles.open(destination_path, "wb") as f:
|
|
975
|
+
await f.write(content)
|
|
976
|
+
|
|
977
|
+
self.logger.info(f"Downloaded attachment '{attachment.name}' to {destination_path}")
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
"status": "downloaded",
|
|
981
|
+
"attachment_id": attachment.id,
|
|
982
|
+
"attachment_name": attachment.name,
|
|
983
|
+
"size": attachment.size,
|
|
984
|
+
"content_type": attachment.content_type,
|
|
985
|
+
"destination": str(destination_path)
|
|
986
|
+
}
|
|
987
|
+
else:
|
|
988
|
+
raise ValueError(f"Attachment {attachment_id} has no downloadable content")
|
|
989
|
+
|
|
990
|
+
except Exception as e:
|
|
991
|
+
self.logger.error(f"Failed to download attachment {attachment_id}: {e}", exc_info=True)
|
|
992
|
+
raise
|