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
parrot/tools/msteams.py
ADDED
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MS Teams Toolkit - A unified toolkit for Microsoft Teams operations.
|
|
3
|
+
|
|
4
|
+
This toolkit wraps common MS Teams actions as async tools, extending AbstractToolkit.
|
|
5
|
+
It supports authentication via Azure AD (service principal or delegated user).
|
|
6
|
+
|
|
7
|
+
Dependencies:
|
|
8
|
+
- msgraph-sdk
|
|
9
|
+
- azure-identity
|
|
10
|
+
- msal
|
|
11
|
+
- aiohttp
|
|
12
|
+
- pydantic
|
|
13
|
+
|
|
14
|
+
Example usage:
|
|
15
|
+
toolkit = MSTeamsToolkit(
|
|
16
|
+
tenant_id="your-tenant-id",
|
|
17
|
+
client_id="your-client-id",
|
|
18
|
+
client_secret="your-client-secret",
|
|
19
|
+
as_user=False # Set to True for delegated auth
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Initialize the toolkit
|
|
23
|
+
await toolkit.connect()
|
|
24
|
+
|
|
25
|
+
# Get all tools
|
|
26
|
+
tools = toolkit.get_tools()
|
|
27
|
+
|
|
28
|
+
# Or use methods directly
|
|
29
|
+
await toolkit.send_message_to_channel(
|
|
30
|
+
team_id="team-id",
|
|
31
|
+
channel_id="channel-id",
|
|
32
|
+
message="Hello Teams!"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
Notes:
|
|
36
|
+
- All public async methods become tools via AbstractToolkit
|
|
37
|
+
- Supports both application permissions and delegated user permissions
|
|
38
|
+
- Adaptive cards can be sent as strings, dicts, or created via create_adaptive_card
|
|
39
|
+
"""
|
|
40
|
+
import contextlib
|
|
41
|
+
from typing import Dict, List, Optional, Union, Any
|
|
42
|
+
from datetime import datetime, timezone, timedelta
|
|
43
|
+
import json
|
|
44
|
+
import uuid
|
|
45
|
+
import msal
|
|
46
|
+
from pydantic import BaseModel, Field
|
|
47
|
+
import aiohttp
|
|
48
|
+
from azure.identity.aio import ClientSecretCredential
|
|
49
|
+
from azure.identity import UsernamePasswordCredential
|
|
50
|
+
from msgraph import GraphServiceClient
|
|
51
|
+
from msgraph.generated.models.chat import Chat
|
|
52
|
+
from msgraph.generated.models.chat_type import ChatType
|
|
53
|
+
from msgraph.generated.models.chat_message import ChatMessage
|
|
54
|
+
from msgraph.generated.models.chat_message_collection_response import ChatMessageCollectionResponse
|
|
55
|
+
from msgraph.generated.models.item_body import ItemBody
|
|
56
|
+
from msgraph.generated.models.body_type import BodyType
|
|
57
|
+
from msgraph.generated.models.chat_message_attachment import ChatMessageAttachment
|
|
58
|
+
from msgraph.generated.models.aad_user_conversation_member import AadUserConversationMember
|
|
59
|
+
from msgraph.generated.models.o_data_errors.o_data_error import ODataError
|
|
60
|
+
from msgraph.generated.chats.chats_request_builder import ChatsRequestBuilder
|
|
61
|
+
from msgraph.generated.chats.item.messages.messages_request_builder import MessagesRequestBuilder
|
|
62
|
+
from msgraph.generated.teams.teams_request_builder import TeamsRequestBuilder
|
|
63
|
+
from msgraph.generated.teams.item.channels.channels_request_builder import ChannelsRequestBuilder
|
|
64
|
+
from kiota_abstractions.base_request_configuration import RequestConfiguration
|
|
65
|
+
try:
|
|
66
|
+
from navconfig import config as nav_config
|
|
67
|
+
from navconfig.logging import logging
|
|
68
|
+
except ImportError:
|
|
69
|
+
import logging
|
|
70
|
+
nav_config = None
|
|
71
|
+
|
|
72
|
+
from .toolkit import AbstractToolkit
|
|
73
|
+
from .decorators import tool_schema
|
|
74
|
+
from ..conf import (
|
|
75
|
+
MS_TEAMS_TENANT_ID,
|
|
76
|
+
MS_TEAMS_CLIENT_ID,
|
|
77
|
+
MS_TEAMS_CLIENT_SECRET,
|
|
78
|
+
MS_TEAMS_USERNAME,
|
|
79
|
+
MS_TEAMS_PASSWORD,
|
|
80
|
+
MS_TEAMS_DEFAULT_TEAMS_ID,
|
|
81
|
+
MS_TEAMS_DEFAULT_CHANNEL_ID
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Disable verbose logging for external libraries
|
|
85
|
+
logging.getLogger('msal').setLevel(logging.INFO)
|
|
86
|
+
logging.getLogger('httpcore').setLevel(logging.INFO)
|
|
87
|
+
logging.getLogger('azure').setLevel(logging.WARNING)
|
|
88
|
+
logging.getLogger('hpack').setLevel(logging.INFO)
|
|
89
|
+
logging.getLogger('aiohttp').setLevel(logging.INFO)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ============================================================================
|
|
93
|
+
# Input Schemas
|
|
94
|
+
# ============================================================================
|
|
95
|
+
|
|
96
|
+
class SendMessageToChannelInput(BaseModel):
|
|
97
|
+
"""Input schema for sending message to a Teams channel."""
|
|
98
|
+
team_id: str = Field(description="The Team ID where the channel exists")
|
|
99
|
+
channel_id: str = Field(description="The Channel ID to post the message to")
|
|
100
|
+
webhook_url: Optional[str] = Field(
|
|
101
|
+
default=None,
|
|
102
|
+
description="Incoming webhook URL for the channel (alternative to team_id/channel_id)"
|
|
103
|
+
)
|
|
104
|
+
message: Union[str, Dict[str, Any]] = Field(
|
|
105
|
+
description="Message content: plain text, Adaptive Card JSON string, or dict"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SendMessageToChatInput(BaseModel):
|
|
110
|
+
"""Input schema for sending message to a Teams chat."""
|
|
111
|
+
chat_id: str = Field(description="The Chat ID to send the message to")
|
|
112
|
+
message: Union[str, Dict[str, Any]] = Field(
|
|
113
|
+
description="Message content: plain text, Adaptive Card JSON string, or dict"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SendDirectMessageInput(BaseModel):
|
|
118
|
+
"""Input schema for sending direct message to a user."""
|
|
119
|
+
recipient_email: str = Field(
|
|
120
|
+
description="Email address of the recipient user"
|
|
121
|
+
)
|
|
122
|
+
message: Union[str, Dict[str, Any]] = Field(
|
|
123
|
+
description="Message content: plain text, Adaptive Card JSON string, or dict"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class CreateAdaptiveCardInput(BaseModel):
|
|
128
|
+
"""Input schema for creating an Adaptive Card."""
|
|
129
|
+
title: str = Field(description="Card title")
|
|
130
|
+
body_text: str = Field(description="Main body text of the card")
|
|
131
|
+
image_url: Optional[str] = Field(
|
|
132
|
+
default=None,
|
|
133
|
+
description="Optional image URL to include in the card"
|
|
134
|
+
)
|
|
135
|
+
link_url: Optional[str] = Field(
|
|
136
|
+
default=None,
|
|
137
|
+
description="Optional link URL"
|
|
138
|
+
)
|
|
139
|
+
link_text: Optional[str] = Field(
|
|
140
|
+
default="Learn more",
|
|
141
|
+
description="Text for the link button"
|
|
142
|
+
)
|
|
143
|
+
facts: Optional[List[Dict[str, str]]] = Field(
|
|
144
|
+
default=None,
|
|
145
|
+
description="Optional list of facts, each with 'title' and 'value' keys"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class GetUserInput(BaseModel):
|
|
150
|
+
"""Input schema for getting user information."""
|
|
151
|
+
email: str = Field(description="Email address of the user to look up")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class CreateChatInput(BaseModel):
|
|
155
|
+
"""Input schema for creating a one-on-one chat."""
|
|
156
|
+
recipient_email: str = Field(
|
|
157
|
+
description="Email address of the user to create chat with"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
class FindTeamByNameInput(BaseModel):
|
|
161
|
+
"""Input schema for finding a team by name."""
|
|
162
|
+
team_name: str = Field(description="Name of the team to search for")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class FindChannelByNameInput(BaseModel):
|
|
166
|
+
"""Input schema for finding a channel by name within a team."""
|
|
167
|
+
team_id: str = Field(description="The Team ID to search in")
|
|
168
|
+
channel_name: str = Field(description="Name of the channel to search for")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class GetChannelDetailsInput(BaseModel):
|
|
172
|
+
"""Input schema for getting channel details."""
|
|
173
|
+
team_id: str = Field(description="The Team ID")
|
|
174
|
+
channel_id: str = Field(description="The Channel ID")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class GetChannelMembersInput(BaseModel):
|
|
178
|
+
"""Input schema for getting channel members."""
|
|
179
|
+
team_id: str = Field(description="The Team ID")
|
|
180
|
+
channel_id: str = Field(description="The Channel ID")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ExtractChannelMessagesInput(BaseModel):
|
|
184
|
+
"""Input schema for extracting channel messages."""
|
|
185
|
+
team_id: str = Field(description="The Team ID")
|
|
186
|
+
channel_id: str = Field(description="The Channel ID")
|
|
187
|
+
start_time: Optional[str] = Field(
|
|
188
|
+
default=None,
|
|
189
|
+
description="Start time for message filter (ISO format, e.g., '2025-01-01T00:00:00Z')"
|
|
190
|
+
)
|
|
191
|
+
end_time: Optional[str] = Field(
|
|
192
|
+
default=None,
|
|
193
|
+
description="End time for message filter (ISO format, e.g., '2025-01-31T23:59:59Z')"
|
|
194
|
+
)
|
|
195
|
+
max_messages: Optional[int] = Field(
|
|
196
|
+
default=None,
|
|
197
|
+
description="Maximum number of messages to retrieve"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ListUserChatsInput(BaseModel):
|
|
202
|
+
"""Input schema for listing user chats."""
|
|
203
|
+
max_chats: Optional[int] = Field(
|
|
204
|
+
default=50,
|
|
205
|
+
description="Maximum number of chats to retrieve"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class FindChatByNameInput(BaseModel):
|
|
210
|
+
"""Input schema for finding a chat by name/topic."""
|
|
211
|
+
chat_name: str = Field(description="Name or topic of the chat to search for")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class FindOneOnOneChatInput(BaseModel):
|
|
215
|
+
"""Input schema for finding a one-on-one chat between two users."""
|
|
216
|
+
user1_email: str = Field(description="Email of the first user")
|
|
217
|
+
user2_email: str = Field(description="Email of the second user")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class GetChatMessagesInput(BaseModel):
|
|
221
|
+
"""Input schema for getting messages from a chat."""
|
|
222
|
+
chat_id: str = Field(description="The Chat ID")
|
|
223
|
+
start_time: Optional[str] = Field(
|
|
224
|
+
default=None,
|
|
225
|
+
description="Start time for message filter (ISO format)"
|
|
226
|
+
)
|
|
227
|
+
end_time: Optional[str] = Field(
|
|
228
|
+
default=None,
|
|
229
|
+
description="End time for message filter (ISO format)"
|
|
230
|
+
)
|
|
231
|
+
max_messages: Optional[int] = Field(
|
|
232
|
+
default=50,
|
|
233
|
+
description="Maximum number of messages to retrieve"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ChatMessagesFromUserInput(BaseModel):
|
|
238
|
+
"""Input schema for extracting messages from a specific user in a chat."""
|
|
239
|
+
chat_id: str = Field(description="The Chat ID")
|
|
240
|
+
user_email: str = Field(description="Email of the user whose messages to extract")
|
|
241
|
+
start_time: Optional[str] = Field(
|
|
242
|
+
default=None,
|
|
243
|
+
description="Start time for message filter (ISO format)"
|
|
244
|
+
)
|
|
245
|
+
end_time: Optional[str] = Field(
|
|
246
|
+
default=None,
|
|
247
|
+
description="End time for message filter (ISO format)"
|
|
248
|
+
)
|
|
249
|
+
max_messages: Optional[int] = Field(
|
|
250
|
+
default=50,
|
|
251
|
+
description="Maximum number of messages to retrieve"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class MSTeamsToolkit(AbstractToolkit):
|
|
256
|
+
"""
|
|
257
|
+
Toolkit for interacting with Microsoft Teams via Microsoft Graph API.
|
|
258
|
+
|
|
259
|
+
Provides methods for:
|
|
260
|
+
- Sending messages to channels
|
|
261
|
+
- Sending messages to chats
|
|
262
|
+
- Sending direct messages to users
|
|
263
|
+
- Creating adaptive cards
|
|
264
|
+
- Managing chats and users
|
|
265
|
+
- Finding teams and channels by name
|
|
266
|
+
- Extracting messages from channels and chats
|
|
267
|
+
All public async methods are exposed as tools via AbstractToolkit.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(
|
|
271
|
+
self,
|
|
272
|
+
tenant_id: Optional[str] = None,
|
|
273
|
+
client_id: Optional[str] = None,
|
|
274
|
+
client_secret: Optional[str] = None,
|
|
275
|
+
as_user: bool = False,
|
|
276
|
+
username: Optional[str] = None,
|
|
277
|
+
password: Optional[str] = None,
|
|
278
|
+
**kwargs
|
|
279
|
+
):
|
|
280
|
+
"""
|
|
281
|
+
Initialize the MS Teams toolkit.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
tenant_id: Azure AD tenant ID
|
|
285
|
+
client_id: Azure AD application client ID
|
|
286
|
+
client_secret: Azure AD application client secret (for app-only auth)
|
|
287
|
+
as_user: If True, use delegated user permissions instead of application
|
|
288
|
+
username: Username for delegated auth (required if as_user=True)
|
|
289
|
+
password: Password for delegated auth (required if as_user=True)
|
|
290
|
+
**kwargs: Additional toolkit arguments
|
|
291
|
+
"""
|
|
292
|
+
super().__init__(**kwargs)
|
|
293
|
+
|
|
294
|
+
# Load from config if not provided
|
|
295
|
+
if nav_config:
|
|
296
|
+
self.tenant_id = tenant_id or MS_TEAMS_TENANT_ID or nav_config.get('MS_TEAMS_TENANT_ID')
|
|
297
|
+
self.client_id = client_id or MS_TEAMS_CLIENT_ID or nav_config.get('MS_TEAMS_CLIENT_ID')
|
|
298
|
+
self.client_secret = client_secret or MS_TEAMS_CLIENT_SECRET or nav_config.get('MS_TEAMS_CLIENT_SECRET') # noqa
|
|
299
|
+
self.username = username or MS_TEAMS_USERNAME or nav_config.get('O365_USER')
|
|
300
|
+
self.password = password or MS_TEAMS_PASSWORD or nav_config.get('O365_PASSWORD')
|
|
301
|
+
else:
|
|
302
|
+
self.tenant_id = tenant_id
|
|
303
|
+
self.client_id = client_id
|
|
304
|
+
self.client_secret = client_secret
|
|
305
|
+
self.username = username
|
|
306
|
+
self.password = password
|
|
307
|
+
|
|
308
|
+
if not all([self.tenant_id, self.client_id]):
|
|
309
|
+
raise ValueError(
|
|
310
|
+
"tenant_id and client_id are required. "
|
|
311
|
+
"Provide them as arguments or set MS_TEAMS_TENANT_ID and MS_TEAMS_CLIENT_ID in config."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
self.as_user = as_user
|
|
315
|
+
|
|
316
|
+
if self.as_user and not all([self.username, self.password]):
|
|
317
|
+
raise ValueError(
|
|
318
|
+
"username and password are required when as_user=True. "
|
|
319
|
+
"Provide them as arguments or set O365_USER and O365_PASSWORD in config."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if not self.as_user and not self.client_secret:
|
|
323
|
+
raise ValueError(
|
|
324
|
+
"client_secret is required for application auth. "
|
|
325
|
+
"Provide it as argument or set MS_TEAMS_CLIENT_SECRET in config."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# These will be set during connect()
|
|
329
|
+
self._client = None
|
|
330
|
+
self._graph: Optional[GraphServiceClient] = None
|
|
331
|
+
self._token = None
|
|
332
|
+
self._owner_id = None
|
|
333
|
+
self._connected = False
|
|
334
|
+
|
|
335
|
+
async def _connect(self):
|
|
336
|
+
"""
|
|
337
|
+
Establish connection to Microsoft Graph API.
|
|
338
|
+
|
|
339
|
+
This method must be called before using any toolkit methods.
|
|
340
|
+
"""
|
|
341
|
+
if self._connected:
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
scopes = ["https://graph.microsoft.com/.default"]
|
|
345
|
+
authority = f"https://login.microsoftonline.com/{self.tenant_id}"
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
if self.as_user:
|
|
349
|
+
# Delegated user authentication
|
|
350
|
+
app = msal.PublicClientApplication(
|
|
351
|
+
self.client_id,
|
|
352
|
+
authority=authority
|
|
353
|
+
)
|
|
354
|
+
result = app.acquire_token_by_username_password(
|
|
355
|
+
username=self.username,
|
|
356
|
+
password=self.password,
|
|
357
|
+
scopes=scopes
|
|
358
|
+
)
|
|
359
|
+
self._client = UsernamePasswordCredential(
|
|
360
|
+
tenant_id=self.tenant_id,
|
|
361
|
+
client_id=self.client_id,
|
|
362
|
+
username=self.username,
|
|
363
|
+
password=self.password
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
# Application authentication
|
|
367
|
+
app = msal.ConfidentialClientApplication(
|
|
368
|
+
self.client_id,
|
|
369
|
+
authority=authority,
|
|
370
|
+
client_credential=self.client_secret
|
|
371
|
+
)
|
|
372
|
+
result = app.acquire_token_for_client(scopes=scopes)
|
|
373
|
+
self._client = ClientSecretCredential(
|
|
374
|
+
tenant_id=self.tenant_id,
|
|
375
|
+
client_id=self.client_id,
|
|
376
|
+
client_secret=self.client_secret
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Extract token
|
|
380
|
+
if "access_token" not in result:
|
|
381
|
+
error = result.get("error", "Unknown error")
|
|
382
|
+
desc = result.get("error_description", "No description")
|
|
383
|
+
raise RuntimeError(f"Authentication failed: {error} - {desc}")
|
|
384
|
+
|
|
385
|
+
self._token = result["access_token"]
|
|
386
|
+
|
|
387
|
+
# Create Graph client
|
|
388
|
+
self._graph = GraphServiceClient(
|
|
389
|
+
credentials=self._client,
|
|
390
|
+
scopes=scopes
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Get owner ID if using delegated auth
|
|
394
|
+
if self.as_user:
|
|
395
|
+
me = await self._graph.me.get()
|
|
396
|
+
self._owner_id = me.id
|
|
397
|
+
|
|
398
|
+
self._connected = True
|
|
399
|
+
logging.info("Successfully connected to Microsoft Teams")
|
|
400
|
+
|
|
401
|
+
except Exception as e:
|
|
402
|
+
raise RuntimeError(f"Failed to connect to Microsoft Teams: {e}") from e
|
|
403
|
+
|
|
404
|
+
async def _ensure_connected(self):
|
|
405
|
+
"""Ensure the toolkit is connected before operations."""
|
|
406
|
+
if not self._connected:
|
|
407
|
+
await self._connect()
|
|
408
|
+
|
|
409
|
+
@tool_schema(SendMessageToChannelInput)
|
|
410
|
+
async def send_message_to_channel(
|
|
411
|
+
self,
|
|
412
|
+
team_id: Optional[str] = None,
|
|
413
|
+
channel_id: Optional[str] = None,
|
|
414
|
+
webhook_url: Optional[str] = None,
|
|
415
|
+
message: Union[str, Dict[str, Any]] = None
|
|
416
|
+
) -> Dict[str, Any]:
|
|
417
|
+
"""
|
|
418
|
+
Send a message or Adaptive Card to a public Teams channel.
|
|
419
|
+
|
|
420
|
+
Can use either:
|
|
421
|
+
1. Webhook URL (recommended for application permissions) - works without Graph API
|
|
422
|
+
2. Team ID + Channel ID (requires delegated user permissions)
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
team_id: The Team ID where the channel exists (not needed if webhook_url is provided)
|
|
426
|
+
channel_id: The Channel ID to post the message to (not needed if webhook_url is provided)
|
|
427
|
+
webhook_url: Incoming webhook URL for the channel (alternative to team_id/channel_id)
|
|
428
|
+
message: Message content - can be:
|
|
429
|
+
- Plain text string
|
|
430
|
+
- Adaptive Card JSON string
|
|
431
|
+
- Dict with 'body' and 'attachments' keys
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Dict containing the sent message information
|
|
435
|
+
|
|
436
|
+
Note:
|
|
437
|
+
With application permissions, you must use webhook_url.
|
|
438
|
+
With delegated user permissions, you can use either method.
|
|
439
|
+
"""
|
|
440
|
+
await self._ensure_connected()
|
|
441
|
+
|
|
442
|
+
# Parse and prepare the message
|
|
443
|
+
prepared_message = await self._prepare_message(message)
|
|
444
|
+
|
|
445
|
+
if webhook_url:
|
|
446
|
+
return await self._send_via_webhook(webhook_url, prepared_message)
|
|
447
|
+
|
|
448
|
+
if not team_id or not channel_id:
|
|
449
|
+
team_id = MS_TEAMS_DEFAULT_TEAMS_ID
|
|
450
|
+
channel_id = MS_TEAMS_DEFAULT_CHANNEL_ID
|
|
451
|
+
|
|
452
|
+
# Create the ChatMessage request
|
|
453
|
+
request_body = ChatMessage(
|
|
454
|
+
subject=None,
|
|
455
|
+
body=ItemBody(
|
|
456
|
+
content_type=BodyType.Html,
|
|
457
|
+
content=prepared_message["body"]["content"]
|
|
458
|
+
),
|
|
459
|
+
attachments=[
|
|
460
|
+
ChatMessageAttachment(
|
|
461
|
+
id=att.get("id"),
|
|
462
|
+
content_type=att.get(
|
|
463
|
+
"contentType",
|
|
464
|
+
"application/vnd.microsoft.card.adaptive"
|
|
465
|
+
),
|
|
466
|
+
content=att.get("content", ""),
|
|
467
|
+
content_url=None,
|
|
468
|
+
name=None,
|
|
469
|
+
thumbnail_url=None,
|
|
470
|
+
)
|
|
471
|
+
for att in prepared_message.get("attachments", [])
|
|
472
|
+
]
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Send the message
|
|
476
|
+
result = await self._graph.teams.by_team_id(
|
|
477
|
+
team_id
|
|
478
|
+
).channels.by_channel_id(channel_id).messages.post(request_body)
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
"id": result.id,
|
|
482
|
+
"created_datetime": str(result.created_date_time),
|
|
483
|
+
"web_url": result.web_url,
|
|
484
|
+
"success": True
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@tool_schema(SendMessageToChatInput)
|
|
488
|
+
async def send_message_to_chat(
|
|
489
|
+
self,
|
|
490
|
+
chat_id: str,
|
|
491
|
+
message: Union[str, Dict[str, Any]]
|
|
492
|
+
) -> Dict[str, Any]:
|
|
493
|
+
"""
|
|
494
|
+
Send a message or Adaptive Card to a private chat (one-to-one or group chat).
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
chat_id: The Chat ID to send the message to
|
|
498
|
+
message: Message content - can be:
|
|
499
|
+
- Plain text string
|
|
500
|
+
- Adaptive Card JSON string
|
|
501
|
+
- Dict with 'body' and 'attachments' keys
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Dict containing the sent message information
|
|
505
|
+
"""
|
|
506
|
+
await self._ensure_connected()
|
|
507
|
+
|
|
508
|
+
# Parse and prepare the message
|
|
509
|
+
prepared_message = await self._prepare_message(message)
|
|
510
|
+
|
|
511
|
+
# Create the ChatMessage request
|
|
512
|
+
request_body = ChatMessage(
|
|
513
|
+
subject=None,
|
|
514
|
+
body=ItemBody(
|
|
515
|
+
content_type=BodyType.Html,
|
|
516
|
+
content=prepared_message["body"]["content"]
|
|
517
|
+
),
|
|
518
|
+
attachments=[
|
|
519
|
+
ChatMessageAttachment(
|
|
520
|
+
id=att.get("id"),
|
|
521
|
+
content_type=att.get(
|
|
522
|
+
"contentType",
|
|
523
|
+
"application/vnd.microsoft.card.adaptive"
|
|
524
|
+
),
|
|
525
|
+
content=att.get("content", ""),
|
|
526
|
+
content_url=None,
|
|
527
|
+
name=None,
|
|
528
|
+
thumbnail_url=None,
|
|
529
|
+
)
|
|
530
|
+
for att in prepared_message.get("attachments", [])
|
|
531
|
+
]
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Send the message
|
|
535
|
+
result = await self._graph.chats.by_chat_id(chat_id).messages.post(request_body)
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"id": result.id,
|
|
539
|
+
"created_datetime": str(result.created_date_time),
|
|
540
|
+
"web_url": result.web_url,
|
|
541
|
+
"success": True
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@tool_schema(SendDirectMessageInput)
|
|
545
|
+
async def send_direct_message(
|
|
546
|
+
self,
|
|
547
|
+
recipient_email: str,
|
|
548
|
+
message: Union[str, Dict[str, Any]]
|
|
549
|
+
) -> Dict[str, Any]:
|
|
550
|
+
"""
|
|
551
|
+
Send a direct message or Adaptive Card to a user identified by email address.
|
|
552
|
+
|
|
553
|
+
This method will:
|
|
554
|
+
1. Look up the user by email
|
|
555
|
+
2. Find or create a one-on-one chat with the user
|
|
556
|
+
3. Send the message to that chat
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
recipient_email: Email address of the recipient user
|
|
560
|
+
message: Message content - can be:
|
|
561
|
+
- Plain text string
|
|
562
|
+
- Adaptive Card JSON string
|
|
563
|
+
- Dict with 'body' and 'attachments' keys
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Dict containing the sent message information
|
|
567
|
+
"""
|
|
568
|
+
await self._ensure_connected()
|
|
569
|
+
|
|
570
|
+
# Get the recipient user
|
|
571
|
+
user = await self.get_user(recipient_email)
|
|
572
|
+
user_id = user["id"]
|
|
573
|
+
|
|
574
|
+
# Find or create chat
|
|
575
|
+
chat_id = await self._find_or_create_chat(user_id)
|
|
576
|
+
|
|
577
|
+
# Send the message to the chat
|
|
578
|
+
return await self.send_message_to_chat(chat_id, message)
|
|
579
|
+
|
|
580
|
+
@tool_schema(CreateAdaptiveCardInput)
|
|
581
|
+
async def create_adaptive_card(
|
|
582
|
+
self,
|
|
583
|
+
title: str,
|
|
584
|
+
body_text: str,
|
|
585
|
+
image_url: Optional[str] = None,
|
|
586
|
+
link_url: Optional[str] = None,
|
|
587
|
+
link_text: str = "Learn more",
|
|
588
|
+
facts: Optional[List[Dict[str, str]]] = None
|
|
589
|
+
) -> Dict[str, Any]:
|
|
590
|
+
"""
|
|
591
|
+
Create a basic Adaptive Card that can be used in Teams messages.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
title: Card title
|
|
595
|
+
body_text: Main body text of the card
|
|
596
|
+
image_url: Optional image URL to include in the card
|
|
597
|
+
link_url: Optional link URL for a button
|
|
598
|
+
link_text: Text for the link button (default: "Learn more")
|
|
599
|
+
facts: Optional list of facts, each with 'title' and 'value' keys
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Dict representing an Adaptive Card that can be passed to send methods
|
|
603
|
+
"""
|
|
604
|
+
# Build the card body
|
|
605
|
+
card_body = [
|
|
606
|
+
{
|
|
607
|
+
"type": "TextBlock",
|
|
608
|
+
"text": title,
|
|
609
|
+
"weight": "Bolder",
|
|
610
|
+
"size": "Large",
|
|
611
|
+
"wrap": True
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"type": "TextBlock",
|
|
615
|
+
"text": body_text,
|
|
616
|
+
"wrap": True,
|
|
617
|
+
"spacing": "Medium"
|
|
618
|
+
}
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
# Add image if provided
|
|
622
|
+
if image_url:
|
|
623
|
+
card_body.append({
|
|
624
|
+
"type": "Image",
|
|
625
|
+
"url": image_url,
|
|
626
|
+
"size": "Large",
|
|
627
|
+
"spacing": "Medium"
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
# Add facts if provided
|
|
631
|
+
if facts:
|
|
632
|
+
fact_set = {
|
|
633
|
+
"type": "FactSet",
|
|
634
|
+
"facts": [
|
|
635
|
+
{"title": f"{fact['title']}:", "value": fact["value"]}
|
|
636
|
+
for fact in facts
|
|
637
|
+
],
|
|
638
|
+
"spacing": "Medium"
|
|
639
|
+
}
|
|
640
|
+
card_body.append(fact_set)
|
|
641
|
+
|
|
642
|
+
# Build the card
|
|
643
|
+
adaptive_card = {
|
|
644
|
+
"type": "AdaptiveCard",
|
|
645
|
+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
646
|
+
"version": "1.4",
|
|
647
|
+
"body": card_body
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
# Add actions if link provided
|
|
651
|
+
if link_url:
|
|
652
|
+
adaptive_card["actions"] = [
|
|
653
|
+
{
|
|
654
|
+
"type": "Action.OpenUrl",
|
|
655
|
+
"title": link_text,
|
|
656
|
+
"url": link_url
|
|
657
|
+
}
|
|
658
|
+
]
|
|
659
|
+
|
|
660
|
+
return adaptive_card
|
|
661
|
+
|
|
662
|
+
@tool_schema(GetUserInput)
|
|
663
|
+
async def get_user(self, email: str) -> Dict[str, Any]:
|
|
664
|
+
"""
|
|
665
|
+
Get user information from Microsoft Graph by email address.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
email: Email address of the user to look up
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
Dict containing user information (id, displayName, mail, etc.)
|
|
672
|
+
"""
|
|
673
|
+
await self._ensure_connected()
|
|
674
|
+
|
|
675
|
+
try:
|
|
676
|
+
# Try direct lookup first
|
|
677
|
+
user_info = await self._graph.users.by_user_id(email).get()
|
|
678
|
+
|
|
679
|
+
if not user_info:
|
|
680
|
+
# If direct lookup fails, search by mail filter
|
|
681
|
+
users = await self._graph.users.get(
|
|
682
|
+
request_configuration=RequestConfiguration(
|
|
683
|
+
query_parameters={
|
|
684
|
+
"$filter": f"mail eq '{email}'"
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
if not users.value:
|
|
690
|
+
raise ValueError(f"No user found with email: {email}")
|
|
691
|
+
|
|
692
|
+
user_info = users.value[0]
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
"id": user_info.id,
|
|
696
|
+
"displayName": user_info.display_name,
|
|
697
|
+
"mail": user_info.mail,
|
|
698
|
+
"userPrincipalName": user_info.user_principal_name,
|
|
699
|
+
"jobTitle": user_info.job_title,
|
|
700
|
+
"officeLocation": user_info.office_location
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
except Exception as e:
|
|
704
|
+
raise RuntimeError(f"Failed to get user info for {email}: {e}") from e
|
|
705
|
+
|
|
706
|
+
@tool_schema(CreateChatInput)
|
|
707
|
+
async def create_one_on_one_chat(self, recipient_email: str) -> Dict[str, Any]:
|
|
708
|
+
"""
|
|
709
|
+
Create a new one-on-one chat with a user (or return existing chat ID).
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
recipient_email: Email address of the user to chat with
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
Dict containing chat information
|
|
716
|
+
"""
|
|
717
|
+
await self._ensure_connected()
|
|
718
|
+
|
|
719
|
+
# Get the recipient user
|
|
720
|
+
user = await self.get_user(recipient_email)
|
|
721
|
+
user_id = user["id"]
|
|
722
|
+
|
|
723
|
+
# Find or create chat
|
|
724
|
+
chat_id = await self._find_or_create_chat(user_id)
|
|
725
|
+
|
|
726
|
+
# Get chat details
|
|
727
|
+
chat = await self._graph.chats.by_chat_id(chat_id).get()
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
"id": chat.id,
|
|
731
|
+
"chatType": str(chat.chat_type),
|
|
732
|
+
"webUrl": chat.web_url,
|
|
733
|
+
"createdDateTime": str(chat.created_date_time)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
@tool_schema(FindTeamByNameInput)
|
|
737
|
+
async def find_team_by_name(self, team_name: str) -> Optional[Dict[str, Any]]:
|
|
738
|
+
"""
|
|
739
|
+
Find a team by its name and return the team information including ID.
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
team_name: Name of the team to search for
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
Dict containing team information (id, displayName, description) or None if not found
|
|
746
|
+
"""
|
|
747
|
+
await self._ensure_connected()
|
|
748
|
+
|
|
749
|
+
try:
|
|
750
|
+
# Get all teams (joined teams if using delegated permissions)
|
|
751
|
+
teams = await self._graph.teams.get()
|
|
752
|
+
|
|
753
|
+
if not teams or not teams.value:
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
# Search for team by name (case-insensitive)
|
|
757
|
+
for team in teams.value:
|
|
758
|
+
if team.display_name and team_name.lower() in team.display_name.lower():
|
|
759
|
+
return {
|
|
760
|
+
"id": team.id,
|
|
761
|
+
"displayName": team.display_name,
|
|
762
|
+
"description": team.description,
|
|
763
|
+
"webUrl": team.web_url if hasattr(team, 'web_url') else None
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return None
|
|
767
|
+
|
|
768
|
+
except Exception as e:
|
|
769
|
+
raise RuntimeError(f"Failed to find team '{team_name}': {e}") from e
|
|
770
|
+
|
|
771
|
+
@tool_schema(FindChannelByNameInput)
|
|
772
|
+
async def find_channel_by_name(
|
|
773
|
+
self,
|
|
774
|
+
team_id: str,
|
|
775
|
+
channel_name: str
|
|
776
|
+
) -> Optional[Dict[str, Any]]:
|
|
777
|
+
"""
|
|
778
|
+
Find a channel by name within a specific team.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
team_id: The Team ID to search in
|
|
782
|
+
channel_name: Name of the channel to search for
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
Dict containing channel information (id, displayName, description) or None if not found
|
|
786
|
+
"""
|
|
787
|
+
await self._ensure_connected()
|
|
788
|
+
|
|
789
|
+
try:
|
|
790
|
+
# Get all channels in the team
|
|
791
|
+
channels = await self._graph.teams.by_team_id(team_id).channels.get()
|
|
792
|
+
|
|
793
|
+
if not channels or not channels.value:
|
|
794
|
+
return None
|
|
795
|
+
|
|
796
|
+
# Search for channel by name (case-insensitive)
|
|
797
|
+
for channel in channels.value:
|
|
798
|
+
if channel.display_name and channel_name.lower() in channel.display_name.lower():
|
|
799
|
+
return {
|
|
800
|
+
"id": channel.id,
|
|
801
|
+
"displayName": channel.display_name,
|
|
802
|
+
"description": channel.description,
|
|
803
|
+
"webUrl": channel.web_url if hasattr(channel, 'web_url') else None,
|
|
804
|
+
"membershipType": str(channel.membership_type) if hasattr(channel, 'membership_type') else None
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return None
|
|
808
|
+
|
|
809
|
+
except Exception as e:
|
|
810
|
+
raise RuntimeError(f"Failed to find channel '{channel_name}' in team {team_id}: {e}") from e
|
|
811
|
+
|
|
812
|
+
@tool_schema(GetChannelDetailsInput)
|
|
813
|
+
async def get_channel_details(
|
|
814
|
+
self,
|
|
815
|
+
team_id: str,
|
|
816
|
+
channel_id: str
|
|
817
|
+
) -> Dict[str, Any]:
|
|
818
|
+
"""
|
|
819
|
+
Get detailed information about a specific channel.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
team_id: The Team ID
|
|
823
|
+
channel_id: The Channel ID
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
Dict containing detailed channel information
|
|
827
|
+
"""
|
|
828
|
+
await self._ensure_connected()
|
|
829
|
+
|
|
830
|
+
try:
|
|
831
|
+
channel = await self._graph.teams.by_team_id(team_id).channels.by_channel_id(channel_id).get()
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
"id": channel.id,
|
|
835
|
+
"displayName": channel.display_name,
|
|
836
|
+
"description": channel.description,
|
|
837
|
+
"email": channel.email if hasattr(channel, 'email') else None,
|
|
838
|
+
"webUrl": channel.web_url if hasattr(channel, 'web_url') else None,
|
|
839
|
+
"membershipType": str(channel.membership_type) if hasattr(channel, 'membership_type') else None,
|
|
840
|
+
"createdDateTime": str(channel.created_date_time) if hasattr(channel, 'created_date_time') else None
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
except Exception as e:
|
|
844
|
+
raise RuntimeError(f"Failed to get channel details: {e}") from e
|
|
845
|
+
|
|
846
|
+
@tool_schema(GetChannelMembersInput)
|
|
847
|
+
async def get_channel_members(
|
|
848
|
+
self,
|
|
849
|
+
team_id: str,
|
|
850
|
+
channel_id: str
|
|
851
|
+
) -> List[Dict[str, Any]]:
|
|
852
|
+
"""
|
|
853
|
+
Get all members of a specific channel.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
team_id: The Team ID
|
|
857
|
+
channel_id: The Channel ID
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
List of dicts containing member information
|
|
861
|
+
"""
|
|
862
|
+
await self._ensure_connected()
|
|
863
|
+
|
|
864
|
+
try:
|
|
865
|
+
members = await self._graph.teams.by_team_id(
|
|
866
|
+
team_id
|
|
867
|
+
).channels.by_channel_id(channel_id).members.get()
|
|
868
|
+
|
|
869
|
+
if not members or not members.value:
|
|
870
|
+
return []
|
|
871
|
+
|
|
872
|
+
members_list = []
|
|
873
|
+
for member in members.value:
|
|
874
|
+
member_info = {
|
|
875
|
+
"id": member.id,
|
|
876
|
+
"displayName": member.display_name if hasattr(member, 'display_name') else None,
|
|
877
|
+
"email": member.email if hasattr(member, 'email') else None,
|
|
878
|
+
"roles": member.roles if hasattr(member, 'roles') else [],
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
# Get user details if available
|
|
882
|
+
if hasattr(member, 'user_id'):
|
|
883
|
+
member_info["userId"] = member.user_id
|
|
884
|
+
|
|
885
|
+
members_list.append(member_info)
|
|
886
|
+
|
|
887
|
+
return members_list
|
|
888
|
+
|
|
889
|
+
except Exception as e:
|
|
890
|
+
raise RuntimeError(f"Failed to get channel members: {e}") from e
|
|
891
|
+
|
|
892
|
+
@tool_schema(ExtractChannelMessagesInput)
|
|
893
|
+
async def extract_channel_messages(
|
|
894
|
+
self,
|
|
895
|
+
team_id: str,
|
|
896
|
+
channel_id: str,
|
|
897
|
+
start_time: Optional[str] = None,
|
|
898
|
+
end_time: Optional[str] = None,
|
|
899
|
+
max_messages: Optional[int] = None
|
|
900
|
+
) -> List[Dict[str, Any]]:
|
|
901
|
+
"""
|
|
902
|
+
Extract messages from a channel within a time range.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
team_id: The Team ID
|
|
906
|
+
channel_id: The Channel ID
|
|
907
|
+
start_time: Start time for message filter (ISO format, e.g., '2025-01-01T00:00:00Z')
|
|
908
|
+
end_time: End time for message filter (ISO format, e.g., '2025-01-31T23:59:59Z')
|
|
909
|
+
max_messages: Maximum number of messages to retrieve
|
|
910
|
+
|
|
911
|
+
Returns:
|
|
912
|
+
List of dicts containing message information
|
|
913
|
+
"""
|
|
914
|
+
await self._ensure_connected()
|
|
915
|
+
|
|
916
|
+
try:
|
|
917
|
+
# Build query parameters
|
|
918
|
+
query_params = {
|
|
919
|
+
"orderby": ["lastModifiedDateTime desc"],
|
|
920
|
+
"top": min(50, max_messages) if max_messages else 50
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
# Add time filter if provided
|
|
924
|
+
if start_time and end_time:
|
|
925
|
+
query_params["filter"] = (
|
|
926
|
+
f"lastModifiedDateTime gt {start_time} and "
|
|
927
|
+
f"lastModifiedDateTime lt {end_time}"
|
|
928
|
+
)
|
|
929
|
+
elif start_time:
|
|
930
|
+
query_params["filter"] = f"lastModifiedDateTime gt {start_time}"
|
|
931
|
+
elif end_time:
|
|
932
|
+
query_params["filter"] = f"lastModifiedDateTime lt {end_time}"
|
|
933
|
+
|
|
934
|
+
# Create request configuration
|
|
935
|
+
request_config = RequestConfiguration(
|
|
936
|
+
query_parameters=query_params
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
# Get messages
|
|
940
|
+
messages = []
|
|
941
|
+
response = await self._graph.teams.by_team_id(
|
|
942
|
+
team_id
|
|
943
|
+
).channels.by_channel_id(channel_id).messages.get(
|
|
944
|
+
request_configuration=request_config
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
if response and response.value:
|
|
948
|
+
messages.extend(response.value)
|
|
949
|
+
|
|
950
|
+
# Handle pagination
|
|
951
|
+
next_link = response.odata_next_link if response else None
|
|
952
|
+
while next_link and (not max_messages or len(messages) < max_messages):
|
|
953
|
+
response = await self._graph.teams.by_team_id(
|
|
954
|
+
team_id
|
|
955
|
+
).channels.by_channel_id(channel_id).messages.with_url(next_link).get()
|
|
956
|
+
|
|
957
|
+
if response and response.value:
|
|
958
|
+
messages.extend(response.value)
|
|
959
|
+
|
|
960
|
+
next_link = response.odata_next_link if response else None
|
|
961
|
+
|
|
962
|
+
# Trim to max_messages if specified
|
|
963
|
+
if max_messages:
|
|
964
|
+
messages = messages[:max_messages]
|
|
965
|
+
|
|
966
|
+
# Convert to dicts
|
|
967
|
+
return self._format_messages(messages)
|
|
968
|
+
|
|
969
|
+
except Exception as e:
|
|
970
|
+
raise RuntimeError(f"Failed to extract channel messages: {e}") from e
|
|
971
|
+
|
|
972
|
+
@tool_schema(ListUserChatsInput)
|
|
973
|
+
async def list_user_chats(self, max_chats: int = 50) -> List[Dict[str, Any]]:
|
|
974
|
+
"""
|
|
975
|
+
List all chats for the current user (requires delegated permissions).
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
max_chats: Maximum number of chats to retrieve (default: 50)
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
List of dicts containing chat information
|
|
982
|
+
"""
|
|
983
|
+
await self._ensure_connected()
|
|
984
|
+
|
|
985
|
+
if not self.as_user:
|
|
986
|
+
raise RuntimeError(
|
|
987
|
+
"Listing user chats requires delegated user permissions. "
|
|
988
|
+
"Initialize toolkit with as_user=True."
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
try:
|
|
992
|
+
# Get chats
|
|
993
|
+
query_params = ChatsRequestBuilder.ChatsRequestBuilderGetQueryParameters(
|
|
994
|
+
expand=["members"],
|
|
995
|
+
top=min(max_chats, 50)
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
request_config = RequestConfiguration(
|
|
999
|
+
query_parameters=query_params
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
chats = []
|
|
1003
|
+
response = await self._graph.chats.get(request_configuration=request_config)
|
|
1004
|
+
|
|
1005
|
+
if response and response.value:
|
|
1006
|
+
chats.extend(response.value)
|
|
1007
|
+
|
|
1008
|
+
# Handle pagination
|
|
1009
|
+
next_link = response.odata_next_link if response else None
|
|
1010
|
+
while next_link and len(chats) < max_chats:
|
|
1011
|
+
response = await self._graph.chats.with_url(next_link).get()
|
|
1012
|
+
|
|
1013
|
+
if response and response.value:
|
|
1014
|
+
chats.extend(response.value)
|
|
1015
|
+
|
|
1016
|
+
next_link = response.odata_next_link if response else None
|
|
1017
|
+
|
|
1018
|
+
# Trim to max_chats
|
|
1019
|
+
chats = chats[:max_chats]
|
|
1020
|
+
|
|
1021
|
+
# Format results
|
|
1022
|
+
chats_list = []
|
|
1023
|
+
for chat in chats:
|
|
1024
|
+
chat_info = {
|
|
1025
|
+
"id": chat.id,
|
|
1026
|
+
"topic": chat.topic,
|
|
1027
|
+
"chatType": str(chat.chat_type),
|
|
1028
|
+
"createdDateTime": str(chat.created_date_time) if hasattr(chat, 'created_date_time') else None,
|
|
1029
|
+
"lastUpdatedDateTime": str(chat.last_updated_date_time) if hasattr(chat, 'last_updated_date_time') else None,
|
|
1030
|
+
"webUrl": chat.web_url if hasattr(chat, 'web_url') else None
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
# Add member info if available
|
|
1034
|
+
if hasattr(chat, 'members') and chat.members:
|
|
1035
|
+
chat_info["members"] = [
|
|
1036
|
+
{
|
|
1037
|
+
"displayName": m.display_name if hasattr(m, 'display_name') else None,
|
|
1038
|
+
"userId": m.user_id if hasattr(m, 'user_id') else None
|
|
1039
|
+
}
|
|
1040
|
+
for m in chat.members
|
|
1041
|
+
]
|
|
1042
|
+
|
|
1043
|
+
chats_list.append(chat_info)
|
|
1044
|
+
|
|
1045
|
+
return chats_list
|
|
1046
|
+
|
|
1047
|
+
except Exception as e:
|
|
1048
|
+
raise RuntimeError(f"Failed to list user chats: {e}") from e
|
|
1049
|
+
|
|
1050
|
+
@tool_schema(FindChatByNameInput)
|
|
1051
|
+
async def find_chat_by_name(self, chat_name: str) -> Optional[Dict[str, Any]]:
|
|
1052
|
+
"""
|
|
1053
|
+
Find a chat by its name/topic (requires delegated permissions).
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
chat_name: Name or topic of the chat to search for
|
|
1057
|
+
|
|
1058
|
+
Returns:
|
|
1059
|
+
Dict containing chat information or None if not found
|
|
1060
|
+
"""
|
|
1061
|
+
await self._ensure_connected()
|
|
1062
|
+
|
|
1063
|
+
if not self.as_user:
|
|
1064
|
+
raise RuntimeError(
|
|
1065
|
+
"Finding chats by name requires delegated user permissions. "
|
|
1066
|
+
"Initialize toolkit with as_user=True."
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
# Get all chats
|
|
1071
|
+
chats = await self.list_user_chats(max_chats=100)
|
|
1072
|
+
|
|
1073
|
+
# Search for chat by name/topic (case-insensitive)
|
|
1074
|
+
return next(
|
|
1075
|
+
(
|
|
1076
|
+
chat
|
|
1077
|
+
for chat in chats
|
|
1078
|
+
if chat.get("topic")
|
|
1079
|
+
and chat_name.lower() in chat["topic"].lower()
|
|
1080
|
+
),
|
|
1081
|
+
None,
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
except Exception as e:
|
|
1085
|
+
raise RuntimeError(
|
|
1086
|
+
f"Failed to find chat '{chat_name}': {e}"
|
|
1087
|
+
) from e
|
|
1088
|
+
|
|
1089
|
+
@tool_schema(FindOneOnOneChatInput)
|
|
1090
|
+
async def find_one_on_one_chat(
|
|
1091
|
+
self,
|
|
1092
|
+
user1_email: str,
|
|
1093
|
+
user2_email: str
|
|
1094
|
+
) -> Optional[Dict[str, Any]]:
|
|
1095
|
+
"""
|
|
1096
|
+
Find a one-on-one chat between two users (requires delegated permissions).
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
user1_email: Email of the first user
|
|
1100
|
+
user2_email: Email of the second user
|
|
1101
|
+
|
|
1102
|
+
Returns:
|
|
1103
|
+
Dict containing chat information or None if not found
|
|
1104
|
+
"""
|
|
1105
|
+
await self._ensure_connected()
|
|
1106
|
+
|
|
1107
|
+
if not self.as_user:
|
|
1108
|
+
raise RuntimeError(
|
|
1109
|
+
"Finding one-on-one chats requires delegated user permissions. "
|
|
1110
|
+
"Initialize toolkit with as_user=True."
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
try:
|
|
1114
|
+
# Get user IDs
|
|
1115
|
+
user1 = await self.get_user(user1_email)
|
|
1116
|
+
user2 = await self.get_user(user2_email)
|
|
1117
|
+
|
|
1118
|
+
user1_id = user1["id"]
|
|
1119
|
+
user2_id = user2["id"]
|
|
1120
|
+
|
|
1121
|
+
# Search for existing chat
|
|
1122
|
+
query_params = ChatsRequestBuilder.ChatsRequestBuilderGetQueryParameters(
|
|
1123
|
+
filter="chatType eq 'oneOnOne'",
|
|
1124
|
+
expand=["members"]
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
request_config = RequestConfiguration(
|
|
1128
|
+
query_parameters=query_params
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
chats = await self._graph.chats.get(request_configuration=request_config)
|
|
1132
|
+
|
|
1133
|
+
if not chats or not chats.value:
|
|
1134
|
+
return None
|
|
1135
|
+
|
|
1136
|
+
# Find chat with both users
|
|
1137
|
+
for chat in chats.value:
|
|
1138
|
+
if not chat.members:
|
|
1139
|
+
continue
|
|
1140
|
+
|
|
1141
|
+
member_ids = {m.user_id for m in chat.members if hasattr(m, 'user_id')}
|
|
1142
|
+
|
|
1143
|
+
if user1_id in member_ids and user2_id in member_ids:
|
|
1144
|
+
return {
|
|
1145
|
+
"id": chat.id,
|
|
1146
|
+
"topic": chat.topic,
|
|
1147
|
+
"chatType": str(chat.chat_type),
|
|
1148
|
+
"webUrl": chat.web_url if hasattr(chat, 'web_url') else None,
|
|
1149
|
+
"createdDateTime": str(chat.created_date_time) if hasattr(chat, 'created_date_time') else None
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return None
|
|
1153
|
+
|
|
1154
|
+
except Exception as e:
|
|
1155
|
+
raise RuntimeError(f"Failed to find one-on-one chat: {e}") from e
|
|
1156
|
+
|
|
1157
|
+
@tool_schema(GetChatMessagesInput)
|
|
1158
|
+
async def get_chat_messages(
|
|
1159
|
+
self,
|
|
1160
|
+
chat_id: str,
|
|
1161
|
+
start_time: Optional[str] = None,
|
|
1162
|
+
end_time: Optional[str] = None,
|
|
1163
|
+
max_messages: int = 50
|
|
1164
|
+
) -> List[Dict[str, Any]]:
|
|
1165
|
+
"""
|
|
1166
|
+
Get messages from a specific chat within a time range.
|
|
1167
|
+
|
|
1168
|
+
Args:
|
|
1169
|
+
chat_id: The Chat ID
|
|
1170
|
+
start_time: Start time for message filter (ISO format)
|
|
1171
|
+
end_time: End time for message filter (ISO format)
|
|
1172
|
+
max_messages: Maximum number of messages to retrieve (default: 50)
|
|
1173
|
+
|
|
1174
|
+
Returns:
|
|
1175
|
+
List of dicts containing message information
|
|
1176
|
+
"""
|
|
1177
|
+
await self._ensure_connected()
|
|
1178
|
+
|
|
1179
|
+
try:
|
|
1180
|
+
# Build query parameters
|
|
1181
|
+
query_params = {
|
|
1182
|
+
"orderby": ["lastModifiedDateTime desc"],
|
|
1183
|
+
"top": min(50, max_messages)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
# Add time filter
|
|
1187
|
+
if start_time and end_time:
|
|
1188
|
+
query_params["filter"] = (
|
|
1189
|
+
f"lastModifiedDateTime gt {start_time} and "
|
|
1190
|
+
f"lastModifiedDateTime lt {end_time}"
|
|
1191
|
+
)
|
|
1192
|
+
elif start_time:
|
|
1193
|
+
query_params["filter"] = f"lastModifiedDateTime gt {start_time}"
|
|
1194
|
+
elif end_time:
|
|
1195
|
+
query_params["filter"] = f"lastModifiedDateTime lt {end_time}"
|
|
1196
|
+
else:
|
|
1197
|
+
# Default to last 24 hours if no time specified
|
|
1198
|
+
start = (datetime.utcnow() - timedelta(days=1)).isoformat() + 'Z'
|
|
1199
|
+
end = datetime.utcnow().isoformat() + 'Z'
|
|
1200
|
+
query_params["filter"] = (
|
|
1201
|
+
f"lastModifiedDateTime gt {start} and "
|
|
1202
|
+
f"lastModifiedDateTime lt {end}"
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
request_config = RequestConfiguration(
|
|
1206
|
+
query_parameters=query_params
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
# Get messages
|
|
1210
|
+
messages = []
|
|
1211
|
+
response = await self._graph.chats.by_chat_id(chat_id).messages.get(
|
|
1212
|
+
request_configuration=request_config
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
if isinstance(response, ChatMessageCollectionResponse) and response.value:
|
|
1216
|
+
messages.extend(response.value)
|
|
1217
|
+
|
|
1218
|
+
# Handle pagination
|
|
1219
|
+
next_link = response.odata_next_link if response else None
|
|
1220
|
+
while next_link and len(messages) < max_messages:
|
|
1221
|
+
response = await self._graph.chats.by_chat_id(chat_id).messages.with_url(next_link).get()
|
|
1222
|
+
|
|
1223
|
+
if response and response.value:
|
|
1224
|
+
messages.extend(response.value)
|
|
1225
|
+
|
|
1226
|
+
next_link = response.odata_next_link if response else None
|
|
1227
|
+
|
|
1228
|
+
# Trim to max_messages
|
|
1229
|
+
messages = messages[:max_messages]
|
|
1230
|
+
|
|
1231
|
+
return self._format_messages(messages)
|
|
1232
|
+
|
|
1233
|
+
except Exception as e:
|
|
1234
|
+
raise RuntimeError(f"Failed to get chat messages: {e}") from e
|
|
1235
|
+
|
|
1236
|
+
@tool_schema(ChatMessagesFromUserInput)
|
|
1237
|
+
async def chat_messages_from_user(
|
|
1238
|
+
self,
|
|
1239
|
+
chat_id: str,
|
|
1240
|
+
user_email: str,
|
|
1241
|
+
start_time: Optional[str] = None,
|
|
1242
|
+
end_time: Optional[str] = None,
|
|
1243
|
+
max_messages: int = 50
|
|
1244
|
+
) -> List[Dict[str, Any]]:
|
|
1245
|
+
"""
|
|
1246
|
+
Extract all messages from a specific user in a chat within a time range.
|
|
1247
|
+
|
|
1248
|
+
Args:
|
|
1249
|
+
chat_id: The Chat ID
|
|
1250
|
+
user_email: Email of the user whose messages to extract
|
|
1251
|
+
start_time: Start time for message filter (ISO format)
|
|
1252
|
+
end_time: End time for message filter (ISO format)
|
|
1253
|
+
max_messages: Maximum number of messages to retrieve (default: 50)
|
|
1254
|
+
|
|
1255
|
+
Returns:
|
|
1256
|
+
List of dicts containing message information from the specified user
|
|
1257
|
+
"""
|
|
1258
|
+
await self._ensure_connected()
|
|
1259
|
+
|
|
1260
|
+
try:
|
|
1261
|
+
# Get user info
|
|
1262
|
+
user = await self.get_user(user_email)
|
|
1263
|
+
user_id = user["id"]
|
|
1264
|
+
|
|
1265
|
+
# Get all messages
|
|
1266
|
+
all_messages = await self.get_chat_messages(
|
|
1267
|
+
chat_id=chat_id,
|
|
1268
|
+
start_time=start_time,
|
|
1269
|
+
end_time=end_time,
|
|
1270
|
+
max_messages=max_messages
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
# Filter messages by user
|
|
1274
|
+
return [
|
|
1275
|
+
msg for msg in all_messages
|
|
1276
|
+
if msg.get("from") and msg["from"].get("userId") == user_id
|
|
1277
|
+
]
|
|
1278
|
+
|
|
1279
|
+
except Exception as e:
|
|
1280
|
+
raise RuntimeError(f"Failed to get messages from user {user_email}: {e}") from e
|
|
1281
|
+
|
|
1282
|
+
async def _prepare_message(
|
|
1283
|
+
self,
|
|
1284
|
+
message: Union[str, Dict[str, Any]]
|
|
1285
|
+
) -> Dict[str, Any]:
|
|
1286
|
+
"""
|
|
1287
|
+
Prepare a message for sending.
|
|
1288
|
+
|
|
1289
|
+
Converts various message formats into the standard format expected by Graph API.
|
|
1290
|
+
"""
|
|
1291
|
+
if isinstance(message, dict):
|
|
1292
|
+
# Already in dict format
|
|
1293
|
+
if "body" in message and "attachments" in message:
|
|
1294
|
+
return message
|
|
1295
|
+
elif "type" in message and message["type"] == "AdaptiveCard":
|
|
1296
|
+
# It's an Adaptive Card dict
|
|
1297
|
+
attachment_id = str(uuid.uuid4())
|
|
1298
|
+
return {
|
|
1299
|
+
"body": {
|
|
1300
|
+
"content": f'<attachment id="{attachment_id}"></attachment>'
|
|
1301
|
+
},
|
|
1302
|
+
"attachments": [
|
|
1303
|
+
{
|
|
1304
|
+
"id": attachment_id,
|
|
1305
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
1306
|
+
"content": json.dumps(message)
|
|
1307
|
+
}
|
|
1308
|
+
]
|
|
1309
|
+
}
|
|
1310
|
+
else:
|
|
1311
|
+
# Treat as plain message
|
|
1312
|
+
return {
|
|
1313
|
+
"body": {"content": str(message)},
|
|
1314
|
+
"attachments": []
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
elif isinstance(message, str):
|
|
1318
|
+
# Check if it's JSON string containing an Adaptive Card
|
|
1319
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
1320
|
+
parsed = json.loads(message)
|
|
1321
|
+
if parsed.get("type") == "AdaptiveCard":
|
|
1322
|
+
attachment_id = str(uuid.uuid4())
|
|
1323
|
+
return {
|
|
1324
|
+
"body": {
|
|
1325
|
+
"content": f'<attachment id="{attachment_id}"></attachment>'
|
|
1326
|
+
},
|
|
1327
|
+
"attachments": [
|
|
1328
|
+
{
|
|
1329
|
+
"id": attachment_id,
|
|
1330
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
1331
|
+
"content": message # Keep as JSON string
|
|
1332
|
+
}
|
|
1333
|
+
]
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
# Plain text message
|
|
1337
|
+
return {
|
|
1338
|
+
"body": {"content": message},
|
|
1339
|
+
"attachments": []
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
else:
|
|
1343
|
+
raise ValueError(f"Unsupported message type: {type(message)}")
|
|
1344
|
+
|
|
1345
|
+
async def _find_or_create_chat(self, user_id: str) -> str:
|
|
1346
|
+
"""
|
|
1347
|
+
Find an existing one-on-one chat with a user or create a new one.
|
|
1348
|
+
|
|
1349
|
+
Args:
|
|
1350
|
+
user_id: The user ID to find/create chat with
|
|
1351
|
+
|
|
1352
|
+
Returns:
|
|
1353
|
+
Chat ID
|
|
1354
|
+
"""
|
|
1355
|
+
# Try to find existing chat
|
|
1356
|
+
existing_chat_id = await self._find_existing_chat(user_id)
|
|
1357
|
+
|
|
1358
|
+
if existing_chat_id:
|
|
1359
|
+
return existing_chat_id
|
|
1360
|
+
|
|
1361
|
+
# Create new chat
|
|
1362
|
+
if not self.as_user or not self._owner_id:
|
|
1363
|
+
raise RuntimeError(
|
|
1364
|
+
"Creating chats requires delegated user authentication (as_user=True)"
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
return await self._create_new_chat(self._owner_id, user_id)
|
|
1368
|
+
|
|
1369
|
+
async def _find_existing_chat(self, user_id: str) -> Optional[str]:
|
|
1370
|
+
"""Find an existing one-on-one chat with a user."""
|
|
1371
|
+
query_params = ChatsRequestBuilder.ChatsRequestBuilderGetQueryParameters(
|
|
1372
|
+
filter="chatType eq 'oneOnOne'",
|
|
1373
|
+
expand=["members"]
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
request_configuration = RequestConfiguration(
|
|
1377
|
+
query_parameters=query_params
|
|
1378
|
+
)
|
|
1379
|
+
|
|
1380
|
+
chats = await self._graph.chats.get(
|
|
1381
|
+
request_configuration=request_configuration
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
if not chats.value:
|
|
1385
|
+
return None
|
|
1386
|
+
|
|
1387
|
+
for chat in chats.value:
|
|
1388
|
+
if not chat.members:
|
|
1389
|
+
continue
|
|
1390
|
+
member_ids = [m.user_id for m in chat.members]
|
|
1391
|
+
if user_id in member_ids:
|
|
1392
|
+
return chat.id
|
|
1393
|
+
|
|
1394
|
+
return None
|
|
1395
|
+
|
|
1396
|
+
async def _create_new_chat(self, owner_id: str, user_id: str) -> str:
|
|
1397
|
+
"""Create a new one-on-one chat."""
|
|
1398
|
+
request_body = Chat(
|
|
1399
|
+
chat_type=ChatType.OneOnOne,
|
|
1400
|
+
members=[
|
|
1401
|
+
AadUserConversationMember(
|
|
1402
|
+
odata_type="#microsoft.graph.aadUserConversationMember",
|
|
1403
|
+
roles=["owner"],
|
|
1404
|
+
additional_data={
|
|
1405
|
+
"user@odata.bind": f"https://graph.microsoft.com/beta/users('{owner_id}')"
|
|
1406
|
+
}
|
|
1407
|
+
),
|
|
1408
|
+
AadUserConversationMember(
|
|
1409
|
+
odata_type="#microsoft.graph.aadUserConversationMember",
|
|
1410
|
+
roles=["owner"],
|
|
1411
|
+
additional_data={
|
|
1412
|
+
"user@odata.bind": f"https://graph.microsoft.com/beta/users('{user_id}')"
|
|
1413
|
+
}
|
|
1414
|
+
)
|
|
1415
|
+
]
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
result = await self._graph.chats.post(request_body)
|
|
1419
|
+
return result.id
|
|
1420
|
+
|
|
1421
|
+
async def _send_via_webhook(
|
|
1422
|
+
self,
|
|
1423
|
+
webhook_url: str,
|
|
1424
|
+
message: Union[str, Dict[str, Any]]
|
|
1425
|
+
) -> Dict[str, Any]:
|
|
1426
|
+
"""
|
|
1427
|
+
Send a message via Teams incoming webhook.
|
|
1428
|
+
|
|
1429
|
+
This method works with application permissions and doesn't require Graph API.
|
|
1430
|
+
|
|
1431
|
+
Args:
|
|
1432
|
+
webhook_url: The incoming webhook URL for the channel
|
|
1433
|
+
message: Message content (text, dict, or adaptive card)
|
|
1434
|
+
|
|
1435
|
+
Returns:
|
|
1436
|
+
Dict with success status
|
|
1437
|
+
"""
|
|
1438
|
+
# Prepare webhook payload
|
|
1439
|
+
if isinstance(message, dict):
|
|
1440
|
+
if "type" in message and message["type"] == "AdaptiveCard":
|
|
1441
|
+
# It's an Adaptive Card
|
|
1442
|
+
payload = {
|
|
1443
|
+
"type": "message",
|
|
1444
|
+
"attachments": [
|
|
1445
|
+
{
|
|
1446
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
1447
|
+
"content": message
|
|
1448
|
+
}
|
|
1449
|
+
]
|
|
1450
|
+
}
|
|
1451
|
+
elif "@type" in message:
|
|
1452
|
+
# Already a webhook message card
|
|
1453
|
+
payload = message
|
|
1454
|
+
else:
|
|
1455
|
+
# Plain dict with text
|
|
1456
|
+
payload = {
|
|
1457
|
+
"@type": "MessageCard",
|
|
1458
|
+
"@context": "http://schema.org/extensions",
|
|
1459
|
+
"text": json.dumps(message)
|
|
1460
|
+
}
|
|
1461
|
+
elif isinstance(message, str):
|
|
1462
|
+
# Check if it's JSON
|
|
1463
|
+
try:
|
|
1464
|
+
parsed = json.loads(message)
|
|
1465
|
+
if parsed.get("type") == "AdaptiveCard":
|
|
1466
|
+
payload = {
|
|
1467
|
+
"type": "message",
|
|
1468
|
+
"attachments": [
|
|
1469
|
+
{
|
|
1470
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
1471
|
+
"content": parsed
|
|
1472
|
+
}
|
|
1473
|
+
]
|
|
1474
|
+
}
|
|
1475
|
+
else:
|
|
1476
|
+
# Plain text
|
|
1477
|
+
payload = {
|
|
1478
|
+
"@type": "MessageCard",
|
|
1479
|
+
"@context": "http://schema.org/extensions",
|
|
1480
|
+
"text": message
|
|
1481
|
+
}
|
|
1482
|
+
except json.JSONDecodeError:
|
|
1483
|
+
# Plain text
|
|
1484
|
+
payload = {
|
|
1485
|
+
"@type": "MessageCard",
|
|
1486
|
+
"@context": "http://schema.org/extensions",
|
|
1487
|
+
"text": message
|
|
1488
|
+
}
|
|
1489
|
+
else:
|
|
1490
|
+
raise ValueError(f"Unsupported message type: {type(message)}")
|
|
1491
|
+
|
|
1492
|
+
# Send via webhook
|
|
1493
|
+
async with aiohttp.ClientSession() as session:
|
|
1494
|
+
async with session.post(
|
|
1495
|
+
webhook_url,
|
|
1496
|
+
json=payload,
|
|
1497
|
+
headers={"Content-Type": "application/json"}
|
|
1498
|
+
) as response:
|
|
1499
|
+
if response.status not in [200, 201]:
|
|
1500
|
+
error_text = await response.text()
|
|
1501
|
+
raise RuntimeError(
|
|
1502
|
+
f"Webhook request failed with status {response.status}: {error_text}"
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
return {
|
|
1506
|
+
"success": True,
|
|
1507
|
+
"status": response.status,
|
|
1508
|
+
"method": "webhook"
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
def _format_messages(self, messages: List) -> List[Dict[str, Any]]:
|
|
1512
|
+
"""
|
|
1513
|
+
Format ChatMessage objects into dictionaries.
|
|
1514
|
+
|
|
1515
|
+
Args:
|
|
1516
|
+
messages: List of ChatMessage objects
|
|
1517
|
+
|
|
1518
|
+
Returns:
|
|
1519
|
+
List of dictionaries with formatted message data
|
|
1520
|
+
"""
|
|
1521
|
+
formatted = []
|
|
1522
|
+
|
|
1523
|
+
for msg in messages:
|
|
1524
|
+
if not isinstance(msg, ChatMessage):
|
|
1525
|
+
continue
|
|
1526
|
+
|
|
1527
|
+
message_dict = {
|
|
1528
|
+
"id": msg.id,
|
|
1529
|
+
"messageType": str(msg.message_type) if hasattr(msg, 'message_type') else None,
|
|
1530
|
+
"createdDateTime": str(msg.created_date_time) if hasattr(msg, 'created_date_time') else None,
|
|
1531
|
+
"lastModifiedDateTime": str(msg.last_modified_date_time) if hasattr(msg, 'last_modified_date_time') else None,
|
|
1532
|
+
"subject": msg.subject if hasattr(msg, 'subject') else None,
|
|
1533
|
+
"importance": str(msg.importance) if hasattr(msg, 'importance') else None,
|
|
1534
|
+
"webUrl": msg.web_url if hasattr(msg, 'web_url') else None
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
# Add body content
|
|
1538
|
+
if hasattr(msg, 'body') and msg.body:
|
|
1539
|
+
message_dict["body"] = {
|
|
1540
|
+
"contentType": str(msg.body.content_type) if hasattr(msg.body, 'content_type') else None,
|
|
1541
|
+
"content": msg.body.content if hasattr(msg.body, 'content') else None
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
# Add sender information
|
|
1545
|
+
if hasattr(msg, 'from_') and msg.from_:
|
|
1546
|
+
from_info = {}
|
|
1547
|
+
if hasattr(msg.from_, 'user') and msg.from_.user:
|
|
1548
|
+
from_info["userId"] = msg.from_.user.id if hasattr(msg.from_.user, 'id') else None
|
|
1549
|
+
from_info["displayName"] = msg.from_.user.display_name if hasattr(msg.from_.user, 'display_name') else None
|
|
1550
|
+
message_dict["from"] = from_info
|
|
1551
|
+
|
|
1552
|
+
# Add attachments if any
|
|
1553
|
+
if hasattr(msg, 'attachments') and msg.attachments:
|
|
1554
|
+
message_dict["attachments"] = [
|
|
1555
|
+
{
|
|
1556
|
+
"id": att.id if hasattr(att, 'id') else None,
|
|
1557
|
+
"contentType": att.content_type if hasattr(att, 'content_type') else None,
|
|
1558
|
+
"name": att.name if hasattr(att, 'name') else None,
|
|
1559
|
+
"contentUrl": att.content_url if hasattr(att, 'content_url') else None
|
|
1560
|
+
}
|
|
1561
|
+
for att in msg.attachments
|
|
1562
|
+
]
|
|
1563
|
+
|
|
1564
|
+
# Add reactions if any
|
|
1565
|
+
if hasattr(msg, 'reactions') and msg.reactions:
|
|
1566
|
+
message_dict["reactions"] = [
|
|
1567
|
+
{
|
|
1568
|
+
"reactionType": r.reaction_type if hasattr(r, 'reaction_type') else None,
|
|
1569
|
+
"createdDateTime": str(r.created_date_time) if hasattr(r, 'created_date_time') else None
|
|
1570
|
+
}
|
|
1571
|
+
for r in msg.reactions
|
|
1572
|
+
]
|
|
1573
|
+
|
|
1574
|
+
formatted.append(message_dict)
|
|
1575
|
+
|
|
1576
|
+
return formatted
|
|
1577
|
+
|
|
1578
|
+
def __del__(self):
|
|
1579
|
+
"""Cleanup resources."""
|
|
1580
|
+
self._client = None
|
|
1581
|
+
self._graph = None
|
|
1582
|
+
self._token = None
|
|
1583
|
+
self._connected = False
|
|
1584
|
+
|
|
1585
|
+
# ============================================================================
|
|
1586
|
+
# Helper function for easy initialization
|
|
1587
|
+
# ============================================================================
|
|
1588
|
+
|
|
1589
|
+
def create_msteams_toolkit(
|
|
1590
|
+
tenant_id: Optional[str] = None,
|
|
1591
|
+
client_id: Optional[str] = None,
|
|
1592
|
+
client_secret: Optional[str] = None,
|
|
1593
|
+
as_user: bool = False,
|
|
1594
|
+
username: Optional[str] = None,
|
|
1595
|
+
password: Optional[str] = None,
|
|
1596
|
+
**kwargs
|
|
1597
|
+
) -> MSTeamsToolkit:
|
|
1598
|
+
"""
|
|
1599
|
+
Create and return a configured MSTeamsToolkit instance.
|
|
1600
|
+
|
|
1601
|
+
Args:
|
|
1602
|
+
tenant_id: Azure AD tenant ID
|
|
1603
|
+
client_id: Azure AD application client ID
|
|
1604
|
+
client_secret: Azure AD application client secret
|
|
1605
|
+
as_user: If True, use delegated user permissions
|
|
1606
|
+
username: Username for delegated auth
|
|
1607
|
+
password: Password for delegated auth
|
|
1608
|
+
**kwargs: Additional toolkit arguments
|
|
1609
|
+
|
|
1610
|
+
Returns:
|
|
1611
|
+
Configured MSTeamsToolkit instance
|
|
1612
|
+
"""
|
|
1613
|
+
return MSTeamsToolkit(
|
|
1614
|
+
tenant_id=tenant_id,
|
|
1615
|
+
client_id=client_id,
|
|
1616
|
+
client_secret=client_secret,
|
|
1617
|
+
as_user=as_user,
|
|
1618
|
+
username=username,
|
|
1619
|
+
password=password,
|
|
1620
|
+
**kwargs
|
|
1621
|
+
)
|