agno 2.0.0rc2__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 (331) hide show
  1. agno/agent/agent.py +6009 -2874
  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 +595 -187
  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 +3 -0
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +339 -266
  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 +1011 -566
  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 +110 -37
  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 +143 -4
  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 +60 -6
  142. agno/models/openai/chat.py +102 -43
  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 +81 -5
  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 -175
  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 +266 -112
  205. agno/run/base.py +53 -24
  206. agno/run/team.py +252 -111
  207. agno/run/workflow.py +156 -45
  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 -1692
  213. agno/tools/brightdata.py +3 -3
  214. agno/tools/cartesia.py +3 -5
  215. agno/tools/dalle.py +9 -8
  216. agno/tools/decorator.py +4 -2
  217. agno/tools/desi_vocal.py +2 -2
  218. agno/tools/duckduckgo.py +15 -11
  219. agno/tools/e2b.py +20 -13
  220. agno/tools/eleven_labs.py +26 -28
  221. agno/tools/exa.py +21 -16
  222. agno/tools/fal.py +4 -4
  223. agno/tools/file.py +153 -23
  224. agno/tools/file_generation.py +350 -0
  225. agno/tools/firecrawl.py +4 -4
  226. agno/tools/function.py +257 -37
  227. agno/tools/giphy.py +2 -2
  228. agno/tools/gmail.py +238 -14
  229. agno/tools/google_drive.py +270 -0
  230. agno/tools/googlecalendar.py +36 -8
  231. agno/tools/googlesheets.py +20 -5
  232. agno/tools/jira.py +20 -0
  233. agno/tools/knowledge.py +3 -3
  234. agno/tools/lumalab.py +3 -3
  235. agno/tools/mcp/__init__.py +10 -0
  236. agno/tools/mcp/mcp.py +331 -0
  237. agno/tools/mcp/multi_mcp.py +347 -0
  238. agno/tools/mcp/params.py +24 -0
  239. agno/tools/mcp_toolbox.py +284 -0
  240. agno/tools/mem0.py +11 -17
  241. agno/tools/memori.py +1 -53
  242. agno/tools/memory.py +419 -0
  243. agno/tools/models/azure_openai.py +2 -2
  244. agno/tools/models/gemini.py +3 -3
  245. agno/tools/models/groq.py +3 -5
  246. agno/tools/models/nebius.py +7 -7
  247. agno/tools/models_labs.py +25 -15
  248. agno/tools/notion.py +204 -0
  249. agno/tools/openai.py +4 -9
  250. agno/tools/opencv.py +3 -3
  251. agno/tools/parallel.py +314 -0
  252. agno/tools/replicate.py +7 -7
  253. agno/tools/scrapegraph.py +58 -31
  254. agno/tools/searxng.py +2 -2
  255. agno/tools/serper.py +2 -2
  256. agno/tools/slack.py +18 -3
  257. agno/tools/spider.py +2 -2
  258. agno/tools/tavily.py +146 -0
  259. agno/tools/whatsapp.py +1 -1
  260. agno/tools/workflow.py +278 -0
  261. agno/tools/yfinance.py +12 -11
  262. agno/utils/agent.py +820 -0
  263. agno/utils/audio.py +27 -0
  264. agno/utils/common.py +90 -1
  265. agno/utils/events.py +222 -7
  266. agno/utils/gemini.py +181 -23
  267. agno/utils/hooks.py +57 -0
  268. agno/utils/http.py +111 -0
  269. agno/utils/knowledge.py +12 -5
  270. agno/utils/log.py +1 -0
  271. agno/utils/mcp.py +95 -5
  272. agno/utils/media.py +188 -10
  273. agno/utils/merge_dict.py +22 -1
  274. agno/utils/message.py +60 -0
  275. agno/utils/models/claude.py +40 -11
  276. agno/utils/models/cohere.py +1 -1
  277. agno/utils/models/watsonx.py +1 -1
  278. agno/utils/openai.py +1 -1
  279. agno/utils/print_response/agent.py +105 -21
  280. agno/utils/print_response/team.py +103 -38
  281. agno/utils/print_response/workflow.py +251 -34
  282. agno/utils/reasoning.py +22 -1
  283. agno/utils/serialize.py +32 -0
  284. agno/utils/streamlit.py +16 -10
  285. agno/utils/string.py +41 -0
  286. agno/utils/team.py +98 -9
  287. agno/utils/tools.py +1 -1
  288. agno/vectordb/base.py +23 -4
  289. agno/vectordb/cassandra/cassandra.py +65 -9
  290. agno/vectordb/chroma/chromadb.py +182 -38
  291. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  292. agno/vectordb/couchbase/couchbase.py +105 -10
  293. agno/vectordb/lancedb/lance_db.py +183 -135
  294. agno/vectordb/langchaindb/langchaindb.py +25 -7
  295. agno/vectordb/lightrag/lightrag.py +17 -3
  296. agno/vectordb/llamaindex/__init__.py +3 -0
  297. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  298. agno/vectordb/milvus/milvus.py +126 -9
  299. agno/vectordb/mongodb/__init__.py +7 -1
  300. agno/vectordb/mongodb/mongodb.py +112 -7
  301. agno/vectordb/pgvector/pgvector.py +142 -21
  302. agno/vectordb/pineconedb/pineconedb.py +80 -8
  303. agno/vectordb/qdrant/qdrant.py +125 -39
  304. agno/vectordb/redis/__init__.py +9 -0
  305. agno/vectordb/redis/redisdb.py +694 -0
  306. agno/vectordb/singlestore/singlestore.py +111 -25
  307. agno/vectordb/surrealdb/surrealdb.py +31 -5
  308. agno/vectordb/upstashdb/upstashdb.py +76 -8
  309. agno/vectordb/weaviate/weaviate.py +86 -15
  310. agno/workflow/__init__.py +2 -0
  311. agno/workflow/agent.py +299 -0
  312. agno/workflow/condition.py +112 -18
  313. agno/workflow/loop.py +69 -10
  314. agno/workflow/parallel.py +266 -118
  315. agno/workflow/router.py +110 -17
  316. agno/workflow/step.py +645 -136
  317. agno/workflow/steps.py +65 -6
  318. agno/workflow/types.py +71 -33
  319. agno/workflow/workflow.py +2113 -300
  320. agno-2.3.0.dist-info/METADATA +618 -0
  321. agno-2.3.0.dist-info/RECORD +577 -0
  322. agno-2.3.0.dist-info/licenses/LICENSE +201 -0
  323. agno/knowledge/reader/url_reader.py +0 -128
  324. agno/tools/googlesearch.py +0 -98
  325. agno/tools/mcp.py +0 -610
  326. agno/utils/models/aws_claude.py +0 -170
  327. agno-2.0.0rc2.dist-info/METADATA +0 -355
  328. agno-2.0.0rc2.dist-info/RECORD +0 -515
  329. agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
  330. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  331. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/utils/gemini.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from pathlib import Path
2
- from typing import Any, Dict, List, Optional
2
+ from typing import Any, Dict, List, Optional, Type, Union
3
+
4
+ from pydantic import BaseModel
3
5
 
4
6
  from agno.media import Image
5
7
  from agno.utils.log import log_error, log_warning
@@ -9,17 +11,124 @@ try:
9
11
  FunctionDeclaration,
10
12
  Schema,
11
13
  Tool,
12
- Type,
14
+ )
15
+ from google.genai.types import (
16
+ Type as GeminiType,
13
17
  )
14
18
  except ImportError:
15
19
  raise ImportError("`google-genai` not installed. Please install it using `pip install google-genai`")
16
20
 
17
21
 
22
+ def prepare_response_schema(pydantic_model: Type[BaseModel]) -> Union[Type[BaseModel], Schema]:
23
+ """
24
+ Prepare a Pydantic model for use as Gemini response schema.
25
+
26
+ Returns the model directly if Gemini can handle it natively,
27
+ otherwise converts to Gemini's Schema format.
28
+
29
+ Args:
30
+ pydantic_model: A Pydantic model class
31
+
32
+ Returns:
33
+ Either the original Pydantic model or a converted Schema object
34
+ """
35
+ schema_dict = pydantic_model.model_json_schema()
36
+
37
+ # Convert to Gemini Schema if the model has problematic patterns
38
+ if needs_conversion(schema_dict):
39
+ try:
40
+ converted = convert_schema(schema_dict)
41
+ except Exception as e:
42
+ log_warning(f"Failed to convert schema for {pydantic_model}: {e}")
43
+ converted = None
44
+
45
+ if converted is None:
46
+ # If conversion fails, let Gemini handle it directly
47
+ return pydantic_model
48
+ return converted
49
+
50
+ # Gemini can handle this model directly
51
+ return pydantic_model
52
+
53
+
54
+ def needs_conversion(schema_dict: Dict[str, Any]) -> bool:
55
+ """
56
+ Check if a schema needs conversion for Gemini.
57
+
58
+ Returns True if the schema has:
59
+ - Self-references or circular references
60
+ - Dict fields (additionalProperties) that Gemini doesn't handle well
61
+ - Empty object definitions that Gemini rejects
62
+ """
63
+ # Check for dict fields (additionalProperties) anywhere in the schema
64
+ if has_additional_properties(schema_dict):
65
+ return True
66
+
67
+ # Check if schema has $defs with circular references
68
+ if "$defs" in schema_dict:
69
+ defs = schema_dict["$defs"]
70
+ for def_name, def_schema in defs.items():
71
+ ref_path = f"#/$defs/{def_name}"
72
+ if has_self_reference(def_schema, ref_path):
73
+ return True
74
+
75
+ return False
76
+
77
+
78
+ def has_additional_properties(schema: Any) -> bool:
79
+ """Check if schema has additionalProperties (Dict fields)"""
80
+ if isinstance(schema, dict):
81
+ # Direct check
82
+ if "additionalProperties" in schema:
83
+ return True
84
+
85
+ # Check properties recursively
86
+ if "properties" in schema:
87
+ for prop_schema in schema["properties"].values():
88
+ if has_additional_properties(prop_schema):
89
+ return True
90
+
91
+ # Check array items
92
+ if "items" in schema:
93
+ if has_additional_properties(schema["items"]):
94
+ return True
95
+
96
+ return False
97
+
98
+
99
+ def has_self_reference(schema: Dict, target_ref: str) -> bool:
100
+ """Check if a schema references itself (directly or indirectly)"""
101
+ if isinstance(schema, dict):
102
+ # Direct self-reference
103
+ if schema.get("$ref") == target_ref:
104
+ return True
105
+
106
+ # Check properties
107
+ if "properties" in schema:
108
+ for prop_schema in schema["properties"].values():
109
+ if has_self_reference(prop_schema, target_ref):
110
+ return True
111
+
112
+ # Check array items
113
+ if "items" in schema:
114
+ if has_self_reference(schema["items"], target_ref):
115
+ return True
116
+
117
+ # Check anyOf/oneOf/allOf
118
+ for key in ["anyOf", "oneOf", "allOf"]:
119
+ if key in schema:
120
+ for sub_schema in schema[key]:
121
+ if has_self_reference(sub_schema, target_ref):
122
+ return True
123
+
124
+ return False
125
+
126
+
18
127
  def format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
19
128
  # Case 1: Image is a URL
20
129
  # Download the image from the URL and add it as base64 encoded data
21
130
  if image.url is not None:
22
- content_bytes = image.image_url_content
131
+ content_bytes = image.get_content_bytes() # type: ignore
23
132
  if content_bytes is not None:
24
133
  try:
25
134
  import base64
@@ -66,7 +175,9 @@ def format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
66
175
  return None
67
176
 
68
177
 
69
- def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str, Any]] = None) -> Optional[Schema]:
178
+ def convert_schema(
179
+ schema_dict: Dict[str, Any], root_schema: Optional[Dict[str, Any]] = None, visited_refs: Optional[set] = None
180
+ ) -> Optional[Schema]:
70
181
  """
71
182
  Recursively convert a JSON-like schema dictionary to a types.Schema object.
72
183
 
@@ -74,23 +185,39 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
74
185
  schema_dict (dict): The JSON schema dictionary with keys like "type", "description",
75
186
  "properties", and "required".
76
187
  root_schema (dict, optional): The root schema containing $defs for resolving $ref
188
+ visited_refs (set, optional): Set of visited $ref paths to detect circular references
77
189
 
78
190
  Returns:
79
191
  types.Schema: The converted schema.
80
192
  """
81
193
 
82
- # If this is the initial call, set root_schema to self
194
+ # If this is the initial call, set root_schema to self and initialize visited_refs
83
195
  if root_schema is None:
84
196
  root_schema = schema_dict
197
+ if visited_refs is None:
198
+ visited_refs = set()
85
199
 
86
- # Handle $ref references
200
+ # Handle $ref references with cycle detection
87
201
  if "$ref" in schema_dict:
88
202
  ref_path = schema_dict["$ref"]
203
+
204
+ # Check for circular reference
205
+ if ref_path in visited_refs:
206
+ # Return a basic object schema to break the cycle
207
+ return Schema(
208
+ type=GeminiType.OBJECT,
209
+ description=f"Circular reference to {ref_path}",
210
+ )
211
+
89
212
  if ref_path.startswith("#/$defs/"):
90
213
  def_name = ref_path.split("/")[-1]
91
214
  if "$defs" in root_schema and def_name in root_schema["$defs"]:
215
+ # Add to visited set before recursing
216
+ new_visited = visited_refs.copy()
217
+ new_visited.add(ref_path)
218
+
92
219
  referenced_schema = root_schema["$defs"][def_name]
93
- return convert_schema(referenced_schema, root_schema)
220
+ return convert_schema(referenced_schema, root_schema, new_visited)
94
221
  # If we can't resolve the reference, return None
95
222
  return None
96
223
 
@@ -98,12 +225,13 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
98
225
  if schema_type is None or schema_type == "null":
99
226
  return None
100
227
  description = schema_dict.get("description", None)
228
+ title = schema_dict.get("title", None)
101
229
  default = schema_dict.get("default", None)
102
230
 
103
231
  # Handle enum types
104
232
  if "enum" in schema_dict:
105
233
  enum_values = schema_dict["enum"]
106
- return Schema(type=Type.STRING, enum=enum_values, description=description, default=default)
234
+ return Schema(type=GeminiType.STRING, enum=enum_values, description=description, default=default, title=title)
107
235
 
108
236
  if schema_type == "object":
109
237
  # Handle regular objects with properties
@@ -117,25 +245,30 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
117
245
  prop_def["type"] = prop_type[0]
118
246
  is_nullable = True
119
247
 
120
- # Process property schema (pass root_schema for $ref resolution)
121
- converted_schema = convert_schema(prop_def, root_schema)
248
+ # Process property schema (pass root_schema and visited_refs for $ref resolution)
249
+ converted_schema = convert_schema(prop_def, root_schema, visited_refs)
122
250
  if converted_schema is not None:
123
251
  if is_nullable:
124
252
  converted_schema.nullable = True
125
253
  properties[key] = converted_schema
254
+ else:
255
+ properties[key] = Schema(
256
+ title=prop_def.get("title", None), description=prop_def.get("description", None)
257
+ )
126
258
 
127
259
  required = schema_dict.get("required", [])
128
260
 
129
261
  if properties:
130
262
  return Schema(
131
- type=Type.OBJECT,
263
+ type=GeminiType.OBJECT,
132
264
  properties=properties,
133
265
  required=required,
134
266
  description=description,
135
267
  default=default,
268
+ title=title,
136
269
  )
137
270
  else:
138
- return Schema(type=Type.OBJECT, description=description, default=default)
271
+ return Schema(type=GeminiType.OBJECT, description=description, default=default, title=title)
139
272
 
140
273
  # Handle Dict types (objects with additionalProperties but no properties)
141
274
  elif "additionalProperties" in schema_dict:
@@ -146,50 +279,67 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
146
279
  # For Gemini, we need to represent Dict[str, T] as an object with at least one property
147
280
  # to avoid the "properties should be non-empty" error.
148
281
  # We'll create a generic property that represents the dictionary structure
149
- value_type = additional_props.get("type", "string").upper()
282
+
283
+ # Handle both single types and union types (arrays) from Zod schemas
284
+ type_value = additional_props.get("type", "string")
285
+ if isinstance(type_value, list):
286
+ value_type = type_value[0].upper() if type_value else "STRING"
287
+ union_types = ", ".join(type_value)
288
+ type_description_suffix = f" (supports union types: {union_types})"
289
+ else:
290
+ # Single type
291
+ value_type = type_value.upper()
292
+ type_description_suffix = ""
293
+
150
294
  # Create a placeholder property to satisfy Gemini's requirements
151
295
  # This is a workaround since Gemini doesn't support additionalProperties directly
152
296
  placeholder_properties = {
153
297
  "example_key": Schema(
154
298
  type=value_type,
155
- description=f"Example key-value pair. This object can contain any number of keys with {value_type.lower()} values.",
299
+ description=f"Example key-value pair. This object can contain any number of keys with {value_type.lower()} values{type_description_suffix}.",
156
300
  )
157
301
  }
158
302
  if value_type == "ARRAY":
159
303
  placeholder_properties["example_key"].items = {} # type: ignore
160
304
 
161
305
  return Schema(
162
- type=Type.OBJECT,
306
+ type=GeminiType.OBJECT,
163
307
  properties=placeholder_properties,
164
308
  description=description
165
- or f"Dictionary with {value_type.lower()} values. Can contain any number of key-value pairs.",
309
+ or f"Dictionary with {value_type.lower()} values{type_description_suffix}. Can contain any number of key-value pairs.",
166
310
  default=default,
167
311
  )
168
312
  else:
169
313
  # additionalProperties is false or true
170
- return Schema(type=Type.OBJECT, description=description, default=default)
314
+ return Schema(type=GeminiType.OBJECT, description=description, default=default, title=title)
171
315
 
172
316
  # Handle empty objects
173
317
  else:
174
- return Schema(type=Type.OBJECT, description=description, default=default)
318
+ return Schema(type=GeminiType.OBJECT, description=description, default=default, title=title)
175
319
 
176
320
  elif schema_type == "array" and "items" in schema_dict:
177
- items = convert_schema(schema_dict["items"], root_schema)
321
+ if not schema_dict["items"]: # Handle empty {}
322
+ items = Schema(type=GeminiType.STRING)
323
+ else:
324
+ converted_items = convert_schema(schema_dict["items"], root_schema, visited_refs)
325
+ items = converted_items if converted_items is not None else Schema(type=GeminiType.STRING)
178
326
  min_items = schema_dict.get("minItems")
179
327
  max_items = schema_dict.get("maxItems")
180
328
  return Schema(
181
- type=Type.ARRAY,
329
+ type=GeminiType.ARRAY,
182
330
  description=description,
183
331
  items=items,
184
332
  min_items=min_items,
185
333
  max_items=max_items,
334
+ title=title,
186
335
  )
187
336
 
188
337
  elif schema_type == "string":
189
338
  schema_kwargs = {
190
- "type": Type.STRING,
339
+ "type": GeminiType.STRING,
191
340
  "description": description,
192
341
  "default": default,
342
+ "title": title,
193
343
  }
194
344
  if "format" in schema_dict:
195
345
  schema_kwargs["format"] = schema_dict["format"]
@@ -200,6 +350,7 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
200
350
  "type": schema_type.upper(),
201
351
  "description": description,
202
352
  "default": default,
353
+ "title": title,
203
354
  }
204
355
  if "maximum" in schema_dict:
205
356
  schema_kwargs["maximum"] = schema_dict["maximum"]
@@ -210,7 +361,7 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
210
361
  elif schema_type == "" and "anyOf" in schema_dict:
211
362
  any_of = []
212
363
  for sub_schema in schema_dict["anyOf"]:
213
- sub_schema_converted = convert_schema(sub_schema, root_schema)
364
+ sub_schema_converted = convert_schema(sub_schema, root_schema, visited_refs)
214
365
  any_of.append(sub_schema_converted)
215
366
 
216
367
  is_nullable = False
@@ -231,12 +382,19 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
231
382
  any_of=any_of,
232
383
  description=description,
233
384
  default=default,
385
+ title=title,
234
386
  )
235
387
  else:
388
+ if isinstance(schema_type, list):
389
+ non_null_types = [t for t in schema_type if t != "null"]
390
+ if non_null_types:
391
+ schema_type = non_null_types[0]
392
+ else:
393
+ schema_type = ""
236
394
  # Only convert to uppercase if schema_type is not empty
237
395
  if schema_type:
238
396
  schema_type = schema_type.upper()
239
- return Schema(type=schema_type, description=description, default=default)
397
+ return Schema(type=schema_type, description=description, default=default, title=title)
240
398
  else:
241
399
  # If we get here with an empty type and no other handlers matched,
242
400
  # something is wrong with the schema
agno/utils/hooks.py ADDED
@@ -0,0 +1,57 @@
1
+ from typing import Any, Callable, Dict, List, Optional, Union
2
+
3
+ from agno.guardrails.base import BaseGuardrail
4
+ from agno.utils.log import log_warning
5
+
6
+
7
+ def normalize_hooks(
8
+ hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]],
9
+ async_mode: bool = False,
10
+ ) -> Optional[List[Callable[..., Any]]]:
11
+ """Normalize hooks to a list format"""
12
+ result_hooks: List[Callable[..., Any]] = []
13
+
14
+ if hooks is not None:
15
+ for hook in hooks:
16
+ if isinstance(hook, BaseGuardrail):
17
+ if async_mode:
18
+ result_hooks.append(hook.async_check)
19
+ else:
20
+ result_hooks.append(hook.check)
21
+ else:
22
+ # Check if the hook is async and used within sync methods
23
+ if not async_mode:
24
+ import asyncio
25
+
26
+ if asyncio.iscoroutinefunction(hook):
27
+ raise ValueError(
28
+ f"Cannot use {hook.__name__} (an async hook) with `run()`. Use `arun()` instead."
29
+ )
30
+
31
+ result_hooks.append(hook)
32
+ return result_hooks if result_hooks else None
33
+
34
+
35
+ def filter_hook_args(hook: Callable[..., Any], all_args: Dict[str, Any]) -> Dict[str, Any]:
36
+ """Filter arguments to only include those that the hook function accepts."""
37
+ import inspect
38
+
39
+ try:
40
+ sig = inspect.signature(hook)
41
+ accepted_params = set(sig.parameters.keys())
42
+
43
+ has_var_keyword = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values())
44
+
45
+ # If the function has **kwargs, pass all arguments
46
+ if has_var_keyword:
47
+ return all_args
48
+
49
+ # Otherwise, filter to only include accepted parameters
50
+ filtered_args = {key: value for key, value in all_args.items() if key in accepted_params}
51
+
52
+ return filtered_args
53
+
54
+ except Exception as e:
55
+ log_warning(f"Could not inspect hook signature, passing all arguments: {e}")
56
+ # If signature inspection fails, pass all arguments as fallback
57
+ return all_args
agno/utils/http.py CHANGED
@@ -10,6 +10,117 @@ logger = logging.getLogger(__name__)
10
10
  DEFAULT_MAX_RETRIES = 3
11
11
  DEFAULT_BACKOFF_FACTOR = 2 # Exponential backoff: 1, 2, 4, 8...
12
12
 
13
+ # Global httpx clients for resource efficiency
14
+ # These are shared across all models to reuse connection pools and avoid resource leaks.
15
+ # Consumers can override these at application startup using set_default_sync_client()
16
+ # and set_default_async_client() to customize limits, timeouts, proxies, etc.
17
+ _global_sync_client: Optional[httpx.Client] = None
18
+ _global_async_client: Optional[httpx.AsyncClient] = None
19
+
20
+
21
+ def get_default_sync_client() -> httpx.Client:
22
+ """Get or create the global synchronous httpx client.
23
+
24
+ Returns:
25
+ A singleton httpx.Client instance with default limits.
26
+ """
27
+ global _global_sync_client
28
+ if _global_sync_client is None or _global_sync_client.is_closed:
29
+ _global_sync_client = httpx.Client(
30
+ limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200), http2=True, follow_redirects=True
31
+ )
32
+ return _global_sync_client
33
+
34
+
35
+ def get_default_async_client() -> httpx.AsyncClient:
36
+ """Get or create the global asynchronous httpx client.
37
+
38
+ Returns:
39
+ A singleton httpx.AsyncClient instance with default limits.
40
+ """
41
+ global _global_async_client
42
+ if _global_async_client is None or _global_async_client.is_closed:
43
+ _global_async_client = httpx.AsyncClient(
44
+ limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200), http2=True, follow_redirects=True
45
+ )
46
+ return _global_async_client
47
+
48
+
49
+ def close_sync_client() -> None:
50
+ """Closes the global sync httpx client.
51
+
52
+ Should be called during application shutdown.
53
+ """
54
+ global _global_sync_client
55
+ if _global_sync_client is not None and not _global_sync_client.is_closed:
56
+ _global_sync_client.close()
57
+
58
+
59
+ async def aclose_default_clients() -> None:
60
+ """Asynchronously close the global httpx clients.
61
+
62
+ Should be called during application shutdown in async contexts.
63
+ """
64
+ global _global_sync_client, _global_async_client
65
+ if _global_sync_client is not None and not _global_sync_client.is_closed:
66
+ _global_sync_client.close()
67
+ if _global_async_client is not None and not _global_async_client.is_closed:
68
+ await _global_async_client.aclose()
69
+
70
+
71
+ def set_default_sync_client(client: httpx.Client) -> None:
72
+ """Set the global synchronous httpx client.
73
+
74
+ IMPORTANT: Call before creating any model instances. Models cache clients on first use.
75
+
76
+ Allows consumers to override the default httpx client with custom configuration
77
+ (e.g., custom limits, timeouts, proxies, SSL verification, etc.).
78
+ This is useful at application startup to customize how all models connect.
79
+
80
+ Example:
81
+ >>> import httpx
82
+ >>> from agno.utils.http import set_default_sync_client
83
+ >>> custom_client = httpx.Client(
84
+ ... limits=httpx.Limits(max_connections=500),
85
+ ... timeout=httpx.Timeout(30.0),
86
+ ... verify=False # for dev environments
87
+ ... )
88
+ >>> set_default_sync_client(custom_client)
89
+ >>> # All models will now use this custom client
90
+
91
+ Args:
92
+ client: An httpx.Client instance to use as the global sync client.
93
+ """
94
+ global _global_sync_client
95
+ _global_sync_client = client
96
+
97
+
98
+ def set_default_async_client(client: httpx.AsyncClient) -> None:
99
+ """Set the global asynchronous httpx client.
100
+
101
+ IMPORTANT: Call before creating any model instances. Models cache clients on first use.
102
+
103
+ Allows consumers to override the default async httpx client with custom configuration
104
+ (e.g., custom limits, timeouts, proxies, SSL verification, etc.).
105
+ This is useful at application startup to customize how all models connect.
106
+
107
+ Example:
108
+ >>> import httpx
109
+ >>> from agno.utils.http import set_default_async_client
110
+ >>> custom_client = httpx.AsyncClient(
111
+ ... limits=httpx.Limits(max_connections=500),
112
+ ... timeout=httpx.Timeout(30.0),
113
+ ... verify=False # for dev environments
114
+ ... )
115
+ >>> set_default_async_client(custom_client)
116
+ >>> # All models will now use this custom client
117
+
118
+ Args:
119
+ client: An httpx.AsyncClient instance to use as the global async client.
120
+ """
121
+ global _global_async_client
122
+ _global_async_client = client
123
+
13
124
 
14
125
  def fetch_with_retry(
15
126
  url: str,
agno/utils/knowledge.py CHANGED
@@ -1,10 +1,11 @@
1
- from typing import Any, Dict, Optional
1
+ from typing import Any, Dict, List, Optional, Union
2
2
 
3
+ from agno.filters import FilterExpr
3
4
  from agno.utils.log import log_info
4
5
 
5
6
 
6
7
  def get_agentic_or_user_search_filters(
7
- filters: Optional[Dict[str, Any]], effective_filters: Optional[Dict[str, Any]]
8
+ filters: Optional[Dict[str, Any]], effective_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]]
8
9
  ) -> Dict[str, Any]:
9
10
  """Helper function to determine the final filters to use for the search.
10
11
 
@@ -15,7 +16,7 @@ def get_agentic_or_user_search_filters(
15
16
  Returns:
16
17
  Dict[str, Any]: The final filters to use for the search.
17
18
  """
18
- search_filters = {}
19
+ search_filters = None
19
20
 
20
21
  # If agentic filters exist and manual filters (passed by user) do not, use agentic filters
21
22
  if filters and not effective_filters:
@@ -23,7 +24,13 @@ def get_agentic_or_user_search_filters(
23
24
 
24
25
  # If both agentic filters exist and manual filters (passed by user) exist, use manual filters (give priority to user and override)
25
26
  if filters and effective_filters:
26
- search_filters = effective_filters
27
+ if isinstance(effective_filters, dict):
28
+ search_filters = effective_filters
29
+ elif isinstance(effective_filters, list):
30
+ # If effective_filters is a list (likely List[FilterExpr]), convert both filters and effective_filters to a dict if possible, otherwise raise
31
+ raise ValueError(
32
+ "Merging dict and list of filters is not supported; effective_filters should be a dict for search compatibility."
33
+ )
27
34
 
28
35
  log_info(f"Filters used by Agent: {search_filters}")
29
- return search_filters
36
+ return search_filters or {}
agno/utils/log.py CHANGED
@@ -108,6 +108,7 @@ workflow_logger: AgnoLogger = build_logger(WORKFLOW_LOGGER_NAME, source_type="wo
108
108
  # Set the default logger to the agent logger
109
109
  logger: AgnoLogger = agent_logger
110
110
 
111
+
111
112
  debug_on: bool = False
112
113
  debug_level: Literal[1, 2] = 1
113
114