agno 2.1.2__py3-none-any.whl → 2.3.13__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 +5540 -2273
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/compression/__init__.py +3 -0
  5. agno/compression/manager.py +247 -0
  6. agno/culture/__init__.py +3 -0
  7. agno/culture/manager.py +956 -0
  8. agno/db/async_postgres/__init__.py +3 -0
  9. agno/db/base.py +689 -6
  10. agno/db/dynamo/dynamo.py +933 -37
  11. agno/db/dynamo/schemas.py +174 -10
  12. agno/db/dynamo/utils.py +63 -4
  13. agno/db/firestore/firestore.py +831 -9
  14. agno/db/firestore/schemas.py +51 -0
  15. agno/db/firestore/utils.py +102 -4
  16. agno/db/gcs_json/gcs_json_db.py +660 -12
  17. agno/db/gcs_json/utils.py +60 -26
  18. agno/db/in_memory/in_memory_db.py +287 -14
  19. agno/db/in_memory/utils.py +60 -2
  20. agno/db/json/json_db.py +590 -14
  21. agno/db/json/utils.py +60 -26
  22. agno/db/migrations/manager.py +199 -0
  23. agno/db/migrations/v1_to_v2.py +43 -13
  24. agno/db/migrations/versions/__init__.py +0 -0
  25. agno/db/migrations/versions/v2_3_0.py +938 -0
  26. agno/db/mongo/__init__.py +15 -1
  27. agno/db/mongo/async_mongo.py +2760 -0
  28. agno/db/mongo/mongo.py +879 -11
  29. agno/db/mongo/schemas.py +42 -0
  30. agno/db/mongo/utils.py +80 -8
  31. agno/db/mysql/__init__.py +2 -1
  32. agno/db/mysql/async_mysql.py +2912 -0
  33. agno/db/mysql/mysql.py +946 -68
  34. agno/db/mysql/schemas.py +72 -10
  35. agno/db/mysql/utils.py +198 -7
  36. agno/db/postgres/__init__.py +2 -1
  37. agno/db/postgres/async_postgres.py +2579 -0
  38. agno/db/postgres/postgres.py +942 -57
  39. agno/db/postgres/schemas.py +81 -18
  40. agno/db/postgres/utils.py +164 -2
  41. agno/db/redis/redis.py +671 -7
  42. agno/db/redis/schemas.py +50 -0
  43. agno/db/redis/utils.py +65 -7
  44. agno/db/schemas/__init__.py +2 -1
  45. agno/db/schemas/culture.py +120 -0
  46. agno/db/schemas/evals.py +1 -0
  47. agno/db/schemas/memory.py +17 -2
  48. agno/db/singlestore/schemas.py +63 -0
  49. agno/db/singlestore/singlestore.py +949 -83
  50. agno/db/singlestore/utils.py +60 -2
  51. agno/db/sqlite/__init__.py +2 -1
  52. agno/db/sqlite/async_sqlite.py +2911 -0
  53. agno/db/sqlite/schemas.py +62 -0
  54. agno/db/sqlite/sqlite.py +965 -46
  55. agno/db/sqlite/utils.py +169 -8
  56. agno/db/surrealdb/__init__.py +3 -0
  57. agno/db/surrealdb/metrics.py +292 -0
  58. agno/db/surrealdb/models.py +334 -0
  59. agno/db/surrealdb/queries.py +71 -0
  60. agno/db/surrealdb/surrealdb.py +1908 -0
  61. agno/db/surrealdb/utils.py +147 -0
  62. agno/db/utils.py +2 -0
  63. agno/eval/__init__.py +10 -0
  64. agno/eval/accuracy.py +75 -55
  65. agno/eval/agent_as_judge.py +861 -0
  66. agno/eval/base.py +29 -0
  67. agno/eval/performance.py +16 -7
  68. agno/eval/reliability.py +28 -16
  69. agno/eval/utils.py +35 -17
  70. agno/exceptions.py +27 -2
  71. agno/filters.py +354 -0
  72. agno/guardrails/prompt_injection.py +1 -0
  73. agno/hooks/__init__.py +3 -0
  74. agno/hooks/decorator.py +164 -0
  75. agno/integrations/discord/client.py +1 -1
  76. agno/knowledge/chunking/agentic.py +13 -10
  77. agno/knowledge/chunking/fixed.py +4 -1
  78. agno/knowledge/chunking/semantic.py +9 -4
  79. agno/knowledge/chunking/strategy.py +59 -15
  80. agno/knowledge/embedder/fastembed.py +1 -1
  81. agno/knowledge/embedder/nebius.py +1 -1
  82. agno/knowledge/embedder/ollama.py +8 -0
  83. agno/knowledge/embedder/openai.py +8 -8
  84. agno/knowledge/embedder/sentence_transformer.py +6 -2
  85. agno/knowledge/embedder/vllm.py +262 -0
  86. agno/knowledge/knowledge.py +1618 -318
  87. agno/knowledge/reader/base.py +6 -2
  88. agno/knowledge/reader/csv_reader.py +8 -10
  89. agno/knowledge/reader/docx_reader.py +5 -6
  90. agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
  91. agno/knowledge/reader/json_reader.py +5 -4
  92. agno/knowledge/reader/markdown_reader.py +8 -8
  93. agno/knowledge/reader/pdf_reader.py +17 -19
  94. agno/knowledge/reader/pptx_reader.py +101 -0
  95. agno/knowledge/reader/reader_factory.py +32 -3
  96. agno/knowledge/reader/s3_reader.py +3 -3
  97. agno/knowledge/reader/tavily_reader.py +193 -0
  98. agno/knowledge/reader/text_reader.py +22 -10
  99. agno/knowledge/reader/web_search_reader.py +1 -48
  100. agno/knowledge/reader/website_reader.py +10 -10
  101. agno/knowledge/reader/wikipedia_reader.py +33 -1
  102. agno/knowledge/types.py +1 -0
  103. agno/knowledge/utils.py +72 -7
  104. agno/media.py +22 -6
  105. agno/memory/__init__.py +14 -1
  106. agno/memory/manager.py +544 -83
  107. agno/memory/strategies/__init__.py +15 -0
  108. agno/memory/strategies/base.py +66 -0
  109. agno/memory/strategies/summarize.py +196 -0
  110. agno/memory/strategies/types.py +37 -0
  111. agno/models/aimlapi/aimlapi.py +17 -0
  112. agno/models/anthropic/claude.py +515 -40
  113. agno/models/aws/bedrock.py +102 -21
  114. agno/models/aws/claude.py +131 -274
  115. agno/models/azure/ai_foundry.py +41 -19
  116. agno/models/azure/openai_chat.py +39 -8
  117. agno/models/base.py +1249 -525
  118. agno/models/cerebras/cerebras.py +91 -21
  119. agno/models/cerebras/cerebras_openai.py +21 -2
  120. agno/models/cohere/chat.py +40 -6
  121. agno/models/cometapi/cometapi.py +18 -1
  122. agno/models/dashscope/dashscope.py +2 -3
  123. agno/models/deepinfra/deepinfra.py +18 -1
  124. agno/models/deepseek/deepseek.py +69 -3
  125. agno/models/fireworks/fireworks.py +18 -1
  126. agno/models/google/gemini.py +877 -80
  127. agno/models/google/utils.py +22 -0
  128. agno/models/groq/groq.py +51 -18
  129. agno/models/huggingface/huggingface.py +17 -6
  130. agno/models/ibm/watsonx.py +16 -6
  131. agno/models/internlm/internlm.py +18 -1
  132. agno/models/langdb/langdb.py +13 -1
  133. agno/models/litellm/chat.py +44 -9
  134. agno/models/litellm/litellm_openai.py +18 -1
  135. agno/models/message.py +28 -5
  136. agno/models/meta/llama.py +47 -14
  137. agno/models/meta/llama_openai.py +22 -17
  138. agno/models/mistral/mistral.py +8 -4
  139. agno/models/nebius/nebius.py +6 -7
  140. agno/models/nvidia/nvidia.py +20 -3
  141. agno/models/ollama/chat.py +24 -8
  142. agno/models/openai/chat.py +104 -29
  143. agno/models/openai/responses.py +101 -81
  144. agno/models/openrouter/openrouter.py +60 -3
  145. agno/models/perplexity/perplexity.py +17 -1
  146. agno/models/portkey/portkey.py +7 -6
  147. agno/models/requesty/requesty.py +24 -4
  148. agno/models/response.py +73 -2
  149. agno/models/sambanova/sambanova.py +20 -3
  150. agno/models/siliconflow/siliconflow.py +19 -2
  151. agno/models/together/together.py +20 -3
  152. agno/models/utils.py +254 -8
  153. agno/models/vercel/v0.py +20 -3
  154. agno/models/vertexai/__init__.py +0 -0
  155. agno/models/vertexai/claude.py +190 -0
  156. agno/models/vllm/vllm.py +19 -14
  157. agno/models/xai/xai.py +19 -2
  158. agno/os/app.py +549 -152
  159. agno/os/auth.py +190 -3
  160. agno/os/config.py +23 -0
  161. agno/os/interfaces/a2a/router.py +8 -11
  162. agno/os/interfaces/a2a/utils.py +1 -1
  163. agno/os/interfaces/agui/router.py +18 -3
  164. agno/os/interfaces/agui/utils.py +152 -39
  165. agno/os/interfaces/slack/router.py +55 -37
  166. agno/os/interfaces/slack/slack.py +9 -1
  167. agno/os/interfaces/whatsapp/router.py +0 -1
  168. agno/os/interfaces/whatsapp/security.py +3 -1
  169. agno/os/mcp.py +110 -52
  170. agno/os/middleware/__init__.py +2 -0
  171. agno/os/middleware/jwt.py +676 -112
  172. agno/os/router.py +40 -1478
  173. agno/os/routers/agents/__init__.py +3 -0
  174. agno/os/routers/agents/router.py +599 -0
  175. agno/os/routers/agents/schema.py +261 -0
  176. agno/os/routers/evals/evals.py +96 -39
  177. agno/os/routers/evals/schemas.py +65 -33
  178. agno/os/routers/evals/utils.py +80 -10
  179. agno/os/routers/health.py +10 -4
  180. agno/os/routers/knowledge/knowledge.py +196 -38
  181. agno/os/routers/knowledge/schemas.py +82 -22
  182. agno/os/routers/memory/memory.py +279 -52
  183. agno/os/routers/memory/schemas.py +46 -17
  184. agno/os/routers/metrics/metrics.py +20 -8
  185. agno/os/routers/metrics/schemas.py +16 -16
  186. agno/os/routers/session/session.py +462 -34
  187. agno/os/routers/teams/__init__.py +3 -0
  188. agno/os/routers/teams/router.py +512 -0
  189. agno/os/routers/teams/schema.py +257 -0
  190. agno/os/routers/traces/__init__.py +3 -0
  191. agno/os/routers/traces/schemas.py +414 -0
  192. agno/os/routers/traces/traces.py +499 -0
  193. agno/os/routers/workflows/__init__.py +3 -0
  194. agno/os/routers/workflows/router.py +624 -0
  195. agno/os/routers/workflows/schema.py +75 -0
  196. agno/os/schema.py +256 -693
  197. agno/os/scopes.py +469 -0
  198. agno/os/utils.py +514 -36
  199. agno/reasoning/anthropic.py +80 -0
  200. agno/reasoning/gemini.py +73 -0
  201. agno/reasoning/openai.py +5 -0
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +155 -32
  205. agno/run/base.py +55 -3
  206. agno/run/requirement.py +181 -0
  207. agno/run/team.py +125 -38
  208. agno/run/workflow.py +72 -18
  209. agno/session/agent.py +102 -89
  210. agno/session/summary.py +56 -15
  211. agno/session/team.py +164 -90
  212. agno/session/workflow.py +405 -40
  213. agno/table.py +10 -0
  214. agno/team/team.py +3974 -1903
  215. agno/tools/dalle.py +2 -4
  216. agno/tools/eleven_labs.py +23 -25
  217. agno/tools/exa.py +21 -16
  218. agno/tools/file.py +153 -23
  219. agno/tools/file_generation.py +16 -10
  220. agno/tools/firecrawl.py +15 -7
  221. agno/tools/function.py +193 -38
  222. agno/tools/gmail.py +238 -14
  223. agno/tools/google_drive.py +271 -0
  224. agno/tools/googlecalendar.py +36 -8
  225. agno/tools/googlesheets.py +20 -5
  226. agno/tools/jira.py +20 -0
  227. agno/tools/mcp/__init__.py +10 -0
  228. agno/tools/mcp/mcp.py +331 -0
  229. agno/tools/mcp/multi_mcp.py +347 -0
  230. agno/tools/mcp/params.py +24 -0
  231. agno/tools/mcp_toolbox.py +3 -3
  232. agno/tools/models/nebius.py +5 -5
  233. agno/tools/models_labs.py +20 -10
  234. agno/tools/nano_banana.py +151 -0
  235. agno/tools/notion.py +204 -0
  236. agno/tools/parallel.py +314 -0
  237. agno/tools/postgres.py +76 -36
  238. agno/tools/redshift.py +406 -0
  239. agno/tools/scrapegraph.py +1 -1
  240. agno/tools/shopify.py +1519 -0
  241. agno/tools/slack.py +18 -3
  242. agno/tools/spotify.py +919 -0
  243. agno/tools/tavily.py +146 -0
  244. agno/tools/toolkit.py +25 -0
  245. agno/tools/workflow.py +8 -1
  246. agno/tools/yfinance.py +12 -11
  247. agno/tracing/__init__.py +12 -0
  248. agno/tracing/exporter.py +157 -0
  249. agno/tracing/schemas.py +276 -0
  250. agno/tracing/setup.py +111 -0
  251. agno/utils/agent.py +938 -0
  252. agno/utils/cryptography.py +22 -0
  253. agno/utils/dttm.py +33 -0
  254. agno/utils/events.py +151 -3
  255. agno/utils/gemini.py +15 -5
  256. agno/utils/hooks.py +118 -4
  257. agno/utils/http.py +113 -2
  258. agno/utils/knowledge.py +12 -5
  259. agno/utils/log.py +1 -0
  260. agno/utils/mcp.py +92 -2
  261. agno/utils/media.py +187 -1
  262. agno/utils/merge_dict.py +3 -3
  263. agno/utils/message.py +60 -0
  264. agno/utils/models/ai_foundry.py +9 -2
  265. agno/utils/models/claude.py +49 -14
  266. agno/utils/models/cohere.py +9 -2
  267. agno/utils/models/llama.py +9 -2
  268. agno/utils/models/mistral.py +4 -2
  269. agno/utils/print_response/agent.py +109 -16
  270. agno/utils/print_response/team.py +223 -30
  271. agno/utils/print_response/workflow.py +251 -34
  272. agno/utils/streamlit.py +1 -1
  273. agno/utils/team.py +98 -9
  274. agno/utils/tokens.py +657 -0
  275. agno/vectordb/base.py +39 -7
  276. agno/vectordb/cassandra/cassandra.py +21 -5
  277. agno/vectordb/chroma/chromadb.py +43 -12
  278. agno/vectordb/clickhouse/clickhousedb.py +21 -5
  279. agno/vectordb/couchbase/couchbase.py +29 -5
  280. agno/vectordb/lancedb/lance_db.py +92 -181
  281. agno/vectordb/langchaindb/langchaindb.py +24 -4
  282. agno/vectordb/lightrag/lightrag.py +17 -3
  283. agno/vectordb/llamaindex/llamaindexdb.py +25 -5
  284. agno/vectordb/milvus/milvus.py +50 -37
  285. agno/vectordb/mongodb/__init__.py +7 -1
  286. agno/vectordb/mongodb/mongodb.py +36 -30
  287. agno/vectordb/pgvector/pgvector.py +201 -77
  288. agno/vectordb/pineconedb/pineconedb.py +41 -23
  289. agno/vectordb/qdrant/qdrant.py +67 -54
  290. agno/vectordb/redis/__init__.py +9 -0
  291. agno/vectordb/redis/redisdb.py +682 -0
  292. agno/vectordb/singlestore/singlestore.py +50 -29
  293. agno/vectordb/surrealdb/surrealdb.py +31 -41
  294. agno/vectordb/upstashdb/upstashdb.py +34 -6
  295. agno/vectordb/weaviate/weaviate.py +53 -14
  296. agno/workflow/__init__.py +2 -0
  297. agno/workflow/agent.py +299 -0
  298. agno/workflow/condition.py +120 -18
  299. agno/workflow/loop.py +77 -10
  300. agno/workflow/parallel.py +231 -143
  301. agno/workflow/router.py +118 -17
  302. agno/workflow/step.py +609 -170
  303. agno/workflow/steps.py +73 -6
  304. agno/workflow/types.py +96 -21
  305. agno/workflow/workflow.py +2039 -262
  306. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
  307. agno-2.3.13.dist-info/RECORD +613 -0
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -679
  310. agno/tools/memori.py +0 -339
  311. agno-2.1.2.dist-info/RECORD +0 -543
  312. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
  313. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
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
 
agno/utils/mcp.py CHANGED
@@ -27,9 +27,13 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
27
27
  Returns:
28
28
  Callable: The entrypoint function for the tool
29
29
  """
30
- from agno.agent import Agent
31
30
 
32
- async def call_tool(agent: Agent, tool_name: str, **kwargs) -> ToolResult:
31
+ async def call_tool(tool_name: str, **kwargs) -> ToolResult:
32
+ try:
33
+ await session.send_ping()
34
+ except Exception as e:
35
+ log_exception(e)
36
+
33
37
  try:
34
38
  log_debug(f"Calling MCP Tool '{tool_name}' with args: {kwargs}")
35
39
  result: CallToolResult = await session.call_tool(tool_name, kwargs) # type: ignore
@@ -122,3 +126,89 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
122
126
  return ToolResult(content=f"Error: {e}")
123
127
 
124
128
  return partial(call_tool, tool_name=tool.name)
129
+
130
+
131
+ def prepare_command(command: str) -> list[str]:
132
+ """Sanitize a command and split it into parts before using it to run a MCP server."""
133
+ import os
134
+ import shutil
135
+ from shlex import split
136
+
137
+ # Block dangerous characters
138
+ if any(char in command for char in ["&", "|", ";", "`", "$", "(", ")"]):
139
+ raise ValueError("MCP command can't contain shell metacharacters")
140
+
141
+ parts = split(command)
142
+ if not parts:
143
+ raise ValueError("MCP command can't be empty")
144
+
145
+ # Only allow specific executables
146
+ ALLOWED_COMMANDS = {
147
+ # Python
148
+ "python",
149
+ "python3",
150
+ "uv",
151
+ "uvx",
152
+ "pipx",
153
+ # Node
154
+ "node",
155
+ "npm",
156
+ "npx",
157
+ "yarn",
158
+ "pnpm",
159
+ "bun",
160
+ # Other runtimes
161
+ "deno",
162
+ "java",
163
+ "ruby",
164
+ "docker",
165
+ }
166
+
167
+ executable = parts[0].split("/")[-1]
168
+
169
+ # Check if it's a relative path starting with ./ or ../
170
+ if executable.startswith("./") or executable.startswith("../"):
171
+ # Allow relative paths to binaries
172
+ return parts
173
+
174
+ # Check if it's an absolute path to a binary
175
+ if executable.startswith("/") and os.path.isfile(executable):
176
+ # Allow absolute paths to existing files
177
+ return parts
178
+
179
+ # Check if it's a binary in current directory without ./
180
+ if "/" not in executable and os.path.isfile(executable):
181
+ # Allow binaries in current directory
182
+ return parts
183
+
184
+ # Check if it's a binary in PATH
185
+ if shutil.which(executable):
186
+ return parts
187
+
188
+ if executable not in ALLOWED_COMMANDS:
189
+ raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
190
+
191
+ first_part = parts[0]
192
+ executable = first_part.split("/")[-1]
193
+
194
+ # Allow known commands
195
+ if executable in ALLOWED_COMMANDS:
196
+ return parts
197
+
198
+ # Allow relative paths to custom binaries
199
+ if first_part.startswith(("./", "../")):
200
+ return parts
201
+
202
+ # Allow absolute paths to existing files
203
+ if first_part.startswith("/") and os.path.isfile(first_part):
204
+ return parts
205
+
206
+ # Allow binaries in current directory without ./
207
+ if "/" not in first_part and os.path.isfile(first_part):
208
+ return parts
209
+
210
+ # Allow binaries in PATH
211
+ if shutil.which(first_part):
212
+ return parts
213
+
214
+ raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
agno/utils/media.py CHANGED
@@ -2,10 +2,11 @@ import base64
2
2
  import time
3
3
  from enum import Enum
4
4
  from pathlib import Path
5
- from typing import List
5
+ from typing import List, Optional
6
6
 
7
7
  import httpx
8
8
 
9
+ from agno.media import Audio, File, Image, Video
9
10
  from agno.utils.log import log_info, log_warning
10
11
 
11
12
 
@@ -55,6 +56,17 @@ def download_image(url: str, output_path: str) -> bool:
55
56
  return False
56
57
 
57
58
 
59
+ def download_audio(url: str, output_path: str) -> str:
60
+ """Download audio from URL"""
61
+ response = httpx.get(url)
62
+ response.raise_for_status()
63
+
64
+ with open(output_path, "wb") as f:
65
+ for chunk in response.iter_bytes(chunk_size=8192):
66
+ f.write(chunk)
67
+ return output_path
68
+
69
+
58
70
  def download_video(url: str, output_path: str) -> str:
59
71
  """Download video from URL"""
60
72
  response = httpx.get(url)
@@ -185,3 +197,177 @@ def download_knowledge_filters_sample_data(
185
197
  )
186
198
  file_paths.append(str(download_path))
187
199
  return file_paths
200
+
201
+
202
+ def reconstruct_image_from_dict(img_data):
203
+ """
204
+ Reconstruct an Image object from dictionary data.
205
+
206
+ Handles both base64-encoded content (from database) and regular image data (url/filepath).
207
+ """
208
+ try:
209
+ if isinstance(img_data, dict):
210
+ # If content is base64 string, decode it back to bytes
211
+ if "content" in img_data and isinstance(img_data["content"], str):
212
+ return Image.from_base64(
213
+ img_data["content"],
214
+ id=img_data.get("id"),
215
+ mime_type=img_data.get("mime_type"),
216
+ format=img_data.get("format"),
217
+ detail=img_data.get("detail"),
218
+ original_prompt=img_data.get("original_prompt"),
219
+ revised_prompt=img_data.get("revised_prompt"),
220
+ alt_text=img_data.get("alt_text"),
221
+ )
222
+ else:
223
+ # Regular image (filepath/url)
224
+ return Image(**img_data)
225
+ return img_data
226
+ except Exception as e:
227
+ log_warning(f"Failed to reconstruct image from dict: {e}")
228
+ return None
229
+
230
+
231
+ def reconstruct_video_from_dict(vid_data):
232
+ """
233
+ Reconstruct a Video object from dictionary data.
234
+
235
+ Handles both base64-encoded content (from database) and regular video data (url/filepath).
236
+ """
237
+ try:
238
+ if isinstance(vid_data, dict):
239
+ # If content is base64 string, decode it back to bytes
240
+ if "content" in vid_data and isinstance(vid_data["content"], str):
241
+ return Video.from_base64(
242
+ vid_data["content"],
243
+ id=vid_data.get("id"),
244
+ mime_type=vid_data.get("mime_type"),
245
+ format=vid_data.get("format"),
246
+ )
247
+ else:
248
+ # Regular video (filepath/url)
249
+ return Video(**vid_data)
250
+ return vid_data
251
+ except Exception as e:
252
+ log_warning(f"Failed to reconstruct video from dict: {e}")
253
+ return None
254
+
255
+
256
+ def reconstruct_audio_from_dict(aud_data):
257
+ """
258
+ Reconstruct an Audio object from dictionary data.
259
+
260
+ Handles both base64-encoded content (from database) and regular audio data (url/filepath).
261
+ """
262
+ try:
263
+ if isinstance(aud_data, dict):
264
+ # If content is base64 string, decode it back to bytes
265
+ if "content" in aud_data and isinstance(aud_data["content"], str):
266
+ return Audio.from_base64(
267
+ aud_data["content"],
268
+ id=aud_data.get("id"),
269
+ mime_type=aud_data.get("mime_type"),
270
+ transcript=aud_data.get("transcript"),
271
+ expires_at=aud_data.get("expires_at"),
272
+ sample_rate=aud_data.get("sample_rate", 24000),
273
+ channels=aud_data.get("channels", 1),
274
+ )
275
+ else:
276
+ # Regular audio (filepath/url)
277
+ return Audio(**aud_data)
278
+ return aud_data
279
+ except Exception as e:
280
+ log_warning(f"Failed to reconstruct audio from dict: {e}")
281
+ return None
282
+
283
+
284
+ def reconstruct_file_from_dict(file_data):
285
+ """
286
+ Reconstruct a File object from dictionary data.
287
+
288
+ Handles both base64-encoded content (from database) and regular file data (url/filepath).
289
+ """
290
+ try:
291
+ if isinstance(file_data, dict):
292
+ # If content is base64 string, decode it back to bytes
293
+ if "content" in file_data and isinstance(file_data["content"], str):
294
+ file_obj = File.from_base64(
295
+ file_data["content"],
296
+ id=file_data.get("id"),
297
+ mime_type=file_data.get("mime_type"),
298
+ filename=file_data.get("filename"),
299
+ name=file_data.get("name"),
300
+ format=file_data.get("format"),
301
+ )
302
+ # Preserve additional fields that from_base64 doesn't handle
303
+ if file_data.get("size") is not None:
304
+ file_obj.size = file_data.get("size")
305
+ if file_data.get("file_type") is not None:
306
+ file_obj.file_type = file_data.get("file_type")
307
+ if file_data.get("filepath") is not None:
308
+ file_obj.filepath = file_data.get("filepath")
309
+ if file_data.get("url") is not None:
310
+ file_obj.url = file_data.get("url")
311
+ return file_obj
312
+ else:
313
+ # Regular file (filepath/url)
314
+ return File(**file_data)
315
+ return file_data
316
+ except Exception as e:
317
+ log_warning(f"Failed to reconstruct file from dict: {e}")
318
+ return None
319
+
320
+
321
+ def reconstruct_images(images: Optional[List[dict]]) -> Optional[List[Image]]:
322
+ """Reconstruct a list of Image objects from list of dictionaries.
323
+
324
+ Failed reconstructions are skipped with a warning logged.
325
+ """
326
+ if not images:
327
+ return None
328
+ reconstructed = [reconstruct_image_from_dict(img_data) for img_data in images]
329
+ valid_images = [img for img in reconstructed if img is not None]
330
+ return valid_images if valid_images else None
331
+
332
+
333
+ def reconstruct_videos(videos: Optional[List[dict]]) -> Optional[List[Video]]:
334
+ """Reconstruct a list of Video objects from list of dictionaries.
335
+
336
+ Failed reconstructions are skipped with a warning logged.
337
+ """
338
+ if not videos:
339
+ return None
340
+ reconstructed = [reconstruct_video_from_dict(vid_data) for vid_data in videos]
341
+ valid_videos = [vid for vid in reconstructed if vid is not None]
342
+ return valid_videos if valid_videos else None
343
+
344
+
345
+ def reconstruct_audio_list(audio: Optional[List[dict]]) -> Optional[List[Audio]]:
346
+ """Reconstruct a list of Audio objects from list of dictionaries.
347
+
348
+ Failed reconstructions are skipped with a warning logged.
349
+ """
350
+ if not audio:
351
+ return None
352
+ reconstructed = [reconstruct_audio_from_dict(aud_data) for aud_data in audio]
353
+ valid_audio = [aud for aud in reconstructed if aud is not None]
354
+ return valid_audio if valid_audio else None
355
+
356
+
357
+ def reconstruct_files(files: Optional[List[dict]]) -> Optional[List[File]]:
358
+ """Reconstruct a list of File objects from list of dictionaries.
359
+
360
+ Failed reconstructions are skipped with a warning logged.
361
+ """
362
+ if not files:
363
+ return None
364
+ reconstructed = [reconstruct_file_from_dict(file_data) for file_data in files]
365
+ valid_files = [f for f in reconstructed if f is not None]
366
+ return valid_files if valid_files else None
367
+
368
+
369
+ def reconstruct_response_audio(audio: Optional[dict]) -> Optional[Audio]:
370
+ """Reconstruct a single Audio object for response audio."""
371
+ if not audio:
372
+ return None
373
+ return reconstruct_audio_from_dict(audio)
agno/utils/merge_dict.py CHANGED
@@ -27,7 +27,7 @@ def merge_parallel_session_states(original_state: Dict[str, Any], modified_state
27
27
  """
28
28
  if not original_state or not modified_states:
29
29
  return
30
-
30
+
31
31
  # Collect all actual changes (keys where value differs from original)
32
32
  all_changes = {}
33
33
  for modified_state in modified_states:
@@ -35,7 +35,7 @@ def merge_parallel_session_states(original_state: Dict[str, Any], modified_state
35
35
  for key, value in modified_state.items():
36
36
  if key not in original_state or original_state[key] != value:
37
37
  all_changes[key] = value
38
-
38
+
39
39
  # Apply all collected changes to the original state
40
40
  for key, value in all_changes.items():
41
- original_state[key] = value
41
+ original_state[key] = value
agno/utils/message.py CHANGED
@@ -1,8 +1,68 @@
1
+ from copy import deepcopy
1
2
  from typing import Dict, List, Union
2
3
 
3
4
  from pydantic import BaseModel
4
5
 
5
6
  from agno.models.message import Message
7
+ from agno.utils.log import log_debug
8
+
9
+
10
+ def filter_tool_calls(messages: List[Message], max_tool_calls: int) -> None:
11
+ """
12
+ Filter messages (in-place) to keep only the most recent N tool calls.
13
+
14
+ Args:
15
+ messages: List of messages to filter (modified in-place)
16
+ max_tool_calls: Number of recent tool calls to keep
17
+ """
18
+ # Count total tool calls
19
+ tool_call_count = sum(1 for m in messages if m.role == "tool")
20
+
21
+ # No filtering needed
22
+ if tool_call_count <= max_tool_calls:
23
+ return
24
+
25
+ # Collect tool_call_ids to keep (most recent N)
26
+ tool_call_ids_list: List[str] = []
27
+ for msg in reversed(messages):
28
+ if msg.role == "tool" and len(tool_call_ids_list) < max_tool_calls:
29
+ if msg.tool_call_id:
30
+ tool_call_ids_list.append(msg.tool_call_id)
31
+
32
+ tool_call_ids_to_keep: set[str] = set(tool_call_ids_list)
33
+
34
+ # Filter messages in-place
35
+ filtered_messages = []
36
+ for msg in messages:
37
+ if msg.role == "tool":
38
+ # Keep only tool results in our window
39
+ if msg.tool_call_id in tool_call_ids_to_keep:
40
+ filtered_messages.append(msg)
41
+ elif msg.role == "assistant" and msg.tool_calls:
42
+ # Filter tool_calls within the assistant message
43
+ # Use deepcopy to ensure complete isolation of the filtered message
44
+ filtered_msg = deepcopy(msg)
45
+ # Filter tool_calls
46
+ if filtered_msg.tool_calls is not None:
47
+ filtered_msg.tool_calls = [
48
+ tc for tc in filtered_msg.tool_calls if tc.get("id") in tool_call_ids_to_keep
49
+ ]
50
+
51
+ if filtered_msg.tool_calls:
52
+ # Has tool_calls remaining, keep it
53
+ filtered_messages.append(filtered_msg)
54
+ # skip empty messages
55
+ elif filtered_msg.content:
56
+ filtered_msg.tool_calls = None
57
+ filtered_messages.append(filtered_msg)
58
+ else:
59
+ filtered_messages.append(msg)
60
+
61
+ messages[:] = filtered_messages
62
+
63
+ # Log filtering information
64
+ num_filtered = tool_call_count - len(tool_call_ids_to_keep)
65
+ log_debug(f"Filtered {num_filtered} tool calls, kept {len(tool_call_ids_to_keep)}")
6
66
 
7
67
 
8
68
  def get_text_from_message(message: Union[List, Dict, str, Message, BaseModel]) -> str:
@@ -5,19 +5,26 @@ from agno.utils.log import log_warning
5
5
  from agno.utils.openai import images_to_message
6
6
 
7
7
 
8
- def format_message(message: Message) -> Dict[str, Any]:
8
+ def format_message(message: Message, compress_tool_results: bool = False) -> Dict[str, Any]:
9
9
  """
10
10
  Format a message into the format expected by OpenAI.
11
11
 
12
12
  Args:
13
13
  message (Message): The message to format.
14
+ compress_tool_results: Whether to compress tool results.
14
15
 
15
16
  Returns:
16
17
  Dict[str, Any]: The formatted message.
17
18
  """
19
+ # Use compressed content for tool messages if compression is active
20
+ content = message.content
21
+
22
+ if message.role == "tool":
23
+ content = message.get_content(use_compressed_content=compress_tool_results)
24
+
18
25
  message_dict: Dict[str, Any] = {
19
26
  "role": message.role,
20
- "content": message.content,
27
+ "content": content,
21
28
  "name": message.name,
22
29
  "tool_call_id": message.tool_call_id,
23
30
  "tool_calls": message.tool_calls,
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from dataclasses import dataclass, field
3
- from typing import Any, Dict, List, Optional, Tuple
3
+ from typing import Any, Dict, List, Optional, Tuple, Union
4
4
 
5
5
  from agno.media import File, Image
6
6
  from agno.models.message import Message
@@ -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",
@@ -67,6 +68,8 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
67
68
  }
68
69
 
69
70
  try:
71
+ img_type = None
72
+
70
73
  # Case 0: Image is an Anthropic uploaded file
71
74
  if image.content is not None and hasattr(image.content, "id"):
72
75
  content_bytes = image.content
@@ -75,6 +78,16 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
75
78
  if image.url is not None:
76
79
  content_bytes = image.get_content_bytes() # type: ignore
77
80
 
81
+ # If image URL has a suffix, use it as the type (without dot)
82
+ import os
83
+ from urllib.parse import urlparse
84
+
85
+ if image.url:
86
+ parsed_url = urlparse(image.url)
87
+ _, ext = os.path.splitext(parsed_url.path)
88
+ if ext:
89
+ img_type = ext.lstrip(".").lower()
90
+
78
91
  # Case 2: Image is a local file path
79
92
  elif image.filepath is not None:
80
93
  from pathlib import Path
@@ -83,6 +96,11 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
83
96
  if path.exists() and path.is_file():
84
97
  with open(image.filepath, "rb") as f:
85
98
  content_bytes = f.read()
99
+
100
+ # If image file path has a suffix, use it as the type (without dot)
101
+ path_ext = path.suffix.lstrip(".")
102
+ if path_ext:
103
+ img_type = path_ext.lower()
86
104
  else:
87
105
  log_error(f"Image file not found: {image}")
88
106
  return None
@@ -95,15 +113,16 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
95
113
  log_error(f"Unsupported image type: {type(image)}")
96
114
  return None
97
115
 
98
- if using_filetype:
99
- kind = filetype.guess(content_bytes)
100
- if not kind:
101
- log_error("Unable to determine image type")
102
- return None
116
+ if not img_type:
117
+ if using_filetype:
118
+ kind = filetype.guess(content_bytes)
119
+ if not kind:
120
+ log_error("Unable to determine image type")
121
+ return None
103
122
 
104
- img_type = kind.extension
105
- else:
106
- img_type = imghdr.what(None, h=content_bytes) # type: ignore
123
+ img_type = kind.extension
124
+ else:
125
+ img_type = imghdr.what(None, h=content_bytes) # type: ignore
107
126
 
108
127
  if not img_type:
109
128
  log_error("Unable to determine image type")
@@ -202,22 +221,26 @@ def _format_file_for_message(file: File) -> Optional[Dict[str, Any]]:
202
221
  return None
203
222
 
204
223
 
205
- def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]:
224
+ def format_messages(
225
+ messages: List[Message], compress_tool_results: bool = False
226
+ ) -> Tuple[List[Dict[str, Union[str, list]]], str]:
206
227
  """
207
228
  Process the list of messages and separate them into API messages and system messages.
208
229
 
209
230
  Args:
210
231
  messages (List[Message]): The list of messages to process.
232
+ compress_tool_results: Whether to compress tool results.
211
233
 
212
234
  Returns:
213
- Tuple[List[Dict[str, str]], str]: A tuple containing the list of API messages and the concatenated system messages.
235
+ Tuple[List[Dict[str, Union[str, list]]], str]: A tuple containing the list of API messages and the concatenated system messages.
214
236
  """
215
- chat_messages: List[Dict[str, str]] = []
237
+ chat_messages: List[Dict[str, Union[str, list]]] = []
216
238
  system_messages: List[str] = []
217
239
 
218
240
  for message in messages:
219
241
  content = message.content or ""
220
- if message.role == "system":
242
+ # Both "system" and "developer" roles should be extracted as system messages
243
+ if message.role in ("system", "developer"):
221
244
  if content is not None:
222
245
  system_messages.append(content) # type: ignore
223
246
  continue
@@ -281,11 +304,15 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
281
304
  )
282
305
  elif message.role == "tool":
283
306
  content = []
307
+
308
+ # Use compressed content for tool messages if compression is active
309
+ tool_result = message.get_content(use_compressed_content=compress_tool_results)
310
+
284
311
  content.append(
285
312
  {
286
313
  "type": "tool_result",
287
314
  "tool_use_id": message.tool_call_id,
288
- "content": str(message.content),
315
+ "content": str(tool_result),
289
316
  }
290
317
  )
291
318
 
@@ -300,6 +327,7 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
300
327
  def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Optional[List[Dict[str, Any]]]:
301
328
  """
302
329
  Transforms function definitions into a format accepted by the Anthropic API.
330
+ Now supports strict mode for structured outputs.
303
331
  """
304
332
  if not tools:
305
333
  return None
@@ -332,7 +360,14 @@ def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Opti
332
360
  "type": parameters.get("type", "object"),
333
361
  "properties": input_properties,
334
362
  "required": required_params,
363
+ "additionalProperties": False,
335
364
  },
336
365
  }
366
+
367
+ # Add strict mode if specified (check both function dict and tool_def top level)
368
+ strict_mode = func_def.get("strict") or tool_def.get("strict")
369
+ if strict_mode is True:
370
+ tool["strict"] = True
371
+
337
372
  parsed_tools.append(tool)
338
373
  return parsed_tools
@@ -46,21 +46,28 @@ def _format_images_for_message(message: Message, images: Sequence[Image]) -> Lis
46
46
  return message_content_with_image
47
47
 
48
48
 
49
- def format_messages(messages: List[Message]) -> List[Dict[str, Any]]:
49
+ def format_messages(messages: List[Message], compress_tool_results: bool = False) -> List[Dict[str, Any]]:
50
50
  """
51
51
  Format messages for the Cohere API.
52
52
 
53
53
  Args:
54
54
  messages (List[Message]): The list of messages.
55
+ compress_tool_results: Whether to compress tool results.
55
56
 
56
57
  Returns:
57
58
  List[Dict[str, Any]]: The formatted messages.
58
59
  """
59
60
  formatted_messages = []
60
61
  for message in messages:
62
+ # Use compressed content for tool messages if compression is active
63
+ content = message.content
64
+
65
+ if message.role == "tool":
66
+ content = message.get_content(use_compressed_content=compress_tool_results)
67
+
61
68
  message_dict = {
62
69
  "role": message.role,
63
- "content": message.content,
70
+ "content": content,
64
71
  "name": message.name,
65
72
  "tool_call_id": message.tool_call_id,
66
73
  "tool_calls": message.tool_calls,