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.
Files changed (94) hide show
  1. agno/agent/agent.py +1779 -577
  2. agno/db/async_postgres/__init__.py +3 -0
  3. agno/db/async_postgres/async_postgres.py +1668 -0
  4. agno/db/async_postgres/schemas.py +124 -0
  5. agno/db/async_postgres/utils.py +289 -0
  6. agno/db/base.py +237 -2
  7. agno/db/dynamo/dynamo.py +10 -8
  8. agno/db/dynamo/schemas.py +1 -10
  9. agno/db/dynamo/utils.py +2 -2
  10. agno/db/firestore/firestore.py +2 -2
  11. agno/db/firestore/utils.py +4 -2
  12. agno/db/gcs_json/gcs_json_db.py +2 -2
  13. agno/db/in_memory/in_memory_db.py +2 -2
  14. agno/db/json/json_db.py +2 -2
  15. agno/db/migrations/v1_to_v2.py +30 -13
  16. agno/db/mongo/mongo.py +18 -6
  17. agno/db/mysql/mysql.py +35 -13
  18. agno/db/postgres/postgres.py +29 -6
  19. agno/db/redis/redis.py +2 -2
  20. agno/db/singlestore/singlestore.py +2 -2
  21. agno/db/sqlite/sqlite.py +34 -12
  22. agno/db/sqlite/utils.py +8 -3
  23. agno/eval/accuracy.py +50 -43
  24. agno/eval/performance.py +6 -3
  25. agno/eval/reliability.py +6 -3
  26. agno/eval/utils.py +33 -16
  27. agno/exceptions.py +8 -2
  28. agno/knowledge/embedder/fastembed.py +1 -1
  29. agno/knowledge/knowledge.py +260 -46
  30. agno/knowledge/reader/pdf_reader.py +4 -6
  31. agno/knowledge/reader/reader_factory.py +2 -3
  32. agno/memory/manager.py +241 -33
  33. agno/models/anthropic/claude.py +37 -0
  34. agno/os/app.py +15 -10
  35. agno/os/interfaces/a2a/router.py +3 -5
  36. agno/os/interfaces/agui/router.py +4 -1
  37. agno/os/interfaces/agui/utils.py +33 -6
  38. agno/os/interfaces/slack/router.py +2 -4
  39. agno/os/mcp.py +98 -41
  40. agno/os/router.py +23 -0
  41. agno/os/routers/evals/evals.py +52 -20
  42. agno/os/routers/evals/utils.py +14 -14
  43. agno/os/routers/knowledge/knowledge.py +130 -9
  44. agno/os/routers/knowledge/schemas.py +57 -0
  45. agno/os/routers/memory/memory.py +116 -44
  46. agno/os/routers/metrics/metrics.py +16 -6
  47. agno/os/routers/session/session.py +65 -22
  48. agno/os/schema.py +38 -0
  49. agno/os/utils.py +69 -13
  50. agno/reasoning/anthropic.py +80 -0
  51. agno/reasoning/gemini.py +73 -0
  52. agno/reasoning/openai.py +5 -0
  53. agno/reasoning/vertexai.py +76 -0
  54. agno/session/workflow.py +69 -1
  55. agno/team/team.py +934 -241
  56. agno/tools/function.py +36 -18
  57. agno/tools/google_drive.py +270 -0
  58. agno/tools/googlesheets.py +20 -5
  59. agno/tools/mcp_toolbox.py +3 -3
  60. agno/tools/scrapegraph.py +1 -1
  61. agno/utils/models/claude.py +3 -1
  62. agno/utils/print_response/workflow.py +112 -12
  63. agno/utils/streamlit.py +1 -1
  64. agno/vectordb/base.py +22 -1
  65. agno/vectordb/cassandra/cassandra.py +9 -0
  66. agno/vectordb/chroma/chromadb.py +26 -6
  67. agno/vectordb/clickhouse/clickhousedb.py +9 -1
  68. agno/vectordb/couchbase/couchbase.py +11 -0
  69. agno/vectordb/lancedb/lance_db.py +20 -0
  70. agno/vectordb/langchaindb/langchaindb.py +11 -0
  71. agno/vectordb/lightrag/lightrag.py +9 -0
  72. agno/vectordb/llamaindex/llamaindexdb.py +15 -1
  73. agno/vectordb/milvus/milvus.py +23 -0
  74. agno/vectordb/mongodb/mongodb.py +22 -0
  75. agno/vectordb/pgvector/pgvector.py +19 -0
  76. agno/vectordb/pineconedb/pineconedb.py +35 -4
  77. agno/vectordb/qdrant/qdrant.py +24 -0
  78. agno/vectordb/singlestore/singlestore.py +25 -17
  79. agno/vectordb/surrealdb/surrealdb.py +18 -1
  80. agno/vectordb/upstashdb/upstashdb.py +26 -1
  81. agno/vectordb/weaviate/weaviate.py +18 -0
  82. agno/workflow/condition.py +29 -0
  83. agno/workflow/loop.py +29 -0
  84. agno/workflow/parallel.py +141 -113
  85. agno/workflow/router.py +29 -0
  86. agno/workflow/step.py +146 -25
  87. agno/workflow/steps.py +29 -0
  88. agno/workflow/types.py +26 -1
  89. agno/workflow/workflow.py +507 -22
  90. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/METADATA +100 -41
  91. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/RECORD +94 -86
  92. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/WHEEL +0 -0
  93. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/licenses/LICENSE +0 -0
  94. {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
- args_str = str(copy_entrypoint_args)
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
- raise
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
- return FunctionExecutionResult(status="failure", error=str(e))
842
+ execution_result = FunctionExecutionResult(status="failure", error=str(e))
834
843
 
835
- # Execute post-hook if it exists
836
- self._handle_post_hook()
844
+ finally:
845
+ self._handle_post_hook()
837
846
 
838
- return FunctionExecutionResult(
839
- status="success", result=self.result, updated_session_state=updated_session_state
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
- raise
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
- return FunctionExecutionResult(status="failure", error=str(e))
1045
+ execution_result = FunctionExecutionResult(status="failure", error=str(e))
1029
1046
 
1030
- # Execute post-hook if it exists
1031
- if iscoroutinefunction(self.function.post_hook):
1032
- await self._handle_post_hook_async()
1033
- else:
1034
- self._handle_post_hook()
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
- return FunctionExecutionResult(
1037
- status="success", result=self.result, updated_session_state=updated_session_state
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
@@ -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, render_heavy_js=self.render_heavy_js)
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)}"
@@ -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
- if message.role == "system":
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