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.
Files changed (535) hide show
  1. agentui/.prettierrc +15 -0
  2. agentui/QUICKSTART.md +272 -0
  3. agentui/README.md +59 -0
  4. agentui/env.example +16 -0
  5. agentui/jsconfig.json +14 -0
  6. agentui/package-lock.json +4242 -0
  7. agentui/package.json +34 -0
  8. agentui/scripts/postinstall/apply-patches.mjs +260 -0
  9. agentui/src/app.css +61 -0
  10. agentui/src/app.d.ts +13 -0
  11. agentui/src/app.html +12 -0
  12. agentui/src/components/LoadingSpinner.svelte +64 -0
  13. agentui/src/components/ThemeSwitcher.svelte +159 -0
  14. agentui/src/components/index.js +4 -0
  15. agentui/src/lib/api/bots.ts +60 -0
  16. agentui/src/lib/api/chat.ts +22 -0
  17. agentui/src/lib/api/http.ts +25 -0
  18. agentui/src/lib/components/BotCard.svelte +33 -0
  19. agentui/src/lib/components/ChatBubble.svelte +63 -0
  20. agentui/src/lib/components/Toast.svelte +21 -0
  21. agentui/src/lib/config.ts +20 -0
  22. agentui/src/lib/stores/auth.svelte.ts +73 -0
  23. agentui/src/lib/stores/theme.svelte.js +64 -0
  24. agentui/src/lib/stores/toast.svelte.ts +31 -0
  25. agentui/src/lib/utils/conversation.ts +39 -0
  26. agentui/src/routes/+layout.svelte +20 -0
  27. agentui/src/routes/+page.svelte +232 -0
  28. agentui/src/routes/login/+page.svelte +200 -0
  29. agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
  30. agentui/src/routes/talk/[agentId]/+page.ts +7 -0
  31. agentui/static/README.md +1 -0
  32. agentui/svelte.config.js +11 -0
  33. agentui/tailwind.config.ts +53 -0
  34. agentui/tsconfig.json +3 -0
  35. agentui/vite.config.ts +10 -0
  36. ai_parrot-0.17.2.dist-info/METADATA +472 -0
  37. ai_parrot-0.17.2.dist-info/RECORD +535 -0
  38. ai_parrot-0.17.2.dist-info/WHEEL +6 -0
  39. ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
  40. ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
  41. ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
  42. crew-builder/.prettierrc +15 -0
  43. crew-builder/QUICKSTART.md +259 -0
  44. crew-builder/README.md +113 -0
  45. crew-builder/env.example +17 -0
  46. crew-builder/jsconfig.json +14 -0
  47. crew-builder/package-lock.json +4182 -0
  48. crew-builder/package.json +37 -0
  49. crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
  50. crew-builder/src/app.css +62 -0
  51. crew-builder/src/app.d.ts +13 -0
  52. crew-builder/src/app.html +12 -0
  53. crew-builder/src/components/LoadingSpinner.svelte +64 -0
  54. crew-builder/src/components/ThemeSwitcher.svelte +149 -0
  55. crew-builder/src/components/index.js +9 -0
  56. crew-builder/src/lib/api/bots.ts +60 -0
  57. crew-builder/src/lib/api/chat.ts +80 -0
  58. crew-builder/src/lib/api/client.ts +56 -0
  59. crew-builder/src/lib/api/crew/crew.ts +136 -0
  60. crew-builder/src/lib/api/index.ts +5 -0
  61. crew-builder/src/lib/api/o365/auth.ts +65 -0
  62. crew-builder/src/lib/auth/auth.ts +54 -0
  63. crew-builder/src/lib/components/AgentNode.svelte +43 -0
  64. crew-builder/src/lib/components/BotCard.svelte +33 -0
  65. crew-builder/src/lib/components/ChatBubble.svelte +67 -0
  66. crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
  67. crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
  68. crew-builder/src/lib/components/JsonViewer.svelte +24 -0
  69. crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
  70. crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
  71. crew-builder/src/lib/components/Toast.svelte +67 -0
  72. crew-builder/src/lib/components/Toolbar.svelte +157 -0
  73. crew-builder/src/lib/components/index.ts +10 -0
  74. crew-builder/src/lib/config.ts +8 -0
  75. crew-builder/src/lib/stores/auth.svelte.ts +228 -0
  76. crew-builder/src/lib/stores/crewStore.ts +369 -0
  77. crew-builder/src/lib/stores/theme.svelte.js +145 -0
  78. crew-builder/src/lib/stores/toast.svelte.ts +69 -0
  79. crew-builder/src/lib/utils/conversation.ts +39 -0
  80. crew-builder/src/lib/utils/markdown.ts +122 -0
  81. crew-builder/src/lib/utils/talkHistory.ts +47 -0
  82. crew-builder/src/routes/+layout.svelte +20 -0
  83. crew-builder/src/routes/+page.svelte +539 -0
  84. crew-builder/src/routes/agents/+page.svelte +247 -0
  85. crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
  86. crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
  87. crew-builder/src/routes/builder/+page.svelte +204 -0
  88. crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
  89. crew-builder/src/routes/crew/ask/+page.ts +1 -0
  90. crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
  91. crew-builder/src/routes/login/+page.svelte +197 -0
  92. crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
  93. crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
  94. crew-builder/static/README.md +1 -0
  95. crew-builder/svelte.config.js +11 -0
  96. crew-builder/tailwind.config.ts +53 -0
  97. crew-builder/tsconfig.json +3 -0
  98. crew-builder/vite.config.ts +10 -0
  99. mcp_servers/calculator_server.py +309 -0
  100. parrot/__init__.py +27 -0
  101. parrot/__pycache__/__init__.cpython-310.pyc +0 -0
  102. parrot/__pycache__/version.cpython-310.pyc +0 -0
  103. parrot/_version.py +34 -0
  104. parrot/a2a/__init__.py +48 -0
  105. parrot/a2a/client.py +658 -0
  106. parrot/a2a/discovery.py +89 -0
  107. parrot/a2a/mixin.py +257 -0
  108. parrot/a2a/models.py +376 -0
  109. parrot/a2a/server.py +770 -0
  110. parrot/agents/__init__.py +29 -0
  111. parrot/bots/__init__.py +12 -0
  112. parrot/bots/a2a_agent.py +19 -0
  113. parrot/bots/abstract.py +3139 -0
  114. parrot/bots/agent.py +1129 -0
  115. parrot/bots/basic.py +9 -0
  116. parrot/bots/chatbot.py +669 -0
  117. parrot/bots/data.py +1618 -0
  118. parrot/bots/database/__init__.py +5 -0
  119. parrot/bots/database/abstract.py +3071 -0
  120. parrot/bots/database/cache.py +286 -0
  121. parrot/bots/database/models.py +468 -0
  122. parrot/bots/database/prompts.py +154 -0
  123. parrot/bots/database/retries.py +98 -0
  124. parrot/bots/database/router.py +269 -0
  125. parrot/bots/database/sql.py +41 -0
  126. parrot/bots/db/__init__.py +6 -0
  127. parrot/bots/db/abstract.py +556 -0
  128. parrot/bots/db/bigquery.py +602 -0
  129. parrot/bots/db/cache.py +85 -0
  130. parrot/bots/db/documentdb.py +668 -0
  131. parrot/bots/db/elastic.py +1014 -0
  132. parrot/bots/db/influx.py +898 -0
  133. parrot/bots/db/mock.py +96 -0
  134. parrot/bots/db/multi.py +783 -0
  135. parrot/bots/db/prompts.py +185 -0
  136. parrot/bots/db/sql.py +1255 -0
  137. parrot/bots/db/tools.py +212 -0
  138. parrot/bots/document.py +680 -0
  139. parrot/bots/hrbot.py +15 -0
  140. parrot/bots/kb.py +170 -0
  141. parrot/bots/mcp.py +36 -0
  142. parrot/bots/orchestration/README.md +463 -0
  143. parrot/bots/orchestration/__init__.py +1 -0
  144. parrot/bots/orchestration/agent.py +155 -0
  145. parrot/bots/orchestration/crew.py +3330 -0
  146. parrot/bots/orchestration/fsm.py +1179 -0
  147. parrot/bots/orchestration/hr.py +434 -0
  148. parrot/bots/orchestration/storage/__init__.py +4 -0
  149. parrot/bots/orchestration/storage/memory.py +100 -0
  150. parrot/bots/orchestration/storage/mixin.py +119 -0
  151. parrot/bots/orchestration/verify.py +202 -0
  152. parrot/bots/product.py +204 -0
  153. parrot/bots/prompts/__init__.py +96 -0
  154. parrot/bots/prompts/agents.py +155 -0
  155. parrot/bots/prompts/data.py +216 -0
  156. parrot/bots/prompts/output_generation.py +8 -0
  157. parrot/bots/scraper/__init__.py +3 -0
  158. parrot/bots/scraper/models.py +122 -0
  159. parrot/bots/scraper/scraper.py +1173 -0
  160. parrot/bots/scraper/templates.py +115 -0
  161. parrot/bots/stores/__init__.py +5 -0
  162. parrot/bots/stores/local.py +172 -0
  163. parrot/bots/webdev.py +81 -0
  164. parrot/cli.py +17 -0
  165. parrot/clients/__init__.py +16 -0
  166. parrot/clients/base.py +1491 -0
  167. parrot/clients/claude.py +1191 -0
  168. parrot/clients/factory.py +129 -0
  169. parrot/clients/google.py +4567 -0
  170. parrot/clients/gpt.py +1975 -0
  171. parrot/clients/grok.py +432 -0
  172. parrot/clients/groq.py +986 -0
  173. parrot/clients/hf.py +582 -0
  174. parrot/clients/models.py +18 -0
  175. parrot/conf.py +395 -0
  176. parrot/embeddings/__init__.py +9 -0
  177. parrot/embeddings/base.py +157 -0
  178. parrot/embeddings/google.py +98 -0
  179. parrot/embeddings/huggingface.py +74 -0
  180. parrot/embeddings/openai.py +84 -0
  181. parrot/embeddings/processor.py +88 -0
  182. parrot/exceptions.c +13868 -0
  183. parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
  184. parrot/exceptions.pxd +22 -0
  185. parrot/exceptions.pxi +15 -0
  186. parrot/exceptions.pyx +44 -0
  187. parrot/generators/__init__.py +29 -0
  188. parrot/generators/base.py +200 -0
  189. parrot/generators/html.py +293 -0
  190. parrot/generators/react.py +205 -0
  191. parrot/generators/streamlit.py +203 -0
  192. parrot/generators/template.py +105 -0
  193. parrot/handlers/__init__.py +4 -0
  194. parrot/handlers/agent.py +861 -0
  195. parrot/handlers/agents/__init__.py +1 -0
  196. parrot/handlers/agents/abstract.py +900 -0
  197. parrot/handlers/bots.py +338 -0
  198. parrot/handlers/chat.py +915 -0
  199. parrot/handlers/creation.sql +192 -0
  200. parrot/handlers/crew/ARCHITECTURE.md +362 -0
  201. parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
  202. parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
  203. parrot/handlers/crew/__init__.py +0 -0
  204. parrot/handlers/crew/handler.py +801 -0
  205. parrot/handlers/crew/models.py +229 -0
  206. parrot/handlers/crew/redis_persistence.py +523 -0
  207. parrot/handlers/jobs/__init__.py +10 -0
  208. parrot/handlers/jobs/job.py +384 -0
  209. parrot/handlers/jobs/mixin.py +627 -0
  210. parrot/handlers/jobs/models.py +115 -0
  211. parrot/handlers/jobs/worker.py +31 -0
  212. parrot/handlers/models.py +596 -0
  213. parrot/handlers/o365_auth.py +105 -0
  214. parrot/handlers/stream.py +337 -0
  215. parrot/interfaces/__init__.py +6 -0
  216. parrot/interfaces/aws.py +143 -0
  217. parrot/interfaces/credentials.py +113 -0
  218. parrot/interfaces/database.py +27 -0
  219. parrot/interfaces/google.py +1123 -0
  220. parrot/interfaces/hierarchy.py +1227 -0
  221. parrot/interfaces/http.py +651 -0
  222. parrot/interfaces/images/__init__.py +0 -0
  223. parrot/interfaces/images/plugins/__init__.py +24 -0
  224. parrot/interfaces/images/plugins/abstract.py +58 -0
  225. parrot/interfaces/images/plugins/analisys.py +148 -0
  226. parrot/interfaces/images/plugins/classify.py +150 -0
  227. parrot/interfaces/images/plugins/classifybase.py +182 -0
  228. parrot/interfaces/images/plugins/detect.py +150 -0
  229. parrot/interfaces/images/plugins/exif.py +1103 -0
  230. parrot/interfaces/images/plugins/hash.py +52 -0
  231. parrot/interfaces/images/plugins/vision.py +104 -0
  232. parrot/interfaces/images/plugins/yolo.py +66 -0
  233. parrot/interfaces/images/plugins/zerodetect.py +197 -0
  234. parrot/interfaces/o365.py +978 -0
  235. parrot/interfaces/onedrive.py +822 -0
  236. parrot/interfaces/sharepoint.py +1435 -0
  237. parrot/interfaces/soap.py +257 -0
  238. parrot/loaders/__init__.py +8 -0
  239. parrot/loaders/abstract.py +1131 -0
  240. parrot/loaders/audio.py +199 -0
  241. parrot/loaders/basepdf.py +53 -0
  242. parrot/loaders/basevideo.py +1568 -0
  243. parrot/loaders/csv.py +409 -0
  244. parrot/loaders/docx.py +116 -0
  245. parrot/loaders/epubloader.py +316 -0
  246. parrot/loaders/excel.py +199 -0
  247. parrot/loaders/factory.py +55 -0
  248. parrot/loaders/files/__init__.py +0 -0
  249. parrot/loaders/files/abstract.py +39 -0
  250. parrot/loaders/files/html.py +26 -0
  251. parrot/loaders/files/text.py +63 -0
  252. parrot/loaders/html.py +152 -0
  253. parrot/loaders/markdown.py +442 -0
  254. parrot/loaders/pdf.py +373 -0
  255. parrot/loaders/pdfmark.py +320 -0
  256. parrot/loaders/pdftables.py +506 -0
  257. parrot/loaders/ppt.py +476 -0
  258. parrot/loaders/qa.py +63 -0
  259. parrot/loaders/splitters/__init__.py +10 -0
  260. parrot/loaders/splitters/base.py +138 -0
  261. parrot/loaders/splitters/md.py +228 -0
  262. parrot/loaders/splitters/token.py +143 -0
  263. parrot/loaders/txt.py +26 -0
  264. parrot/loaders/video.py +89 -0
  265. parrot/loaders/videolocal.py +218 -0
  266. parrot/loaders/videounderstanding.py +377 -0
  267. parrot/loaders/vimeo.py +167 -0
  268. parrot/loaders/web.py +599 -0
  269. parrot/loaders/youtube.py +504 -0
  270. parrot/manager/__init__.py +5 -0
  271. parrot/manager/manager.py +1030 -0
  272. parrot/mcp/__init__.py +28 -0
  273. parrot/mcp/adapter.py +105 -0
  274. parrot/mcp/cli.py +174 -0
  275. parrot/mcp/client.py +119 -0
  276. parrot/mcp/config.py +75 -0
  277. parrot/mcp/integration.py +842 -0
  278. parrot/mcp/oauth.py +933 -0
  279. parrot/mcp/server.py +225 -0
  280. parrot/mcp/transports/__init__.py +3 -0
  281. parrot/mcp/transports/base.py +279 -0
  282. parrot/mcp/transports/grpc_session.py +163 -0
  283. parrot/mcp/transports/http.py +312 -0
  284. parrot/mcp/transports/mcp.proto +108 -0
  285. parrot/mcp/transports/quic.py +1082 -0
  286. parrot/mcp/transports/sse.py +330 -0
  287. parrot/mcp/transports/stdio.py +309 -0
  288. parrot/mcp/transports/unix.py +395 -0
  289. parrot/mcp/transports/websocket.py +547 -0
  290. parrot/memory/__init__.py +16 -0
  291. parrot/memory/abstract.py +209 -0
  292. parrot/memory/agent.py +32 -0
  293. parrot/memory/cache.py +175 -0
  294. parrot/memory/core.py +555 -0
  295. parrot/memory/file.py +153 -0
  296. parrot/memory/mem.py +131 -0
  297. parrot/memory/redis.py +613 -0
  298. parrot/models/__init__.py +46 -0
  299. parrot/models/basic.py +118 -0
  300. parrot/models/compliance.py +208 -0
  301. parrot/models/crew.py +395 -0
  302. parrot/models/detections.py +654 -0
  303. parrot/models/generation.py +85 -0
  304. parrot/models/google.py +223 -0
  305. parrot/models/groq.py +23 -0
  306. parrot/models/openai.py +30 -0
  307. parrot/models/outputs.py +285 -0
  308. parrot/models/responses.py +938 -0
  309. parrot/notifications/__init__.py +743 -0
  310. parrot/openapi/__init__.py +3 -0
  311. parrot/openapi/components.yaml +641 -0
  312. parrot/openapi/config.py +322 -0
  313. parrot/outputs/__init__.py +32 -0
  314. parrot/outputs/formats/__init__.py +108 -0
  315. parrot/outputs/formats/altair.py +359 -0
  316. parrot/outputs/formats/application.py +122 -0
  317. parrot/outputs/formats/base.py +351 -0
  318. parrot/outputs/formats/bokeh.py +356 -0
  319. parrot/outputs/formats/card.py +424 -0
  320. parrot/outputs/formats/chart.py +436 -0
  321. parrot/outputs/formats/d3.py +255 -0
  322. parrot/outputs/formats/echarts.py +310 -0
  323. parrot/outputs/formats/generators/__init__.py +0 -0
  324. parrot/outputs/formats/generators/abstract.py +61 -0
  325. parrot/outputs/formats/generators/panel.py +145 -0
  326. parrot/outputs/formats/generators/streamlit.py +86 -0
  327. parrot/outputs/formats/generators/terminal.py +63 -0
  328. parrot/outputs/formats/holoviews.py +310 -0
  329. parrot/outputs/formats/html.py +147 -0
  330. parrot/outputs/formats/jinja2.py +46 -0
  331. parrot/outputs/formats/json.py +87 -0
  332. parrot/outputs/formats/map.py +933 -0
  333. parrot/outputs/formats/markdown.py +172 -0
  334. parrot/outputs/formats/matplotlib.py +237 -0
  335. parrot/outputs/formats/mixins/__init__.py +0 -0
  336. parrot/outputs/formats/mixins/emaps.py +855 -0
  337. parrot/outputs/formats/plotly.py +341 -0
  338. parrot/outputs/formats/seaborn.py +310 -0
  339. parrot/outputs/formats/table.py +397 -0
  340. parrot/outputs/formats/template_report.py +138 -0
  341. parrot/outputs/formats/yaml.py +125 -0
  342. parrot/outputs/formatter.py +152 -0
  343. parrot/outputs/templates/__init__.py +95 -0
  344. parrot/pipelines/__init__.py +0 -0
  345. parrot/pipelines/abstract.py +210 -0
  346. parrot/pipelines/detector.py +124 -0
  347. parrot/pipelines/models.py +90 -0
  348. parrot/pipelines/planogram.py +3002 -0
  349. parrot/pipelines/table.sql +97 -0
  350. parrot/plugins/__init__.py +106 -0
  351. parrot/plugins/importer.py +80 -0
  352. parrot/py.typed +0 -0
  353. parrot/registry/__init__.py +18 -0
  354. parrot/registry/registry.py +594 -0
  355. parrot/scheduler/__init__.py +1189 -0
  356. parrot/scheduler/models.py +60 -0
  357. parrot/security/__init__.py +16 -0
  358. parrot/security/prompt_injection.py +268 -0
  359. parrot/security/security_events.sql +25 -0
  360. parrot/services/__init__.py +1 -0
  361. parrot/services/mcp/__init__.py +8 -0
  362. parrot/services/mcp/config.py +13 -0
  363. parrot/services/mcp/server.py +295 -0
  364. parrot/services/o365_remote_auth.py +235 -0
  365. parrot/stores/__init__.py +7 -0
  366. parrot/stores/abstract.py +352 -0
  367. parrot/stores/arango.py +1090 -0
  368. parrot/stores/bigquery.py +1377 -0
  369. parrot/stores/cache.py +106 -0
  370. parrot/stores/empty.py +10 -0
  371. parrot/stores/faiss_store.py +1157 -0
  372. parrot/stores/kb/__init__.py +9 -0
  373. parrot/stores/kb/abstract.py +68 -0
  374. parrot/stores/kb/cache.py +165 -0
  375. parrot/stores/kb/doc.py +325 -0
  376. parrot/stores/kb/hierarchy.py +346 -0
  377. parrot/stores/kb/local.py +457 -0
  378. parrot/stores/kb/prompt.py +28 -0
  379. parrot/stores/kb/redis.py +659 -0
  380. parrot/stores/kb/store.py +115 -0
  381. parrot/stores/kb/user.py +374 -0
  382. parrot/stores/models.py +59 -0
  383. parrot/stores/pgvector.py +3 -0
  384. parrot/stores/postgres.py +2853 -0
  385. parrot/stores/utils/__init__.py +0 -0
  386. parrot/stores/utils/chunking.py +197 -0
  387. parrot/telemetry/__init__.py +3 -0
  388. parrot/telemetry/mixin.py +111 -0
  389. parrot/template/__init__.py +3 -0
  390. parrot/template/engine.py +259 -0
  391. parrot/tools/__init__.py +23 -0
  392. parrot/tools/abstract.py +644 -0
  393. parrot/tools/agent.py +363 -0
  394. parrot/tools/arangodbsearch.py +537 -0
  395. parrot/tools/arxiv_tool.py +188 -0
  396. parrot/tools/calculator/__init__.py +3 -0
  397. parrot/tools/calculator/operations/__init__.py +38 -0
  398. parrot/tools/calculator/operations/calculus.py +80 -0
  399. parrot/tools/calculator/operations/statistics.py +76 -0
  400. parrot/tools/calculator/tool.py +150 -0
  401. parrot/tools/cloudwatch.py +988 -0
  402. parrot/tools/codeinterpreter/__init__.py +127 -0
  403. parrot/tools/codeinterpreter/executor.py +371 -0
  404. parrot/tools/codeinterpreter/internals.py +473 -0
  405. parrot/tools/codeinterpreter/models.py +643 -0
  406. parrot/tools/codeinterpreter/prompts.py +224 -0
  407. parrot/tools/codeinterpreter/tool.py +664 -0
  408. parrot/tools/company_info/__init__.py +6 -0
  409. parrot/tools/company_info/tool.py +1138 -0
  410. parrot/tools/correlationanalysis.py +437 -0
  411. parrot/tools/database/abstract.py +286 -0
  412. parrot/tools/database/bq.py +115 -0
  413. parrot/tools/database/cache.py +284 -0
  414. parrot/tools/database/models.py +95 -0
  415. parrot/tools/database/pg.py +343 -0
  416. parrot/tools/databasequery.py +1159 -0
  417. parrot/tools/db.py +1800 -0
  418. parrot/tools/ddgo.py +370 -0
  419. parrot/tools/decorators.py +271 -0
  420. parrot/tools/dftohtml.py +282 -0
  421. parrot/tools/document.py +549 -0
  422. parrot/tools/ecs.py +819 -0
  423. parrot/tools/edareport.py +368 -0
  424. parrot/tools/elasticsearch.py +1049 -0
  425. parrot/tools/employees.py +462 -0
  426. parrot/tools/epson/__init__.py +96 -0
  427. parrot/tools/excel.py +683 -0
  428. parrot/tools/file/__init__.py +13 -0
  429. parrot/tools/file/abstract.py +76 -0
  430. parrot/tools/file/gcs.py +378 -0
  431. parrot/tools/file/local.py +284 -0
  432. parrot/tools/file/s3.py +511 -0
  433. parrot/tools/file/tmp.py +309 -0
  434. parrot/tools/file/tool.py +501 -0
  435. parrot/tools/file_reader.py +129 -0
  436. parrot/tools/flowtask/__init__.py +19 -0
  437. parrot/tools/flowtask/tool.py +761 -0
  438. parrot/tools/gittoolkit.py +508 -0
  439. parrot/tools/google/__init__.py +18 -0
  440. parrot/tools/google/base.py +169 -0
  441. parrot/tools/google/tools.py +1251 -0
  442. parrot/tools/googlelocation.py +5 -0
  443. parrot/tools/googleroutes.py +5 -0
  444. parrot/tools/googlesearch.py +5 -0
  445. parrot/tools/googlesitesearch.py +5 -0
  446. parrot/tools/googlevoice.py +2 -0
  447. parrot/tools/gvoice.py +695 -0
  448. parrot/tools/ibisworld/README.md +225 -0
  449. parrot/tools/ibisworld/__init__.py +11 -0
  450. parrot/tools/ibisworld/tool.py +366 -0
  451. parrot/tools/jiratoolkit.py +1718 -0
  452. parrot/tools/manager.py +1098 -0
  453. parrot/tools/math.py +152 -0
  454. parrot/tools/metadata.py +476 -0
  455. parrot/tools/msteams.py +1621 -0
  456. parrot/tools/msword.py +635 -0
  457. parrot/tools/multidb.py +580 -0
  458. parrot/tools/multistoresearch.py +369 -0
  459. parrot/tools/networkninja.py +167 -0
  460. parrot/tools/nextstop/__init__.py +4 -0
  461. parrot/tools/nextstop/base.py +286 -0
  462. parrot/tools/nextstop/employee.py +733 -0
  463. parrot/tools/nextstop/store.py +462 -0
  464. parrot/tools/notification.py +435 -0
  465. parrot/tools/o365/__init__.py +42 -0
  466. parrot/tools/o365/base.py +295 -0
  467. parrot/tools/o365/bundle.py +522 -0
  468. parrot/tools/o365/events.py +554 -0
  469. parrot/tools/o365/mail.py +992 -0
  470. parrot/tools/o365/onedrive.py +497 -0
  471. parrot/tools/o365/sharepoint.py +641 -0
  472. parrot/tools/openapi_toolkit.py +904 -0
  473. parrot/tools/openweather.py +527 -0
  474. parrot/tools/pdfprint.py +1001 -0
  475. parrot/tools/powerbi.py +518 -0
  476. parrot/tools/powerpoint.py +1113 -0
  477. parrot/tools/pricestool.py +146 -0
  478. parrot/tools/products/__init__.py +246 -0
  479. parrot/tools/prophet_tool.py +171 -0
  480. parrot/tools/pythonpandas.py +630 -0
  481. parrot/tools/pythonrepl.py +910 -0
  482. parrot/tools/qsource.py +436 -0
  483. parrot/tools/querytoolkit.py +395 -0
  484. parrot/tools/quickeda.py +827 -0
  485. parrot/tools/resttool.py +553 -0
  486. parrot/tools/retail/__init__.py +0 -0
  487. parrot/tools/retail/bby.py +528 -0
  488. parrot/tools/sandboxtool.py +703 -0
  489. parrot/tools/sassie/__init__.py +352 -0
  490. parrot/tools/scraping/__init__.py +7 -0
  491. parrot/tools/scraping/docs/select.md +466 -0
  492. parrot/tools/scraping/documentation.md +1278 -0
  493. parrot/tools/scraping/driver.py +436 -0
  494. parrot/tools/scraping/models.py +576 -0
  495. parrot/tools/scraping/options.py +85 -0
  496. parrot/tools/scraping/orchestrator.py +517 -0
  497. parrot/tools/scraping/readme.md +740 -0
  498. parrot/tools/scraping/tool.py +3115 -0
  499. parrot/tools/seasonaldetection.py +642 -0
  500. parrot/tools/shell_tool/__init__.py +5 -0
  501. parrot/tools/shell_tool/actions.py +408 -0
  502. parrot/tools/shell_tool/engine.py +155 -0
  503. parrot/tools/shell_tool/models.py +322 -0
  504. parrot/tools/shell_tool/tool.py +442 -0
  505. parrot/tools/site_search.py +214 -0
  506. parrot/tools/textfile.py +418 -0
  507. parrot/tools/think.py +378 -0
  508. parrot/tools/toolkit.py +298 -0
  509. parrot/tools/webapp_tool.py +187 -0
  510. parrot/tools/whatif.py +1279 -0
  511. parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
  512. parrot/tools/workday/__init__.py +6 -0
  513. parrot/tools/workday/models.py +1389 -0
  514. parrot/tools/workday/tool.py +1293 -0
  515. parrot/tools/yfinance_tool.py +306 -0
  516. parrot/tools/zipcode.py +217 -0
  517. parrot/utils/__init__.py +2 -0
  518. parrot/utils/helpers.py +73 -0
  519. parrot/utils/parsers/__init__.py +5 -0
  520. parrot/utils/parsers/toml.c +12078 -0
  521. parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
  522. parrot/utils/parsers/toml.pyx +21 -0
  523. parrot/utils/toml.py +11 -0
  524. parrot/utils/types.cpp +20936 -0
  525. parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
  526. parrot/utils/types.pyx +213 -0
  527. parrot/utils/uv.py +11 -0
  528. parrot/version.py +10 -0
  529. parrot/yaml-rs/Cargo.lock +350 -0
  530. parrot/yaml-rs/Cargo.toml +19 -0
  531. parrot/yaml-rs/pyproject.toml +19 -0
  532. parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
  533. parrot/yaml-rs/src/lib.rs +222 -0
  534. requirements/docker-compose.yml +24 -0
  535. requirements/requirements-dev.txt +21 -0
@@ -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
+ )