agno 2.1.3__py3-none-any.whl → 2.1.5__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 +1779 -577
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/async_postgres/async_postgres.py +1668 -0
- agno/db/async_postgres/schemas.py +124 -0
- agno/db/async_postgres/utils.py +289 -0
- agno/db/base.py +237 -2
- agno/db/dynamo/dynamo.py +10 -8
- agno/db/dynamo/schemas.py +1 -10
- agno/db/dynamo/utils.py +2 -2
- agno/db/firestore/firestore.py +2 -2
- agno/db/firestore/utils.py +4 -2
- agno/db/gcs_json/gcs_json_db.py +2 -2
- agno/db/in_memory/in_memory_db.py +2 -2
- agno/db/json/json_db.py +2 -2
- agno/db/migrations/v1_to_v2.py +30 -13
- agno/db/mongo/mongo.py +18 -6
- agno/db/mysql/mysql.py +35 -13
- agno/db/postgres/postgres.py +29 -6
- agno/db/redis/redis.py +2 -2
- agno/db/singlestore/singlestore.py +2 -2
- agno/db/sqlite/sqlite.py +34 -12
- agno/db/sqlite/utils.py +8 -3
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +8 -2
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/knowledge.py +260 -46
- agno/knowledge/reader/pdf_reader.py +4 -6
- agno/knowledge/reader/reader_factory.py +2 -3
- agno/memory/manager.py +241 -33
- agno/models/anthropic/claude.py +37 -0
- agno/os/app.py +15 -10
- agno/os/interfaces/a2a/router.py +3 -5
- agno/os/interfaces/agui/router.py +4 -1
- agno/os/interfaces/agui/utils.py +33 -6
- agno/os/interfaces/slack/router.py +2 -4
- agno/os/mcp.py +98 -41
- agno/os/router.py +23 -0
- agno/os/routers/evals/evals.py +52 -20
- agno/os/routers/evals/utils.py +14 -14
- agno/os/routers/knowledge/knowledge.py +130 -9
- agno/os/routers/knowledge/schemas.py +57 -0
- agno/os/routers/memory/memory.py +116 -44
- agno/os/routers/metrics/metrics.py +16 -6
- agno/os/routers/session/session.py +65 -22
- agno/os/schema.py +38 -0
- agno/os/utils.py +69 -13
- 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/session/workflow.py +69 -1
- agno/team/team.py +934 -241
- agno/tools/function.py +36 -18
- agno/tools/google_drive.py +270 -0
- agno/tools/googlesheets.py +20 -5
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/scrapegraph.py +1 -1
- agno/utils/models/claude.py +3 -1
- agno/utils/print_response/workflow.py +112 -12
- agno/utils/streamlit.py +1 -1
- agno/vectordb/base.py +22 -1
- agno/vectordb/cassandra/cassandra.py +9 -0
- agno/vectordb/chroma/chromadb.py +26 -6
- agno/vectordb/clickhouse/clickhousedb.py +9 -1
- agno/vectordb/couchbase/couchbase.py +11 -0
- agno/vectordb/lancedb/lance_db.py +20 -0
- agno/vectordb/langchaindb/langchaindb.py +11 -0
- agno/vectordb/lightrag/lightrag.py +9 -0
- agno/vectordb/llamaindex/llamaindexdb.py +15 -1
- agno/vectordb/milvus/milvus.py +23 -0
- agno/vectordb/mongodb/mongodb.py +22 -0
- agno/vectordb/pgvector/pgvector.py +19 -0
- agno/vectordb/pineconedb/pineconedb.py +35 -4
- agno/vectordb/qdrant/qdrant.py +24 -0
- agno/vectordb/singlestore/singlestore.py +25 -17
- agno/vectordb/surrealdb/surrealdb.py +18 -1
- agno/vectordb/upstashdb/upstashdb.py +26 -1
- agno/vectordb/weaviate/weaviate.py +18 -0
- agno/workflow/condition.py +29 -0
- agno/workflow/loop.py +29 -0
- agno/workflow/parallel.py +141 -113
- agno/workflow/router.py +29 -0
- agno/workflow/step.py +146 -25
- agno/workflow/steps.py +29 -0
- agno/workflow/types.py +26 -1
- agno/workflow/workflow.py +507 -22
- {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/METADATA +100 -41
- {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/RECORD +94 -86
- {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/WHEEL +0 -0
- {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/top_level.txt +0 -0
agno/tools/function.py
CHANGED
|
@@ -473,6 +473,7 @@ class Function(BaseModel):
|
|
|
473
473
|
|
|
474
474
|
def _get_cache_key(self, entrypoint_args: Dict[str, Any], call_args: Optional[Dict[str, Any]] = None) -> str:
|
|
475
475
|
"""Generate a cache key based on function name and arguments."""
|
|
476
|
+
import json
|
|
476
477
|
from hashlib import md5
|
|
477
478
|
|
|
478
479
|
copy_entrypoint_args = entrypoint_args.copy()
|
|
@@ -493,7 +494,8 @@ class Function(BaseModel):
|
|
|
493
494
|
del copy_entrypoint_args["files"]
|
|
494
495
|
if "dependencies" in copy_entrypoint_args:
|
|
495
496
|
del copy_entrypoint_args["dependencies"]
|
|
496
|
-
|
|
497
|
+
# Use json.dumps with sort_keys=True to ensure consistent ordering regardless of dict key order
|
|
498
|
+
args_str = json.dumps(copy_entrypoint_args, sort_keys=True, default=str)
|
|
497
499
|
|
|
498
500
|
kwargs_str = str(sorted((call_args or {}).items()))
|
|
499
501
|
key_str = f"{self.name}:{args_str}:{kwargs_str}"
|
|
@@ -799,6 +801,9 @@ class FunctionCall(BaseModel):
|
|
|
799
801
|
return FunctionExecutionResult(status="success", result=cached_result)
|
|
800
802
|
|
|
801
803
|
# Execute function
|
|
804
|
+
execution_result = None
|
|
805
|
+
exception_to_raise = None
|
|
806
|
+
|
|
802
807
|
try:
|
|
803
808
|
# Build and execute the nested chain of hooks
|
|
804
809
|
if self.function.tool_hooks is not None:
|
|
@@ -822,22 +827,27 @@ class FunctionCall(BaseModel):
|
|
|
822
827
|
cache_file = self.function._get_cache_file_path(cache_key)
|
|
823
828
|
self.function._save_to_cache(cache_file, self.result)
|
|
824
829
|
|
|
830
|
+
execution_result = FunctionExecutionResult(
|
|
831
|
+
status="success", result=self.result, updated_session_state=updated_session_state
|
|
832
|
+
)
|
|
833
|
+
|
|
825
834
|
except AgentRunException as e:
|
|
826
835
|
log_debug(f"{e.__class__.__name__}: {e}")
|
|
827
836
|
self.error = str(e)
|
|
828
|
-
|
|
837
|
+
exception_to_raise = e
|
|
829
838
|
except Exception as e:
|
|
830
839
|
log_warning(f"Could not run function {self.get_call_str()}")
|
|
831
840
|
log_exception(e)
|
|
832
841
|
self.error = str(e)
|
|
833
|
-
|
|
842
|
+
execution_result = FunctionExecutionResult(status="failure", error=str(e))
|
|
834
843
|
|
|
835
|
-
|
|
836
|
-
|
|
844
|
+
finally:
|
|
845
|
+
self._handle_post_hook()
|
|
837
846
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
847
|
+
if exception_to_raise is not None:
|
|
848
|
+
raise exception_to_raise
|
|
849
|
+
|
|
850
|
+
return execution_result # type: ignore[return-value]
|
|
841
851
|
|
|
842
852
|
async def _handle_pre_hook_async(self):
|
|
843
853
|
"""Handles the async pre-hook for the function call."""
|
|
@@ -991,6 +1001,9 @@ class FunctionCall(BaseModel):
|
|
|
991
1001
|
return FunctionExecutionResult(status="success", result=cached_result)
|
|
992
1002
|
|
|
993
1003
|
# Execute function
|
|
1004
|
+
execution_result = None
|
|
1005
|
+
exception_to_raise = None
|
|
1006
|
+
|
|
994
1007
|
try:
|
|
995
1008
|
# Build and execute the nested chain of hooks
|
|
996
1009
|
if self.function.tool_hooks is not None:
|
|
@@ -1017,25 +1030,30 @@ class FunctionCall(BaseModel):
|
|
|
1017
1030
|
if entrypoint_args.get("session_state") is not None:
|
|
1018
1031
|
updated_session_state = entrypoint_args.get("session_state")
|
|
1019
1032
|
|
|
1033
|
+
execution_result = FunctionExecutionResult(
|
|
1034
|
+
status="success", result=self.result, updated_session_state=updated_session_state
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1020
1037
|
except AgentRunException as e:
|
|
1021
1038
|
log_debug(f"{e.__class__.__name__}: {e}")
|
|
1022
1039
|
self.error = str(e)
|
|
1023
|
-
|
|
1040
|
+
exception_to_raise = e
|
|
1024
1041
|
except Exception as e:
|
|
1025
1042
|
log_warning(f"Could not run function {self.get_call_str()}")
|
|
1026
1043
|
log_exception(e)
|
|
1027
1044
|
self.error = str(e)
|
|
1028
|
-
|
|
1045
|
+
execution_result = FunctionExecutionResult(status="failure", error=str(e))
|
|
1029
1046
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1047
|
+
finally:
|
|
1048
|
+
if iscoroutinefunction(self.function.post_hook):
|
|
1049
|
+
await self._handle_post_hook_async()
|
|
1050
|
+
else:
|
|
1051
|
+
self._handle_post_hook()
|
|
1035
1052
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1053
|
+
if exception_to_raise is not None:
|
|
1054
|
+
raise exception_to_raise
|
|
1055
|
+
|
|
1056
|
+
return execution_result # type: ignore[return-value]
|
|
1039
1057
|
|
|
1040
1058
|
|
|
1041
1059
|
class ToolResult(BaseModel):
|
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
|
|
73
|
+
try:
|
|
74
|
+
from google.auth.transport.requests import Request
|
|
75
|
+
from google.oauth2.credentials import Credentials
|
|
76
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
77
|
+
from googleapiclient.discovery import Resource, build
|
|
78
|
+
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
|
79
|
+
except ImportError:
|
|
80
|
+
raise ImportError(
|
|
81
|
+
"Google client library for Python not found , install it using `pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib`"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def authenticate(func):
|
|
86
|
+
"""Decorator to ensure authentication before executing a function."""
|
|
87
|
+
|
|
88
|
+
@wraps(func)
|
|
89
|
+
def wrapper(self, *args, **kwargs):
|
|
90
|
+
if not self.creds or not self.creds.valid:
|
|
91
|
+
self._auth()
|
|
92
|
+
if not self.service:
|
|
93
|
+
# Set quota project on credentials if available
|
|
94
|
+
creds_to_use = self.creds
|
|
95
|
+
if hasattr(self, "quota_project_id") and self.quota_project_id:
|
|
96
|
+
creds_to_use = self.creds.with_quota_project(self.quota_project_id)
|
|
97
|
+
self.service = build("drive", "v3", credentials=creds_to_use)
|
|
98
|
+
return func(self, *args, **kwargs)
|
|
99
|
+
|
|
100
|
+
return wrapper
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class GoogleDriveTools(Toolkit):
|
|
104
|
+
# Default scopes for Google Drive API access
|
|
105
|
+
DEFAULT_SCOPES = ["https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive.readonly"]
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
auth_port: Optional[int] = 5050,
|
|
110
|
+
creds: Optional[Credentials] = None,
|
|
111
|
+
scopes: Optional[List[str]] = None,
|
|
112
|
+
creds_path: Optional[str] = None,
|
|
113
|
+
token_path: Optional[str] = None,
|
|
114
|
+
quota_project_id: Optional[str] = None,
|
|
115
|
+
**kwargs,
|
|
116
|
+
):
|
|
117
|
+
self.creds: Optional[Credentials] = creds
|
|
118
|
+
self.service: Optional[Resource] = None
|
|
119
|
+
self.credentials_path = creds_path
|
|
120
|
+
self.token_path = token_path
|
|
121
|
+
self.scopes = scopes or []
|
|
122
|
+
self.scopes.extend(self.DEFAULT_SCOPES)
|
|
123
|
+
|
|
124
|
+
self.quota_project_id = quota_project_id or getenv("GOOGLE_CLOUD_QUOTA_PROJECT_ID")
|
|
125
|
+
if not self.quota_project_id:
|
|
126
|
+
raise ValueError("GOOGLE_CLOUD_QUOTA_PROJECT_ID is not set")
|
|
127
|
+
|
|
128
|
+
self.auth_port: int = int(getenv("GOOGLE_AUTH_PORT", str(auth_port)))
|
|
129
|
+
if not self.auth_port:
|
|
130
|
+
raise ValueError("GOOGLE_AUTH_PORT is not set")
|
|
131
|
+
|
|
132
|
+
tools: List[Any] = [
|
|
133
|
+
self.list_files,
|
|
134
|
+
]
|
|
135
|
+
super().__init__(name="google_drive_tools", tools=tools, **kwargs)
|
|
136
|
+
if not self.scopes:
|
|
137
|
+
# Add read permission by default
|
|
138
|
+
self.scopes.append(self.DEFAULT_SCOPES[1]) # 'drive.readonly'
|
|
139
|
+
# Add write permission if allow_update is True
|
|
140
|
+
if getattr(self, "allow_update", False):
|
|
141
|
+
self.scopes.append(self.DEFAULT_SCOPES[0]) # 'drive.file'
|
|
142
|
+
|
|
143
|
+
def _auth(self):
|
|
144
|
+
"""
|
|
145
|
+
Authenticate and set up the Google Drive API client.
|
|
146
|
+
This method checks if credentials are valid and refreshes or requests them if needed.
|
|
147
|
+
"""
|
|
148
|
+
if self.creds and self.creds.valid:
|
|
149
|
+
# Already authenticated
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
token_file = Path(self.token_path or "token.json")
|
|
153
|
+
creds_file = Path(self.credentials_path or "credentials.json")
|
|
154
|
+
|
|
155
|
+
if token_file.exists():
|
|
156
|
+
self.creds = Credentials.from_authorized_user_file(str(token_file), self.scopes)
|
|
157
|
+
if not self.creds or not self.creds.valid:
|
|
158
|
+
if self.creds and self.creds.expired and self.creds.refresh_token:
|
|
159
|
+
self.creds.refresh(Request())
|
|
160
|
+
else:
|
|
161
|
+
client_config = {
|
|
162
|
+
"installed": {
|
|
163
|
+
"client_id": getenv("GOOGLE_CLIENT_ID"),
|
|
164
|
+
"client_secret": getenv("GOOGLE_CLIENT_SECRET"),
|
|
165
|
+
"project_id": getenv("GOOGLE_PROJECT_ID"),
|
|
166
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
167
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
168
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
169
|
+
"redirect_uris": [getenv("GOOGLE_REDIRECT_URI", "http://localhost")],
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
# File based authentication
|
|
173
|
+
if creds_file.exists():
|
|
174
|
+
flow = InstalledAppFlow.from_client_secrets_file(str(creds_file), self.scopes)
|
|
175
|
+
else:
|
|
176
|
+
flow = InstalledAppFlow.from_client_config(client_config, self.scopes)
|
|
177
|
+
# Opens up a browser window for OAuth authentication
|
|
178
|
+
self.creds = flow.run_local_server(port=self.auth_port) # type: ignore
|
|
179
|
+
|
|
180
|
+
token_file.write_text(self.creds.to_json()) if self.creds else None
|
|
181
|
+
|
|
182
|
+
@authenticate
|
|
183
|
+
def list_files(self, query: Optional[str] = None, page_size: int = 10) -> List[dict]:
|
|
184
|
+
"""
|
|
185
|
+
List files in your Google Drive.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
query (Optional[str]): Optional search query to filter files (see Google Drive API docs).
|
|
189
|
+
page_size (int): Maximum number of files to return.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List[dict]: List of file metadata dictionaries.
|
|
193
|
+
"""
|
|
194
|
+
if not self.service:
|
|
195
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
196
|
+
try:
|
|
197
|
+
results = (
|
|
198
|
+
self.service.files() # type: ignore
|
|
199
|
+
.list(q=query, pageSize=page_size, fields="nextPageToken, files(id, name, mimeType, modifiedTime)")
|
|
200
|
+
.execute()
|
|
201
|
+
)
|
|
202
|
+
items = results.get("files", [])
|
|
203
|
+
return items
|
|
204
|
+
except Exception as error:
|
|
205
|
+
print(f"Could not list files: {error}")
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
@authenticate
|
|
209
|
+
def upload_file(self, file_path: Union[str, Path], mime_type: Optional[str] = None) -> Optional[dict]:
|
|
210
|
+
"""
|
|
211
|
+
Upload a file to your Google Drive.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
file_path (Union[str, Path]): Path to the file you want to upload.
|
|
215
|
+
mime_type (Optional[str]): MIME type of the file. If not provided, it will be guessed.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Optional[dict]: Metadata of the uploaded file, or None if upload failed.
|
|
219
|
+
"""
|
|
220
|
+
if not self.service:
|
|
221
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
222
|
+
file_path = Path(file_path)
|
|
223
|
+
if not file_path.exists() or not file_path.is_file():
|
|
224
|
+
raise ValueError(f"The file '{file_path}' does not exist or is not a file.")
|
|
225
|
+
if mime_type is None:
|
|
226
|
+
mime_type, _ = mimetypes.guess_type(file_path.as_posix())
|
|
227
|
+
if mime_type is None:
|
|
228
|
+
mime_type = "application/octet-stream" # Default MIME type
|
|
229
|
+
|
|
230
|
+
file_metadata = {"name": file_path.name}
|
|
231
|
+
media = MediaFileUpload(file_path.as_posix(), mimetype=mime_type)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
uploaded_file = (
|
|
235
|
+
self.service.files() # type: ignore
|
|
236
|
+
.create(body=file_metadata, media_body=media, fields="id, name, mimeType, modifiedTime")
|
|
237
|
+
.execute()
|
|
238
|
+
)
|
|
239
|
+
return uploaded_file
|
|
240
|
+
except Exception as error:
|
|
241
|
+
print(f"Could not upload file '{file_path}': {error}")
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
@authenticate
|
|
245
|
+
def download_file(self, file_id: str, dest_path: Union[str, Path]) -> Optional[Path]:
|
|
246
|
+
"""
|
|
247
|
+
Download a file from your Google Drive.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
file_id (str): The ID of the file you want to download.
|
|
251
|
+
dest_path (Union[str, Path]): Where to save the downloaded file.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Optional[Path]: The path to the downloaded file, or None if download failed.
|
|
255
|
+
"""
|
|
256
|
+
if not self.service:
|
|
257
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
258
|
+
dest_path = Path(dest_path)
|
|
259
|
+
try:
|
|
260
|
+
request = self.service.files().get_media(fileId=file_id) # type: ignore
|
|
261
|
+
with open(dest_path, "wb") as fh:
|
|
262
|
+
downloader = MediaIoBaseDownload(fh, request)
|
|
263
|
+
done = False
|
|
264
|
+
while not done:
|
|
265
|
+
status, done = downloader.next_chunk()
|
|
266
|
+
print(f"Download progress: {int(status.progress() * 100)}%.")
|
|
267
|
+
return dest_path
|
|
268
|
+
except Exception as error:
|
|
269
|
+
print(f"Could not download file '{file_id}': {error}")
|
|
270
|
+
return None
|
agno/tools/googlesheets.py
CHANGED
|
@@ -48,13 +48,14 @@ import json
|
|
|
48
48
|
from functools import wraps
|
|
49
49
|
from os import getenv
|
|
50
50
|
from pathlib import Path
|
|
51
|
-
from typing import Any, List, Optional
|
|
51
|
+
from typing import Any, List, Optional, Union
|
|
52
52
|
|
|
53
53
|
from agno.tools import Toolkit
|
|
54
54
|
|
|
55
55
|
try:
|
|
56
56
|
from google.auth.transport.requests import Request
|
|
57
57
|
from google.oauth2.credentials import Credentials
|
|
58
|
+
from google.oauth2.service_account import Credentials as ServiceAccountCredentials
|
|
58
59
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
59
60
|
from googleapiclient.discovery import Resource, build
|
|
60
61
|
except ImportError:
|
|
@@ -91,9 +92,10 @@ class GoogleSheetsTools(Toolkit):
|
|
|
91
92
|
scopes: Optional[List[str]] = None,
|
|
92
93
|
spreadsheet_id: Optional[str] = None,
|
|
93
94
|
spreadsheet_range: Optional[str] = None,
|
|
94
|
-
creds: Optional[Credentials] = None,
|
|
95
|
+
creds: Optional[Union[Credentials, ServiceAccountCredentials]] = None,
|
|
95
96
|
creds_path: Optional[str] = None,
|
|
96
97
|
token_path: Optional[str] = None,
|
|
98
|
+
service_account_path: Optional[str] = None,
|
|
97
99
|
oauth_port: int = 0,
|
|
98
100
|
enable_read_sheet: bool = True,
|
|
99
101
|
enable_create_sheet: bool = False,
|
|
@@ -108,9 +110,10 @@ class GoogleSheetsTools(Toolkit):
|
|
|
108
110
|
scopes (Optional[List[str]]): Custom OAuth scopes. If None, uses write scope by default.
|
|
109
111
|
spreadsheet_id (Optional[str]): ID of the target spreadsheet.
|
|
110
112
|
spreadsheet_range (Optional[str]): Range within the spreadsheet.
|
|
111
|
-
creds (Optional[Credentials]): Pre-existing credentials.
|
|
113
|
+
creds (Optional[Credentials | ServiceAccountCredentials]): Pre-existing credentials.
|
|
112
114
|
creds_path (Optional[str]): Path to credentials file.
|
|
113
115
|
token_path (Optional[str]): Path to token file.
|
|
116
|
+
service_account_path (Optional[str]): Path to a service account file.
|
|
114
117
|
oauth_port (int): Port to use for OAuth authentication. Defaults to 0.
|
|
115
118
|
enable_read_sheet (bool): Enable reading from a sheet.
|
|
116
119
|
enable_create_sheet (bool): Enable creating a sheet.
|
|
@@ -126,6 +129,7 @@ class GoogleSheetsTools(Toolkit):
|
|
|
126
129
|
self.token_path = token_path
|
|
127
130
|
self.oauth_port = oauth_port
|
|
128
131
|
self.service: Optional[Resource] = None
|
|
132
|
+
self.service_account_path = service_account_path
|
|
129
133
|
|
|
130
134
|
# Determine required scopes based on operations if no custom scopes provided
|
|
131
135
|
if scopes is None:
|
|
@@ -171,6 +175,17 @@ class GoogleSheetsTools(Toolkit):
|
|
|
171
175
|
if self.creds and self.creds.valid:
|
|
172
176
|
return
|
|
173
177
|
|
|
178
|
+
service_account_path = self.service_account_path or getenv("GOOGLE_SERVICE_ACCOUNT_FILE")
|
|
179
|
+
|
|
180
|
+
if service_account_path:
|
|
181
|
+
self.creds = ServiceAccountCredentials.from_service_account_file(
|
|
182
|
+
service_account_path,
|
|
183
|
+
scopes=self.scopes,
|
|
184
|
+
)
|
|
185
|
+
if self.creds and self.creds.expired:
|
|
186
|
+
self.creds.refresh(Request())
|
|
187
|
+
return
|
|
188
|
+
|
|
174
189
|
token_file = Path(self.token_path or "token.json")
|
|
175
190
|
creds_file = Path(self.credentials_path or "credentials.json")
|
|
176
191
|
|
|
@@ -178,7 +193,7 @@ class GoogleSheetsTools(Toolkit):
|
|
|
178
193
|
self.creds = Credentials.from_authorized_user_file(str(token_file), self.scopes)
|
|
179
194
|
|
|
180
195
|
if not self.creds or not self.creds.valid:
|
|
181
|
-
if self.creds and self.creds.expired and self.creds.refresh_token:
|
|
196
|
+
if self.creds and self.creds.expired and self.creds.refresh_token: # type: ignore
|
|
182
197
|
self.creds.refresh(Request())
|
|
183
198
|
else:
|
|
184
199
|
client_config = {
|
|
@@ -199,7 +214,7 @@ class GoogleSheetsTools(Toolkit):
|
|
|
199
214
|
flow = InstalledAppFlow.from_client_config(client_config, self.scopes)
|
|
200
215
|
# Opens up a browser window for OAuth authentication
|
|
201
216
|
self.creds = flow.run_local_server(port=self.oauth_port)
|
|
202
|
-
token_file.write_text(self.creds.to_json()) if self.creds else None
|
|
217
|
+
token_file.write_text(self.creds.to_json()) if self.creds else None # type: ignore
|
|
203
218
|
|
|
204
219
|
@authenticate
|
|
205
220
|
def read_sheet(self, spreadsheet_id: Optional[str] = None, spreadsheet_range: Optional[str] = None) -> str:
|
agno/tools/mcp_toolbox.py
CHANGED
|
@@ -35,6 +35,7 @@ class MCPToolbox(MCPTools, metaclass=MCPToolsMeta):
|
|
|
35
35
|
tool_name: Optional[str] = None,
|
|
36
36
|
headers: Optional[Dict[str, Any]] = None,
|
|
37
37
|
transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http",
|
|
38
|
+
append_mcp_to_url: bool = True,
|
|
38
39
|
**kwargs,
|
|
39
40
|
):
|
|
40
41
|
"""Initialize MCPToolbox with filtering capabilities.
|
|
@@ -45,11 +46,10 @@ class MCPToolbox(MCPTools, metaclass=MCPToolsMeta):
|
|
|
45
46
|
tool_name (Optional[str], optional): Single tool name to load. Defaults to None.
|
|
46
47
|
headers (Optional[Dict[str, Any]], optional): Headers for toolbox-core client requests. Defaults to None.
|
|
47
48
|
transport (Literal["stdio", "sse", "streamable-http"], optional): MCP transport protocol. Defaults to "streamable-http".
|
|
49
|
+
append_mcp_to_url (bool, optional): Whether to append "/mcp" to the URL if it doesn't end with it. Defaults to True.
|
|
48
50
|
|
|
49
51
|
"""
|
|
50
|
-
|
|
51
|
-
# Ensure the URL ends in "/mcp" as expected
|
|
52
|
-
if not url.endswith("/mcp"):
|
|
52
|
+
if append_mcp_to_url and not url.endswith("/mcp"):
|
|
53
53
|
url = url + "/mcp"
|
|
54
54
|
|
|
55
55
|
super().__init__(url=url, transport=transport, **kwargs)
|
agno/tools/scrapegraph.py
CHANGED
|
@@ -187,7 +187,7 @@ class ScrapeGraphTools(Toolkit):
|
|
|
187
187
|
"""
|
|
188
188
|
try:
|
|
189
189
|
log_debug(f"ScrapeGraph searchscraper request with prompt: {user_prompt}")
|
|
190
|
-
response = self.client.searchscraper(user_prompt=user_prompt
|
|
190
|
+
response = self.client.searchscraper(user_prompt=user_prompt)
|
|
191
191
|
return json.dumps(response["result"])
|
|
192
192
|
except Exception as e:
|
|
193
193
|
error_msg = f"Searchscraper failed: {str(e)}"
|
agno/utils/models/claude.py
CHANGED
|
@@ -32,6 +32,7 @@ class MCPServerConfiguration:
|
|
|
32
32
|
|
|
33
33
|
ROLE_MAP = {
|
|
34
34
|
"system": "system",
|
|
35
|
+
"developer": "system",
|
|
35
36
|
"user": "user",
|
|
36
37
|
"assistant": "assistant",
|
|
37
38
|
"tool": "user",
|
|
@@ -217,7 +218,8 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
|
|
|
217
218
|
|
|
218
219
|
for message in messages:
|
|
219
220
|
content = message.content or ""
|
|
220
|
-
|
|
221
|
+
# Both "system" and "developer" roles should be extracted as system messages
|
|
222
|
+
if message.role in ("system", "developer"):
|
|
221
223
|
if content is not None:
|
|
222
224
|
system_messages.append(content) # type: ignore
|
|
223
225
|
continue
|