agno 0.1.2__py3-none-any.whl → 2.3.13__py3-none-any.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 (723) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +44 -5
  3. agno/agent/agent.py +10531 -2975
  4. agno/api/agent.py +14 -53
  5. agno/api/api.py +7 -46
  6. agno/api/evals.py +22 -0
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -25
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +6 -9
  11. agno/api/schemas/evals.py +16 -0
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +10 -10
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +16 -0
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +22 -26
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/compression/__init__.py +3 -0
  25. agno/compression/manager.py +247 -0
  26. agno/culture/__init__.py +3 -0
  27. agno/culture/manager.py +956 -0
  28. agno/db/__init__.py +24 -0
  29. agno/db/async_postgres/__init__.py +3 -0
  30. agno/db/base.py +946 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2781 -0
  33. agno/db/dynamo/schemas.py +442 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +2379 -0
  37. agno/db/firestore/schemas.py +181 -0
  38. agno/db/firestore/utils.py +376 -0
  39. agno/db/gcs_json/__init__.py +3 -0
  40. agno/db/gcs_json/gcs_json_db.py +1791 -0
  41. agno/db/gcs_json/utils.py +228 -0
  42. agno/db/in_memory/__init__.py +3 -0
  43. agno/db/in_memory/in_memory_db.py +1312 -0
  44. agno/db/in_memory/utils.py +230 -0
  45. agno/db/json/__init__.py +3 -0
  46. agno/db/json/json_db.py +1777 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/manager.py +199 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/migrations/versions/v2_3_0.py +938 -0
  51. agno/db/mongo/__init__.py +17 -0
  52. agno/db/mongo/async_mongo.py +2760 -0
  53. agno/db/mongo/mongo.py +2597 -0
  54. agno/db/mongo/schemas.py +119 -0
  55. agno/db/mongo/utils.py +276 -0
  56. agno/db/mysql/__init__.py +4 -0
  57. agno/db/mysql/async_mysql.py +2912 -0
  58. agno/db/mysql/mysql.py +2923 -0
  59. agno/db/mysql/schemas.py +186 -0
  60. agno/db/mysql/utils.py +488 -0
  61. agno/db/postgres/__init__.py +4 -0
  62. agno/db/postgres/async_postgres.py +2579 -0
  63. agno/db/postgres/postgres.py +2870 -0
  64. agno/db/postgres/schemas.py +187 -0
  65. agno/db/postgres/utils.py +442 -0
  66. agno/db/redis/__init__.py +3 -0
  67. agno/db/redis/redis.py +2141 -0
  68. agno/db/redis/schemas.py +159 -0
  69. agno/db/redis/utils.py +346 -0
  70. agno/db/schemas/__init__.py +4 -0
  71. agno/db/schemas/culture.py +120 -0
  72. agno/db/schemas/evals.py +34 -0
  73. agno/db/schemas/knowledge.py +40 -0
  74. agno/db/schemas/memory.py +61 -0
  75. agno/db/singlestore/__init__.py +3 -0
  76. agno/db/singlestore/schemas.py +179 -0
  77. agno/db/singlestore/singlestore.py +2877 -0
  78. agno/db/singlestore/utils.py +384 -0
  79. agno/db/sqlite/__init__.py +4 -0
  80. agno/db/sqlite/async_sqlite.py +2911 -0
  81. agno/db/sqlite/schemas.py +181 -0
  82. agno/db/sqlite/sqlite.py +2908 -0
  83. agno/db/sqlite/utils.py +429 -0
  84. agno/db/surrealdb/__init__.py +3 -0
  85. agno/db/surrealdb/metrics.py +292 -0
  86. agno/db/surrealdb/models.py +334 -0
  87. agno/db/surrealdb/queries.py +71 -0
  88. agno/db/surrealdb/surrealdb.py +1908 -0
  89. agno/db/surrealdb/utils.py +147 -0
  90. agno/db/utils.py +118 -0
  91. agno/eval/__init__.py +24 -0
  92. agno/eval/accuracy.py +666 -276
  93. agno/eval/agent_as_judge.py +861 -0
  94. agno/eval/base.py +29 -0
  95. agno/eval/performance.py +779 -0
  96. agno/eval/reliability.py +241 -62
  97. agno/eval/utils.py +120 -0
  98. agno/exceptions.py +143 -1
  99. agno/filters.py +354 -0
  100. agno/guardrails/__init__.py +6 -0
  101. agno/guardrails/base.py +19 -0
  102. agno/guardrails/openai.py +144 -0
  103. agno/guardrails/pii.py +94 -0
  104. agno/guardrails/prompt_injection.py +52 -0
  105. agno/hooks/__init__.py +3 -0
  106. agno/hooks/decorator.py +164 -0
  107. agno/integrations/discord/__init__.py +3 -0
  108. agno/integrations/discord/client.py +203 -0
  109. agno/knowledge/__init__.py +5 -1
  110. agno/{document → knowledge}/chunking/agentic.py +22 -14
  111. agno/{document → knowledge}/chunking/document.py +2 -2
  112. agno/{document → knowledge}/chunking/fixed.py +7 -6
  113. agno/knowledge/chunking/markdown.py +151 -0
  114. agno/{document → knowledge}/chunking/recursive.py +15 -3
  115. agno/knowledge/chunking/row.py +39 -0
  116. agno/knowledge/chunking/semantic.py +91 -0
  117. agno/knowledge/chunking/strategy.py +165 -0
  118. agno/knowledge/content.py +74 -0
  119. agno/knowledge/document/__init__.py +5 -0
  120. agno/{document → knowledge/document}/base.py +12 -2
  121. agno/knowledge/embedder/__init__.py +5 -0
  122. agno/knowledge/embedder/aws_bedrock.py +343 -0
  123. agno/knowledge/embedder/azure_openai.py +210 -0
  124. agno/{embedder → knowledge/embedder}/base.py +8 -0
  125. agno/knowledge/embedder/cohere.py +323 -0
  126. agno/knowledge/embedder/fastembed.py +62 -0
  127. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  128. agno/knowledge/embedder/google.py +258 -0
  129. agno/knowledge/embedder/huggingface.py +94 -0
  130. agno/knowledge/embedder/jina.py +182 -0
  131. agno/knowledge/embedder/langdb.py +22 -0
  132. agno/knowledge/embedder/mistral.py +206 -0
  133. agno/knowledge/embedder/nebius.py +13 -0
  134. agno/knowledge/embedder/ollama.py +154 -0
  135. agno/knowledge/embedder/openai.py +195 -0
  136. agno/knowledge/embedder/sentence_transformer.py +63 -0
  137. agno/{embedder → knowledge/embedder}/together.py +1 -1
  138. agno/knowledge/embedder/vllm.py +262 -0
  139. agno/knowledge/embedder/voyageai.py +165 -0
  140. agno/knowledge/knowledge.py +3006 -0
  141. agno/knowledge/reader/__init__.py +7 -0
  142. agno/knowledge/reader/arxiv_reader.py +81 -0
  143. agno/knowledge/reader/base.py +95 -0
  144. agno/knowledge/reader/csv_reader.py +164 -0
  145. agno/knowledge/reader/docx_reader.py +82 -0
  146. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  147. agno/knowledge/reader/firecrawl_reader.py +201 -0
  148. agno/knowledge/reader/json_reader.py +88 -0
  149. agno/knowledge/reader/markdown_reader.py +137 -0
  150. agno/knowledge/reader/pdf_reader.py +431 -0
  151. agno/knowledge/reader/pptx_reader.py +101 -0
  152. agno/knowledge/reader/reader_factory.py +313 -0
  153. agno/knowledge/reader/s3_reader.py +89 -0
  154. agno/knowledge/reader/tavily_reader.py +193 -0
  155. agno/knowledge/reader/text_reader.py +127 -0
  156. agno/knowledge/reader/web_search_reader.py +325 -0
  157. agno/knowledge/reader/website_reader.py +455 -0
  158. agno/knowledge/reader/wikipedia_reader.py +91 -0
  159. agno/knowledge/reader/youtube_reader.py +78 -0
  160. agno/knowledge/remote_content/remote_content.py +88 -0
  161. agno/knowledge/reranker/__init__.py +3 -0
  162. agno/{reranker → knowledge/reranker}/base.py +1 -1
  163. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  164. agno/knowledge/reranker/infinity.py +195 -0
  165. agno/knowledge/reranker/sentence_transformer.py +54 -0
  166. agno/knowledge/types.py +39 -0
  167. agno/knowledge/utils.py +234 -0
  168. agno/media.py +439 -95
  169. agno/memory/__init__.py +16 -3
  170. agno/memory/manager.py +1474 -123
  171. agno/memory/strategies/__init__.py +15 -0
  172. agno/memory/strategies/base.py +66 -0
  173. agno/memory/strategies/summarize.py +196 -0
  174. agno/memory/strategies/types.py +37 -0
  175. agno/models/aimlapi/__init__.py +5 -0
  176. agno/models/aimlapi/aimlapi.py +62 -0
  177. agno/models/anthropic/__init__.py +4 -0
  178. agno/models/anthropic/claude.py +960 -496
  179. agno/models/aws/__init__.py +15 -0
  180. agno/models/aws/bedrock.py +686 -451
  181. agno/models/aws/claude.py +190 -183
  182. agno/models/azure/__init__.py +18 -1
  183. agno/models/azure/ai_foundry.py +489 -0
  184. agno/models/azure/openai_chat.py +89 -40
  185. agno/models/base.py +2477 -550
  186. agno/models/cerebras/__init__.py +12 -0
  187. agno/models/cerebras/cerebras.py +565 -0
  188. agno/models/cerebras/cerebras_openai.py +131 -0
  189. agno/models/cohere/__init__.py +4 -0
  190. agno/models/cohere/chat.py +306 -492
  191. agno/models/cometapi/__init__.py +5 -0
  192. agno/models/cometapi/cometapi.py +74 -0
  193. agno/models/dashscope/__init__.py +5 -0
  194. agno/models/dashscope/dashscope.py +90 -0
  195. agno/models/deepinfra/__init__.py +5 -0
  196. agno/models/deepinfra/deepinfra.py +45 -0
  197. agno/models/deepseek/__init__.py +4 -0
  198. agno/models/deepseek/deepseek.py +110 -9
  199. agno/models/fireworks/__init__.py +4 -0
  200. agno/models/fireworks/fireworks.py +19 -22
  201. agno/models/google/__init__.py +3 -7
  202. agno/models/google/gemini.py +1717 -662
  203. agno/models/google/utils.py +22 -0
  204. agno/models/groq/__init__.py +4 -0
  205. agno/models/groq/groq.py +391 -666
  206. agno/models/huggingface/__init__.py +4 -0
  207. agno/models/huggingface/huggingface.py +266 -538
  208. agno/models/ibm/__init__.py +5 -0
  209. agno/models/ibm/watsonx.py +432 -0
  210. agno/models/internlm/__init__.py +3 -0
  211. agno/models/internlm/internlm.py +20 -3
  212. agno/models/langdb/__init__.py +1 -0
  213. agno/models/langdb/langdb.py +60 -0
  214. agno/models/litellm/__init__.py +14 -0
  215. agno/models/litellm/chat.py +503 -0
  216. agno/models/litellm/litellm_openai.py +42 -0
  217. agno/models/llama_cpp/__init__.py +5 -0
  218. agno/models/llama_cpp/llama_cpp.py +22 -0
  219. agno/models/lmstudio/__init__.py +5 -0
  220. agno/models/lmstudio/lmstudio.py +25 -0
  221. agno/models/message.py +361 -39
  222. agno/models/meta/__init__.py +12 -0
  223. agno/models/meta/llama.py +502 -0
  224. agno/models/meta/llama_openai.py +79 -0
  225. agno/models/metrics.py +120 -0
  226. agno/models/mistral/__init__.py +4 -0
  227. agno/models/mistral/mistral.py +293 -393
  228. agno/models/nebius/__init__.py +3 -0
  229. agno/models/nebius/nebius.py +53 -0
  230. agno/models/nexus/__init__.py +3 -0
  231. agno/models/nexus/nexus.py +22 -0
  232. agno/models/nvidia/__init__.py +4 -0
  233. agno/models/nvidia/nvidia.py +22 -3
  234. agno/models/ollama/__init__.py +4 -2
  235. agno/models/ollama/chat.py +257 -492
  236. agno/models/openai/__init__.py +7 -0
  237. agno/models/openai/chat.py +725 -770
  238. agno/models/openai/like.py +16 -2
  239. agno/models/openai/responses.py +1121 -0
  240. agno/models/openrouter/__init__.py +4 -0
  241. agno/models/openrouter/openrouter.py +62 -5
  242. agno/models/perplexity/__init__.py +5 -0
  243. agno/models/perplexity/perplexity.py +203 -0
  244. agno/models/portkey/__init__.py +3 -0
  245. agno/models/portkey/portkey.py +82 -0
  246. agno/models/requesty/__init__.py +5 -0
  247. agno/models/requesty/requesty.py +69 -0
  248. agno/models/response.py +177 -7
  249. agno/models/sambanova/__init__.py +4 -0
  250. agno/models/sambanova/sambanova.py +23 -4
  251. agno/models/siliconflow/__init__.py +5 -0
  252. agno/models/siliconflow/siliconflow.py +42 -0
  253. agno/models/together/__init__.py +4 -0
  254. agno/models/together/together.py +21 -164
  255. agno/models/utils.py +266 -0
  256. agno/models/vercel/__init__.py +3 -0
  257. agno/models/vercel/v0.py +43 -0
  258. agno/models/vertexai/__init__.py +0 -1
  259. agno/models/vertexai/claude.py +190 -0
  260. agno/models/vllm/__init__.py +3 -0
  261. agno/models/vllm/vllm.py +83 -0
  262. agno/models/xai/__init__.py +2 -0
  263. agno/models/xai/xai.py +111 -7
  264. agno/os/__init__.py +3 -0
  265. agno/os/app.py +1027 -0
  266. agno/os/auth.py +244 -0
  267. agno/os/config.py +126 -0
  268. agno/os/interfaces/__init__.py +1 -0
  269. agno/os/interfaces/a2a/__init__.py +3 -0
  270. agno/os/interfaces/a2a/a2a.py +42 -0
  271. agno/os/interfaces/a2a/router.py +249 -0
  272. agno/os/interfaces/a2a/utils.py +924 -0
  273. agno/os/interfaces/agui/__init__.py +3 -0
  274. agno/os/interfaces/agui/agui.py +47 -0
  275. agno/os/interfaces/agui/router.py +147 -0
  276. agno/os/interfaces/agui/utils.py +574 -0
  277. agno/os/interfaces/base.py +25 -0
  278. agno/os/interfaces/slack/__init__.py +3 -0
  279. agno/os/interfaces/slack/router.py +148 -0
  280. agno/os/interfaces/slack/security.py +30 -0
  281. agno/os/interfaces/slack/slack.py +47 -0
  282. agno/os/interfaces/whatsapp/__init__.py +3 -0
  283. agno/os/interfaces/whatsapp/router.py +210 -0
  284. agno/os/interfaces/whatsapp/security.py +55 -0
  285. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  286. agno/os/mcp.py +293 -0
  287. agno/os/middleware/__init__.py +9 -0
  288. agno/os/middleware/jwt.py +797 -0
  289. agno/os/router.py +258 -0
  290. agno/os/routers/__init__.py +3 -0
  291. agno/os/routers/agents/__init__.py +3 -0
  292. agno/os/routers/agents/router.py +599 -0
  293. agno/os/routers/agents/schema.py +261 -0
  294. agno/os/routers/evals/__init__.py +3 -0
  295. agno/os/routers/evals/evals.py +450 -0
  296. agno/os/routers/evals/schemas.py +174 -0
  297. agno/os/routers/evals/utils.py +231 -0
  298. agno/os/routers/health.py +31 -0
  299. agno/os/routers/home.py +52 -0
  300. agno/os/routers/knowledge/__init__.py +3 -0
  301. agno/os/routers/knowledge/knowledge.py +1008 -0
  302. agno/os/routers/knowledge/schemas.py +178 -0
  303. agno/os/routers/memory/__init__.py +3 -0
  304. agno/os/routers/memory/memory.py +661 -0
  305. agno/os/routers/memory/schemas.py +88 -0
  306. agno/os/routers/metrics/__init__.py +3 -0
  307. agno/os/routers/metrics/metrics.py +190 -0
  308. agno/os/routers/metrics/schemas.py +47 -0
  309. agno/os/routers/session/__init__.py +3 -0
  310. agno/os/routers/session/session.py +997 -0
  311. agno/os/routers/teams/__init__.py +3 -0
  312. agno/os/routers/teams/router.py +512 -0
  313. agno/os/routers/teams/schema.py +257 -0
  314. agno/os/routers/traces/__init__.py +3 -0
  315. agno/os/routers/traces/schemas.py +414 -0
  316. agno/os/routers/traces/traces.py +499 -0
  317. agno/os/routers/workflows/__init__.py +3 -0
  318. agno/os/routers/workflows/router.py +624 -0
  319. agno/os/routers/workflows/schema.py +75 -0
  320. agno/os/schema.py +534 -0
  321. agno/os/scopes.py +469 -0
  322. agno/{playground → os}/settings.py +7 -15
  323. agno/os/utils.py +973 -0
  324. agno/reasoning/anthropic.py +80 -0
  325. agno/reasoning/azure_ai_foundry.py +67 -0
  326. agno/reasoning/deepseek.py +63 -0
  327. agno/reasoning/default.py +97 -0
  328. agno/reasoning/gemini.py +73 -0
  329. agno/reasoning/groq.py +71 -0
  330. agno/reasoning/helpers.py +24 -1
  331. agno/reasoning/ollama.py +67 -0
  332. agno/reasoning/openai.py +86 -0
  333. agno/reasoning/step.py +2 -1
  334. agno/reasoning/vertexai.py +76 -0
  335. agno/run/__init__.py +6 -0
  336. agno/run/agent.py +822 -0
  337. agno/run/base.py +247 -0
  338. agno/run/cancel.py +81 -0
  339. agno/run/requirement.py +181 -0
  340. agno/run/team.py +767 -0
  341. agno/run/workflow.py +708 -0
  342. agno/session/__init__.py +10 -0
  343. agno/session/agent.py +260 -0
  344. agno/session/summary.py +265 -0
  345. agno/session/team.py +342 -0
  346. agno/session/workflow.py +501 -0
  347. agno/table.py +10 -0
  348. agno/team/__init__.py +37 -0
  349. agno/team/team.py +9536 -0
  350. agno/tools/__init__.py +7 -0
  351. agno/tools/agentql.py +120 -0
  352. agno/tools/airflow.py +22 -12
  353. agno/tools/api.py +122 -0
  354. agno/tools/apify.py +276 -83
  355. agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
  356. agno/tools/aws_lambda.py +28 -7
  357. agno/tools/aws_ses.py +66 -0
  358. agno/tools/baidusearch.py +11 -4
  359. agno/tools/bitbucket.py +292 -0
  360. agno/tools/brandfetch.py +213 -0
  361. agno/tools/bravesearch.py +106 -0
  362. agno/tools/brightdata.py +367 -0
  363. agno/tools/browserbase.py +209 -0
  364. agno/tools/calcom.py +32 -23
  365. agno/tools/calculator.py +24 -37
  366. agno/tools/cartesia.py +187 -0
  367. agno/tools/{clickup_tool.py → clickup.py} +17 -28
  368. agno/tools/confluence.py +91 -26
  369. agno/tools/crawl4ai.py +139 -43
  370. agno/tools/csv_toolkit.py +28 -22
  371. agno/tools/dalle.py +36 -22
  372. agno/tools/daytona.py +475 -0
  373. agno/tools/decorator.py +169 -14
  374. agno/tools/desi_vocal.py +23 -11
  375. agno/tools/discord.py +32 -29
  376. agno/tools/docker.py +716 -0
  377. agno/tools/duckdb.py +76 -81
  378. agno/tools/duckduckgo.py +43 -40
  379. agno/tools/e2b.py +703 -0
  380. agno/tools/eleven_labs.py +65 -54
  381. agno/tools/email.py +13 -5
  382. agno/tools/evm.py +129 -0
  383. agno/tools/exa.py +324 -42
  384. agno/tools/fal.py +39 -35
  385. agno/tools/file.py +196 -30
  386. agno/tools/file_generation.py +356 -0
  387. agno/tools/financial_datasets.py +288 -0
  388. agno/tools/firecrawl.py +108 -33
  389. agno/tools/function.py +960 -122
  390. agno/tools/giphy.py +34 -12
  391. agno/tools/github.py +1294 -97
  392. agno/tools/gmail.py +922 -0
  393. agno/tools/google_bigquery.py +117 -0
  394. agno/tools/google_drive.py +271 -0
  395. agno/tools/google_maps.py +253 -0
  396. agno/tools/googlecalendar.py +607 -107
  397. agno/tools/googlesheets.py +377 -0
  398. agno/tools/hackernews.py +20 -12
  399. agno/tools/jina.py +24 -14
  400. agno/tools/jira.py +48 -19
  401. agno/tools/knowledge.py +218 -0
  402. agno/tools/linear.py +82 -43
  403. agno/tools/linkup.py +58 -0
  404. agno/tools/local_file_system.py +15 -7
  405. agno/tools/lumalab.py +41 -26
  406. agno/tools/mcp/__init__.py +10 -0
  407. agno/tools/mcp/mcp.py +331 -0
  408. agno/tools/mcp/multi_mcp.py +347 -0
  409. agno/tools/mcp/params.py +24 -0
  410. agno/tools/mcp_toolbox.py +284 -0
  411. agno/tools/mem0.py +193 -0
  412. agno/tools/memory.py +419 -0
  413. agno/tools/mlx_transcribe.py +11 -9
  414. agno/tools/models/azure_openai.py +190 -0
  415. agno/tools/models/gemini.py +203 -0
  416. agno/tools/models/groq.py +158 -0
  417. agno/tools/models/morph.py +186 -0
  418. agno/tools/models/nebius.py +124 -0
  419. agno/tools/models_labs.py +163 -82
  420. agno/tools/moviepy_video.py +18 -13
  421. agno/tools/nano_banana.py +151 -0
  422. agno/tools/neo4j.py +134 -0
  423. agno/tools/newspaper.py +15 -4
  424. agno/tools/newspaper4k.py +19 -6
  425. agno/tools/notion.py +204 -0
  426. agno/tools/openai.py +181 -17
  427. agno/tools/openbb.py +27 -20
  428. agno/tools/opencv.py +321 -0
  429. agno/tools/openweather.py +233 -0
  430. agno/tools/oxylabs.py +385 -0
  431. agno/tools/pandas.py +25 -15
  432. agno/tools/parallel.py +314 -0
  433. agno/tools/postgres.py +238 -185
  434. agno/tools/pubmed.py +125 -13
  435. agno/tools/python.py +48 -35
  436. agno/tools/reasoning.py +283 -0
  437. agno/tools/reddit.py +207 -29
  438. agno/tools/redshift.py +406 -0
  439. agno/tools/replicate.py +69 -26
  440. agno/tools/resend.py +11 -6
  441. agno/tools/scrapegraph.py +179 -19
  442. agno/tools/searxng.py +23 -31
  443. agno/tools/serpapi.py +15 -10
  444. agno/tools/serper.py +255 -0
  445. agno/tools/shell.py +23 -12
  446. agno/tools/shopify.py +1519 -0
  447. agno/tools/slack.py +56 -14
  448. agno/tools/sleep.py +8 -6
  449. agno/tools/spider.py +35 -11
  450. agno/tools/spotify.py +919 -0
  451. agno/tools/sql.py +34 -19
  452. agno/tools/tavily.py +158 -8
  453. agno/tools/telegram.py +18 -8
  454. agno/tools/todoist.py +218 -0
  455. agno/tools/toolkit.py +134 -9
  456. agno/tools/trafilatura.py +388 -0
  457. agno/tools/trello.py +25 -28
  458. agno/tools/twilio.py +18 -9
  459. agno/tools/user_control_flow.py +78 -0
  460. agno/tools/valyu.py +228 -0
  461. agno/tools/visualization.py +467 -0
  462. agno/tools/webbrowser.py +28 -0
  463. agno/tools/webex.py +76 -0
  464. agno/tools/website.py +23 -19
  465. agno/tools/webtools.py +45 -0
  466. agno/tools/whatsapp.py +286 -0
  467. agno/tools/wikipedia.py +28 -19
  468. agno/tools/workflow.py +285 -0
  469. agno/tools/{twitter.py → x.py} +142 -46
  470. agno/tools/yfinance.py +41 -39
  471. agno/tools/youtube.py +34 -17
  472. agno/tools/zendesk.py +15 -5
  473. agno/tools/zep.py +454 -0
  474. agno/tools/zoom.py +86 -37
  475. agno/tracing/__init__.py +12 -0
  476. agno/tracing/exporter.py +157 -0
  477. agno/tracing/schemas.py +276 -0
  478. agno/tracing/setup.py +111 -0
  479. agno/utils/agent.py +938 -0
  480. agno/utils/audio.py +37 -1
  481. agno/utils/certs.py +27 -0
  482. agno/utils/code_execution.py +11 -0
  483. agno/utils/common.py +103 -20
  484. agno/utils/cryptography.py +22 -0
  485. agno/utils/dttm.py +33 -0
  486. agno/utils/events.py +700 -0
  487. agno/utils/functions.py +107 -37
  488. agno/utils/gemini.py +426 -0
  489. agno/utils/hooks.py +171 -0
  490. agno/utils/http.py +185 -0
  491. agno/utils/json_schema.py +159 -37
  492. agno/utils/knowledge.py +36 -0
  493. agno/utils/location.py +19 -0
  494. agno/utils/log.py +221 -8
  495. agno/utils/mcp.py +214 -0
  496. agno/utils/media.py +335 -14
  497. agno/utils/merge_dict.py +22 -1
  498. agno/utils/message.py +77 -2
  499. agno/utils/models/ai_foundry.py +50 -0
  500. agno/utils/models/claude.py +373 -0
  501. agno/utils/models/cohere.py +94 -0
  502. agno/utils/models/llama.py +85 -0
  503. agno/utils/models/mistral.py +100 -0
  504. agno/utils/models/openai_responses.py +140 -0
  505. agno/utils/models/schema_utils.py +153 -0
  506. agno/utils/models/watsonx.py +41 -0
  507. agno/utils/openai.py +257 -0
  508. agno/utils/pickle.py +1 -1
  509. agno/utils/pprint.py +124 -8
  510. agno/utils/print_response/agent.py +930 -0
  511. agno/utils/print_response/team.py +1914 -0
  512. agno/utils/print_response/workflow.py +1668 -0
  513. agno/utils/prompts.py +111 -0
  514. agno/utils/reasoning.py +108 -0
  515. agno/utils/response.py +163 -0
  516. agno/utils/serialize.py +32 -0
  517. agno/utils/shell.py +4 -4
  518. agno/utils/streamlit.py +487 -0
  519. agno/utils/string.py +204 -51
  520. agno/utils/team.py +139 -0
  521. agno/utils/timer.py +9 -2
  522. agno/utils/tokens.py +657 -0
  523. agno/utils/tools.py +19 -1
  524. agno/utils/whatsapp.py +305 -0
  525. agno/utils/yaml_io.py +3 -3
  526. agno/vectordb/__init__.py +2 -0
  527. agno/vectordb/base.py +87 -9
  528. agno/vectordb/cassandra/__init__.py +5 -1
  529. agno/vectordb/cassandra/cassandra.py +383 -27
  530. agno/vectordb/chroma/__init__.py +4 -0
  531. agno/vectordb/chroma/chromadb.py +748 -83
  532. agno/vectordb/clickhouse/__init__.py +7 -1
  533. agno/vectordb/clickhouse/clickhousedb.py +554 -53
  534. agno/vectordb/couchbase/__init__.py +3 -0
  535. agno/vectordb/couchbase/couchbase.py +1446 -0
  536. agno/vectordb/lancedb/__init__.py +5 -0
  537. agno/vectordb/lancedb/lance_db.py +730 -98
  538. agno/vectordb/langchaindb/__init__.py +5 -0
  539. agno/vectordb/langchaindb/langchaindb.py +163 -0
  540. agno/vectordb/lightrag/__init__.py +5 -0
  541. agno/vectordb/lightrag/lightrag.py +388 -0
  542. agno/vectordb/llamaindex/__init__.py +3 -0
  543. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  544. agno/vectordb/milvus/__init__.py +3 -0
  545. agno/vectordb/milvus/milvus.py +966 -78
  546. agno/vectordb/mongodb/__init__.py +9 -1
  547. agno/vectordb/mongodb/mongodb.py +1175 -172
  548. agno/vectordb/pgvector/__init__.py +8 -0
  549. agno/vectordb/pgvector/pgvector.py +599 -115
  550. agno/vectordb/pineconedb/__init__.py +5 -1
  551. agno/vectordb/pineconedb/pineconedb.py +406 -43
  552. agno/vectordb/qdrant/__init__.py +4 -0
  553. agno/vectordb/qdrant/qdrant.py +914 -61
  554. agno/vectordb/redis/__init__.py +9 -0
  555. agno/vectordb/redis/redisdb.py +682 -0
  556. agno/vectordb/singlestore/__init__.py +8 -1
  557. agno/vectordb/singlestore/singlestore.py +771 -0
  558. agno/vectordb/surrealdb/__init__.py +3 -0
  559. agno/vectordb/surrealdb/surrealdb.py +663 -0
  560. agno/vectordb/upstashdb/__init__.py +5 -0
  561. agno/vectordb/upstashdb/upstashdb.py +718 -0
  562. agno/vectordb/weaviate/__init__.py +8 -0
  563. agno/vectordb/weaviate/index.py +15 -0
  564. agno/vectordb/weaviate/weaviate.py +1009 -0
  565. agno/workflow/__init__.py +23 -1
  566. agno/workflow/agent.py +299 -0
  567. agno/workflow/condition.py +759 -0
  568. agno/workflow/loop.py +756 -0
  569. agno/workflow/parallel.py +853 -0
  570. agno/workflow/router.py +723 -0
  571. agno/workflow/step.py +1564 -0
  572. agno/workflow/steps.py +613 -0
  573. agno/workflow/types.py +556 -0
  574. agno/workflow/workflow.py +4327 -514
  575. agno-2.3.13.dist-info/METADATA +639 -0
  576. agno-2.3.13.dist-info/RECORD +613 -0
  577. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
  578. agno-2.3.13.dist-info/licenses/LICENSE +201 -0
  579. agno/api/playground.py +0 -91
  580. agno/api/schemas/playground.py +0 -22
  581. agno/api/schemas/user.py +0 -22
  582. agno/api/schemas/workspace.py +0 -46
  583. agno/api/user.py +0 -160
  584. agno/api/workspace.py +0 -151
  585. agno/cli/auth_server.py +0 -118
  586. agno/cli/config.py +0 -275
  587. agno/cli/console.py +0 -88
  588. agno/cli/credentials.py +0 -23
  589. agno/cli/entrypoint.py +0 -571
  590. agno/cli/operator.py +0 -355
  591. agno/cli/settings.py +0 -85
  592. agno/cli/ws/ws_cli.py +0 -817
  593. agno/constants.py +0 -13
  594. agno/document/__init__.py +0 -1
  595. agno/document/chunking/semantic.py +0 -47
  596. agno/document/chunking/strategy.py +0 -31
  597. agno/document/reader/__init__.py +0 -1
  598. agno/document/reader/arxiv_reader.py +0 -41
  599. agno/document/reader/base.py +0 -22
  600. agno/document/reader/csv_reader.py +0 -84
  601. agno/document/reader/docx_reader.py +0 -46
  602. agno/document/reader/firecrawl_reader.py +0 -99
  603. agno/document/reader/json_reader.py +0 -43
  604. agno/document/reader/pdf_reader.py +0 -219
  605. agno/document/reader/s3/pdf_reader.py +0 -46
  606. agno/document/reader/s3/text_reader.py +0 -51
  607. agno/document/reader/text_reader.py +0 -41
  608. agno/document/reader/website_reader.py +0 -175
  609. agno/document/reader/youtube_reader.py +0 -50
  610. agno/embedder/__init__.py +0 -1
  611. agno/embedder/azure_openai.py +0 -86
  612. agno/embedder/cohere.py +0 -72
  613. agno/embedder/fastembed.py +0 -37
  614. agno/embedder/google.py +0 -73
  615. agno/embedder/huggingface.py +0 -54
  616. agno/embedder/mistral.py +0 -80
  617. agno/embedder/ollama.py +0 -57
  618. agno/embedder/openai.py +0 -74
  619. agno/embedder/sentence_transformer.py +0 -38
  620. agno/embedder/voyageai.py +0 -64
  621. agno/eval/perf.py +0 -201
  622. agno/file/__init__.py +0 -1
  623. agno/file/file.py +0 -16
  624. agno/file/local/csv.py +0 -32
  625. agno/file/local/txt.py +0 -19
  626. agno/infra/app.py +0 -240
  627. agno/infra/base.py +0 -144
  628. agno/infra/context.py +0 -20
  629. agno/infra/db_app.py +0 -52
  630. agno/infra/resource.py +0 -205
  631. agno/infra/resources.py +0 -55
  632. agno/knowledge/agent.py +0 -230
  633. agno/knowledge/arxiv.py +0 -22
  634. agno/knowledge/combined.py +0 -22
  635. agno/knowledge/csv.py +0 -28
  636. agno/knowledge/csv_url.py +0 -19
  637. agno/knowledge/document.py +0 -20
  638. agno/knowledge/docx.py +0 -30
  639. agno/knowledge/json.py +0 -28
  640. agno/knowledge/langchain.py +0 -71
  641. agno/knowledge/llamaindex.py +0 -66
  642. agno/knowledge/pdf.py +0 -28
  643. agno/knowledge/pdf_url.py +0 -26
  644. agno/knowledge/s3/base.py +0 -60
  645. agno/knowledge/s3/pdf.py +0 -21
  646. agno/knowledge/s3/text.py +0 -23
  647. agno/knowledge/text.py +0 -30
  648. agno/knowledge/website.py +0 -88
  649. agno/knowledge/wikipedia.py +0 -31
  650. agno/knowledge/youtube.py +0 -22
  651. agno/memory/agent.py +0 -392
  652. agno/memory/classifier.py +0 -104
  653. agno/memory/db/__init__.py +0 -1
  654. agno/memory/db/base.py +0 -42
  655. agno/memory/db/mongodb.py +0 -189
  656. agno/memory/db/postgres.py +0 -203
  657. agno/memory/db/sqlite.py +0 -193
  658. agno/memory/memory.py +0 -15
  659. agno/memory/row.py +0 -36
  660. agno/memory/summarizer.py +0 -192
  661. agno/memory/summary.py +0 -19
  662. agno/memory/workflow.py +0 -38
  663. agno/models/google/gemini_openai.py +0 -26
  664. agno/models/ollama/hermes.py +0 -221
  665. agno/models/ollama/tools.py +0 -362
  666. agno/models/vertexai/gemini.py +0 -595
  667. agno/playground/__init__.py +0 -3
  668. agno/playground/async_router.py +0 -421
  669. agno/playground/deploy.py +0 -249
  670. agno/playground/operator.py +0 -92
  671. agno/playground/playground.py +0 -91
  672. agno/playground/schemas.py +0 -76
  673. agno/playground/serve.py +0 -55
  674. agno/playground/sync_router.py +0 -405
  675. agno/reasoning/agent.py +0 -68
  676. agno/run/response.py +0 -112
  677. agno/storage/agent/__init__.py +0 -0
  678. agno/storage/agent/base.py +0 -38
  679. agno/storage/agent/dynamodb.py +0 -350
  680. agno/storage/agent/json.py +0 -92
  681. agno/storage/agent/mongodb.py +0 -228
  682. agno/storage/agent/postgres.py +0 -367
  683. agno/storage/agent/session.py +0 -79
  684. agno/storage/agent/singlestore.py +0 -303
  685. agno/storage/agent/sqlite.py +0 -357
  686. agno/storage/agent/yaml.py +0 -93
  687. agno/storage/workflow/__init__.py +0 -0
  688. agno/storage/workflow/base.py +0 -40
  689. agno/storage/workflow/mongodb.py +0 -233
  690. agno/storage/workflow/postgres.py +0 -366
  691. agno/storage/workflow/session.py +0 -60
  692. agno/storage/workflow/sqlite.py +0 -359
  693. agno/tools/googlesearch.py +0 -88
  694. agno/utils/defaults.py +0 -57
  695. agno/utils/filesystem.py +0 -39
  696. agno/utils/git.py +0 -52
  697. agno/utils/json_io.py +0 -30
  698. agno/utils/load_env.py +0 -19
  699. agno/utils/py_io.py +0 -19
  700. agno/utils/pyproject.py +0 -18
  701. agno/utils/resource_filter.py +0 -31
  702. agno/vectordb/singlestore/s2vectordb.py +0 -390
  703. agno/vectordb/singlestore/s2vectordb2.py +0 -355
  704. agno/workspace/__init__.py +0 -0
  705. agno/workspace/config.py +0 -325
  706. agno/workspace/enums.py +0 -6
  707. agno/workspace/helpers.py +0 -48
  708. agno/workspace/operator.py +0 -758
  709. agno/workspace/settings.py +0 -63
  710. agno-0.1.2.dist-info/LICENSE +0 -375
  711. agno-0.1.2.dist-info/METADATA +0 -502
  712. agno-0.1.2.dist-info/RECORD +0 -352
  713. agno-0.1.2.dist-info/entry_points.txt +0 -3
  714. /agno/{cli → db/migrations}/__init__.py +0 -0
  715. /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
  716. /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
  717. /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
  718. /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
  719. /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
  720. /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
  721. /agno/{reranker → utils/models}/__init__.py +0 -0
  722. /agno/{storage → utils/print_response}/__init__.py +0 -0
  723. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/tools/gmail.py ADDED
@@ -0,0 +1,922 @@
1
+ """
2
+ Gmail Toolkit for interacting with Gmail API
3
+
4
+ Required Environment Variables:
5
+ -----------------------------
6
+ - GOOGLE_CLIENT_ID: Google OAuth client ID
7
+ - GOOGLE_CLIENT_SECRET: Google OAuth client secret
8
+ - GOOGLE_PROJECT_ID: Google Cloud project ID
9
+ - GOOGLE_REDIRECT_URI: Google OAuth redirect URI (default: http://localhost)
10
+
11
+ How to Get These Credentials:
12
+ ---------------------------
13
+ 1. Go to Google Cloud Console (https://console.cloud.google.com)
14
+ 2. Create a new project or select an existing one
15
+ 3. Enable the Gmail API:
16
+ - Go to "APIs & Services" > "Enable APIs and Services"
17
+ - Search for "Gmail API"
18
+ - Click "Enable"
19
+
20
+ 4. Create OAuth 2.0 credentials:
21
+ - Go to "APIs & Services" > "Credentials"
22
+ - Click "Create Credentials" > "OAuth client ID"
23
+ - Go through the OAuth consent screen setup
24
+ - Give it a name and click "Create"
25
+ - You'll receive:
26
+ * Client ID (GOOGLE_CLIENT_ID)
27
+ * Client Secret (GOOGLE_CLIENT_SECRET)
28
+ - The Project ID (GOOGLE_PROJECT_ID) is visible in the project dropdown at the top of the page
29
+
30
+ 5. Add auth redirect URI:
31
+ - Go to https://console.cloud.google.com/auth/clients and add the redirect URI as http://127.0.0.1/
32
+
33
+ 6. Set up environment variables:
34
+ Create a .envrc file in your project root with:
35
+ ```
36
+ export GOOGLE_CLIENT_ID=your_client_id_here
37
+ export GOOGLE_CLIENT_SECRET=your_client_secret_here
38
+ export GOOGLE_PROJECT_ID=your_project_id_here
39
+ export GOOGLE_REDIRECT_URI=http://127.0.0.1/ # Default value
40
+ ```
41
+
42
+ Note: The first time you run the application, it will open a browser window for OAuth authentication.
43
+ A token.json file will be created to store the authentication credentials for future use.
44
+ """
45
+
46
+ import base64
47
+ import mimetypes
48
+ import re
49
+ from datetime import datetime, timedelta
50
+ from functools import wraps
51
+ from os import getenv
52
+ from pathlib import Path
53
+ from typing import Any, List, Optional, Union
54
+
55
+ from agno.tools import Toolkit
56
+
57
+ try:
58
+ from email.mime.application import MIMEApplication
59
+ from email.mime.multipart import MIMEMultipart
60
+ from email.mime.text import MIMEText
61
+
62
+ from google.auth.transport.requests import Request
63
+ from google.oauth2.credentials import Credentials
64
+ from google_auth_oauthlib.flow import InstalledAppFlow
65
+ from googleapiclient.discovery import build
66
+ from googleapiclient.errors import HttpError
67
+ except ImportError:
68
+ raise ImportError(
69
+ "Google client library for Python not found , install it using `pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib`"
70
+ )
71
+
72
+
73
+ def authenticate(func):
74
+ """Decorator to ensure authentication before executing a function."""
75
+
76
+ @wraps(func)
77
+ def wrapper(self, *args, **kwargs):
78
+ if not self.creds or not self.creds.valid:
79
+ self._auth()
80
+ if not self.service:
81
+ self.service = build("gmail", "v1", credentials=self.creds)
82
+ return func(self, *args, **kwargs)
83
+
84
+ return wrapper
85
+
86
+
87
+ def validate_email(email: str) -> bool:
88
+ """Validate email format."""
89
+ email = email.strip()
90
+ pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
91
+ return bool(re.match(pattern, email))
92
+
93
+
94
+ class GmailTools(Toolkit):
95
+ # Default scopes for Gmail API access
96
+ DEFAULT_SCOPES = [
97
+ "https://www.googleapis.com/auth/gmail.readonly",
98
+ "https://www.googleapis.com/auth/gmail.modify",
99
+ "https://www.googleapis.com/auth/gmail.compose",
100
+ ]
101
+
102
+ def __init__(
103
+ self,
104
+ creds: Optional[Credentials] = None,
105
+ credentials_path: Optional[str] = None,
106
+ token_path: Optional[str] = None,
107
+ scopes: Optional[List[str]] = None,
108
+ port: Optional[int] = None,
109
+ **kwargs,
110
+ ):
111
+ """Initialize GmailTools and authenticate with Gmail API
112
+
113
+ Args:
114
+ creds (Optional[Credentials]): Pre-fetched OAuth credentials. Use this to skip a new auth flow. Defaults to None.
115
+ credentials_path (Optional[str]): Path to credentials file. Defaults to None.
116
+ token_path (Optional[str]): Path to token file. Defaults to None.
117
+ scopes (Optional[List[str]]): Custom OAuth scopes. If None, uses DEFAULT_SCOPES.
118
+ port (Optional[int]): Port to use for OAuth authentication. Defaults to None.
119
+ """
120
+ self.creds = creds
121
+ self.credentials_path = credentials_path
122
+ self.token_path = token_path
123
+ self.service = None
124
+ self.scopes = scopes or self.DEFAULT_SCOPES
125
+ self.port = port
126
+
127
+ tools: List[Any] = [
128
+ # Reading emails
129
+ self.get_latest_emails,
130
+ self.get_emails_from_user,
131
+ self.get_unread_emails,
132
+ self.get_starred_emails,
133
+ self.get_emails_by_context,
134
+ self.get_emails_by_date,
135
+ self.get_emails_by_thread,
136
+ self.search_emails,
137
+ # Email management
138
+ self.mark_email_as_read,
139
+ self.mark_email_as_unread,
140
+ # Composing emails
141
+ self.create_draft_email,
142
+ self.send_email,
143
+ self.send_email_reply,
144
+ # Label management
145
+ self.list_custom_labels,
146
+ self.apply_label,
147
+ self.remove_label,
148
+ self.delete_custom_label,
149
+ ]
150
+
151
+ super().__init__(name="gmail_tools", tools=tools, **kwargs)
152
+
153
+ # Validate that required scopes are present for requested operations (only check registered functions)
154
+ if (
155
+ "create_draft_email" in self.functions or "send_email" in self.functions
156
+ ) and "https://www.googleapis.com/auth/gmail.compose" not in self.scopes:
157
+ raise ValueError(
158
+ "The scope https://www.googleapis.com/auth/gmail.compose is required for email composition operations"
159
+ )
160
+ read_operations = [
161
+ "get_latest_emails",
162
+ "get_emails_from_user",
163
+ "get_unread_emails",
164
+ "get_starred_emails",
165
+ "get_emails_by_context",
166
+ "get_emails_by_date",
167
+ "get_emails_by_thread",
168
+ "search_emails",
169
+ "list_custom_labels",
170
+ ]
171
+ modify_operations = ["mark_email_as_read", "mark_email_as_unread"]
172
+ if any(read_operation in self.functions for read_operation in read_operations):
173
+ read_scope = "https://www.googleapis.com/auth/gmail.readonly"
174
+ write_scope = "https://www.googleapis.com/auth/gmail.modify"
175
+ if read_scope not in self.scopes and write_scope not in self.scopes:
176
+ raise ValueError(f"The scope {read_scope} is required for email reading operations")
177
+
178
+ if any(modify_operation in self.functions for modify_operation in modify_operations):
179
+ modify_scope = "https://www.googleapis.com/auth/gmail.modify"
180
+ if modify_scope not in self.scopes:
181
+ raise ValueError(f"The scope {modify_scope} is required for email modification operations")
182
+
183
+ def _auth(self) -> None:
184
+ """Authenticate with Gmail API"""
185
+ token_file = Path(self.token_path or "token.json")
186
+ creds_file = Path(self.credentials_path or "credentials.json")
187
+
188
+ if token_file.exists():
189
+ self.creds = Credentials.from_authorized_user_file(str(token_file), self.scopes)
190
+
191
+ if not self.creds or not self.creds.valid:
192
+ if self.creds and self.creds.expired and self.creds.refresh_token:
193
+ self.creds.refresh(Request())
194
+ else:
195
+ client_config = {
196
+ "installed": {
197
+ "client_id": getenv("GOOGLE_CLIENT_ID"),
198
+ "client_secret": getenv("GOOGLE_CLIENT_SECRET"),
199
+ "project_id": getenv("GOOGLE_PROJECT_ID"),
200
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
201
+ "token_uri": "https://oauth2.googleapis.com/token",
202
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
203
+ "redirect_uris": [getenv("GOOGLE_REDIRECT_URI", "http://localhost")],
204
+ }
205
+ }
206
+ if creds_file.exists():
207
+ flow = InstalledAppFlow.from_client_secrets_file(str(creds_file), self.scopes)
208
+ else:
209
+ flow = InstalledAppFlow.from_client_config(client_config, self.scopes)
210
+ self.creds = flow.run_local_server(port=self.port)
211
+
212
+ # Save the credentials for future use
213
+ if self.creds and self.creds.valid:
214
+ token_file.write_text(self.creds.to_json())
215
+
216
+ def _format_emails(self, emails: List[dict]) -> str:
217
+ """Format list of email dictionaries into a readable string"""
218
+ if not emails:
219
+ return "No emails found"
220
+
221
+ formatted_emails = []
222
+ for email in emails:
223
+ formatted_email = (
224
+ f"From: {email['from']}\n"
225
+ f"Subject: {email['subject']}\n"
226
+ f"Date: {email['date']}\n"
227
+ f"Body: {email['body']}\n"
228
+ f"Message ID: {email['id']}\n"
229
+ f"In-Reply-To: {email['in-reply-to']}\n"
230
+ f"References: {email['references']}\n"
231
+ f"Thread ID: {email['thread_id']}\n"
232
+ "----------------------------------------"
233
+ )
234
+ formatted_emails.append(formatted_email)
235
+
236
+ return "\n\n".join(formatted_emails)
237
+
238
+ @authenticate
239
+ def get_latest_emails(self, count: int) -> str:
240
+ """
241
+ Get the latest X emails from the user's inbox.
242
+
243
+ Args:
244
+ count (int): Number of latest emails to retrieve
245
+
246
+ Returns:
247
+ str: Formatted string containing email details
248
+ """
249
+ try:
250
+ results = self.service.users().messages().list(userId="me", maxResults=count).execute() # type: ignore
251
+ emails = self._get_message_details(results.get("messages", []))
252
+ return self._format_emails(emails)
253
+ except HttpError as error:
254
+ return f"Error retrieving latest emails: {error}"
255
+ except Exception as error:
256
+ return f"Unexpected error retrieving latest emails: {type(error).__name__}: {error}"
257
+
258
+ @authenticate
259
+ def get_emails_from_user(self, user: str, count: int) -> str:
260
+ """
261
+ Get X number of emails from a specific user (name or email).
262
+
263
+ Args:
264
+ user (str): Name or email address of the sender
265
+ count (int): Maximum number of emails to retrieve
266
+
267
+ Returns:
268
+ str: Formatted string containing email details
269
+ """
270
+ try:
271
+ query = f"from:{user}" if "@" in user else f"from:{user}*"
272
+ results = self.service.users().messages().list(userId="me", q=query, maxResults=count).execute() # type: ignore
273
+ emails = self._get_message_details(results.get("messages", []))
274
+ return self._format_emails(emails)
275
+ except HttpError as error:
276
+ return f"Error retrieving emails from {user}: {error}"
277
+ except Exception as error:
278
+ return f"Unexpected error retrieving emails from {user}: {type(error).__name__}: {error}"
279
+
280
+ @authenticate
281
+ def get_unread_emails(self, count: int) -> str:
282
+ """
283
+ Get the X number of latest unread emails from the user's inbox.
284
+
285
+ Args:
286
+ count (int): Maximum number of unread emails to retrieve
287
+
288
+ Returns:
289
+ str: Formatted string containing email details
290
+ """
291
+ try:
292
+ results = self.service.users().messages().list(userId="me", q="is:unread", maxResults=count).execute() # type: ignore
293
+ emails = self._get_message_details(results.get("messages", []))
294
+ return self._format_emails(emails)
295
+ except HttpError as error:
296
+ return f"Error retrieving unread emails: {error}"
297
+ except Exception as error:
298
+ return f"Unexpected error retrieving unread emails: {type(error).__name__}: {error}"
299
+
300
+ @authenticate
301
+ def get_emails_by_thread(self, thread_id: str) -> str:
302
+ """
303
+ Retrieve all emails from a specific thread.
304
+
305
+ Args:
306
+ thread_id (str): The ID of the email thread.
307
+
308
+ Returns:
309
+ str: Formatted string containing email thread details.
310
+ """
311
+ try:
312
+ thread = self.service.users().threads().get(userId="me", id=thread_id).execute() # type: ignore
313
+ messages = thread.get("messages", [])
314
+ emails = self._get_message_details(messages)
315
+ return self._format_emails(emails)
316
+ except HttpError as error:
317
+ return f"Error retrieving emails from thread {thread_id}: {error}"
318
+ except Exception as error:
319
+ return f"Unexpected error retrieving emails from thread {thread_id}: {type(error).__name__}: {error}"
320
+
321
+ @authenticate
322
+ def get_starred_emails(self, count: int) -> str:
323
+ """
324
+ Get X number of starred emails from the user's inbox.
325
+
326
+ Args:
327
+ count (int): Maximum number of starred emails to retrieve
328
+
329
+ Returns:
330
+ str: Formatted string containing email details
331
+ """
332
+ try:
333
+ results = self.service.users().messages().list(userId="me", q="is:starred", maxResults=count).execute() # type: ignore
334
+ emails = self._get_message_details(results.get("messages", []))
335
+ return self._format_emails(emails)
336
+ except HttpError as error:
337
+ return f"Error retrieving starred emails: {error}"
338
+ except Exception as error:
339
+ return f"Unexpected error retrieving starred emails: {type(error).__name__}: {error}"
340
+
341
+ @authenticate
342
+ def get_emails_by_context(self, context: str, count: int) -> str:
343
+ """
344
+ Get X number of emails matching a specific context or search term.
345
+
346
+ Args:
347
+ context (str): Search term or context to match in emails
348
+ count (int): Maximum number of emails to retrieve
349
+
350
+ Returns:
351
+ str: Formatted string containing email details
352
+ """
353
+ try:
354
+ results = self.service.users().messages().list(userId="me", q=context, maxResults=count).execute() # type: ignore
355
+ emails = self._get_message_details(results.get("messages", []))
356
+ return self._format_emails(emails)
357
+ except HttpError as error:
358
+ return f"Error retrieving emails by context '{context}': {error}"
359
+ except Exception as error:
360
+ return f"Unexpected error retrieving emails by context '{context}': {type(error).__name__}: {error}"
361
+
362
+ @authenticate
363
+ def get_emails_by_date(
364
+ self, start_date: int, range_in_days: Optional[int] = None, num_emails: Optional[int] = 10
365
+ ) -> str:
366
+ """
367
+ Get emails based on date range. start_date is an integer representing a unix timestamp
368
+
369
+ Args:
370
+ start_date (datetime): Start date for the query
371
+ range_in_days (Optional[int]): Number of days to include in the range (default: None)
372
+ num_emails (Optional[int]): Maximum number of emails to retrieve (default: 10)
373
+
374
+ Returns:
375
+ str: Formatted string containing email details
376
+ """
377
+ try:
378
+ start_date_dt = datetime.fromtimestamp(start_date)
379
+ if range_in_days:
380
+ end_date = start_date_dt + timedelta(days=range_in_days)
381
+ query = f"after:{start_date_dt.strftime('%Y/%m/%d')} before:{end_date.strftime('%Y/%m/%d')}"
382
+ else:
383
+ query = f"after:{start_date_dt.strftime('%Y/%m/%d')}"
384
+
385
+ results = self.service.users().messages().list(userId="me", q=query, maxResults=num_emails).execute() # type: ignore
386
+ emails = self._get_message_details(results.get("messages", []))
387
+ return self._format_emails(emails)
388
+ except HttpError as error:
389
+ return f"Error retrieving emails by date: {error}"
390
+ except Exception as error:
391
+ return f"Unexpected error retrieving emails by date: {type(error).__name__}: {error}"
392
+
393
+ @authenticate
394
+ def create_draft_email(
395
+ self,
396
+ to: str,
397
+ subject: str,
398
+ body: str,
399
+ cc: Optional[str] = None,
400
+ attachments: Optional[Union[str, List[str]]] = None,
401
+ ) -> str:
402
+ """
403
+ Create and save a draft email. to and cc are comma separated string of email ids
404
+ Args:
405
+ to (str): Comma separated string of recipient email addresses
406
+ subject (str): Email subject
407
+ body (str): Email body content
408
+ cc (Optional[str]): Comma separated string of CC email addresses (optional)
409
+ attachments (Optional[Union[str, List[str]]]): File path(s) for attachments (optional)
410
+
411
+ Returns:
412
+ str: Stringified dictionary containing draft email details including id
413
+ """
414
+ self._validate_email_params(to, subject, body)
415
+
416
+ # Process attachments
417
+ attachment_files = []
418
+ if attachments:
419
+ if isinstance(attachments, str):
420
+ attachment_files = [attachments]
421
+ else:
422
+ attachment_files = attachments
423
+
424
+ # Validate attachment files
425
+ for file_path in attachment_files:
426
+ if not Path(file_path).exists():
427
+ raise ValueError(f"Attachment file not found: {file_path}")
428
+
429
+ message = self._create_message(
430
+ to.split(","), subject, body, cc.split(",") if cc else None, attachments=attachment_files
431
+ )
432
+ draft = {"message": message}
433
+ draft = self.service.users().drafts().create(userId="me", body=draft).execute() # type: ignore
434
+ return str(draft)
435
+
436
+ @authenticate
437
+ def send_email(
438
+ self,
439
+ to: str,
440
+ subject: str,
441
+ body: str,
442
+ cc: Optional[str] = None,
443
+ attachments: Optional[Union[str, List[str]]] = None,
444
+ ) -> str:
445
+ """
446
+ Send an email immediately. to and cc are comma separated string of email ids
447
+ Args:
448
+ to (str): Comma separated string of recipient email addresses
449
+ subject (str): Email subject
450
+ body (str): Email body content
451
+ cc (Optional[str]): Comma separated string of CC email addresses (optional)
452
+ attachments (Optional[Union[str, List[str]]]): File path(s) for attachments (optional)
453
+
454
+ Returns:
455
+ str: Stringified dictionary containing sent email details including id
456
+ """
457
+ self._validate_email_params(to, subject, body)
458
+
459
+ # Process attachments
460
+ attachment_files = []
461
+ if attachments:
462
+ if isinstance(attachments, str):
463
+ attachment_files = [attachments]
464
+ else:
465
+ attachment_files = attachments
466
+
467
+ # Validate attachment files
468
+ for file_path in attachment_files:
469
+ if not Path(file_path).exists():
470
+ raise ValueError(f"Attachment file not found: {file_path}")
471
+
472
+ body = body.replace("\n", "<br>")
473
+ message = self._create_message(
474
+ to.split(","), subject, body, cc.split(",") if cc else None, attachments=attachment_files
475
+ )
476
+ message = self.service.users().messages().send(userId="me", body=message).execute() # type: ignore
477
+ return str(message)
478
+
479
+ @authenticate
480
+ def send_email_reply(
481
+ self,
482
+ thread_id: str,
483
+ message_id: str,
484
+ to: str,
485
+ subject: str,
486
+ body: str,
487
+ cc: Optional[str] = None,
488
+ attachments: Optional[Union[str, List[str]]] = None,
489
+ ) -> str:
490
+ """
491
+ Respond to an existing email thread.
492
+
493
+ Args:
494
+ thread_id (str): The ID of the email thread to reply to.
495
+ message_id (str): The ID of the email being replied to.
496
+ to (str): Comma-separated recipient email addresses.
497
+ subject (str): Email subject (prefixed with "Re:" if not already).
498
+ body (str): Email body content.
499
+ cc (Optional[str]): Comma-separated CC email addresses (optional).
500
+ attachments (Optional[Union[str, List[str]]]): File path(s) for attachments (optional)
501
+
502
+ Returns:
503
+ str: Stringified dictionary containing sent email details including id.
504
+ """
505
+ self._validate_email_params(to, subject, body)
506
+
507
+ # Ensure subject starts with "Re:" for consistency
508
+ if not subject.lower().startswith("re:"):
509
+ subject = f"Re: {subject}"
510
+
511
+ # Process attachments
512
+ attachment_files = []
513
+ if attachments:
514
+ if isinstance(attachments, str):
515
+ attachment_files = [attachments]
516
+ else:
517
+ attachment_files = attachments
518
+
519
+ # Validate attachment files
520
+ for file_path in attachment_files:
521
+ if not Path(file_path).exists():
522
+ raise ValueError(f"Attachment file not found: {file_path}")
523
+
524
+ body = body.replace("\n", "<br>")
525
+ message = self._create_message(
526
+ to.split(","),
527
+ subject,
528
+ body,
529
+ cc.split(",") if cc else None,
530
+ thread_id,
531
+ message_id,
532
+ attachments=attachment_files,
533
+ )
534
+ message = self.service.users().messages().send(userId="me", body=message).execute() # type: ignore
535
+ return str(message)
536
+
537
+ @authenticate
538
+ def search_emails(self, query: str, count: int) -> str:
539
+ """
540
+ Get X number of emails based on a given natural text query.
541
+ Searches in to, from, cc, subject and email body contents.
542
+
543
+ Args:
544
+ query (str): Natural language query to search for
545
+ count (int): Number of emails to retrieve
546
+
547
+ Returns:
548
+ str: Formatted string containing email details
549
+ """
550
+ try:
551
+ results = self.service.users().messages().list(userId="me", q=query, maxResults=count).execute() # type: ignore
552
+ emails = self._get_message_details(results.get("messages", []))
553
+ return self._format_emails(emails)
554
+ except HttpError as error:
555
+ return f"Error retrieving emails with query '{query}': {error}"
556
+ except Exception as error:
557
+ return f"Unexpected error retrieving emails with query '{query}': {type(error).__name__}: {error}"
558
+
559
+ @authenticate
560
+ def mark_email_as_read(self, message_id: str) -> str:
561
+ """
562
+ Mark a specific email as read by removing the 'UNREAD' label.
563
+ This is crucial for long polling scenarios to prevent processing the same email multiple times.
564
+
565
+ Args:
566
+ message_id (str): The ID of the message to mark as read
567
+
568
+ Returns:
569
+ str: Success message or error description
570
+ """
571
+ try:
572
+ # Remove the UNREAD label to mark the email as read
573
+ modify_request = {"removeLabelIds": ["UNREAD"]}
574
+
575
+ self.service.users().messages().modify(userId="me", id=message_id, body=modify_request).execute() # type: ignore
576
+
577
+ return f"Successfully marked email {message_id} as read. Labels removed: UNREAD"
578
+
579
+ except HttpError as error:
580
+ return f"HTTP Error marking email {message_id} as read: {error}"
581
+ except Exception as error:
582
+ return f"Error marking email {message_id} as read: {type(error).__name__}: {error}"
583
+
584
+ @authenticate
585
+ def mark_email_as_unread(self, message_id: str) -> str:
586
+ """
587
+ Mark a specific email as unread by adding the 'UNREAD' label.
588
+ This is useful for flagging emails that need attention or re-processing.
589
+
590
+ Args:
591
+ message_id (str): The ID of the message to mark as unread
592
+
593
+ Returns:
594
+ str: Success message or error description
595
+ """
596
+ try:
597
+ # Add the UNREAD label to mark the email as unread
598
+ modify_request = {"addLabelIds": ["UNREAD"]}
599
+
600
+ self.service.users().messages().modify(userId="me", id=message_id, body=modify_request).execute() # type: ignore
601
+
602
+ return f"Successfully marked email {message_id} as unread. Labels added: UNREAD"
603
+
604
+ except HttpError as error:
605
+ return f"HTTP Error marking email {message_id} as unread: {error}"
606
+ except Exception as error:
607
+ return f"Error marking email {message_id} as unread: {type(error).__name__}: {error}"
608
+
609
+ @authenticate
610
+ def list_custom_labels(self) -> str:
611
+ """
612
+ List only user-created custom labels (filters out system labels) in a numbered format.
613
+
614
+ Returns:
615
+ str: A numbered list of custom labels only
616
+ """
617
+ try:
618
+ results = self.service.users().labels().list(userId="me").execute() # type: ignore
619
+ labels = results.get("labels", [])
620
+
621
+ # Filter out only user-created labels
622
+ custom_labels = [label["name"] for label in labels if label.get("type") == "user"]
623
+
624
+ if not custom_labels:
625
+ return "No custom labels found.\nCreate labels using apply_label function!"
626
+
627
+ # Create numbered list
628
+ numbered_labels = [f"{i}. {name}" for i, name in enumerate(custom_labels, 1)]
629
+ return f"Your Custom Labels ({len(custom_labels)} total):\n\n" + "\n".join(numbered_labels)
630
+
631
+ except HttpError as e:
632
+ return f"Error fetching labels: {e}"
633
+ except Exception as e:
634
+ return f"Unexpected error: {type(e).__name__}: {e}"
635
+
636
+ @authenticate
637
+ def apply_label(self, context: str, label_name: str, count: int = 10) -> str:
638
+ """
639
+ Find emails matching a context (search query) and apply a label, creating it if necessary.
640
+
641
+ Args:
642
+ context (str): Gmail search query (e.g., 'is:unread category:promotions')
643
+ label_name (str): Name of the label to apply
644
+ count (int): Maximum number of emails to process
645
+ Returns:
646
+ str: Summary of labeled emails
647
+ """
648
+ try:
649
+ # Fetch messages matching context
650
+ results = self.service.users().messages().list(userId="me", q=context, maxResults=count).execute() # type: ignore
651
+
652
+ messages = results.get("messages", [])
653
+ if not messages:
654
+ return f"No emails found matching: '{context}'"
655
+
656
+ # Check if label exists, create if not
657
+ labels = self.service.users().labels().list(userId="me").execute().get("labels", []) # type: ignore
658
+ label_id = None
659
+ for label in labels:
660
+ if label["name"].lower() == label_name.lower():
661
+ label_id = label["id"]
662
+ break
663
+
664
+ if not label_id:
665
+ label = (
666
+ self.service.users() # type: ignore
667
+ .labels()
668
+ .create(
669
+ userId="me",
670
+ body={"name": label_name, "labelListVisibility": "labelShow", "messageListVisibility": "show"},
671
+ )
672
+ .execute()
673
+ )
674
+ label_id = label["id"]
675
+
676
+ # Apply label to all matching messages
677
+ for msg in messages:
678
+ self.service.users().messages().modify( # type: ignore
679
+ userId="me", id=msg["id"], body={"addLabelIds": [label_id]}
680
+ ).execute() # type: ignore
681
+
682
+ return f"Applied label '{label_name}' to {len(messages)} emails matching '{context}'."
683
+
684
+ except HttpError as e:
685
+ return f"Error applying label '{label_name}': {e}"
686
+ except Exception as e:
687
+ return f"Unexpected error: {type(e).__name__}: {e}"
688
+
689
+ @authenticate
690
+ def remove_label(self, context: str, label_name: str, count: int = 10) -> str:
691
+ """
692
+ Remove a label from emails matching a context (search query).
693
+
694
+ Args:
695
+ context (str): Gmail search query (e.g., 'is:unread category:promotions')
696
+ label_name (str): Name of the label to remove
697
+ count (int): Maximum number of emails to process
698
+ Returns:
699
+ str: Summary of emails with label removed
700
+ """
701
+ try:
702
+ # Get all labels to find the target label
703
+ labels = self.service.users().labels().list(userId="me").execute().get("labels", []) # type: ignore
704
+ label_id = None
705
+
706
+ for label in labels:
707
+ if label["name"].lower() == label_name.lower():
708
+ label_id = label["id"]
709
+ break
710
+
711
+ if not label_id:
712
+ return f"Label '{label_name}' not found."
713
+
714
+ # Fetch messages matching context that have this label
715
+ results = (
716
+ self.service.users() # type: ignore
717
+ .messages()
718
+ .list(userId="me", q=f"{context} label:{label_name}", maxResults=count)
719
+ .execute()
720
+ )
721
+
722
+ messages = results.get("messages", [])
723
+ if not messages:
724
+ return f"No emails found matching: '{context}' with label '{label_name}'"
725
+
726
+ # Remove label from all matching messages
727
+ removed_count = 0
728
+ for msg in messages:
729
+ self.service.users().messages().modify( # type: ignore
730
+ userId="me", id=msg["id"], body={"removeLabelIds": [label_id]}
731
+ ).execute() # type: ignore
732
+ removed_count += 1
733
+
734
+ return f"Removed label '{label_name}' from {removed_count} emails matching '{context}'."
735
+
736
+ except HttpError as e:
737
+ return f"Error removing label '{label_name}': {e}"
738
+ except Exception as e:
739
+ return f"Unexpected error: {type(e).__name__}: {e}"
740
+
741
+ @authenticate
742
+ def delete_custom_label(self, label_name: str, confirm: bool = False) -> str:
743
+ """
744
+ Delete a custom label (with safety confirmation).
745
+
746
+ Args:
747
+ label_name (str): Name of the label to delete
748
+ confirm (bool): Must be True to actually delete the label
749
+ Returns:
750
+ str: Confirmation message or warning
751
+ """
752
+ if not confirm:
753
+ return f"LABEL DELETION REQUIRES CONFIRMATION. This will permanently delete the label '{label_name}' from all emails. Set confirm=True to proceed."
754
+
755
+ try:
756
+ # Get all labels to find the target label
757
+ labels = self.service.users().labels().list(userId="me").execute().get("labels", []) # type: ignore
758
+ target_label = None
759
+
760
+ for label in labels:
761
+ if label["name"].lower() == label_name.lower():
762
+ target_label = label
763
+ break
764
+
765
+ if not target_label:
766
+ return f"Label '{label_name}' not found."
767
+
768
+ # Check if it's a system label using the type field
769
+ if target_label.get("type") != "user":
770
+ return f"Cannot delete system label '{label_name}'. Only user-created labels can be deleted."
771
+
772
+ # Delete the label
773
+ self.service.users().labels().delete(userId="me", id=target_label["id"]).execute() # type: ignore
774
+
775
+ return f"Successfully deleted label '{label_name}'. This label has been removed from all emails."
776
+
777
+ except HttpError as e:
778
+ return f"Error deleting label '{label_name}': {e}"
779
+ except Exception as e:
780
+ return f"Unexpected error: {type(e).__name__}: {e}"
781
+
782
+ def _validate_email_params(self, to: str, subject: str, body: str) -> None:
783
+ """Validate email parameters."""
784
+ if not to:
785
+ raise ValueError("Recipient email cannot be empty")
786
+
787
+ # Validate each email in the comma-separated list
788
+ for email in to.split(","):
789
+ if not validate_email(email.strip()):
790
+ raise ValueError(f"Invalid recipient email format: {email}")
791
+
792
+ if not subject or not subject.strip():
793
+ raise ValueError("Subject cannot be empty")
794
+
795
+ if body is None:
796
+ raise ValueError("Email body cannot be None")
797
+
798
+ def _create_message(
799
+ self,
800
+ to: List[str],
801
+ subject: str,
802
+ body: str,
803
+ cc: Optional[List[str]] = None,
804
+ thread_id: Optional[str] = None,
805
+ message_id: Optional[str] = None,
806
+ attachments: Optional[List[str]] = None,
807
+ ) -> dict:
808
+ body = body.replace("\\n", "\n")
809
+
810
+ # Create multipart message if attachments exist, otherwise simple text message
811
+ message: Union[MIMEMultipart, MIMEText]
812
+ if attachments:
813
+ message = MIMEMultipart()
814
+
815
+ # Add the text body
816
+ text_part = MIMEText(body, "html")
817
+ message.attach(text_part)
818
+
819
+ # Add attachments
820
+ for file_path in attachments:
821
+ file_path_obj = Path(file_path)
822
+ if not file_path_obj.exists():
823
+ continue
824
+
825
+ # Guess the content type based on the file extension
826
+ content_type, encoding = mimetypes.guess_type(file_path)
827
+ if content_type is None or encoding is not None:
828
+ content_type = "application/octet-stream"
829
+
830
+ main_type, sub_type = content_type.split("/", 1)
831
+
832
+ # Read file and create attachment
833
+ with open(file_path, "rb") as file:
834
+ attachment_data = file.read()
835
+
836
+ attachment = MIMEApplication(attachment_data, _subtype=sub_type)
837
+ attachment.add_header("Content-Disposition", "attachment", filename=file_path_obj.name)
838
+ message.attach(attachment)
839
+ else:
840
+ message = MIMEText(body, "html")
841
+
842
+ # Set headers
843
+ message["to"] = ", ".join(to)
844
+ message["from"] = "me"
845
+ message["subject"] = subject
846
+
847
+ if cc:
848
+ message["Cc"] = ", ".join(cc)
849
+
850
+ # Add reply headers if this is a response
851
+ if thread_id and message_id:
852
+ message["In-Reply-To"] = message_id
853
+ message["References"] = message_id
854
+
855
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
856
+ email_data = {"raw": raw_message}
857
+
858
+ if thread_id:
859
+ email_data["threadId"] = thread_id
860
+
861
+ return email_data
862
+
863
+ def _get_message_details(self, messages: List[dict]) -> List[dict]:
864
+ """Get details for list of messages"""
865
+ details = []
866
+ for msg in messages:
867
+ msg_data = self.service.users().messages().get(userId="me", id=msg["id"], format="full").execute() # type: ignore
868
+ details.append(
869
+ {
870
+ "id": msg_data["id"],
871
+ "thread_id": msg_data.get("threadId"),
872
+ "subject": next(
873
+ (header["value"] for header in msg_data["payload"]["headers"] if header["name"] == "Subject"),
874
+ None,
875
+ ),
876
+ "from": next(
877
+ (header["value"] for header in msg_data["payload"]["headers"] if header["name"] == "From"), None
878
+ ),
879
+ "date": next(
880
+ (header["value"] for header in msg_data["payload"]["headers"] if header["name"] == "Date"), None
881
+ ),
882
+ "in-reply-to": next(
883
+ (
884
+ header["value"]
885
+ for header in msg_data["payload"]["headers"]
886
+ if header["name"] == "In-Reply-To"
887
+ ),
888
+ None,
889
+ ),
890
+ "references": next(
891
+ (
892
+ header["value"]
893
+ for header in msg_data["payload"]["headers"]
894
+ if header["name"] == "References"
895
+ ),
896
+ None,
897
+ ),
898
+ "body": self._get_message_body(msg_data),
899
+ }
900
+ )
901
+ return details
902
+
903
+ def _get_message_body(self, msg_data: dict) -> str:
904
+ """Extract message body from message data"""
905
+ body = ""
906
+ attachments = []
907
+ try:
908
+ if "parts" in msg_data["payload"]:
909
+ for part in msg_data["payload"]["parts"]:
910
+ if part["mimeType"] == "text/plain":
911
+ if "data" in part["body"]:
912
+ body = base64.urlsafe_b64decode(part["body"]["data"]).decode()
913
+ elif "filename" in part:
914
+ attachments.append(part["filename"])
915
+ elif "body" in msg_data["payload"] and "data" in msg_data["payload"]["body"]:
916
+ body = base64.urlsafe_b64decode(msg_data["payload"]["body"]["data"]).decode()
917
+ except Exception:
918
+ return "Unable to decode message body"
919
+
920
+ if attachments:
921
+ return f"{body}\n\nAttachments: {', '.join(attachments)}"
922
+ return body