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.
- agno/__init__.py +8 -0
- agno/agent/__init__.py +44 -5
- agno/agent/agent.py +10531 -2975
- agno/api/agent.py +14 -53
- agno/api/api.py +7 -46
- agno/api/evals.py +22 -0
- agno/api/os.py +17 -0
- agno/api/routes.py +6 -25
- agno/api/schemas/__init__.py +9 -0
- agno/api/schemas/agent.py +6 -9
- agno/api/schemas/evals.py +16 -0
- agno/api/schemas/os.py +14 -0
- agno/api/schemas/team.py +10 -10
- agno/api/schemas/utils.py +21 -0
- agno/api/schemas/workflows.py +16 -0
- agno/api/settings.py +53 -0
- agno/api/team.py +22 -26
- agno/api/workflow.py +28 -0
- agno/cloud/aws/base.py +214 -0
- agno/cloud/aws/s3/__init__.py +2 -0
- agno/cloud/aws/s3/api_client.py +43 -0
- agno/cloud/aws/s3/bucket.py +195 -0
- agno/cloud/aws/s3/object.py +57 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/__init__.py +24 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +946 -0
- agno/db/dynamo/__init__.py +3 -0
- agno/db/dynamo/dynamo.py +2781 -0
- agno/db/dynamo/schemas.py +442 -0
- agno/db/dynamo/utils.py +743 -0
- agno/db/firestore/__init__.py +3 -0
- agno/db/firestore/firestore.py +2379 -0
- agno/db/firestore/schemas.py +181 -0
- agno/db/firestore/utils.py +376 -0
- agno/db/gcs_json/__init__.py +3 -0
- agno/db/gcs_json/gcs_json_db.py +1791 -0
- agno/db/gcs_json/utils.py +228 -0
- agno/db/in_memory/__init__.py +3 -0
- agno/db/in_memory/in_memory_db.py +1312 -0
- agno/db/in_memory/utils.py +230 -0
- agno/db/json/__init__.py +3 -0
- agno/db/json/json_db.py +1777 -0
- agno/db/json/utils.py +230 -0
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +635 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +17 -0
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +2597 -0
- agno/db/mongo/schemas.py +119 -0
- agno/db/mongo/utils.py +276 -0
- agno/db/mysql/__init__.py +4 -0
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +2923 -0
- agno/db/mysql/schemas.py +186 -0
- agno/db/mysql/utils.py +488 -0
- agno/db/postgres/__init__.py +4 -0
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +2870 -0
- agno/db/postgres/schemas.py +187 -0
- agno/db/postgres/utils.py +442 -0
- agno/db/redis/__init__.py +3 -0
- agno/db/redis/redis.py +2141 -0
- agno/db/redis/schemas.py +159 -0
- agno/db/redis/utils.py +346 -0
- agno/db/schemas/__init__.py +4 -0
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +34 -0
- agno/db/schemas/knowledge.py +40 -0
- agno/db/schemas/memory.py +61 -0
- agno/db/singlestore/__init__.py +3 -0
- agno/db/singlestore/schemas.py +179 -0
- agno/db/singlestore/singlestore.py +2877 -0
- agno/db/singlestore/utils.py +384 -0
- agno/db/sqlite/__init__.py +4 -0
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +181 -0
- agno/db/sqlite/sqlite.py +2908 -0
- agno/db/sqlite/utils.py +429 -0
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +118 -0
- agno/eval/__init__.py +24 -0
- agno/eval/accuracy.py +666 -276
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +779 -0
- agno/eval/reliability.py +241 -62
- agno/eval/utils.py +120 -0
- agno/exceptions.py +143 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/__init__.py +3 -0
- agno/integrations/discord/client.py +203 -0
- agno/knowledge/__init__.py +5 -1
- agno/{document → knowledge}/chunking/agentic.py +22 -14
- agno/{document → knowledge}/chunking/document.py +2 -2
- agno/{document → knowledge}/chunking/fixed.py +7 -6
- agno/knowledge/chunking/markdown.py +151 -0
- agno/{document → knowledge}/chunking/recursive.py +15 -3
- agno/knowledge/chunking/row.py +39 -0
- agno/knowledge/chunking/semantic.py +91 -0
- agno/knowledge/chunking/strategy.py +165 -0
- agno/knowledge/content.py +74 -0
- agno/knowledge/document/__init__.py +5 -0
- agno/{document → knowledge/document}/base.py +12 -2
- agno/knowledge/embedder/__init__.py +5 -0
- agno/knowledge/embedder/aws_bedrock.py +343 -0
- agno/knowledge/embedder/azure_openai.py +210 -0
- agno/{embedder → knowledge/embedder}/base.py +8 -0
- agno/knowledge/embedder/cohere.py +323 -0
- agno/knowledge/embedder/fastembed.py +62 -0
- agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
- agno/knowledge/embedder/google.py +258 -0
- agno/knowledge/embedder/huggingface.py +94 -0
- agno/knowledge/embedder/jina.py +182 -0
- agno/knowledge/embedder/langdb.py +22 -0
- agno/knowledge/embedder/mistral.py +206 -0
- agno/knowledge/embedder/nebius.py +13 -0
- agno/knowledge/embedder/ollama.py +154 -0
- agno/knowledge/embedder/openai.py +195 -0
- agno/knowledge/embedder/sentence_transformer.py +63 -0
- agno/{embedder → knowledge/embedder}/together.py +1 -1
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +165 -0
- agno/knowledge/knowledge.py +3006 -0
- agno/knowledge/reader/__init__.py +7 -0
- agno/knowledge/reader/arxiv_reader.py +81 -0
- agno/knowledge/reader/base.py +95 -0
- agno/knowledge/reader/csv_reader.py +164 -0
- agno/knowledge/reader/docx_reader.py +82 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/firecrawl_reader.py +201 -0
- agno/knowledge/reader/json_reader.py +88 -0
- agno/knowledge/reader/markdown_reader.py +137 -0
- agno/knowledge/reader/pdf_reader.py +431 -0
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +313 -0
- agno/knowledge/reader/s3_reader.py +89 -0
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +127 -0
- agno/knowledge/reader/web_search_reader.py +325 -0
- agno/knowledge/reader/website_reader.py +455 -0
- agno/knowledge/reader/wikipedia_reader.py +91 -0
- agno/knowledge/reader/youtube_reader.py +78 -0
- agno/knowledge/remote_content/remote_content.py +88 -0
- agno/knowledge/reranker/__init__.py +3 -0
- agno/{reranker → knowledge/reranker}/base.py +1 -1
- agno/{reranker → knowledge/reranker}/cohere.py +2 -2
- agno/knowledge/reranker/infinity.py +195 -0
- agno/knowledge/reranker/sentence_transformer.py +54 -0
- agno/knowledge/types.py +39 -0
- agno/knowledge/utils.py +234 -0
- agno/media.py +439 -95
- agno/memory/__init__.py +16 -3
- agno/memory/manager.py +1474 -123
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/__init__.py +5 -0
- agno/models/aimlapi/aimlapi.py +62 -0
- agno/models/anthropic/__init__.py +4 -0
- agno/models/anthropic/claude.py +960 -496
- agno/models/aws/__init__.py +15 -0
- agno/models/aws/bedrock.py +686 -451
- agno/models/aws/claude.py +190 -183
- agno/models/azure/__init__.py +18 -1
- agno/models/azure/ai_foundry.py +489 -0
- agno/models/azure/openai_chat.py +89 -40
- agno/models/base.py +2477 -550
- agno/models/cerebras/__init__.py +12 -0
- agno/models/cerebras/cerebras.py +565 -0
- agno/models/cerebras/cerebras_openai.py +131 -0
- agno/models/cohere/__init__.py +4 -0
- agno/models/cohere/chat.py +306 -492
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +74 -0
- agno/models/dashscope/__init__.py +5 -0
- agno/models/dashscope/dashscope.py +90 -0
- agno/models/deepinfra/__init__.py +5 -0
- agno/models/deepinfra/deepinfra.py +45 -0
- agno/models/deepseek/__init__.py +4 -0
- agno/models/deepseek/deepseek.py +110 -9
- agno/models/fireworks/__init__.py +4 -0
- agno/models/fireworks/fireworks.py +19 -22
- agno/models/google/__init__.py +3 -7
- agno/models/google/gemini.py +1717 -662
- agno/models/google/utils.py +22 -0
- agno/models/groq/__init__.py +4 -0
- agno/models/groq/groq.py +391 -666
- agno/models/huggingface/__init__.py +4 -0
- agno/models/huggingface/huggingface.py +266 -538
- agno/models/ibm/__init__.py +5 -0
- agno/models/ibm/watsonx.py +432 -0
- agno/models/internlm/__init__.py +3 -0
- agno/models/internlm/internlm.py +20 -3
- agno/models/langdb/__init__.py +1 -0
- agno/models/langdb/langdb.py +60 -0
- agno/models/litellm/__init__.py +14 -0
- agno/models/litellm/chat.py +503 -0
- agno/models/litellm/litellm_openai.py +42 -0
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/lmstudio/__init__.py +5 -0
- agno/models/lmstudio/lmstudio.py +25 -0
- agno/models/message.py +361 -39
- agno/models/meta/__init__.py +12 -0
- agno/models/meta/llama.py +502 -0
- agno/models/meta/llama_openai.py +79 -0
- agno/models/metrics.py +120 -0
- agno/models/mistral/__init__.py +4 -0
- agno/models/mistral/mistral.py +293 -393
- agno/models/nebius/__init__.py +3 -0
- agno/models/nebius/nebius.py +53 -0
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/__init__.py +4 -0
- agno/models/nvidia/nvidia.py +22 -3
- agno/models/ollama/__init__.py +4 -2
- agno/models/ollama/chat.py +257 -492
- agno/models/openai/__init__.py +7 -0
- agno/models/openai/chat.py +725 -770
- agno/models/openai/like.py +16 -2
- agno/models/openai/responses.py +1121 -0
- agno/models/openrouter/__init__.py +4 -0
- agno/models/openrouter/openrouter.py +62 -5
- agno/models/perplexity/__init__.py +5 -0
- agno/models/perplexity/perplexity.py +203 -0
- agno/models/portkey/__init__.py +3 -0
- agno/models/portkey/portkey.py +82 -0
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +69 -0
- agno/models/response.py +177 -7
- agno/models/sambanova/__init__.py +4 -0
- agno/models/sambanova/sambanova.py +23 -4
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +42 -0
- agno/models/together/__init__.py +4 -0
- agno/models/together/together.py +21 -164
- agno/models/utils.py +266 -0
- agno/models/vercel/__init__.py +3 -0
- agno/models/vercel/v0.py +43 -0
- agno/models/vertexai/__init__.py +0 -1
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/__init__.py +3 -0
- agno/models/vllm/vllm.py +83 -0
- agno/models/xai/__init__.py +2 -0
- agno/models/xai/xai.py +111 -7
- agno/os/__init__.py +3 -0
- agno/os/app.py +1027 -0
- agno/os/auth.py +244 -0
- agno/os/config.py +126 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +249 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/__init__.py +3 -0
- agno/os/interfaces/agui/agui.py +47 -0
- agno/os/interfaces/agui/router.py +147 -0
- agno/os/interfaces/agui/utils.py +574 -0
- agno/os/interfaces/base.py +25 -0
- agno/os/interfaces/slack/__init__.py +3 -0
- agno/os/interfaces/slack/router.py +148 -0
- agno/os/interfaces/slack/security.py +30 -0
- agno/os/interfaces/slack/slack.py +47 -0
- agno/os/interfaces/whatsapp/__init__.py +3 -0
- agno/os/interfaces/whatsapp/router.py +210 -0
- agno/os/interfaces/whatsapp/security.py +55 -0
- agno/os/interfaces/whatsapp/whatsapp.py +36 -0
- agno/os/mcp.py +293 -0
- agno/os/middleware/__init__.py +9 -0
- agno/os/middleware/jwt.py +797 -0
- agno/os/router.py +258 -0
- agno/os/routers/__init__.py +3 -0
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/__init__.py +3 -0
- agno/os/routers/evals/evals.py +450 -0
- agno/os/routers/evals/schemas.py +174 -0
- agno/os/routers/evals/utils.py +231 -0
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/__init__.py +3 -0
- agno/os/routers/knowledge/knowledge.py +1008 -0
- agno/os/routers/knowledge/schemas.py +178 -0
- agno/os/routers/memory/__init__.py +3 -0
- agno/os/routers/memory/memory.py +661 -0
- agno/os/routers/memory/schemas.py +88 -0
- agno/os/routers/metrics/__init__.py +3 -0
- agno/os/routers/metrics/metrics.py +190 -0
- agno/os/routers/metrics/schemas.py +47 -0
- agno/os/routers/session/__init__.py +3 -0
- agno/os/routers/session/session.py +997 -0
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +534 -0
- agno/os/scopes.py +469 -0
- agno/{playground → os}/settings.py +7 -15
- agno/os/utils.py +973 -0
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +67 -0
- agno/reasoning/deepseek.py +63 -0
- agno/reasoning/default.py +97 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +71 -0
- agno/reasoning/helpers.py +24 -1
- agno/reasoning/ollama.py +67 -0
- agno/reasoning/openai.py +86 -0
- agno/reasoning/step.py +2 -1
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +822 -0
- agno/run/base.py +247 -0
- agno/run/cancel.py +81 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +767 -0
- agno/run/workflow.py +708 -0
- agno/session/__init__.py +10 -0
- agno/session/agent.py +260 -0
- agno/session/summary.py +265 -0
- agno/session/team.py +342 -0
- agno/session/workflow.py +501 -0
- agno/table.py +10 -0
- agno/team/__init__.py +37 -0
- agno/team/team.py +9536 -0
- agno/tools/__init__.py +7 -0
- agno/tools/agentql.py +120 -0
- agno/tools/airflow.py +22 -12
- agno/tools/api.py +122 -0
- agno/tools/apify.py +276 -83
- agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
- agno/tools/aws_lambda.py +28 -7
- agno/tools/aws_ses.py +66 -0
- agno/tools/baidusearch.py +11 -4
- agno/tools/bitbucket.py +292 -0
- agno/tools/brandfetch.py +213 -0
- agno/tools/bravesearch.py +106 -0
- agno/tools/brightdata.py +367 -0
- agno/tools/browserbase.py +209 -0
- agno/tools/calcom.py +32 -23
- agno/tools/calculator.py +24 -37
- agno/tools/cartesia.py +187 -0
- agno/tools/{clickup_tool.py → clickup.py} +17 -28
- agno/tools/confluence.py +91 -26
- agno/tools/crawl4ai.py +139 -43
- agno/tools/csv_toolkit.py +28 -22
- agno/tools/dalle.py +36 -22
- agno/tools/daytona.py +475 -0
- agno/tools/decorator.py +169 -14
- agno/tools/desi_vocal.py +23 -11
- agno/tools/discord.py +32 -29
- agno/tools/docker.py +716 -0
- agno/tools/duckdb.py +76 -81
- agno/tools/duckduckgo.py +43 -40
- agno/tools/e2b.py +703 -0
- agno/tools/eleven_labs.py +65 -54
- agno/tools/email.py +13 -5
- agno/tools/evm.py +129 -0
- agno/tools/exa.py +324 -42
- agno/tools/fal.py +39 -35
- agno/tools/file.py +196 -30
- agno/tools/file_generation.py +356 -0
- agno/tools/financial_datasets.py +288 -0
- agno/tools/firecrawl.py +108 -33
- agno/tools/function.py +960 -122
- agno/tools/giphy.py +34 -12
- agno/tools/github.py +1294 -97
- agno/tools/gmail.py +922 -0
- agno/tools/google_bigquery.py +117 -0
- agno/tools/google_drive.py +271 -0
- agno/tools/google_maps.py +253 -0
- agno/tools/googlecalendar.py +607 -107
- agno/tools/googlesheets.py +377 -0
- agno/tools/hackernews.py +20 -12
- agno/tools/jina.py +24 -14
- agno/tools/jira.py +48 -19
- agno/tools/knowledge.py +218 -0
- agno/tools/linear.py +82 -43
- agno/tools/linkup.py +58 -0
- agno/tools/local_file_system.py +15 -7
- agno/tools/lumalab.py +41 -26
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +193 -0
- agno/tools/memory.py +419 -0
- agno/tools/mlx_transcribe.py +11 -9
- agno/tools/models/azure_openai.py +190 -0
- agno/tools/models/gemini.py +203 -0
- agno/tools/models/groq.py +158 -0
- agno/tools/models/morph.py +186 -0
- agno/tools/models/nebius.py +124 -0
- agno/tools/models_labs.py +163 -82
- agno/tools/moviepy_video.py +18 -13
- agno/tools/nano_banana.py +151 -0
- agno/tools/neo4j.py +134 -0
- agno/tools/newspaper.py +15 -4
- agno/tools/newspaper4k.py +19 -6
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +181 -17
- agno/tools/openbb.py +27 -20
- agno/tools/opencv.py +321 -0
- agno/tools/openweather.py +233 -0
- agno/tools/oxylabs.py +385 -0
- agno/tools/pandas.py +25 -15
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +238 -185
- agno/tools/pubmed.py +125 -13
- agno/tools/python.py +48 -35
- agno/tools/reasoning.py +283 -0
- agno/tools/reddit.py +207 -29
- agno/tools/redshift.py +406 -0
- agno/tools/replicate.py +69 -26
- agno/tools/resend.py +11 -6
- agno/tools/scrapegraph.py +179 -19
- agno/tools/searxng.py +23 -31
- agno/tools/serpapi.py +15 -10
- agno/tools/serper.py +255 -0
- agno/tools/shell.py +23 -12
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +56 -14
- agno/tools/sleep.py +8 -6
- agno/tools/spider.py +35 -11
- agno/tools/spotify.py +919 -0
- agno/tools/sql.py +34 -19
- agno/tools/tavily.py +158 -8
- agno/tools/telegram.py +18 -8
- agno/tools/todoist.py +218 -0
- agno/tools/toolkit.py +134 -9
- agno/tools/trafilatura.py +388 -0
- agno/tools/trello.py +25 -28
- agno/tools/twilio.py +18 -9
- agno/tools/user_control_flow.py +78 -0
- agno/tools/valyu.py +228 -0
- agno/tools/visualization.py +467 -0
- agno/tools/webbrowser.py +28 -0
- agno/tools/webex.py +76 -0
- agno/tools/website.py +23 -19
- agno/tools/webtools.py +45 -0
- agno/tools/whatsapp.py +286 -0
- agno/tools/wikipedia.py +28 -19
- agno/tools/workflow.py +285 -0
- agno/tools/{twitter.py → x.py} +142 -46
- agno/tools/yfinance.py +41 -39
- agno/tools/youtube.py +34 -17
- agno/tools/zendesk.py +15 -5
- agno/tools/zep.py +454 -0
- agno/tools/zoom.py +86 -37
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/audio.py +37 -1
- agno/utils/certs.py +27 -0
- agno/utils/code_execution.py +11 -0
- agno/utils/common.py +103 -20
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +700 -0
- agno/utils/functions.py +107 -37
- agno/utils/gemini.py +426 -0
- agno/utils/hooks.py +171 -0
- agno/utils/http.py +185 -0
- agno/utils/json_schema.py +159 -37
- agno/utils/knowledge.py +36 -0
- agno/utils/location.py +19 -0
- agno/utils/log.py +221 -8
- agno/utils/mcp.py +214 -0
- agno/utils/media.py +335 -14
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +77 -2
- agno/utils/models/ai_foundry.py +50 -0
- agno/utils/models/claude.py +373 -0
- agno/utils/models/cohere.py +94 -0
- agno/utils/models/llama.py +85 -0
- agno/utils/models/mistral.py +100 -0
- agno/utils/models/openai_responses.py +140 -0
- agno/utils/models/schema_utils.py +153 -0
- agno/utils/models/watsonx.py +41 -0
- agno/utils/openai.py +257 -0
- agno/utils/pickle.py +1 -1
- agno/utils/pprint.py +124 -8
- agno/utils/print_response/agent.py +930 -0
- agno/utils/print_response/team.py +1914 -0
- agno/utils/print_response/workflow.py +1668 -0
- agno/utils/prompts.py +111 -0
- agno/utils/reasoning.py +108 -0
- agno/utils/response.py +163 -0
- agno/utils/serialize.py +32 -0
- agno/utils/shell.py +4 -4
- agno/utils/streamlit.py +487 -0
- agno/utils/string.py +204 -51
- agno/utils/team.py +139 -0
- agno/utils/timer.py +9 -2
- agno/utils/tokens.py +657 -0
- agno/utils/tools.py +19 -1
- agno/utils/whatsapp.py +305 -0
- agno/utils/yaml_io.py +3 -3
- agno/vectordb/__init__.py +2 -0
- agno/vectordb/base.py +87 -9
- agno/vectordb/cassandra/__init__.py +5 -1
- agno/vectordb/cassandra/cassandra.py +383 -27
- agno/vectordb/chroma/__init__.py +4 -0
- agno/vectordb/chroma/chromadb.py +748 -83
- agno/vectordb/clickhouse/__init__.py +7 -1
- agno/vectordb/clickhouse/clickhousedb.py +554 -53
- agno/vectordb/couchbase/__init__.py +3 -0
- agno/vectordb/couchbase/couchbase.py +1446 -0
- agno/vectordb/lancedb/__init__.py +5 -0
- agno/vectordb/lancedb/lance_db.py +730 -98
- agno/vectordb/langchaindb/__init__.py +5 -0
- agno/vectordb/langchaindb/langchaindb.py +163 -0
- agno/vectordb/lightrag/__init__.py +5 -0
- agno/vectordb/lightrag/lightrag.py +388 -0
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +166 -0
- agno/vectordb/milvus/__init__.py +3 -0
- agno/vectordb/milvus/milvus.py +966 -78
- agno/vectordb/mongodb/__init__.py +9 -1
- agno/vectordb/mongodb/mongodb.py +1175 -172
- agno/vectordb/pgvector/__init__.py +8 -0
- agno/vectordb/pgvector/pgvector.py +599 -115
- agno/vectordb/pineconedb/__init__.py +5 -1
- agno/vectordb/pineconedb/pineconedb.py +406 -43
- agno/vectordb/qdrant/__init__.py +4 -0
- agno/vectordb/qdrant/qdrant.py +914 -61
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/__init__.py +8 -1
- agno/vectordb/singlestore/singlestore.py +771 -0
- agno/vectordb/surrealdb/__init__.py +3 -0
- agno/vectordb/surrealdb/surrealdb.py +663 -0
- agno/vectordb/upstashdb/__init__.py +5 -0
- agno/vectordb/upstashdb/upstashdb.py +718 -0
- agno/vectordb/weaviate/__init__.py +8 -0
- agno/vectordb/weaviate/index.py +15 -0
- agno/vectordb/weaviate/weaviate.py +1009 -0
- agno/workflow/__init__.py +23 -1
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +759 -0
- agno/workflow/loop.py +756 -0
- agno/workflow/parallel.py +853 -0
- agno/workflow/router.py +723 -0
- agno/workflow/step.py +1564 -0
- agno/workflow/steps.py +613 -0
- agno/workflow/types.py +556 -0
- agno/workflow/workflow.py +4327 -514
- agno-2.3.13.dist-info/METADATA +639 -0
- agno-2.3.13.dist-info/RECORD +613 -0
- {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
- agno-2.3.13.dist-info/licenses/LICENSE +201 -0
- agno/api/playground.py +0 -91
- agno/api/schemas/playground.py +0 -22
- agno/api/schemas/user.py +0 -22
- agno/api/schemas/workspace.py +0 -46
- agno/api/user.py +0 -160
- agno/api/workspace.py +0 -151
- agno/cli/auth_server.py +0 -118
- agno/cli/config.py +0 -275
- agno/cli/console.py +0 -88
- agno/cli/credentials.py +0 -23
- agno/cli/entrypoint.py +0 -571
- agno/cli/operator.py +0 -355
- agno/cli/settings.py +0 -85
- agno/cli/ws/ws_cli.py +0 -817
- agno/constants.py +0 -13
- agno/document/__init__.py +0 -1
- agno/document/chunking/semantic.py +0 -47
- agno/document/chunking/strategy.py +0 -31
- agno/document/reader/__init__.py +0 -1
- agno/document/reader/arxiv_reader.py +0 -41
- agno/document/reader/base.py +0 -22
- agno/document/reader/csv_reader.py +0 -84
- agno/document/reader/docx_reader.py +0 -46
- agno/document/reader/firecrawl_reader.py +0 -99
- agno/document/reader/json_reader.py +0 -43
- agno/document/reader/pdf_reader.py +0 -219
- agno/document/reader/s3/pdf_reader.py +0 -46
- agno/document/reader/s3/text_reader.py +0 -51
- agno/document/reader/text_reader.py +0 -41
- agno/document/reader/website_reader.py +0 -175
- agno/document/reader/youtube_reader.py +0 -50
- agno/embedder/__init__.py +0 -1
- agno/embedder/azure_openai.py +0 -86
- agno/embedder/cohere.py +0 -72
- agno/embedder/fastembed.py +0 -37
- agno/embedder/google.py +0 -73
- agno/embedder/huggingface.py +0 -54
- agno/embedder/mistral.py +0 -80
- agno/embedder/ollama.py +0 -57
- agno/embedder/openai.py +0 -74
- agno/embedder/sentence_transformer.py +0 -38
- agno/embedder/voyageai.py +0 -64
- agno/eval/perf.py +0 -201
- agno/file/__init__.py +0 -1
- agno/file/file.py +0 -16
- agno/file/local/csv.py +0 -32
- agno/file/local/txt.py +0 -19
- agno/infra/app.py +0 -240
- agno/infra/base.py +0 -144
- agno/infra/context.py +0 -20
- agno/infra/db_app.py +0 -52
- agno/infra/resource.py +0 -205
- agno/infra/resources.py +0 -55
- agno/knowledge/agent.py +0 -230
- agno/knowledge/arxiv.py +0 -22
- agno/knowledge/combined.py +0 -22
- agno/knowledge/csv.py +0 -28
- agno/knowledge/csv_url.py +0 -19
- agno/knowledge/document.py +0 -20
- agno/knowledge/docx.py +0 -30
- agno/knowledge/json.py +0 -28
- agno/knowledge/langchain.py +0 -71
- agno/knowledge/llamaindex.py +0 -66
- agno/knowledge/pdf.py +0 -28
- agno/knowledge/pdf_url.py +0 -26
- agno/knowledge/s3/base.py +0 -60
- agno/knowledge/s3/pdf.py +0 -21
- agno/knowledge/s3/text.py +0 -23
- agno/knowledge/text.py +0 -30
- agno/knowledge/website.py +0 -88
- agno/knowledge/wikipedia.py +0 -31
- agno/knowledge/youtube.py +0 -22
- agno/memory/agent.py +0 -392
- agno/memory/classifier.py +0 -104
- agno/memory/db/__init__.py +0 -1
- agno/memory/db/base.py +0 -42
- agno/memory/db/mongodb.py +0 -189
- agno/memory/db/postgres.py +0 -203
- agno/memory/db/sqlite.py +0 -193
- agno/memory/memory.py +0 -15
- agno/memory/row.py +0 -36
- agno/memory/summarizer.py +0 -192
- agno/memory/summary.py +0 -19
- agno/memory/workflow.py +0 -38
- agno/models/google/gemini_openai.py +0 -26
- agno/models/ollama/hermes.py +0 -221
- agno/models/ollama/tools.py +0 -362
- agno/models/vertexai/gemini.py +0 -595
- agno/playground/__init__.py +0 -3
- agno/playground/async_router.py +0 -421
- agno/playground/deploy.py +0 -249
- agno/playground/operator.py +0 -92
- agno/playground/playground.py +0 -91
- agno/playground/schemas.py +0 -76
- agno/playground/serve.py +0 -55
- agno/playground/sync_router.py +0 -405
- agno/reasoning/agent.py +0 -68
- agno/run/response.py +0 -112
- agno/storage/agent/__init__.py +0 -0
- agno/storage/agent/base.py +0 -38
- agno/storage/agent/dynamodb.py +0 -350
- agno/storage/agent/json.py +0 -92
- agno/storage/agent/mongodb.py +0 -228
- agno/storage/agent/postgres.py +0 -367
- agno/storage/agent/session.py +0 -79
- agno/storage/agent/singlestore.py +0 -303
- agno/storage/agent/sqlite.py +0 -357
- agno/storage/agent/yaml.py +0 -93
- agno/storage/workflow/__init__.py +0 -0
- agno/storage/workflow/base.py +0 -40
- agno/storage/workflow/mongodb.py +0 -233
- agno/storage/workflow/postgres.py +0 -366
- agno/storage/workflow/session.py +0 -60
- agno/storage/workflow/sqlite.py +0 -359
- agno/tools/googlesearch.py +0 -88
- agno/utils/defaults.py +0 -57
- agno/utils/filesystem.py +0 -39
- agno/utils/git.py +0 -52
- agno/utils/json_io.py +0 -30
- agno/utils/load_env.py +0 -19
- agno/utils/py_io.py +0 -19
- agno/utils/pyproject.py +0 -18
- agno/utils/resource_filter.py +0 -31
- agno/vectordb/singlestore/s2vectordb.py +0 -390
- agno/vectordb/singlestore/s2vectordb2.py +0 -355
- agno/workspace/__init__.py +0 -0
- agno/workspace/config.py +0 -325
- agno/workspace/enums.py +0 -6
- agno/workspace/helpers.py +0 -48
- agno/workspace/operator.py +0 -758
- agno/workspace/settings.py +0 -63
- agno-0.1.2.dist-info/LICENSE +0 -375
- agno-0.1.2.dist-info/METADATA +0 -502
- agno-0.1.2.dist-info/RECORD +0 -352
- agno-0.1.2.dist-info/entry_points.txt +0 -3
- /agno/{cli → db/migrations}/__init__.py +0 -0
- /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
- /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
- /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
- /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
- /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
- /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
- /agno/{reranker → utils/models}/__init__.py +0 -0
- /agno/{storage → utils/print_response}/__init__.py +0 -0
- {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
|