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,743 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notification Mixin for AI-Parrot Agents.
|
|
3
|
+
|
|
4
|
+
Provides notification capabilities to agents using the async-notify library.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Union, List, Optional, Dict, Any
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
import mimetypes
|
|
11
|
+
from notify import Notify
|
|
12
|
+
from notify.models import (
|
|
13
|
+
Actor,
|
|
14
|
+
Channel,
|
|
15
|
+
Chat,
|
|
16
|
+
TeamsChannel,
|
|
17
|
+
TeamsWebhook,
|
|
18
|
+
TeamsCard
|
|
19
|
+
)
|
|
20
|
+
from notify.providers.email import Email
|
|
21
|
+
from notify.providers.slack import Slack
|
|
22
|
+
from notify.providers.telegram import Telegram
|
|
23
|
+
from notify.providers.teams import Teams
|
|
24
|
+
from ..conf import (
|
|
25
|
+
TEAMS_NOTIFY_TENANT_ID,
|
|
26
|
+
TEAMS_NOTIFY_CLIENT_ID,
|
|
27
|
+
TEAMS_NOTIFY_CLIENT_SECRET,
|
|
28
|
+
TEAMS_NOTIFY_USERNAME,
|
|
29
|
+
TEAMS_NOTIFY_PASSWORD
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NotificationProvider(Enum):
|
|
34
|
+
"""Supported notification providers."""
|
|
35
|
+
EMAIL = "email"
|
|
36
|
+
SLACK = "slack"
|
|
37
|
+
TELEGRAM = "telegram"
|
|
38
|
+
TEAMS = "teams"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FileType(Enum):
|
|
42
|
+
"""File types for smart handling."""
|
|
43
|
+
IMAGE = "image"
|
|
44
|
+
DOCUMENT = "document"
|
|
45
|
+
VIDEO = "video"
|
|
46
|
+
AUDIO = "audio"
|
|
47
|
+
UNKNOWN = "unknown"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class NotificationConfig:
|
|
52
|
+
"""Configuration for sending notifications."""
|
|
53
|
+
provider: NotificationProvider = NotificationProvider.EMAIL
|
|
54
|
+
recipients: Union[List[Actor], Actor, Channel, Chat] = None
|
|
55
|
+
subject: Optional[str] = None
|
|
56
|
+
template: Optional[str] = None
|
|
57
|
+
disable_notification: bool = False
|
|
58
|
+
# Email specific
|
|
59
|
+
with_attachments: bool = True
|
|
60
|
+
# Teams specific
|
|
61
|
+
teams_config: Optional[Dict[str, Any]] = None
|
|
62
|
+
# Additional provider kwargs
|
|
63
|
+
provider_kwargs: Optional[Dict[str, Any]] = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NotificationMixin:
|
|
67
|
+
"""
|
|
68
|
+
Mixin to provide notification capabilities to agents.
|
|
69
|
+
|
|
70
|
+
This mixin integrates async-notify library to send messages
|
|
71
|
+
through various channels (email, slack, telegram, teams) with
|
|
72
|
+
smart file handling.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def _classify_file(self, file_path: Path) -> FileType:
|
|
76
|
+
"""
|
|
77
|
+
Classify file type based on extension and MIME type.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
file_path: Path to the file
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
FileType enum value
|
|
84
|
+
"""
|
|
85
|
+
if not file_path.exists():
|
|
86
|
+
return FileType.UNKNOWN
|
|
87
|
+
|
|
88
|
+
# Get MIME type
|
|
89
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
90
|
+
|
|
91
|
+
if mime_type:
|
|
92
|
+
if mime_type.startswith('image/'):
|
|
93
|
+
return FileType.IMAGE
|
|
94
|
+
elif mime_type.startswith('video/'):
|
|
95
|
+
return FileType.VIDEO
|
|
96
|
+
elif mime_type.startswith('audio/'):
|
|
97
|
+
return FileType.AUDIO
|
|
98
|
+
|
|
99
|
+
# Check by extension if MIME failed
|
|
100
|
+
ext = file_path.suffix.lower()
|
|
101
|
+
|
|
102
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'}
|
|
103
|
+
video_extensions = {'.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'}
|
|
104
|
+
audio_extensions = {'.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'}
|
|
105
|
+
|
|
106
|
+
if ext in image_extensions:
|
|
107
|
+
return FileType.IMAGE
|
|
108
|
+
elif ext in video_extensions:
|
|
109
|
+
return FileType.VIDEO
|
|
110
|
+
elif ext in audio_extensions:
|
|
111
|
+
return FileType.AUDIO
|
|
112
|
+
else:
|
|
113
|
+
return FileType.DOCUMENT
|
|
114
|
+
|
|
115
|
+
def _categorize_files(self, files: List[Path]) -> Dict[FileType, List[Path]]:
|
|
116
|
+
"""
|
|
117
|
+
Categorize files by type.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
files: List of file paths
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dictionary mapping FileType to list of files
|
|
124
|
+
"""
|
|
125
|
+
categorized = {
|
|
126
|
+
FileType.IMAGE: [],
|
|
127
|
+
FileType.DOCUMENT: [],
|
|
128
|
+
FileType.VIDEO: [],
|
|
129
|
+
FileType.AUDIO: []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for file in files:
|
|
133
|
+
file_type = self._classify_file(file)
|
|
134
|
+
if file_type != FileType.UNKNOWN:
|
|
135
|
+
categorized[file_type].append(file)
|
|
136
|
+
|
|
137
|
+
return categorized
|
|
138
|
+
|
|
139
|
+
async def send_notification(
|
|
140
|
+
self,
|
|
141
|
+
message: Union[str, Any],
|
|
142
|
+
recipients: Union[List[Actor], Actor, Channel, Chat, str, List[str]],
|
|
143
|
+
provider: Union[str, NotificationProvider] = NotificationProvider.EMAIL,
|
|
144
|
+
subject: Optional[str] = None,
|
|
145
|
+
report: Optional[Any] = None,
|
|
146
|
+
template: Optional[str] = None,
|
|
147
|
+
with_attachments: bool = True,
|
|
148
|
+
**kwargs
|
|
149
|
+
) -> Dict[str, Any]:
|
|
150
|
+
"""
|
|
151
|
+
Send notification to users through various channels.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
message: Message text to send or AgentResponse/AIMessage object
|
|
155
|
+
recipients: Recipients (can be Actor objects, email strings, etc.)
|
|
156
|
+
provider: Notification provider (email, slack, telegram, teams)
|
|
157
|
+
subject: Subject line (mainly for email)
|
|
158
|
+
report: Optional AgentResponse or AIMessage containing output/files
|
|
159
|
+
template: Email template name
|
|
160
|
+
with_attachments: Whether to include file attachments
|
|
161
|
+
**kwargs: Additional provider-specific arguments
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dict with notification status and results
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
# Simple email notification
|
|
168
|
+
await agent.send_notification(
|
|
169
|
+
message="Daily report ready",
|
|
170
|
+
recipients="user@example.com",
|
|
171
|
+
subject="Daily Report"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# With AIMessage containing files (images sent as photos in Telegram)
|
|
175
|
+
response = await agent.chat("Generate report with charts")
|
|
176
|
+
await agent.send_notification(
|
|
177
|
+
message="Your report is ready",
|
|
178
|
+
recipients="123456789", # Telegram chat_id
|
|
179
|
+
report=response,
|
|
180
|
+
provider="telegram"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# To Slack channel
|
|
184
|
+
channel = Channel(channel_id="C123456", channel_name="reports")
|
|
185
|
+
await agent.send_notification(
|
|
186
|
+
message="New insights available",
|
|
187
|
+
recipients=channel,
|
|
188
|
+
provider="slack"
|
|
189
|
+
)
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
# Normalize provider
|
|
193
|
+
if isinstance(provider, str):
|
|
194
|
+
provider = NotificationProvider(provider.lower())
|
|
195
|
+
|
|
196
|
+
# Extract message content from AgentResponse/AIMessage if needed
|
|
197
|
+
message_text, files = self._extract_message_content(message, report)
|
|
198
|
+
|
|
199
|
+
# Parse recipients
|
|
200
|
+
recipient_list = self._parse_recipients(recipients, provider)
|
|
201
|
+
|
|
202
|
+
# Prepare notification arguments
|
|
203
|
+
notify_args = {
|
|
204
|
+
"message": message_text,
|
|
205
|
+
"recipient": recipient_list
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Add provider-specific arguments
|
|
209
|
+
if provider == NotificationProvider.EMAIL:
|
|
210
|
+
if subject:
|
|
211
|
+
notify_args["subject"] = subject
|
|
212
|
+
if template:
|
|
213
|
+
notify_args["template"] = template
|
|
214
|
+
if files and with_attachments:
|
|
215
|
+
notify_args["attachments"] = files
|
|
216
|
+
|
|
217
|
+
elif provider == NotificationProvider.TELEGRAM:
|
|
218
|
+
notify_args["disable_notification"] = kwargs.get(
|
|
219
|
+
"disable_notification", False
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
elif provider == NotificationProvider.TEAMS:
|
|
223
|
+
# Teams might need special card formatting
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# Merge additional kwargs
|
|
227
|
+
notify_args |= kwargs
|
|
228
|
+
|
|
229
|
+
# Send notification with smart file handling
|
|
230
|
+
result = await self._send_with_provider(
|
|
231
|
+
provider=provider,
|
|
232
|
+
notify_args=notify_args,
|
|
233
|
+
files=files if with_attachments else None
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.logger.info(
|
|
237
|
+
f"Notification sent via {provider.value} to {len(recipient_list) if isinstance(recipient_list, list) else 1} recipient(s)"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"status": "success",
|
|
242
|
+
"provider": provider.value,
|
|
243
|
+
"recipients": len(recipient_list) if isinstance(recipient_list, list) else 1,
|
|
244
|
+
"files_sent": len(files) if files else 0,
|
|
245
|
+
"result": result
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
self.logger.error(f"Failed to send notification: {e}", exc_info=True)
|
|
250
|
+
return {
|
|
251
|
+
"status": "error",
|
|
252
|
+
"error": str(e),
|
|
253
|
+
"provider": provider.value if provider else None
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def _extract_message_content(
|
|
257
|
+
self,
|
|
258
|
+
message: Union[str, Any],
|
|
259
|
+
report: Optional[Any] = None
|
|
260
|
+
) -> tuple[str, List[Path]]:
|
|
261
|
+
"""
|
|
262
|
+
Extract message text and files from various input types.
|
|
263
|
+
|
|
264
|
+
Handles:
|
|
265
|
+
- Plain strings
|
|
266
|
+
- AgentResponse objects
|
|
267
|
+
- AIMessage objects
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Tuple of (message_text, list_of_file_paths)
|
|
271
|
+
"""
|
|
272
|
+
message_text = ""
|
|
273
|
+
files = []
|
|
274
|
+
|
|
275
|
+
# Extract from message if it's an object
|
|
276
|
+
if isinstance(message, str):
|
|
277
|
+
message_text = message
|
|
278
|
+
elif hasattr(message, 'content'):
|
|
279
|
+
# AIMessage or similar
|
|
280
|
+
message_text = getattr(message, 'content', str(message))
|
|
281
|
+
elif hasattr(message, 'output'):
|
|
282
|
+
# AgentResponse
|
|
283
|
+
message_text = getattr(message, 'output', str(message))
|
|
284
|
+
else:
|
|
285
|
+
message_text = str(message)
|
|
286
|
+
|
|
287
|
+
# Extract from report if provided
|
|
288
|
+
if report:
|
|
289
|
+
if hasattr(report, 'output'):
|
|
290
|
+
# AgentResponse with output
|
|
291
|
+
output = getattr(report, 'output', None)
|
|
292
|
+
if output and not message_text:
|
|
293
|
+
message_text = output
|
|
294
|
+
|
|
295
|
+
# PRIORITY 1: Check for 'files' attribute first (preferred for AgentResponse)
|
|
296
|
+
if hasattr(report, 'files'):
|
|
297
|
+
report_files = getattr(report, 'files', None)
|
|
298
|
+
if report_files:
|
|
299
|
+
if isinstance(report_files, list):
|
|
300
|
+
for file_path in report_files:
|
|
301
|
+
if file_path and self._is_valid_file_path(file_path):
|
|
302
|
+
files.append(Path(file_path))
|
|
303
|
+
elif isinstance(report_files, (str, Path)):
|
|
304
|
+
if self._is_valid_file_path(report_files):
|
|
305
|
+
files.append(Path(report_files))
|
|
306
|
+
|
|
307
|
+
# PRIORITY 2: Check for 'documents' attribute (but filter out text content)
|
|
308
|
+
elif hasattr(report, 'documents'):
|
|
309
|
+
documents = getattr(report, 'documents', None)
|
|
310
|
+
if documents:
|
|
311
|
+
if isinstance(documents, list):
|
|
312
|
+
for document in documents:
|
|
313
|
+
if isinstance(document, dict) and 'path' in document:
|
|
314
|
+
doc_path = document['path']
|
|
315
|
+
if self._is_valid_file_path(doc_path):
|
|
316
|
+
files.append(Path(doc_path))
|
|
317
|
+
elif isinstance(document, (str, Path)):
|
|
318
|
+
# Only add if it looks like a file path, not text content
|
|
319
|
+
if self._is_valid_file_path(document):
|
|
320
|
+
files.append(Path(document))
|
|
321
|
+
|
|
322
|
+
# Check for content blocks with files (Claude-style responses)
|
|
323
|
+
if hasattr(report, 'content') and isinstance(report.content, list):
|
|
324
|
+
for block in report.content:
|
|
325
|
+
if isinstance(block, dict):
|
|
326
|
+
if block.get('type') == 'file' and 'path' in block:
|
|
327
|
+
file_path = block['path']
|
|
328
|
+
if self._is_valid_file_path(file_path):
|
|
329
|
+
files.append(Path(file_path))
|
|
330
|
+
elif block.get('type') == 'image' and 'source' in block:
|
|
331
|
+
# Handle image sources
|
|
332
|
+
source = block['source']
|
|
333
|
+
if isinstance(source, dict) and 'data' in source:
|
|
334
|
+
# Base64 image - would need to save first
|
|
335
|
+
pass
|
|
336
|
+
elif isinstance(source, (str, Path)):
|
|
337
|
+
if self._is_valid_file_path(source):
|
|
338
|
+
files.append(Path(source))
|
|
339
|
+
|
|
340
|
+
# PRIORITY 3: Check AIMessage nested inside AgentResponse
|
|
341
|
+
if hasattr(report, 'response') and isinstance(report.response, object):
|
|
342
|
+
ai_message = report.response
|
|
343
|
+
|
|
344
|
+
# Extract files from AIMessage
|
|
345
|
+
if hasattr(ai_message, 'files'):
|
|
346
|
+
ai_files = getattr(ai_message, 'files', None)
|
|
347
|
+
if ai_files and isinstance(ai_files, list):
|
|
348
|
+
for file_path in ai_files:
|
|
349
|
+
if file_path and self._is_valid_file_path(file_path):
|
|
350
|
+
path_obj = Path(file_path)
|
|
351
|
+
if path_obj not in files: # Avoid duplicates
|
|
352
|
+
files.append(path_obj)
|
|
353
|
+
|
|
354
|
+
# Remove duplicates while preserving order
|
|
355
|
+
seen = set()
|
|
356
|
+
unique_files = []
|
|
357
|
+
for file in files:
|
|
358
|
+
if file not in seen:
|
|
359
|
+
seen.add(file)
|
|
360
|
+
unique_files.append(file)
|
|
361
|
+
|
|
362
|
+
files = unique_files
|
|
363
|
+
|
|
364
|
+
# Validate files exist
|
|
365
|
+
valid_files = [f for f in files if f.exists()]
|
|
366
|
+
if len(valid_files) < len(files):
|
|
367
|
+
missing_count = len(files) - len(valid_files)
|
|
368
|
+
missing_files = [str(f) for f in files if not f.exists()]
|
|
369
|
+
self.logger.warning(
|
|
370
|
+
f"Some files not found: {missing_count} missing - {missing_files[:3]}"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return message_text, valid_files
|
|
374
|
+
|
|
375
|
+
def _is_valid_file_path(self, path: Union[str, Path]) -> bool:
|
|
376
|
+
"""
|
|
377
|
+
Check if a string looks like a valid file path rather than text content.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
path: String or Path to validate
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
bool: True if it looks like a file path, False otherwise
|
|
384
|
+
"""
|
|
385
|
+
if not path:
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
path_str = str(path)
|
|
389
|
+
|
|
390
|
+
# Filter out obvious text content (too long or contains newlines)
|
|
391
|
+
if len(path_str) > 500 or '\n' in path_str:
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
# Must contain path separators or start with common path patterns
|
|
395
|
+
has_separator = '/' in path_str or '\\' in path_str
|
|
396
|
+
|
|
397
|
+
# Check for common file extensions
|
|
398
|
+
common_extensions = [
|
|
399
|
+
'.txt', '.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
400
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg',
|
|
401
|
+
'.mp4', '.avi', '.mov', '.mp3', '.wav',
|
|
402
|
+
'.json', '.xml', '.csv', '.html', '.md'
|
|
403
|
+
]
|
|
404
|
+
has_extension = any(path_str.lower().endswith(ext) for ext in common_extensions)
|
|
405
|
+
|
|
406
|
+
# Valid if it has separators or a file extension
|
|
407
|
+
return has_separator or has_extension
|
|
408
|
+
|
|
409
|
+
def _parse_recipients(
|
|
410
|
+
self,
|
|
411
|
+
recipients: Union[List[Actor], Actor, Channel, Chat, str, List[str]],
|
|
412
|
+
provider: NotificationProvider
|
|
413
|
+
) -> Union[List[Actor], Actor, Channel, Chat]:
|
|
414
|
+
"""
|
|
415
|
+
Parse recipients into appropriate format for the provider.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
recipients: Various recipient formats
|
|
419
|
+
provider: The notification provider
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Formatted recipient(s) for the provider
|
|
423
|
+
"""
|
|
424
|
+
# Already formatted objects
|
|
425
|
+
if isinstance(recipients, (Actor, Channel, Chat, TeamsChannel, TeamsWebhook)):
|
|
426
|
+
return recipients
|
|
427
|
+
|
|
428
|
+
# List of formatted objects
|
|
429
|
+
if isinstance(recipients, list) and len(recipients) > 0:
|
|
430
|
+
if isinstance(recipients[0], (Actor, Channel, Chat)):
|
|
431
|
+
return recipients
|
|
432
|
+
|
|
433
|
+
# String email address(es)
|
|
434
|
+
if isinstance(recipients, str):
|
|
435
|
+
# Single email
|
|
436
|
+
if '@' in recipients:
|
|
437
|
+
return Actor(
|
|
438
|
+
name=recipients.split('@')[0],
|
|
439
|
+
account={"address": recipients}
|
|
440
|
+
)
|
|
441
|
+
# Might be a chat_id or channel_id
|
|
442
|
+
elif provider == NotificationProvider.TELEGRAM:
|
|
443
|
+
return Chat(chat_id=recipients)
|
|
444
|
+
elif provider == NotificationProvider.SLACK:
|
|
445
|
+
return Channel(channel_id=recipients)
|
|
446
|
+
else:
|
|
447
|
+
# Try as generic actor
|
|
448
|
+
return Actor(
|
|
449
|
+
name="User",
|
|
450
|
+
account={"address": recipients}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# List of email strings
|
|
454
|
+
if isinstance(recipients, list):
|
|
455
|
+
actors = []
|
|
456
|
+
for recipient in recipients:
|
|
457
|
+
if isinstance(recipient, str) and '@' in recipient:
|
|
458
|
+
actors.append(Actor(
|
|
459
|
+
name=recipient.split('@')[0],
|
|
460
|
+
account={"address": recipient}
|
|
461
|
+
))
|
|
462
|
+
elif isinstance(recipient, (Actor, Channel, Chat)):
|
|
463
|
+
actors.append(recipient)
|
|
464
|
+
return actors if actors else recipients
|
|
465
|
+
|
|
466
|
+
# Fallback
|
|
467
|
+
return recipients
|
|
468
|
+
|
|
469
|
+
async def _send_with_provider(
|
|
470
|
+
self,
|
|
471
|
+
provider: NotificationProvider,
|
|
472
|
+
notify_args: Dict[str, Any],
|
|
473
|
+
files: Optional[List[Path]] = None
|
|
474
|
+
) -> Any:
|
|
475
|
+
"""
|
|
476
|
+
Send notification using the specified provider with smart file handling.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
provider: Notification provider to use
|
|
480
|
+
notify_args: Arguments for the notification
|
|
481
|
+
files: Optional list of file attachments
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Provider-specific result
|
|
485
|
+
"""
|
|
486
|
+
if provider == NotificationProvider.EMAIL:
|
|
487
|
+
return await self._send_email(notify_args, files)
|
|
488
|
+
|
|
489
|
+
elif provider == NotificationProvider.SLACK:
|
|
490
|
+
return await self._send_slack(notify_args)
|
|
491
|
+
|
|
492
|
+
elif provider == NotificationProvider.TELEGRAM:
|
|
493
|
+
return await self._send_telegram(notify_args, files)
|
|
494
|
+
|
|
495
|
+
elif provider == NotificationProvider.TEAMS:
|
|
496
|
+
return await self._send_teams(notify_args, files)
|
|
497
|
+
|
|
498
|
+
else:
|
|
499
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
500
|
+
|
|
501
|
+
async def _send_email(
|
|
502
|
+
self,
|
|
503
|
+
notify_args: Dict[str, Any],
|
|
504
|
+
files: Optional[List[Path]] = None
|
|
505
|
+
) -> Any:
|
|
506
|
+
"""Send email notification with attachments."""
|
|
507
|
+
email = Email()
|
|
508
|
+
async with email as conn:
|
|
509
|
+
result = await conn.send(**notify_args)
|
|
510
|
+
return result
|
|
511
|
+
|
|
512
|
+
async def _send_slack(self, notify_args: Dict[str, Any]) -> Any:
|
|
513
|
+
"""Send Slack notification."""
|
|
514
|
+
slack = Slack()
|
|
515
|
+
async with slack as conn:
|
|
516
|
+
result = await conn.send(**notify_args)
|
|
517
|
+
return result
|
|
518
|
+
|
|
519
|
+
async def _send_telegram(
|
|
520
|
+
self,
|
|
521
|
+
notify_args: Dict[str, Any],
|
|
522
|
+
files: Optional[List[Path]] = None
|
|
523
|
+
) -> Any:
|
|
524
|
+
"""
|
|
525
|
+
Send Telegram notification with smart file handling.
|
|
526
|
+
|
|
527
|
+
Images are sent as photos, documents as documents, videos as videos.
|
|
528
|
+
"""
|
|
529
|
+
telegram = Telegram()
|
|
530
|
+
results = []
|
|
531
|
+
|
|
532
|
+
async with telegram as conn:
|
|
533
|
+
# If files, send them appropriately based on type
|
|
534
|
+
if files and len(files) > 0:
|
|
535
|
+
# Categorize files
|
|
536
|
+
categorized_files = self._categorize_files(files)
|
|
537
|
+
|
|
538
|
+
# Send images as photos
|
|
539
|
+
for image in categorized_files[FileType.IMAGE]:
|
|
540
|
+
try:
|
|
541
|
+
result = await conn.send_photo(
|
|
542
|
+
photo=image,
|
|
543
|
+
caption=notify_args.get("message", "")[:1024], # Telegram caption limit
|
|
544
|
+
disable_notification=notify_args.get("disable_notification", False)
|
|
545
|
+
)
|
|
546
|
+
results.append({
|
|
547
|
+
"type": "photo",
|
|
548
|
+
"file": str(image),
|
|
549
|
+
"result": result
|
|
550
|
+
})
|
|
551
|
+
self.logger.info(f"Sent image via Telegram: {image.name}")
|
|
552
|
+
except Exception as e:
|
|
553
|
+
self.logger.error(f"Failed to send image {image}: {e}")
|
|
554
|
+
|
|
555
|
+
# Send videos
|
|
556
|
+
for video in categorized_files[FileType.VIDEO]:
|
|
557
|
+
try:
|
|
558
|
+
result = await conn.send_video(
|
|
559
|
+
video=video,
|
|
560
|
+
caption=notify_args.get("message", "")[:1024],
|
|
561
|
+
supports_streaming=True,
|
|
562
|
+
disable_notification=notify_args.get("disable_notification", False)
|
|
563
|
+
)
|
|
564
|
+
results.append({
|
|
565
|
+
"type": "video",
|
|
566
|
+
"file": str(video),
|
|
567
|
+
"result": result
|
|
568
|
+
})
|
|
569
|
+
self.logger.info(f"Sent video via Telegram: {video.name}")
|
|
570
|
+
except Exception as e:
|
|
571
|
+
self.logger.error(f"Failed to send video {video}: {e}")
|
|
572
|
+
|
|
573
|
+
# Send audio files
|
|
574
|
+
for audio in categorized_files[FileType.AUDIO]:
|
|
575
|
+
try:
|
|
576
|
+
result = await conn.send_audio(
|
|
577
|
+
audio=audio,
|
|
578
|
+
caption=notify_args.get("message", "")[:1024],
|
|
579
|
+
disable_notification=notify_args.get("disable_notification", False)
|
|
580
|
+
)
|
|
581
|
+
results.append({
|
|
582
|
+
"type": "audio",
|
|
583
|
+
"file": str(audio),
|
|
584
|
+
"result": result
|
|
585
|
+
})
|
|
586
|
+
self.logger.info(f"Sent audio via Telegram: {audio.name}")
|
|
587
|
+
except Exception as e:
|
|
588
|
+
self.logger.error(f"Failed to send audio {audio}: {e}")
|
|
589
|
+
|
|
590
|
+
# Send documents (PDFs, Excel, etc.)
|
|
591
|
+
for document in categorized_files[FileType.DOCUMENT]:
|
|
592
|
+
try:
|
|
593
|
+
result = await conn.send_document(
|
|
594
|
+
document=document,
|
|
595
|
+
caption=notify_args.get("message", "")[:1024],
|
|
596
|
+
disable_notification=notify_args.get("disable_notification", False)
|
|
597
|
+
)
|
|
598
|
+
results.append({
|
|
599
|
+
"type": "document",
|
|
600
|
+
"file": str(document),
|
|
601
|
+
"result": result
|
|
602
|
+
})
|
|
603
|
+
self.logger.info(f"Sent document via Telegram: {document.name}")
|
|
604
|
+
except Exception as e:
|
|
605
|
+
self.logger.error(f"Failed to send document {document}: {e}")
|
|
606
|
+
|
|
607
|
+
# If message was used as caption, we're done
|
|
608
|
+
# Otherwise send a separate text message
|
|
609
|
+
if len(results) > 0 and notify_args.get("message"):
|
|
610
|
+
# Message already sent as captions
|
|
611
|
+
pass
|
|
612
|
+
else:
|
|
613
|
+
# Send text message
|
|
614
|
+
text_result = await conn.send(**notify_args)
|
|
615
|
+
results.append({
|
|
616
|
+
"type": "text",
|
|
617
|
+
"result": text_result
|
|
618
|
+
})
|
|
619
|
+
else:
|
|
620
|
+
# No files, just send text message
|
|
621
|
+
result = await conn.send(**notify_args)
|
|
622
|
+
results.append({
|
|
623
|
+
"type": "text",
|
|
624
|
+
"result": result
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
return results
|
|
628
|
+
|
|
629
|
+
async def _send_teams(
|
|
630
|
+
self,
|
|
631
|
+
notify_args: Dict[str, Any],
|
|
632
|
+
files: Optional[List[Path]] = None
|
|
633
|
+
) -> Any:
|
|
634
|
+
"""
|
|
635
|
+
Send Microsoft Teams notification.
|
|
636
|
+
|
|
637
|
+
Teams supports file attachments in cards.
|
|
638
|
+
"""
|
|
639
|
+
teams = Teams(
|
|
640
|
+
as_user=True,
|
|
641
|
+
tenant_id=TEAMS_NOTIFY_TENANT_ID,
|
|
642
|
+
client_id=TEAMS_NOTIFY_CLIENT_ID,
|
|
643
|
+
client_secret=TEAMS_NOTIFY_CLIENT_SECRET,
|
|
644
|
+
username=TEAMS_NOTIFY_USERNAME,
|
|
645
|
+
password=TEAMS_NOTIFY_PASSWORD
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# If files provided, we can add them as attachments or links in the card
|
|
649
|
+
if files and len(files) > 0:
|
|
650
|
+
# Create a Teams card with file information
|
|
651
|
+
message_text = notify_args.get("message", "")
|
|
652
|
+
|
|
653
|
+
# Add file list to message
|
|
654
|
+
file_list = "\n".join([f"- {f.name}" for f in files])
|
|
655
|
+
enhanced_message = f"{message_text}\n\n**Attached Files:**\n{file_list}"
|
|
656
|
+
|
|
657
|
+
notify_args["message"] = enhanced_message
|
|
658
|
+
|
|
659
|
+
# Note: Teams API has limitations on direct file uploads
|
|
660
|
+
# For full file upload support, would need to use Graph API file upload
|
|
661
|
+
self.logger.info(
|
|
662
|
+
f"Teams notification with {len(files)} files (file list added to message)"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
async with teams as conn:
|
|
666
|
+
result = await conn.send(**notify_args)
|
|
667
|
+
return result
|
|
668
|
+
|
|
669
|
+
# Convenience methods for specific providers
|
|
670
|
+
|
|
671
|
+
async def send_email(
|
|
672
|
+
self,
|
|
673
|
+
message: str,
|
|
674
|
+
recipients: Union[List[str], str],
|
|
675
|
+
subject: str,
|
|
676
|
+
report: Optional[Any] = None,
|
|
677
|
+
template: Optional[str] = None,
|
|
678
|
+
**kwargs
|
|
679
|
+
) -> Dict[str, Any]:
|
|
680
|
+
"""Convenience method for sending emails."""
|
|
681
|
+
return await self.send_notification(
|
|
682
|
+
message=message,
|
|
683
|
+
recipients=recipients,
|
|
684
|
+
provider=NotificationProvider.EMAIL,
|
|
685
|
+
subject=subject,
|
|
686
|
+
report=report,
|
|
687
|
+
template=template,
|
|
688
|
+
**kwargs
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
async def send_slack_message(
|
|
692
|
+
self,
|
|
693
|
+
message: str,
|
|
694
|
+
channel: Union[Channel, str],
|
|
695
|
+
report: Optional[Any] = None,
|
|
696
|
+
**kwargs
|
|
697
|
+
) -> Dict[str, Any]:
|
|
698
|
+
"""Convenience method for sending Slack messages."""
|
|
699
|
+
return await self.send_notification(
|
|
700
|
+
message=message,
|
|
701
|
+
recipients=channel,
|
|
702
|
+
provider=NotificationProvider.SLACK,
|
|
703
|
+
report=report,
|
|
704
|
+
**kwargs
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
async def send_telegram_message(
|
|
708
|
+
self,
|
|
709
|
+
message: str,
|
|
710
|
+
chat: Union[Chat, str],
|
|
711
|
+
report: Optional[Any] = None,
|
|
712
|
+
disable_notification: bool = False,
|
|
713
|
+
**kwargs
|
|
714
|
+
) -> Dict[str, Any]:
|
|
715
|
+
"""
|
|
716
|
+
Convenience method for sending Telegram messages.
|
|
717
|
+
|
|
718
|
+
Automatically detects and sends images as photos, documents as files.
|
|
719
|
+
"""
|
|
720
|
+
return await self.send_notification(
|
|
721
|
+
message=message,
|
|
722
|
+
recipients=chat,
|
|
723
|
+
provider=NotificationProvider.TELEGRAM,
|
|
724
|
+
report=report,
|
|
725
|
+
disable_notification=disable_notification,
|
|
726
|
+
**kwargs
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
async def send_teams_message(
|
|
730
|
+
self,
|
|
731
|
+
message: str,
|
|
732
|
+
recipient: Union[Actor, TeamsChannel, TeamsWebhook],
|
|
733
|
+
report: Optional[Any] = None,
|
|
734
|
+
**kwargs
|
|
735
|
+
) -> Dict[str, Any]:
|
|
736
|
+
"""Convenience method for sending Teams messages."""
|
|
737
|
+
return await self.send_notification(
|
|
738
|
+
message=message,
|
|
739
|
+
recipients=recipient,
|
|
740
|
+
provider=NotificationProvider.TEAMS,
|
|
741
|
+
report=report,
|
|
742
|
+
**kwargs
|
|
743
|
+
)
|