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/mcp.py CHANGED
@@ -12,7 +12,7 @@ except (ImportError, ModuleNotFoundError):
12
12
  raise ImportError("`mcp` not installed. Please install using `pip install mcp`")
13
13
 
14
14
 
15
- from agno.media import ImageArtifact
15
+ from agno.media import Image
16
16
  from agno.tools.function import ToolResult
17
17
 
18
18
 
@@ -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
+ print(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
@@ -70,7 +74,7 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
70
74
  image_bytes = None
71
75
 
72
76
  if image_bytes:
73
- img_artifact = ImageArtifact(
77
+ img_artifact = Image(
74
78
  id=str(uuid4()),
75
79
  url=None,
76
80
  content=image_bytes,
@@ -98,7 +102,7 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
98
102
  log_debug(f"Failed to decode base64 image data: {e}")
99
103
  image_data = None
100
104
 
101
- img_artifact = ImageArtifact(
105
+ img_artifact = Image(
102
106
  id=str(uuid4()),
103
107
  url=getattr(content_item, "url", None),
104
108
  content=image_data,
@@ -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,13 @@ 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
10
+ from agno.utils.log import log_info, log_warning
11
+
9
12
 
10
13
  class SampleDataFileExtension(str, Enum):
11
14
  DOCX = "docx"
@@ -30,7 +33,7 @@ def download_image(url: str, output_path: str) -> bool:
30
33
  # Check if the response contains image content
31
34
  content_type = response.headers.get("Content-Type")
32
35
  if not content_type or not content_type.startswith("image"):
33
- print(f"URL does not point to an image. Content-Type: {content_type}")
36
+ log_warning(f"URL does not point to an image. Content-Type: {content_type}")
34
37
  return False
35
38
 
36
39
  path = Path(output_path)
@@ -42,17 +45,28 @@ def download_image(url: str, output_path: str) -> bool:
42
45
  if chunk:
43
46
  file.write(chunk)
44
47
 
45
- print(f"Image successfully downloaded and saved to '{output_path}'.")
48
+ log_info(f"Image successfully downloaded and saved to '{output_path}'.")
46
49
  return True
47
50
 
48
51
  except httpx.HTTPError as e:
49
- print(f"Error downloading the image: {e}")
52
+ log_warning(f"Error downloading the image: {e}")
50
53
  return False
51
54
  except IOError as e:
52
- print(f"Error saving the image to '{output_path}': {e}")
55
+ log_warning(f"Error saving the image to '{output_path}': {e}")
53
56
  return False
54
57
 
55
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
+
56
70
  def download_video(url: str, output_path: str) -> str:
57
71
  """Download video from URL"""
58
72
  response = httpx.get(url)
@@ -109,7 +123,7 @@ def save_base64_data(base64_data: str, output_path: str) -> bool:
109
123
  with open(path, "wb") as file:
110
124
  file.write(decoded_data)
111
125
 
112
- print(f"Data successfully saved to '{path}'.")
126
+ log_info(f"Data successfully saved to '{path}'.")
113
127
  return True
114
128
  except Exception as e:
115
129
  raise Exception(f"An unexpected error occurred while saving data to '{output_path}': {e}")
@@ -131,25 +145,25 @@ def wait_for_media_ready(url: str, timeout: int = 120, interval: int = 5, verbos
131
145
  max_attempts = timeout // interval
132
146
 
133
147
  if verbose:
134
- print("Media generated! Waiting for upload to complete...")
148
+ log_info("Media generated! Waiting for upload to complete...")
135
149
 
136
150
  for attempt in range(max_attempts):
137
151
  try:
138
152
  response = httpx.head(url, timeout=10)
139
153
  response.raise_for_status()
140
154
  if verbose:
141
- print(f"Media ready: {url}")
155
+ log_info(f"Media ready: {url}")
142
156
  return True
143
157
  except httpx.HTTPError:
144
158
  pass
145
159
 
146
160
  if verbose and (attempt + 1) % 3 == 0:
147
- print(f"Still processing... ({(attempt + 1) * interval}s elapsed)")
161
+ log_info(f"Still processing... ({(attempt + 1) * interval}s elapsed)")
148
162
 
149
163
  time.sleep(interval)
150
164
 
151
165
  if verbose:
152
- print(f"Timeout waiting for media. Try this URL later: {url}")
166
+ log_warning(f"Timeout waiting for media. Try this URL later: {url}")
153
167
  return False
154
168
 
155
169
 
@@ -183,3 +197,167 @@ def download_knowledge_filters_sample_data(
183
197
  )
184
198
  file_paths.append(str(download_path))
185
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
+ return 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
+ else:
303
+ # Regular file (filepath/url)
304
+ return File(**file_data)
305
+ return file_data
306
+ except Exception as e:
307
+ log_warning(f"Failed to reconstruct file from dict: {e}")
308
+ return None
309
+
310
+
311
+ def reconstruct_images(images: Optional[List[dict]]) -> Optional[List[Image]]:
312
+ """Reconstruct a list of Image objects from list of dictionaries.
313
+
314
+ Failed reconstructions are skipped with a warning logged.
315
+ """
316
+ if not images:
317
+ return None
318
+ reconstructed = [reconstruct_image_from_dict(img_data) for img_data in images]
319
+ valid_images = [img for img in reconstructed if img is not None]
320
+ return valid_images if valid_images else None
321
+
322
+
323
+ def reconstruct_videos(videos: Optional[List[dict]]) -> Optional[List[Video]]:
324
+ """Reconstruct a list of Video objects from list of dictionaries.
325
+
326
+ Failed reconstructions are skipped with a warning logged.
327
+ """
328
+ if not videos:
329
+ return None
330
+ reconstructed = [reconstruct_video_from_dict(vid_data) for vid_data in videos]
331
+ valid_videos = [vid for vid in reconstructed if vid is not None]
332
+ return valid_videos if valid_videos else None
333
+
334
+
335
+ def reconstruct_audio_list(audio: Optional[List[dict]]) -> Optional[List[Audio]]:
336
+ """Reconstruct a list of Audio objects from list of dictionaries.
337
+
338
+ Failed reconstructions are skipped with a warning logged.
339
+ """
340
+ if not audio:
341
+ return None
342
+ reconstructed = [reconstruct_audio_from_dict(aud_data) for aud_data in audio]
343
+ valid_audio = [aud for aud in reconstructed if aud is not None]
344
+ return valid_audio if valid_audio else None
345
+
346
+
347
+ def reconstruct_files(files: Optional[List[dict]]) -> Optional[List[File]]:
348
+ """Reconstruct a list of File objects from list of dictionaries.
349
+
350
+ Failed reconstructions are skipped with a warning logged.
351
+ """
352
+ if not files:
353
+ return None
354
+ reconstructed = [reconstruct_file_from_dict(file_data) for file_data in files]
355
+ valid_files = [f for f in reconstructed if f is not None]
356
+ return valid_files if valid_files else None
357
+
358
+
359
+ def reconstruct_response_audio(audio: Optional[dict]) -> Optional[Audio]:
360
+ """Reconstruct a single Audio object for response audio."""
361
+ if not audio:
362
+ return None
363
+ return reconstruct_audio_from_dict(audio)
agno/utils/merge_dict.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
1
+ from typing import Any, Dict, List
2
2
 
3
3
 
4
4
  def merge_dictionaries(a: Dict[str, Any], b: Dict[str, Any]) -> None:
@@ -18,3 +18,24 @@ def merge_dictionaries(a: Dict[str, Any], b: Dict[str, Any]) -> None:
18
18
  merge_dictionaries(a[key], b[key])
19
19
  else:
20
20
  a[key] = b[key]
21
+
22
+
23
+ def merge_parallel_session_states(original_state: Dict[str, Any], modified_states: List[Dict[str, Any]]) -> None:
24
+ """
25
+ Smart merge for parallel session states that only applies actual changes.
26
+ This prevents parallel steps from overwriting each other's changes.
27
+ """
28
+ if not original_state or not modified_states:
29
+ return
30
+
31
+ # Collect all actual changes (keys where value differs from original)
32
+ all_changes = {}
33
+ for modified_state in modified_states:
34
+ if modified_state:
35
+ for key, value in modified_state.items():
36
+ if key not in original_state or original_state[key] != value:
37
+ all_changes[key] = value
38
+
39
+ # Apply all collected changes to the original state
40
+ for key, value in all_changes.items():
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:
@@ -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,13 +68,25 @@ 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
- return {"type": "image", "source": {"type": "file", "file_id": image.content.id}}
75
+ content_bytes = image.content
73
76
 
74
77
  # Case 1: Image is a URL
75
78
  if image.url is not None:
76
- return {"type": "image", "source": {"type": "url", "url": image.url}}
79
+ content_bytes = image.get_content_bytes() # type: ignore
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()
77
90
 
78
91
  # Case 2: Image is a local file path
79
92
  elif image.filepath is not None:
@@ -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")
@@ -217,7 +236,8 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
217
236
 
218
237
  for message in messages:
219
238
  content = message.content or ""
220
- if message.role == "system":
239
+ # Both "system" and "developer" roles should be extracted as system messages
240
+ if message.role in ("system", "developer"):
221
241
  if content is not None:
222
242
  system_messages.append(content) # type: ignore
223
243
  continue
@@ -279,6 +299,15 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
279
299
  type="tool_use",
280
300
  )
281
301
  )
302
+ elif message.role == "tool":
303
+ content = []
304
+ content.append(
305
+ {
306
+ "type": "tool_result",
307
+ "tool_use_id": message.tool_call_id,
308
+ "content": str(message.content),
309
+ }
310
+ )
282
311
 
283
312
  # Skip empty assistant responses
284
313
  if message.role == "assistant" and not content:
@@ -20,7 +20,7 @@ def _format_images_for_message(message: Message, images: Sequence[Image]) -> Lis
20
20
  if image.content is not None:
21
21
  image_content = image.content
22
22
  elif image.url is not None:
23
- image_content = image.image_url_content
23
+ image_content = image.get_content_bytes() # type: ignore
24
24
  elif image.filepath is not None:
25
25
  if isinstance(image.filepath, Path):
26
26
  image_content = image.filepath.read_bytes()
@@ -19,7 +19,7 @@ def format_images_for_message(message: Message, images: Sequence[Image]) -> Mess
19
19
  if image.content is not None:
20
20
  image_content = image.content
21
21
  elif image.url is not None:
22
- image_content = image.image_url_content
22
+ image_content = image.get_content_bytes() # type: ignore
23
23
  else:
24
24
  log_warning(f"Unsupported image format: {image}")
25
25
  continue
agno/utils/openai.py CHANGED
@@ -39,7 +39,7 @@ def audio_to_message(audio: Sequence[Audio]) -> List[Dict[str, Any]]:
39
39
 
40
40
  # The audio is a URL
41
41
  elif audio_snippet.url:
42
- audio_bytes = audio_snippet.audio_url_content
42
+ audio_bytes = audio_snippet.get_content_bytes()
43
43
  if audio_bytes is not None:
44
44
  encoded_string = base64.b64encode(audio_bytes).decode("utf-8")
45
45
  if not audio_format: