agno 2.0.1__py3-none-any.whl → 2.3.0__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 (314) hide show
  1. agno/agent/agent.py +6015 -2823
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +594 -186
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +2 -8
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +72 -0
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +999 -519
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +103 -31
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +139 -0
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +59 -5
  142. agno/models/openai/chat.py +69 -29
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +77 -1
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -178
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +248 -94
  205. agno/run/base.py +44 -5
  206. agno/run/team.py +238 -97
  207. agno/run/workflow.py +144 -33
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1610
  213. agno/tools/dalle.py +2 -4
  214. agno/tools/decorator.py +4 -2
  215. agno/tools/duckduckgo.py +15 -11
  216. agno/tools/e2b.py +14 -7
  217. agno/tools/eleven_labs.py +23 -25
  218. agno/tools/exa.py +21 -16
  219. agno/tools/file.py +153 -23
  220. agno/tools/file_generation.py +350 -0
  221. agno/tools/firecrawl.py +4 -4
  222. agno/tools/function.py +250 -30
  223. agno/tools/gmail.py +238 -14
  224. agno/tools/google_drive.py +270 -0
  225. agno/tools/googlecalendar.py +36 -8
  226. agno/tools/googlesheets.py +20 -5
  227. agno/tools/jira.py +20 -0
  228. agno/tools/knowledge.py +3 -3
  229. agno/tools/mcp/__init__.py +10 -0
  230. agno/tools/mcp/mcp.py +331 -0
  231. agno/tools/mcp/multi_mcp.py +347 -0
  232. agno/tools/mcp/params.py +24 -0
  233. agno/tools/mcp_toolbox.py +284 -0
  234. agno/tools/mem0.py +11 -17
  235. agno/tools/memori.py +1 -53
  236. agno/tools/memory.py +419 -0
  237. agno/tools/models/nebius.py +5 -5
  238. agno/tools/models_labs.py +20 -10
  239. agno/tools/notion.py +204 -0
  240. agno/tools/parallel.py +314 -0
  241. agno/tools/scrapegraph.py +58 -31
  242. agno/tools/searxng.py +2 -2
  243. agno/tools/serper.py +2 -2
  244. agno/tools/slack.py +18 -3
  245. agno/tools/spider.py +2 -2
  246. agno/tools/tavily.py +146 -0
  247. agno/tools/whatsapp.py +1 -1
  248. agno/tools/workflow.py +278 -0
  249. agno/tools/yfinance.py +12 -11
  250. agno/utils/agent.py +820 -0
  251. agno/utils/audio.py +27 -0
  252. agno/utils/common.py +90 -1
  253. agno/utils/events.py +217 -2
  254. agno/utils/gemini.py +180 -22
  255. agno/utils/hooks.py +57 -0
  256. agno/utils/http.py +111 -0
  257. agno/utils/knowledge.py +12 -5
  258. agno/utils/log.py +1 -0
  259. agno/utils/mcp.py +92 -2
  260. agno/utils/media.py +188 -10
  261. agno/utils/merge_dict.py +22 -1
  262. agno/utils/message.py +60 -0
  263. agno/utils/models/claude.py +40 -11
  264. agno/utils/print_response/agent.py +105 -21
  265. agno/utils/print_response/team.py +103 -38
  266. agno/utils/print_response/workflow.py +251 -34
  267. agno/utils/reasoning.py +22 -1
  268. agno/utils/serialize.py +32 -0
  269. agno/utils/streamlit.py +16 -10
  270. agno/utils/string.py +41 -0
  271. agno/utils/team.py +98 -9
  272. agno/utils/tools.py +1 -1
  273. agno/vectordb/base.py +23 -4
  274. agno/vectordb/cassandra/cassandra.py +65 -9
  275. agno/vectordb/chroma/chromadb.py +182 -38
  276. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  277. agno/vectordb/couchbase/couchbase.py +105 -10
  278. agno/vectordb/lancedb/lance_db.py +124 -133
  279. agno/vectordb/langchaindb/langchaindb.py +25 -7
  280. agno/vectordb/lightrag/lightrag.py +17 -3
  281. agno/vectordb/llamaindex/__init__.py +3 -0
  282. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  283. agno/vectordb/milvus/milvus.py +126 -9
  284. agno/vectordb/mongodb/__init__.py +7 -1
  285. agno/vectordb/mongodb/mongodb.py +112 -7
  286. agno/vectordb/pgvector/pgvector.py +142 -21
  287. agno/vectordb/pineconedb/pineconedb.py +80 -8
  288. agno/vectordb/qdrant/qdrant.py +125 -39
  289. agno/vectordb/redis/__init__.py +9 -0
  290. agno/vectordb/redis/redisdb.py +694 -0
  291. agno/vectordb/singlestore/singlestore.py +111 -25
  292. agno/vectordb/surrealdb/surrealdb.py +31 -5
  293. agno/vectordb/upstashdb/upstashdb.py +76 -8
  294. agno/vectordb/weaviate/weaviate.py +86 -15
  295. agno/workflow/__init__.py +2 -0
  296. agno/workflow/agent.py +299 -0
  297. agno/workflow/condition.py +112 -18
  298. agno/workflow/loop.py +69 -10
  299. agno/workflow/parallel.py +266 -118
  300. agno/workflow/router.py +110 -17
  301. agno/workflow/step.py +638 -129
  302. agno/workflow/steps.py +65 -6
  303. agno/workflow/types.py +61 -23
  304. agno/workflow/workflow.py +2085 -272
  305. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
  306. agno-2.3.0.dist-info/RECORD +577 -0
  307. agno/knowledge/reader/url_reader.py +0 -128
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -610
  310. agno/utils/models/aws_claude.py +0 -170
  311. agno-2.0.1.dist-info/RECORD +0 -515
  312. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  313. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ import uuid
4
4
  from functools import wraps
5
5
  from os import getenv
6
6
  from pathlib import Path
7
- from typing import Any, Dict, List, Optional
7
+ from typing import Any, Dict, List, Optional, cast
8
8
 
9
9
  from agno.tools import Toolkit
10
10
  from agno.utils.log import log_debug, log_error, log_info
@@ -164,8 +164,10 @@ class GoogleCalendarTools(Toolkit):
164
164
  )
165
165
 
166
166
  try:
167
+ service = cast(Resource, self.service)
168
+
167
169
  events_result = (
168
- self.service.events() # type: ignore
170
+ service.events()
169
171
  .list(
170
172
  calendarId=self.calendar_id,
171
173
  timeMin=start_date,
@@ -194,6 +196,7 @@ class GoogleCalendarTools(Toolkit):
194
196
  timezone: Optional[str] = "UTC",
195
197
  attendees: Optional[List[str]] = None,
196
198
  add_google_meet_link: Optional[bool] = False,
199
+ notify_attendees: Optional[bool] = False,
197
200
  ) -> str:
198
201
  """
199
202
  Create a new event in the Google Calendar.
@@ -207,6 +210,7 @@ class GoogleCalendarTools(Toolkit):
207
210
  timezone (Optional[str]): Timezone for the event (default: UTC)
208
211
  attendees (Optional[List[str]]): List of email addresses of the attendees
209
212
  add_google_meet_link (Optional[bool]): Whether to add a Google Meet video link to the event
213
+ notify_attendees (Optional[bool]): Whether to send email notifications to attendees (default: False)
210
214
 
211
215
  Returns:
212
216
  str: JSON string containing the created Google Calendar event or error message
@@ -241,12 +245,18 @@ class GoogleCalendarTools(Toolkit):
241
245
  # Remove None values
242
246
  event = {k: v for k, v in event.items() if v is not None}
243
247
 
248
+ # Determine sendUpdates value based on notify_attendees parameter
249
+ send_updates = "all" if notify_attendees and attendees else "none"
250
+
251
+ service = cast(Resource, self.service)
252
+
244
253
  event_result = (
245
- self.service.events() # type: ignore
254
+ service.events()
246
255
  .insert(
247
256
  calendarId=self.calendar_id,
248
257
  body=event,
249
258
  conferenceDataVersion=1 if add_google_meet_link else 0,
259
+ sendUpdates=send_updates,
250
260
  )
251
261
  .execute()
252
262
  )
@@ -267,6 +277,7 @@ class GoogleCalendarTools(Toolkit):
267
277
  end_date: Optional[str] = None,
268
278
  timezone: Optional[str] = None,
269
279
  attendees: Optional[List[str]] = None,
280
+ notify_attendees: Optional[bool] = False,
270
281
  ) -> str:
271
282
  """
272
283
  Update an existing event in the Google Calendar.
@@ -280,13 +291,16 @@ class GoogleCalendarTools(Toolkit):
280
291
  end_date (Optional[str]): New end date and time in ISO format (YYYY-MM-DDTHH:MM:SS)
281
292
  timezone (Optional[str]): New timezone for the event
282
293
  attendees (Optional[List[str]]): Updated list of attendee email addresses
294
+ notify_attendees (Optional[bool]): Whether to send email notifications to attendees (default: False)
283
295
 
284
296
  Returns:
285
297
  str: JSON string containing the updated Google Calendar event or error message
286
298
  """
287
299
  try:
300
+ service = cast(Resource, self.service)
301
+
288
302
  # First get the existing event to preserve its structure
289
- event = self.service.events().get(calendarId=self.calendar_id, eventId=event_id).execute() # type: ignore
303
+ event = service.events().get(calendarId=self.calendar_id, eventId=event_id).execute()
290
304
 
291
305
  # Update only the fields that are provided
292
306
  if title is not None:
@@ -317,9 +331,15 @@ class GoogleCalendarTools(Toolkit):
317
331
  except ValueError:
318
332
  return json.dumps({"error": f"Invalid end datetime format: {end_date}. Use ISO format."})
319
333
 
334
+ # Determine sendUpdates value based on notify_attendees parameter
335
+ send_updates = "all" if notify_attendees and attendees else "none"
336
+
320
337
  # Update the event
338
+
321
339
  updated_event = (
322
- self.service.events().update(calendarId=self.calendar_id, eventId=event_id, body=event).execute() # type: ignore
340
+ service.events()
341
+ .update(calendarId=self.calendar_id, eventId=event_id, body=event, sendUpdates=send_updates)
342
+ .execute()
323
343
  )
324
344
 
325
345
  log_debug(f"Event {event_id} updated successfully.")
@@ -329,18 +349,24 @@ class GoogleCalendarTools(Toolkit):
329
349
  return json.dumps({"error": f"An error occurred: {error}"})
330
350
 
331
351
  @authenticate
332
- def delete_event(self, event_id: str) -> str:
352
+ def delete_event(self, event_id: str, notify_attendees: Optional[bool] = True) -> str:
333
353
  """
334
354
  Delete an event from the Google Calendar.
335
355
 
336
356
  Args:
337
357
  event_id (str): ID of the event to delete
358
+ notify_attendees (Optional[bool]): Whether to send email notifications to attendees (default: False)
338
359
 
339
360
  Returns:
340
361
  str: JSON string containing success message or error message
341
362
  """
342
363
  try:
343
- self.service.events().delete(calendarId=self.calendar_id, eventId=event_id).execute() # type: ignore
364
+ # Determine sendUpdates value based on notify_attendees parameter
365
+ send_updates = "all" if notify_attendees else "none"
366
+
367
+ service = cast(Resource, self.service)
368
+
369
+ service.events().delete(calendarId=self.calendar_id, eventId=event_id, sendUpdates=send_updates).execute()
344
370
 
345
371
  log_debug(f"Event {event_id} deleted successfully.")
346
372
  return json.dumps({"success": True, "message": f"Event {event_id} deleted successfully."})
@@ -366,6 +392,8 @@ class GoogleCalendarTools(Toolkit):
366
392
  str: JSON string containing all Google Calendar events or error message
367
393
  """
368
394
  try:
395
+ service = cast(Resource, self.service)
396
+
369
397
  params = {
370
398
  "calendarId": self.calendar_id,
371
399
  "maxResults": min(max_results, 100),
@@ -412,7 +440,7 @@ class GoogleCalendarTools(Toolkit):
412
440
  if page_token:
413
441
  params["pageToken"] = page_token
414
442
 
415
- events_result = self.service.events().list(**params).execute() # type: ignore
443
+ events_result = service.events().list(**params).execute()
416
444
  all_events.extend(events_result.get("items", []))
417
445
 
418
446
  page_token = events_result.get("nextPageToken")
@@ -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/jira.py CHANGED
@@ -22,6 +22,7 @@ class JiraTools(Toolkit):
22
22
  enable_create_issue: bool = True,
23
23
  enable_search_issues: bool = True,
24
24
  enable_add_comment: bool = True,
25
+ enable_add_worklog: bool = True,
25
26
  all: bool = False,
26
27
  **kwargs,
27
28
  ):
@@ -55,6 +56,8 @@ class JiraTools(Toolkit):
55
56
  tools.append(self.search_issues)
56
57
  if enable_add_comment or all:
57
58
  tools.append(self.add_comment)
59
+ if enable_add_worklog or all:
60
+ tools.append(self.add_worklog)
58
61
 
59
62
  super().__init__(name="jira_tools", tools=tools, **kwargs)
60
63
 
@@ -148,3 +151,20 @@ class JiraTools(Toolkit):
148
151
  except Exception as e:
149
152
  logger.error(f"Error adding comment to issue {issue_key}: {e}")
150
153
  return json.dumps({"error": str(e)})
154
+
155
+ def add_worklog(self, issue_key: str, time_spent: str, comment: Optional[str] = None) -> str:
156
+ """
157
+ Adds a worklog entry to log time spent on a specific Jira issue.
158
+
159
+ :param issue_key: The key of the issue to log work against (e.g., 'PROJ-123').
160
+ :param time_spent: The amount of time spent. Use Jira's format, e.g., '2h', '30m', '1d 4h'.
161
+ :param comment: An optional comment describing the work done.
162
+ :return: A JSON string indicating success or containing an error message.
163
+ """
164
+ try:
165
+ self.jira.add_worklog(issue=issue_key, timeSpent=time_spent, comment=comment)
166
+ log_debug(f"Worklog of '{time_spent}' added to issue {issue_key}")
167
+ return json.dumps({"status": "success", "issue_key": issue_key, "time_spent": time_spent})
168
+ except Exception as e:
169
+ logger.error(f"Error adding worklog to issue {issue_key}: {e}")
170
+ return json.dumps({"error": str(e)})
agno/tools/knowledge.py CHANGED
@@ -43,15 +43,15 @@ class KnowledgeTools(Toolkit):
43
43
  if enable_think or all:
44
44
  tools.append(self.think)
45
45
  if enable_search or all:
46
- tools.append(self.search)
46
+ tools.append(self.search_knowledge)
47
47
  if enable_analyze or all:
48
48
  tools.append(self.analyze)
49
49
 
50
50
  super().__init__(
51
51
  name="knowledge_tools",
52
+ tools=tools,
52
53
  instructions=self.instructions,
53
54
  add_instructions=add_instructions,
54
- tools=tools,
55
55
  **kwargs,
56
56
  )
57
57
 
@@ -89,7 +89,7 @@ class KnowledgeTools(Toolkit):
89
89
  log_error(f"Error recording thought: {e}")
90
90
  return f"Error recording thought: {e}"
91
91
 
92
- def search(self, session_state: Dict[str, Any], query: str) -> str:
92
+ def search_knowledge(self, session_state: Dict[str, Any], query: str) -> str:
93
93
  """Use this tool to search the knowledge base for relevant information.
94
94
  After thinking through the question, use this tool as many times as needed to search for relevant information.
95
95
 
@@ -0,0 +1,10 @@
1
+ from agno.tools.mcp.mcp import MCPTools
2
+ from agno.tools.mcp.multi_mcp import MultiMCPTools
3
+ from agno.tools.mcp.params import SSEClientParams, StreamableHTTPClientParams
4
+
5
+ __all__ = [
6
+ "MCPTools",
7
+ "MultiMCPTools",
8
+ "StreamableHTTPClientParams",
9
+ "SSEClientParams",
10
+ ]
agno/tools/mcp/mcp.py ADDED
@@ -0,0 +1,331 @@
1
+ import weakref
2
+ from dataclasses import asdict
3
+ from datetime import timedelta
4
+ from typing import Any, Literal, Optional, Union
5
+
6
+ from agno.tools import Toolkit
7
+ from agno.tools.function import Function
8
+ from agno.tools.mcp.params import SSEClientParams, StreamableHTTPClientParams
9
+ from agno.utils.log import log_debug, log_error, log_info
10
+ from agno.utils.mcp import get_entrypoint_for_tool, prepare_command
11
+
12
+ try:
13
+ from mcp import ClientSession, StdioServerParameters
14
+ from mcp.client.sse import sse_client
15
+ from mcp.client.stdio import get_default_environment, stdio_client
16
+ from mcp.client.streamable_http import streamablehttp_client
17
+ except (ImportError, ModuleNotFoundError):
18
+ raise ImportError("`mcp` not installed. Please install using `pip install mcp`")
19
+
20
+
21
+ class MCPTools(Toolkit):
22
+ """
23
+ A toolkit for integrating Model Context Protocol (MCP) servers with Agno agents.
24
+ This allows agents to access tools, resources, and prompts exposed by MCP servers.
25
+
26
+ Can be used in three ways:
27
+ 1. Direct initialization with a ClientSession
28
+ 2. As an async context manager with StdioServerParameters
29
+ 3. As an async context manager with SSE or Streamable HTTP client parameters
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ command: Optional[str] = None,
35
+ *,
36
+ url: Optional[str] = None,
37
+ env: Optional[dict[str, str]] = None,
38
+ transport: Literal["stdio", "sse", "streamable-http"] = "stdio",
39
+ server_params: Optional[Union[StdioServerParameters, SSEClientParams, StreamableHTTPClientParams]] = None,
40
+ session: Optional[ClientSession] = None,
41
+ timeout_seconds: int = 10,
42
+ client=None,
43
+ include_tools: Optional[list[str]] = None,
44
+ exclude_tools: Optional[list[str]] = None,
45
+ refresh_connection: bool = False,
46
+ tool_name_prefix: Optional[str] = "",
47
+ **kwargs,
48
+ ):
49
+ """
50
+ Initialize the MCP toolkit.
51
+
52
+ Args:
53
+ session: An initialized MCP ClientSession connected to an MCP server
54
+ server_params: Parameters for creating a new session
55
+ command: The command to run to start the server. Should be used in conjunction with env.
56
+ url: The URL endpoint for SSE or Streamable HTTP connection when transport is "sse" or "streamable-http".
57
+ env: The environment variables to pass to the server. Should be used in conjunction with command.
58
+ client: The underlying MCP client (optional, used to prevent garbage collection)
59
+ timeout_seconds: Read timeout in seconds for the MCP client
60
+ include_tools: Optional list of tool names to include (if None, includes all)
61
+ exclude_tools: Optional list of tool names to exclude (if None, excludes none)
62
+ transport: The transport protocol to use, either "stdio" or "sse" or "streamable-http"
63
+ refresh_connection: If True, the connection and tools will be refreshed on each run
64
+ """
65
+ super().__init__(name="MCPTools", **kwargs)
66
+
67
+ if transport == "sse":
68
+ log_info("SSE as a standalone transport is deprecated. Please use Streamable HTTP instead.")
69
+
70
+ # Set these after `__init__` to bypass the `_check_tools_filters`
71
+ # because tools are not available until `initialize()` is called.
72
+ self.include_tools = include_tools
73
+ self.exclude_tools = exclude_tools
74
+ self.refresh_connection = refresh_connection
75
+ self.tool_name_prefix = tool_name_prefix
76
+
77
+ if session is None and server_params is None:
78
+ if transport == "sse" and url is None:
79
+ raise ValueError("One of 'url' or 'server_params' parameters must be provided when using SSE transport")
80
+ if transport == "stdio" and command is None:
81
+ raise ValueError(
82
+ "One of 'command' or 'server_params' parameters must be provided when using stdio transport"
83
+ )
84
+ if transport == "streamable-http" and url is None:
85
+ raise ValueError(
86
+ "One of 'url' or 'server_params' parameters must be provided when using Streamable HTTP transport"
87
+ )
88
+
89
+ # Ensure the received server_params are valid for the given transport
90
+ if server_params is not None:
91
+ if transport == "sse":
92
+ if not isinstance(server_params, SSEClientParams):
93
+ raise ValueError(
94
+ "If using the SSE transport, server_params must be an instance of SSEClientParams."
95
+ )
96
+ elif transport == "stdio":
97
+ if not isinstance(server_params, StdioServerParameters):
98
+ raise ValueError(
99
+ "If using the stdio transport, server_params must be an instance of StdioServerParameters."
100
+ )
101
+ elif transport == "streamable-http":
102
+ if not isinstance(server_params, StreamableHTTPClientParams):
103
+ raise ValueError(
104
+ "If using the streamable-http transport, server_params must be an instance of StreamableHTTPClientParams."
105
+ )
106
+
107
+ self.timeout_seconds = timeout_seconds
108
+ self.session: Optional[ClientSession] = session
109
+ self.server_params: Optional[Union[StdioServerParameters, SSEClientParams, StreamableHTTPClientParams]] = (
110
+ server_params
111
+ )
112
+ self.transport = transport
113
+ self.url = url
114
+
115
+ # Merge provided env with system env
116
+ if env is not None:
117
+ env = {
118
+ **get_default_environment(),
119
+ **env,
120
+ }
121
+ else:
122
+ env = get_default_environment()
123
+
124
+ if command is not None and transport not in ["sse", "streamable-http"]:
125
+ parts = prepare_command(command)
126
+ cmd = parts[0]
127
+ arguments = parts[1:] if len(parts) > 1 else []
128
+ self.server_params = StdioServerParameters(command=cmd, args=arguments, env=env)
129
+
130
+ self._client = client
131
+
132
+ self._initialized = False
133
+ self._connection_task = None
134
+ self._active_contexts: list[Any] = []
135
+ self._context = None
136
+ self._session_context = None
137
+
138
+ def cleanup():
139
+ """Cancel active connections"""
140
+ if self._connection_task and not self._connection_task.done():
141
+ self._connection_task.cancel()
142
+
143
+ # Setup cleanup logic before the instance is garbage collected
144
+ self._cleanup_finalizer = weakref.finalize(self, cleanup)
145
+
146
+ @property
147
+ def initialized(self) -> bool:
148
+ return self._initialized
149
+
150
+ async def is_alive(self) -> bool:
151
+ if self.session is None:
152
+ return False
153
+ try:
154
+ await self.session.send_ping()
155
+ return True
156
+ except (RuntimeError, BaseException):
157
+ return False
158
+
159
+ async def connect(self, force: bool = False):
160
+ """Initialize a MCPTools instance and connect to the contextual MCP server"""
161
+
162
+ if force:
163
+ # Clean up the session and context so we force a new connection
164
+ self.session = None
165
+ self._context = None
166
+ self._session_context = None
167
+ self._initialized = False
168
+ self._connection_task = None
169
+ self._active_contexts = []
170
+
171
+ if self._initialized:
172
+ return
173
+
174
+ try:
175
+ await self._connect()
176
+ except (RuntimeError, BaseException) as e:
177
+ log_error(f"Failed to connect to {str(self)}: {e}")
178
+
179
+ async def _connect(self) -> None:
180
+ """Connects to the MCP server and initializes the tools"""
181
+
182
+ if self._initialized:
183
+ return
184
+
185
+ if self.session is not None:
186
+ await self.initialize()
187
+ return
188
+
189
+ # Create a new studio session
190
+ if self.transport == "sse":
191
+ sse_params = asdict(self.server_params) if self.server_params is not None else {} # type: ignore
192
+ if "url" not in sse_params:
193
+ sse_params["url"] = self.url
194
+ self._context = sse_client(**sse_params) # type: ignore
195
+ client_timeout = min(self.timeout_seconds, sse_params.get("timeout", self.timeout_seconds))
196
+
197
+ # Create a new streamable HTTP session
198
+ elif self.transport == "streamable-http":
199
+ streamable_http_params = asdict(self.server_params) if self.server_params is not None else {} # type: ignore
200
+ if "url" not in streamable_http_params:
201
+ streamable_http_params["url"] = self.url
202
+ self._context = streamablehttp_client(**streamable_http_params) # type: ignore
203
+ params_timeout = streamable_http_params.get("timeout", self.timeout_seconds)
204
+ if isinstance(params_timeout, timedelta):
205
+ params_timeout = int(params_timeout.total_seconds())
206
+ client_timeout = min(self.timeout_seconds, params_timeout)
207
+
208
+ else:
209
+ if self.server_params is None:
210
+ raise ValueError("server_params must be provided when using stdio transport.")
211
+ self._context = stdio_client(self.server_params) # type: ignore
212
+ client_timeout = self.timeout_seconds
213
+
214
+ session_params = await self._context.__aenter__() # type: ignore
215
+ self._active_contexts.append(self._context)
216
+ read, write = session_params[0:2]
217
+
218
+ self._session_context = ClientSession(read, write, read_timeout_seconds=timedelta(seconds=client_timeout)) # type: ignore
219
+ self.session = await self._session_context.__aenter__() # type: ignore
220
+ self._active_contexts.append(self._session_context)
221
+
222
+ # Initialize with the new session
223
+ await self.initialize()
224
+
225
+ async def close(self) -> None:
226
+ """Close the MCP connection and clean up resources"""
227
+ if not self._initialized:
228
+ return
229
+
230
+ try:
231
+ if self._session_context is not None:
232
+ await self._session_context.__aexit__(None, None, None)
233
+ self.session = None
234
+ self._session_context = None
235
+
236
+ if self._context is not None:
237
+ await self._context.__aexit__(None, None, None)
238
+ self._context = None
239
+ except (RuntimeError, BaseException) as e:
240
+ log_error(f"Failed to close MCP connection: {e}")
241
+
242
+ self._initialized = False
243
+
244
+ async def __aenter__(self) -> "MCPTools":
245
+ await self._connect()
246
+ return self
247
+
248
+ async def __aexit__(self, _exc_type, _exc_val, _exc_tb):
249
+ """Exit the async context manager."""
250
+ if self._session_context is not None:
251
+ await self._session_context.__aexit__(_exc_type, _exc_val, _exc_tb)
252
+ self.session = None
253
+ self._session_context = None
254
+
255
+ if self._context is not None:
256
+ await self._context.__aexit__(_exc_type, _exc_val, _exc_tb)
257
+ self._context = None
258
+
259
+ self._initialized = False
260
+
261
+ async def build_tools(self) -> None:
262
+ """Build the tools for the MCP toolkit"""
263
+ if self.session is None:
264
+ raise ValueError("Session is not initialized")
265
+
266
+ try:
267
+ # Get the list of tools from the MCP server
268
+ available_tools = await self.session.list_tools() # type: ignore
269
+
270
+ self._check_tools_filters(
271
+ available_tools=[tool.name for tool in available_tools.tools],
272
+ include_tools=self.include_tools,
273
+ exclude_tools=self.exclude_tools,
274
+ )
275
+
276
+ # Filter tools based on include/exclude lists
277
+ filtered_tools = []
278
+ for tool in available_tools.tools:
279
+ if self.exclude_tools and tool.name in self.exclude_tools:
280
+ continue
281
+ if self.include_tools is None or tool.name in self.include_tools:
282
+ filtered_tools.append(tool)
283
+
284
+ # Get tool name prefix if available
285
+ tool_name_prefix = ""
286
+ if self.tool_name_prefix is not None:
287
+ tool_name_prefix = self.tool_name_prefix + "_"
288
+
289
+ # Register the tools with the toolkit
290
+ for tool in filtered_tools:
291
+ try:
292
+ # Get an entrypoint for the tool
293
+ entrypoint = get_entrypoint_for_tool(tool, self.session) # type: ignore
294
+ # Create a Function for the tool
295
+ f = Function(
296
+ name=tool_name_prefix + tool.name,
297
+ description=tool.description,
298
+ parameters=tool.inputSchema,
299
+ entrypoint=entrypoint,
300
+ # Set skip_entrypoint_processing to True to avoid processing the entrypoint
301
+ skip_entrypoint_processing=True,
302
+ )
303
+
304
+ # Register the Function with the toolkit
305
+ self.functions[f.name] = f
306
+ log_debug(f"Function: {f.name} registered with {self.name}")
307
+ except Exception as e:
308
+ log_error(f"Failed to register tool {tool.name}: {e}")
309
+
310
+ except (RuntimeError, BaseException) as e:
311
+ log_error(f"Failed to get tools for {str(self)}: {e}")
312
+ raise
313
+
314
+ async def initialize(self) -> None:
315
+ """Initialize the MCP toolkit by getting available tools from the MCP server"""
316
+ if self._initialized:
317
+ return
318
+
319
+ try:
320
+ if self.session is None:
321
+ raise ValueError("Session is not initialized")
322
+
323
+ # Initialize the session if not already initialized
324
+ await self.session.initialize()
325
+
326
+ await self.build_tools()
327
+
328
+ self._initialized = True
329
+
330
+ except (RuntimeError, BaseException) as e:
331
+ log_error(f"Failed to initialize MCP toolkit: {e}")