agno 2.2.10__py3-none-any.whl → 2.2.12__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 +75 -48
- agno/db/dynamo/utils.py +1 -1
- agno/db/firestore/utils.py +1 -1
- agno/db/gcs_json/utils.py +1 -1
- agno/db/in_memory/utils.py +1 -1
- agno/db/json/utils.py +1 -1
- agno/db/mongo/utils.py +3 -3
- agno/db/mysql/mysql.py +1 -1
- agno/db/mysql/utils.py +1 -1
- agno/db/postgres/utils.py +1 -1
- agno/db/redis/utils.py +1 -1
- agno/db/singlestore/singlestore.py +1 -1
- agno/db/singlestore/utils.py +1 -1
- agno/db/sqlite/async_sqlite.py +1 -1
- agno/db/sqlite/sqlite.py +1 -1
- agno/db/sqlite/utils.py +1 -1
- agno/filters.py +354 -0
- agno/knowledge/chunking/agentic.py +8 -9
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/knowledge.py +43 -22
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/utils.py +20 -0
- agno/models/anthropic/claude.py +45 -9
- agno/models/base.py +4 -0
- agno/os/app.py +23 -7
- agno/os/interfaces/slack/router.py +53 -33
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/router.py +25 -1
- agno/os/routers/health.py +5 -3
- agno/os/routers/knowledge/knowledge.py +43 -17
- agno/os/routers/knowledge/schemas.py +4 -3
- agno/run/agent.py +11 -1
- agno/run/base.py +3 -2
- agno/session/agent.py +10 -5
- agno/team/team.py +57 -18
- agno/tools/file_generation.py +4 -4
- agno/tools/gmail.py +179 -0
- agno/tools/parallel.py +314 -0
- agno/utils/agent.py +22 -17
- agno/utils/gemini.py +15 -5
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/models/claude.py +2 -1
- agno/utils/print_response/agent.py +5 -4
- agno/utils/print_response/team.py +5 -4
- agno/vectordb/base.py +2 -4
- agno/vectordb/cassandra/cassandra.py +12 -5
- agno/vectordb/chroma/chromadb.py +10 -4
- agno/vectordb/clickhouse/clickhousedb.py +12 -4
- agno/vectordb/couchbase/couchbase.py +12 -3
- agno/vectordb/lancedb/lance_db.py +69 -144
- agno/vectordb/langchaindb/langchaindb.py +13 -4
- agno/vectordb/lightrag/lightrag.py +8 -3
- agno/vectordb/llamaindex/llamaindexdb.py +10 -4
- agno/vectordb/milvus/milvus.py +16 -5
- agno/vectordb/mongodb/mongodb.py +14 -3
- agno/vectordb/pgvector/pgvector.py +73 -15
- agno/vectordb/pineconedb/pineconedb.py +6 -2
- agno/vectordb/qdrant/qdrant.py +25 -13
- agno/vectordb/redis/redisdb.py +37 -30
- agno/vectordb/singlestore/singlestore.py +9 -4
- agno/vectordb/surrealdb/surrealdb.py +13 -3
- agno/vectordb/upstashdb/upstashdb.py +8 -5
- agno/vectordb/weaviate/weaviate.py +29 -12
- agno/workflow/step.py +3 -2
- agno/workflow/types.py +20 -1
- agno/workflow/workflow.py +103 -14
- {agno-2.2.10.dist-info → agno-2.2.12.dist-info}/METADATA +4 -1
- {agno-2.2.10.dist-info → agno-2.2.12.dist-info}/RECORD +73 -71
- {agno-2.2.10.dist-info → agno-2.2.12.dist-info}/WHEEL +0 -0
- {agno-2.2.10.dist-info → agno-2.2.12.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.10.dist-info → agno-2.2.12.dist-info}/top_level.txt +0 -0
agno/session/agent.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import asdict, dataclass
|
|
4
|
-
from typing import Any, Dict, List, Mapping, Optional
|
|
4
|
+
from typing import Any, Dict, List, Mapping, Optional, Union
|
|
5
5
|
|
|
6
6
|
from agno.models.message import Message
|
|
7
7
|
from agno.run.agent import RunOutput
|
|
8
8
|
from agno.run.base import RunStatus
|
|
9
|
+
from agno.run.team import TeamRunOutput
|
|
9
10
|
from agno.session.summary import SessionSummary
|
|
10
11
|
from agno.utils.log import log_debug, log_warning
|
|
11
12
|
|
|
@@ -33,7 +34,7 @@ class AgentSession:
|
|
|
33
34
|
# Agent Data: agent_id, name and model
|
|
34
35
|
agent_data: Optional[Dict[str, Any]] = None
|
|
35
36
|
# List of all runs in the session
|
|
36
|
-
runs: Optional[List[RunOutput]] = None
|
|
37
|
+
runs: Optional[List[Union[RunOutput, TeamRunOutput]]] = None
|
|
37
38
|
# Summary of the session
|
|
38
39
|
summary: Optional["SessionSummary"] = None
|
|
39
40
|
|
|
@@ -57,9 +58,13 @@ class AgentSession:
|
|
|
57
58
|
return None
|
|
58
59
|
|
|
59
60
|
runs = data.get("runs")
|
|
60
|
-
serialized_runs: List[RunOutput] = []
|
|
61
|
+
serialized_runs: List[Union[RunOutput, TeamRunOutput]] = []
|
|
61
62
|
if runs is not None and isinstance(runs[0], dict):
|
|
62
|
-
|
|
63
|
+
for run in runs:
|
|
64
|
+
if "agent_id" in run:
|
|
65
|
+
serialized_runs.append(RunOutput.from_dict(run))
|
|
66
|
+
elif "team_id" in run:
|
|
67
|
+
serialized_runs.append(TeamRunOutput.from_dict(run))
|
|
63
68
|
|
|
64
69
|
summary = data.get("summary")
|
|
65
70
|
if summary is not None and isinstance(summary, dict):
|
|
@@ -101,7 +106,7 @@ class AgentSession:
|
|
|
101
106
|
|
|
102
107
|
log_debug("Added RunOutput to Agent Session")
|
|
103
108
|
|
|
104
|
-
def get_run(self, run_id: str) -> Optional[RunOutput]:
|
|
109
|
+
def get_run(self, run_id: str) -> Optional[Union[RunOutput, TeamRunOutput]]:
|
|
105
110
|
for run in self.runs or []:
|
|
106
111
|
if run.run_id == run_id:
|
|
107
112
|
return run
|
agno/team/team.py
CHANGED
|
@@ -36,6 +36,7 @@ from agno.exceptions import (
|
|
|
36
36
|
OutputCheckError,
|
|
37
37
|
RunCancelledException,
|
|
38
38
|
)
|
|
39
|
+
from agno.filters import FilterExpr
|
|
39
40
|
from agno.guardrails import BaseGuardrail
|
|
40
41
|
from agno.knowledge.knowledge import Knowledge
|
|
41
42
|
from agno.knowledge.types import KnowledgeFilter
|
|
@@ -288,7 +289,7 @@ class Team:
|
|
|
288
289
|
# --- Agent Knowledge ---
|
|
289
290
|
knowledge: Optional[Knowledge] = None
|
|
290
291
|
# Add knowledge_filters to the Agent class attributes
|
|
291
|
-
knowledge_filters: Optional[Dict[str, Any]] = None
|
|
292
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
292
293
|
# Let the agent choose the knowledge filters
|
|
293
294
|
enable_agentic_knowledge_filters: Optional[bool] = False
|
|
294
295
|
# Add a tool that allows the Team to update Knowledge.
|
|
@@ -474,7 +475,7 @@ class Team:
|
|
|
474
475
|
dependencies: Optional[Dict[str, Any]] = None,
|
|
475
476
|
add_dependencies_to_context: bool = False,
|
|
476
477
|
knowledge: Optional[Knowledge] = None,
|
|
477
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
478
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
478
479
|
add_knowledge_to_context: bool = False,
|
|
479
480
|
enable_agentic_knowledge_filters: Optional[bool] = False,
|
|
480
481
|
update_knowledge: bool = False,
|
|
@@ -1742,7 +1743,7 @@ class Team:
|
|
|
1742
1743
|
images: Optional[Sequence[Image]] = None,
|
|
1743
1744
|
videos: Optional[Sequence[Video]] = None,
|
|
1744
1745
|
files: Optional[Sequence[File]] = None,
|
|
1745
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
1746
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
1746
1747
|
add_history_to_context: Optional[bool] = None,
|
|
1747
1748
|
add_dependencies_to_context: Optional[bool] = None,
|
|
1748
1749
|
add_session_state_to_context: Optional[bool] = None,
|
|
@@ -1769,7 +1770,7 @@ class Team:
|
|
|
1769
1770
|
images: Optional[Sequence[Image]] = None,
|
|
1770
1771
|
videos: Optional[Sequence[Video]] = None,
|
|
1771
1772
|
files: Optional[Sequence[File]] = None,
|
|
1772
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
1773
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
1773
1774
|
add_history_to_context: Optional[bool] = None,
|
|
1774
1775
|
add_dependencies_to_context: Optional[bool] = None,
|
|
1775
1776
|
add_session_state_to_context: Optional[bool] = None,
|
|
@@ -1797,7 +1798,7 @@ class Team:
|
|
|
1797
1798
|
images: Optional[Sequence[Image]] = None,
|
|
1798
1799
|
videos: Optional[Sequence[Video]] = None,
|
|
1799
1800
|
files: Optional[Sequence[File]] = None,
|
|
1800
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
1801
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
1801
1802
|
add_history_to_context: Optional[bool] = None,
|
|
1802
1803
|
add_dependencies_to_context: Optional[bool] = None,
|
|
1803
1804
|
add_session_state_to_context: Optional[bool] = None,
|
|
@@ -2602,7 +2603,7 @@ class Team:
|
|
|
2602
2603
|
images: Optional[Sequence[Image]] = None,
|
|
2603
2604
|
videos: Optional[Sequence[Video]] = None,
|
|
2604
2605
|
files: Optional[Sequence[File]] = None,
|
|
2605
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
2606
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
2606
2607
|
add_history_to_context: Optional[bool] = None,
|
|
2607
2608
|
add_dependencies_to_context: Optional[bool] = None,
|
|
2608
2609
|
add_session_state_to_context: Optional[bool] = None,
|
|
@@ -2629,7 +2630,7 @@ class Team:
|
|
|
2629
2630
|
images: Optional[Sequence[Image]] = None,
|
|
2630
2631
|
videos: Optional[Sequence[Video]] = None,
|
|
2631
2632
|
files: Optional[Sequence[File]] = None,
|
|
2632
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
2633
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
2633
2634
|
add_history_to_context: Optional[bool] = None,
|
|
2634
2635
|
add_dependencies_to_context: Optional[bool] = None,
|
|
2635
2636
|
add_session_state_to_context: Optional[bool] = None,
|
|
@@ -2657,7 +2658,7 @@ class Team:
|
|
|
2657
2658
|
images: Optional[Sequence[Image]] = None,
|
|
2658
2659
|
videos: Optional[Sequence[Video]] = None,
|
|
2659
2660
|
files: Optional[Sequence[File]] = None,
|
|
2660
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
2661
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
2661
2662
|
add_history_to_context: Optional[bool] = None,
|
|
2662
2663
|
add_dependencies_to_context: Optional[bool] = None,
|
|
2663
2664
|
add_session_state_to_context: Optional[bool] = None,
|
|
@@ -3872,7 +3873,7 @@ class Team:
|
|
|
3872
3873
|
videos: Optional[Sequence[Video]] = None,
|
|
3873
3874
|
files: Optional[Sequence[File]] = None,
|
|
3874
3875
|
markdown: Optional[bool] = None,
|
|
3875
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
3876
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
3876
3877
|
add_history_to_context: Optional[bool] = None,
|
|
3877
3878
|
add_dependencies_to_context: Optional[bool] = None,
|
|
3878
3879
|
add_session_state_to_context: Optional[bool] = None,
|
|
@@ -3982,7 +3983,7 @@ class Team:
|
|
|
3982
3983
|
videos: Optional[Sequence[Video]] = None,
|
|
3983
3984
|
files: Optional[Sequence[File]] = None,
|
|
3984
3985
|
markdown: Optional[bool] = None,
|
|
3985
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
3986
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
3986
3987
|
add_history_to_context: Optional[bool] = None,
|
|
3987
3988
|
dependencies: Optional[Dict[str, Any]] = None,
|
|
3988
3989
|
add_dependencies_to_context: Optional[bool] = None,
|
|
@@ -4271,7 +4272,7 @@ class Team:
|
|
|
4271
4272
|
"""Calculate session metrics"""
|
|
4272
4273
|
|
|
4273
4274
|
session_messages: List[Message] = []
|
|
4274
|
-
for run in session.runs
|
|
4275
|
+
for run in session.runs or []:
|
|
4275
4276
|
if run.messages is not None:
|
|
4276
4277
|
for m in run.messages:
|
|
4277
4278
|
# Skipping messages from history to avoid duplicates
|
|
@@ -7144,7 +7145,7 @@ class Team:
|
|
|
7144
7145
|
|
|
7145
7146
|
# Yield the member event directly
|
|
7146
7147
|
member_agent_run_response_event.parent_run_id = (
|
|
7147
|
-
member_agent_run_response_event
|
|
7148
|
+
getattr(member_agent_run_response_event, 'parent_run_id', None) or run_response.run_id
|
|
7148
7149
|
)
|
|
7149
7150
|
yield member_agent_run_response_event
|
|
7150
7151
|
else:
|
|
@@ -7519,6 +7520,9 @@ class Team:
|
|
|
7519
7520
|
session = self.db.get_session(session_id=session_id, session_type=session_type)
|
|
7520
7521
|
return session # type: ignore
|
|
7521
7522
|
except Exception as e:
|
|
7523
|
+
import traceback
|
|
7524
|
+
|
|
7525
|
+
traceback.print_exc(limit=3)
|
|
7522
7526
|
log_warning(f"Error getting session from db: {e}")
|
|
7523
7527
|
return None
|
|
7524
7528
|
|
|
@@ -7533,6 +7537,9 @@ class Team:
|
|
|
7533
7537
|
session = await self.db.get_session(session_id=session_id, session_type=session_type)
|
|
7534
7538
|
return session # type: ignore
|
|
7535
7539
|
except Exception as e:
|
|
7540
|
+
import traceback
|
|
7541
|
+
|
|
7542
|
+
traceback.print_exc(limit=3)
|
|
7536
7543
|
log_warning(f"Error getting session from db: {e}")
|
|
7537
7544
|
return None
|
|
7538
7545
|
|
|
@@ -7544,6 +7551,9 @@ class Team:
|
|
|
7544
7551
|
raise ValueError("Db not initialized")
|
|
7545
7552
|
return self.db.upsert_session(session=session) # type: ignore
|
|
7546
7553
|
except Exception as e:
|
|
7554
|
+
import traceback
|
|
7555
|
+
|
|
7556
|
+
traceback.print_exc(limit=3)
|
|
7547
7557
|
log_warning(f"Error upserting session into db: {e}")
|
|
7548
7558
|
return None
|
|
7549
7559
|
|
|
@@ -7555,6 +7565,9 @@ class Team:
|
|
|
7555
7565
|
raise ValueError("Db not initialized")
|
|
7556
7566
|
return await self.db.upsert_session(session=session) # type: ignore
|
|
7557
7567
|
except Exception as e:
|
|
7568
|
+
import traceback
|
|
7569
|
+
|
|
7570
|
+
traceback.print_exc(limit=3)
|
|
7558
7571
|
log_warning(f"Error upserting session into db: {e}")
|
|
7559
7572
|
return None
|
|
7560
7573
|
|
|
@@ -8379,7 +8392,11 @@ class Team:
|
|
|
8379
8392
|
return "Successfully added to knowledge base"
|
|
8380
8393
|
|
|
8381
8394
|
def get_relevant_docs_from_knowledge(
|
|
8382
|
-
self,
|
|
8395
|
+
self,
|
|
8396
|
+
query: str,
|
|
8397
|
+
num_documents: Optional[int] = None,
|
|
8398
|
+
filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
8399
|
+
**kwargs,
|
|
8383
8400
|
) -> Optional[List[Union[Dict[str, Any], str]]]:
|
|
8384
8401
|
"""Return a list of references from the knowledge base"""
|
|
8385
8402
|
from agno.knowledge.document import Document
|
|
@@ -8402,6 +8419,10 @@ class Team:
|
|
|
8402
8419
|
if not filters:
|
|
8403
8420
|
log_warning("No valid filters remain after validation. Search will proceed without filters.")
|
|
8404
8421
|
|
|
8422
|
+
if invalid_keys == [] and valid_filters == {}:
|
|
8423
|
+
log_warning("No valid filters provided. Search will proceed without filters.")
|
|
8424
|
+
filters = None
|
|
8425
|
+
|
|
8405
8426
|
if self.knowledge_retriever is not None and callable(self.knowledge_retriever):
|
|
8406
8427
|
from inspect import signature
|
|
8407
8428
|
|
|
@@ -8439,7 +8460,11 @@ class Team:
|
|
|
8439
8460
|
raise e
|
|
8440
8461
|
|
|
8441
8462
|
async def aget_relevant_docs_from_knowledge(
|
|
8442
|
-
self,
|
|
8463
|
+
self,
|
|
8464
|
+
query: str,
|
|
8465
|
+
num_documents: Optional[int] = None,
|
|
8466
|
+
filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
8467
|
+
**kwargs,
|
|
8443
8468
|
) -> Optional[List[Union[Dict[str, Any], str]]]:
|
|
8444
8469
|
"""Get relevant documents from knowledge base asynchronously."""
|
|
8445
8470
|
from agno.knowledge.document import Document
|
|
@@ -8463,6 +8488,10 @@ class Team:
|
|
|
8463
8488
|
if not filters:
|
|
8464
8489
|
log_warning("No valid filters remain after validation. Search will proceed without filters.")
|
|
8465
8490
|
|
|
8491
|
+
if invalid_keys == [] and valid_filters == {}:
|
|
8492
|
+
log_warning("No valid filters provided. Search will proceed without filters.")
|
|
8493
|
+
filters = None
|
|
8494
|
+
|
|
8466
8495
|
if self.knowledge_retriever is not None and callable(self.knowledge_retriever):
|
|
8467
8496
|
from inspect import isawaitable, signature
|
|
8468
8497
|
|
|
@@ -8519,7 +8548,9 @@ class Team:
|
|
|
8519
8548
|
|
|
8520
8549
|
return json.dumps(docs, indent=2)
|
|
8521
8550
|
|
|
8522
|
-
def _get_effective_filters(
|
|
8551
|
+
def _get_effective_filters(
|
|
8552
|
+
self, knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
8553
|
+
) -> Optional[Any]:
|
|
8523
8554
|
"""
|
|
8524
8555
|
Determine effective filters for the team, considering:
|
|
8525
8556
|
1. Team-level filters (self.knowledge_filters)
|
|
@@ -8536,7 +8567,15 @@ class Team:
|
|
|
8536
8567
|
# Apply run-time filters if they exist
|
|
8537
8568
|
if knowledge_filters:
|
|
8538
8569
|
if effective_filters:
|
|
8539
|
-
effective_filters
|
|
8570
|
+
if isinstance(effective_filters, dict):
|
|
8571
|
+
if isinstance(knowledge_filters, dict):
|
|
8572
|
+
effective_filters.update(cast(Dict[str, Any], knowledge_filters))
|
|
8573
|
+
else:
|
|
8574
|
+
# If knowledge_filters is not a dict (e.g., list of FilterExpr), combine as list if effective_filters is dict
|
|
8575
|
+
# Convert the dict to a list and concatenate
|
|
8576
|
+
effective_filters = cast(Any, [effective_filters, *knowledge_filters])
|
|
8577
|
+
else:
|
|
8578
|
+
effective_filters = [*effective_filters, *knowledge_filters]
|
|
8540
8579
|
else:
|
|
8541
8580
|
effective_filters = knowledge_filters
|
|
8542
8581
|
|
|
@@ -8545,7 +8584,7 @@ class Team:
|
|
|
8545
8584
|
def _get_search_knowledge_base_function(
|
|
8546
8585
|
self,
|
|
8547
8586
|
run_response: TeamRunOutput,
|
|
8548
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
8587
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
8549
8588
|
async_mode: bool = False,
|
|
8550
8589
|
) -> Function:
|
|
8551
8590
|
"""Factory function to create a search_knowledge_base function with filters."""
|
|
@@ -8614,7 +8653,7 @@ class Team:
|
|
|
8614
8653
|
def _get_search_knowledge_base_with_agentic_filters_function(
|
|
8615
8654
|
self,
|
|
8616
8655
|
run_response: TeamRunOutput,
|
|
8617
|
-
knowledge_filters: Optional[Dict[str, Any]] = None,
|
|
8656
|
+
knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
8618
8657
|
async_mode: bool = False,
|
|
8619
8658
|
) -> Function:
|
|
8620
8659
|
"""Factory function to create a search_knowledge_base function with filters."""
|
agno/tools/file_generation.py
CHANGED
|
@@ -116,7 +116,7 @@ class FileGenerationTools(Toolkit):
|
|
|
116
116
|
file_type="json",
|
|
117
117
|
filename=filename,
|
|
118
118
|
size=len(json_content.encode("utf-8")),
|
|
119
|
-
|
|
119
|
+
filepath=file_path if file_path else None,
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
log_debug("JSON file generated successfully")
|
|
@@ -203,7 +203,7 @@ class FileGenerationTools(Toolkit):
|
|
|
203
203
|
file_type="csv",
|
|
204
204
|
filename=filename,
|
|
205
205
|
size=len(csv_content.encode("utf-8")),
|
|
206
|
-
|
|
206
|
+
filepath=file_path if file_path else None,
|
|
207
207
|
)
|
|
208
208
|
|
|
209
209
|
log_debug("CSV file generated successfully")
|
|
@@ -287,7 +287,7 @@ class FileGenerationTools(Toolkit):
|
|
|
287
287
|
file_type="pdf",
|
|
288
288
|
filename=filename,
|
|
289
289
|
size=len(pdf_content),
|
|
290
|
-
|
|
290
|
+
filepath=file_path if file_path else None,
|
|
291
291
|
)
|
|
292
292
|
|
|
293
293
|
log_debug("PDF file generated successfully")
|
|
@@ -333,7 +333,7 @@ class FileGenerationTools(Toolkit):
|
|
|
333
333
|
file_type="txt",
|
|
334
334
|
filename=filename,
|
|
335
335
|
size=len(content.encode("utf-8")),
|
|
336
|
-
|
|
336
|
+
filepath=file_path if file_path else None,
|
|
337
337
|
)
|
|
338
338
|
|
|
339
339
|
log_debug("Text file generated successfully")
|
agno/tools/gmail.py
CHANGED
|
@@ -141,6 +141,11 @@ class GmailTools(Toolkit):
|
|
|
141
141
|
self.create_draft_email,
|
|
142
142
|
self.send_email,
|
|
143
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,
|
|
144
149
|
]
|
|
145
150
|
|
|
146
151
|
super().__init__(name="gmail_tools", tools=tools, **kwargs)
|
|
@@ -161,6 +166,7 @@ class GmailTools(Toolkit):
|
|
|
161
166
|
"get_emails_by_date",
|
|
162
167
|
"get_emails_by_thread",
|
|
163
168
|
"search_emails",
|
|
169
|
+
"list_custom_labels",
|
|
164
170
|
]
|
|
165
171
|
modify_operations = ["mark_email_as_read", "mark_email_as_unread"]
|
|
166
172
|
if any(read_operation in self.functions for read_operation in read_operations):
|
|
@@ -600,6 +606,179 @@ class GmailTools(Toolkit):
|
|
|
600
606
|
except Exception as error:
|
|
601
607
|
return f"Error marking email {message_id} as unread: {type(error).__name__}: {error}"
|
|
602
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
|
+
|
|
603
782
|
def _validate_email_params(self, to: str, subject: str, body: str) -> None:
|
|
604
783
|
"""Validate email parameters."""
|
|
605
784
|
if not to:
|