agno 2.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/agent/agent.py +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- 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/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- 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 +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- 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/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- 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/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- 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 +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- 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 +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- 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/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/tools/gmail.py
CHANGED
|
@@ -124,20 +124,6 @@ class GmailTools(Toolkit):
|
|
|
124
124
|
self.scopes = scopes or self.DEFAULT_SCOPES
|
|
125
125
|
self.port = port
|
|
126
126
|
|
|
127
|
-
""" tools functions:
|
|
128
|
-
enable_get_latest_emails (bool): Enable getting latest emails.
|
|
129
|
-
enable_get_emails_from_user (bool): Enable getting emails from specific user.
|
|
130
|
-
enable_get_unread_emails (bool): Enable getting unread emails.
|
|
131
|
-
enable_get_starred_emails (bool): Enable getting starred emails.
|
|
132
|
-
enable_get_emails_by_context (bool): Enable getting emails by context.
|
|
133
|
-
enable_get_emails_by_date (bool): Enable getting emails by date.
|
|
134
|
-
enable_get_emails_by_thread (bool): Enable getting emails by thread.
|
|
135
|
-
enable_create_draft_email (bool): Enable creating draft emails.
|
|
136
|
-
enable_send_email (bool): Enable sending emails.
|
|
137
|
-
enable_send_email_reply (bool): Enable sending email replies.
|
|
138
|
-
all (bool): Enable all tools.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
127
|
tools: List[Any] = [
|
|
142
128
|
# Reading emails
|
|
143
129
|
self.get_latest_emails,
|
|
@@ -148,10 +134,18 @@ class GmailTools(Toolkit):
|
|
|
148
134
|
self.get_emails_by_date,
|
|
149
135
|
self.get_emails_by_thread,
|
|
150
136
|
self.search_emails,
|
|
137
|
+
# Email management
|
|
138
|
+
self.mark_email_as_read,
|
|
139
|
+
self.mark_email_as_unread,
|
|
151
140
|
# Composing emails
|
|
152
141
|
self.create_draft_email,
|
|
153
142
|
self.send_email,
|
|
154
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,
|
|
155
149
|
]
|
|
156
150
|
|
|
157
151
|
super().__init__(name="gmail_tools", tools=tools, **kwargs)
|
|
@@ -172,13 +166,20 @@ class GmailTools(Toolkit):
|
|
|
172
166
|
"get_emails_by_date",
|
|
173
167
|
"get_emails_by_thread",
|
|
174
168
|
"search_emails",
|
|
169
|
+
"list_custom_labels",
|
|
175
170
|
]
|
|
171
|
+
modify_operations = ["mark_email_as_read", "mark_email_as_unread"]
|
|
176
172
|
if any(read_operation in self.functions for read_operation in read_operations):
|
|
177
173
|
read_scope = "https://www.googleapis.com/auth/gmail.readonly"
|
|
178
174
|
write_scope = "https://www.googleapis.com/auth/gmail.modify"
|
|
179
175
|
if read_scope not in self.scopes and write_scope not in self.scopes:
|
|
180
176
|
raise ValueError(f"The scope {read_scope} is required for email reading operations")
|
|
181
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
|
+
|
|
182
183
|
def _auth(self) -> None:
|
|
183
184
|
"""Authenticate with Gmail API"""
|
|
184
185
|
token_file = Path(self.token_path or "token.json")
|
|
@@ -555,6 +556,229 @@ class GmailTools(Toolkit):
|
|
|
555
556
|
except Exception as error:
|
|
556
557
|
return f"Unexpected error retrieving emails with query '{query}': {type(error).__name__}: {error}"
|
|
557
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
|
+
|
|
558
782
|
def _validate_email_params(self, to: str, subject: str, body: str) -> None:
|
|
559
783
|
"""Validate email parameters."""
|
|
560
784
|
if not to:
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Drive API integration for file management and sharing.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
This module provides functions to interact with Google Drive, including listing,
|
|
6
|
+
uploading, and downloading files.
|
|
7
|
+
It uses the Google Drive API and handles authentication via OAuth2.
|
|
8
|
+
|
|
9
|
+
Required Environment Variables:
|
|
10
|
+
-----------------------------
|
|
11
|
+
- GOOGLE_CLIENT_ID: Google OAuth client ID
|
|
12
|
+
- GOOGLE_CLIENT_SECRET: Google OAuth client secret
|
|
13
|
+
- GOOGLE_PROJECT_ID: Google Cloud project ID
|
|
14
|
+
- GOOGLE_REDIRECT_URI: Google OAuth redirect URI (default: http://localhost)
|
|
15
|
+
- GOOGLE_CLOUD_QUOTA_PROJECT_ID: Google Cloud quota project ID
|
|
16
|
+
|
|
17
|
+
How to Get These Credentials:
|
|
18
|
+
---------------------------
|
|
19
|
+
1. Go to Google Cloud Console (https://console.cloud.google.com)
|
|
20
|
+
2. Create a new project or select an existing one
|
|
21
|
+
3. Enable the Google Drive API:
|
|
22
|
+
- Go to "APIs & Services" > "Enable APIs and Services"
|
|
23
|
+
- Search for "Google Drive API"
|
|
24
|
+
- Click "Enable"
|
|
25
|
+
|
|
26
|
+
4. Create OAuth 2.0 credentials:
|
|
27
|
+
- Go to "APIs & Services" > "Credentials"
|
|
28
|
+
- Click "Create Credentials" > "OAuth client ID"
|
|
29
|
+
- Enable the OAuth Consent Screen if you haven't already
|
|
30
|
+
- After enabling the Consent Screen, click on "Create Credentials" > "OAuth client ID"
|
|
31
|
+
- You'll receive:
|
|
32
|
+
* Client ID (GOOGLE_CLIENT_ID)
|
|
33
|
+
* Client Secret (GOOGLE_CLIENT_SECRET)
|
|
34
|
+
- The Project ID (GOOGLE_PROJECT_ID) is visible in the project dropdown at the top of the page
|
|
35
|
+
|
|
36
|
+
5. Add auth redirect URI:
|
|
37
|
+
- Go to https://console.cloud.google.com/auth/clients
|
|
38
|
+
- Add `http://localhost:5050` as a recognized redirect URI OR with http://localhost:{PORT_NUMBER}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
6. Set up environment variables:
|
|
42
|
+
Create a .envrc file in your project root with:
|
|
43
|
+
``
|
|
44
|
+
export GOOGLE_CLIENT_ID=your_client_id_here
|
|
45
|
+
export GOOGLE_CLIENT_SECRET=your_client_secret_here
|
|
46
|
+
export GOOGLE_PROJECT_ID=your_project_id_here
|
|
47
|
+
export GOOGLE_REDIRECT_URI=http://localhost/ # Default value
|
|
48
|
+
export GOOGLE_AUTHENTICATION_PORT=5050 # Port for OAuth redirect
|
|
49
|
+
export GOOGLE_CLOUD_QUOTA_PROJECT_ID=your_quota_project_id_here
|
|
50
|
+
``
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
Remember to install the dependencies using `pip install google google-auth-oauthlib`
|
|
55
|
+
|
|
56
|
+
Important Points to Note :
|
|
57
|
+
1. The first time you run the application, it will open a browser window for OAuth authentication.
|
|
58
|
+
2. A token.json file will be created to store the authentication credentials for future use.
|
|
59
|
+
|
|
60
|
+
You can customize the authentication port by setting the `GOOGLE_AUTHENTICATION_PORT` environment variable.
|
|
61
|
+
This will be used in the `run_local_server` method for OAuth authentication.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
import mimetypes
|
|
66
|
+
from functools import wraps
|
|
67
|
+
from os import getenv
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
from typing import Any, List, Optional, Union
|
|
70
|
+
|
|
71
|
+
from agno.tools import Toolkit
|
|
72
|
+
from agno.utils.log import log_error
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from google.auth.transport.requests import Request
|
|
76
|
+
from google.oauth2.credentials import Credentials
|
|
77
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
78
|
+
from googleapiclient.discovery import Resource, build
|
|
79
|
+
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
|
80
|
+
except ImportError:
|
|
81
|
+
raise ImportError(
|
|
82
|
+
"Google client library for Python not found , install it using `pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib`"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def authenticate(func):
|
|
87
|
+
"""Decorator to ensure authentication before executing a function."""
|
|
88
|
+
|
|
89
|
+
@wraps(func)
|
|
90
|
+
def wrapper(self, *args, **kwargs):
|
|
91
|
+
if not self.creds or not self.creds.valid:
|
|
92
|
+
self._auth()
|
|
93
|
+
if not self.service:
|
|
94
|
+
# Set quota project on credentials if available
|
|
95
|
+
creds_to_use = self.creds
|
|
96
|
+
if hasattr(self, "quota_project_id") and self.quota_project_id:
|
|
97
|
+
creds_to_use = self.creds.with_quota_project(self.quota_project_id)
|
|
98
|
+
self.service = build("drive", "v3", credentials=creds_to_use)
|
|
99
|
+
return func(self, *args, **kwargs)
|
|
100
|
+
|
|
101
|
+
return wrapper
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class GoogleDriveTools(Toolkit):
|
|
105
|
+
# Default scopes for Google Drive API access
|
|
106
|
+
DEFAULT_SCOPES = ["https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive.readonly"]
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
auth_port: Optional[int] = 5050,
|
|
111
|
+
creds: Optional[Credentials] = None,
|
|
112
|
+
scopes: Optional[List[str]] = None,
|
|
113
|
+
creds_path: Optional[str] = None,
|
|
114
|
+
token_path: Optional[str] = None,
|
|
115
|
+
quota_project_id: Optional[str] = None,
|
|
116
|
+
**kwargs,
|
|
117
|
+
):
|
|
118
|
+
self.creds: Optional[Credentials] = creds
|
|
119
|
+
self.service: Optional[Resource] = None
|
|
120
|
+
self.credentials_path = creds_path
|
|
121
|
+
self.token_path = token_path
|
|
122
|
+
self.scopes = scopes or []
|
|
123
|
+
self.scopes.extend(self.DEFAULT_SCOPES)
|
|
124
|
+
|
|
125
|
+
self.quota_project_id = quota_project_id or getenv("GOOGLE_CLOUD_QUOTA_PROJECT_ID")
|
|
126
|
+
if not self.quota_project_id:
|
|
127
|
+
raise ValueError("GOOGLE_CLOUD_QUOTA_PROJECT_ID is not set")
|
|
128
|
+
|
|
129
|
+
self.auth_port: int = int(getenv("GOOGLE_AUTH_PORT", str(auth_port)))
|
|
130
|
+
if not self.auth_port:
|
|
131
|
+
raise ValueError("GOOGLE_AUTH_PORT is not set")
|
|
132
|
+
|
|
133
|
+
tools: List[Any] = [
|
|
134
|
+
self.list_files,
|
|
135
|
+
]
|
|
136
|
+
super().__init__(name="google_drive_tools", tools=tools, **kwargs)
|
|
137
|
+
if not self.scopes:
|
|
138
|
+
# Add read permission by default
|
|
139
|
+
self.scopes.append(self.DEFAULT_SCOPES[1]) # 'drive.readonly'
|
|
140
|
+
# Add write permission if allow_update is True
|
|
141
|
+
if getattr(self, "allow_update", False):
|
|
142
|
+
self.scopes.append(self.DEFAULT_SCOPES[0]) # 'drive.file'
|
|
143
|
+
|
|
144
|
+
def _auth(self):
|
|
145
|
+
"""
|
|
146
|
+
Authenticate and set up the Google Drive API client.
|
|
147
|
+
This method checks if credentials are valid and refreshes or requests them if needed.
|
|
148
|
+
"""
|
|
149
|
+
if self.creds and self.creds.valid:
|
|
150
|
+
# Already authenticated
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
token_file = Path(self.token_path or "token.json")
|
|
154
|
+
creds_file = Path(self.credentials_path or "credentials.json")
|
|
155
|
+
|
|
156
|
+
if token_file.exists():
|
|
157
|
+
self.creds = Credentials.from_authorized_user_file(str(token_file), self.scopes)
|
|
158
|
+
if not self.creds or not self.creds.valid:
|
|
159
|
+
if self.creds and self.creds.expired and self.creds.refresh_token:
|
|
160
|
+
self.creds.refresh(Request())
|
|
161
|
+
else:
|
|
162
|
+
client_config = {
|
|
163
|
+
"installed": {
|
|
164
|
+
"client_id": getenv("GOOGLE_CLIENT_ID"),
|
|
165
|
+
"client_secret": getenv("GOOGLE_CLIENT_SECRET"),
|
|
166
|
+
"project_id": getenv("GOOGLE_PROJECT_ID"),
|
|
167
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
168
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
169
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
170
|
+
"redirect_uris": [getenv("GOOGLE_REDIRECT_URI", "http://localhost")],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
# File based authentication
|
|
174
|
+
if creds_file.exists():
|
|
175
|
+
flow = InstalledAppFlow.from_client_secrets_file(str(creds_file), self.scopes)
|
|
176
|
+
else:
|
|
177
|
+
flow = InstalledAppFlow.from_client_config(client_config, self.scopes)
|
|
178
|
+
# Opens up a browser window for OAuth authentication
|
|
179
|
+
self.creds = flow.run_local_server(port=self.auth_port) # type: ignore
|
|
180
|
+
|
|
181
|
+
token_file.write_text(self.creds.to_json()) if self.creds else None
|
|
182
|
+
|
|
183
|
+
@authenticate
|
|
184
|
+
def list_files(self, query: Optional[str] = None, page_size: int = 10) -> List[dict]:
|
|
185
|
+
"""
|
|
186
|
+
List files in your Google Drive.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
query (Optional[str]): Optional search query to filter files (see Google Drive API docs).
|
|
190
|
+
page_size (int): Maximum number of files to return.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List[dict]: List of file metadata dictionaries.
|
|
194
|
+
"""
|
|
195
|
+
if not self.service:
|
|
196
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
197
|
+
try:
|
|
198
|
+
results = (
|
|
199
|
+
self.service.files() # type: ignore
|
|
200
|
+
.list(q=query, pageSize=page_size, fields="nextPageToken, files(id, name, mimeType, modifiedTime)")
|
|
201
|
+
.execute()
|
|
202
|
+
)
|
|
203
|
+
items = results.get("files", [])
|
|
204
|
+
return items
|
|
205
|
+
except Exception as error:
|
|
206
|
+
log_error(f"Could not list files: {error}")
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
@authenticate
|
|
210
|
+
def upload_file(self, file_path: Union[str, Path], mime_type: Optional[str] = None) -> Optional[dict]:
|
|
211
|
+
"""
|
|
212
|
+
Upload a file to your Google Drive.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
file_path (Union[str, Path]): Path to the file you want to upload.
|
|
216
|
+
mime_type (Optional[str]): MIME type of the file. If not provided, it will be guessed.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Optional[dict]: Metadata of the uploaded file, or None if upload failed.
|
|
220
|
+
"""
|
|
221
|
+
if not self.service:
|
|
222
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
223
|
+
file_path = Path(file_path)
|
|
224
|
+
if not file_path.exists() or not file_path.is_file():
|
|
225
|
+
raise ValueError(f"The file '{file_path}' does not exist or is not a file.")
|
|
226
|
+
if mime_type is None:
|
|
227
|
+
mime_type, _ = mimetypes.guess_type(file_path.as_posix())
|
|
228
|
+
if mime_type is None:
|
|
229
|
+
mime_type = "application/octet-stream" # Default MIME type
|
|
230
|
+
|
|
231
|
+
file_metadata = {"name": file_path.name}
|
|
232
|
+
media = MediaFileUpload(file_path.as_posix(), mimetype=mime_type)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
uploaded_file = (
|
|
236
|
+
self.service.files() # type: ignore
|
|
237
|
+
.create(body=file_metadata, media_body=media, fields="id, name, mimeType, modifiedTime")
|
|
238
|
+
.execute()
|
|
239
|
+
)
|
|
240
|
+
return uploaded_file
|
|
241
|
+
except Exception as error:
|
|
242
|
+
log_error(f"Could not upload file '{file_path}': {error}")
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
@authenticate
|
|
246
|
+
def download_file(self, file_id: str, dest_path: Union[str, Path]) -> Optional[Path]:
|
|
247
|
+
"""
|
|
248
|
+
Download a file from your Google Drive.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
file_id (str): The ID of the file you want to download.
|
|
252
|
+
dest_path (Union[str, Path]): Where to save the downloaded file.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Optional[Path]: The path to the downloaded file, or None if download failed.
|
|
256
|
+
"""
|
|
257
|
+
if not self.service:
|
|
258
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
259
|
+
dest_path = Path(dest_path)
|
|
260
|
+
try:
|
|
261
|
+
request = self.service.files().get_media(fileId=file_id) # type: ignore
|
|
262
|
+
with open(dest_path, "wb") as fh:
|
|
263
|
+
downloader = MediaIoBaseDownload(fh, request)
|
|
264
|
+
done = False
|
|
265
|
+
while not done:
|
|
266
|
+
status, done = downloader.next_chunk()
|
|
267
|
+
print(f"Download progress: {int(status.progress() * 100)}%.")
|
|
268
|
+
return dest_path
|
|
269
|
+
except Exception as error:
|
|
270
|
+
log_error(f"Could not download file '{file_id}': {error}")
|
|
271
|
+
return None
|