agno 2.0.1__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. agno/agent/agent.py +6015 -2823
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +594 -186
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +2 -8
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +72 -0
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +999 -519
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +103 -31
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +139 -0
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +59 -5
  142. agno/models/openai/chat.py +69 -29
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +77 -1
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -178
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +248 -94
  205. agno/run/base.py +44 -5
  206. agno/run/team.py +238 -97
  207. agno/run/workflow.py +144 -33
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1610
  213. agno/tools/dalle.py +2 -4
  214. agno/tools/decorator.py +4 -2
  215. agno/tools/duckduckgo.py +15 -11
  216. agno/tools/e2b.py +14 -7
  217. agno/tools/eleven_labs.py +23 -25
  218. agno/tools/exa.py +21 -16
  219. agno/tools/file.py +153 -23
  220. agno/tools/file_generation.py +350 -0
  221. agno/tools/firecrawl.py +4 -4
  222. agno/tools/function.py +250 -30
  223. agno/tools/gmail.py +238 -14
  224. agno/tools/google_drive.py +270 -0
  225. agno/tools/googlecalendar.py +36 -8
  226. agno/tools/googlesheets.py +20 -5
  227. agno/tools/jira.py +20 -0
  228. agno/tools/knowledge.py +3 -3
  229. agno/tools/mcp/__init__.py +10 -0
  230. agno/tools/mcp/mcp.py +331 -0
  231. agno/tools/mcp/multi_mcp.py +347 -0
  232. agno/tools/mcp/params.py +24 -0
  233. agno/tools/mcp_toolbox.py +284 -0
  234. agno/tools/mem0.py +11 -17
  235. agno/tools/memori.py +1 -53
  236. agno/tools/memory.py +419 -0
  237. agno/tools/models/nebius.py +5 -5
  238. agno/tools/models_labs.py +20 -10
  239. agno/tools/notion.py +204 -0
  240. agno/tools/parallel.py +314 -0
  241. agno/tools/scrapegraph.py +58 -31
  242. agno/tools/searxng.py +2 -2
  243. agno/tools/serper.py +2 -2
  244. agno/tools/slack.py +18 -3
  245. agno/tools/spider.py +2 -2
  246. agno/tools/tavily.py +146 -0
  247. agno/tools/whatsapp.py +1 -1
  248. agno/tools/workflow.py +278 -0
  249. agno/tools/yfinance.py +12 -11
  250. agno/utils/agent.py +820 -0
  251. agno/utils/audio.py +27 -0
  252. agno/utils/common.py +90 -1
  253. agno/utils/events.py +217 -2
  254. agno/utils/gemini.py +180 -22
  255. agno/utils/hooks.py +57 -0
  256. agno/utils/http.py +111 -0
  257. agno/utils/knowledge.py +12 -5
  258. agno/utils/log.py +1 -0
  259. agno/utils/mcp.py +92 -2
  260. agno/utils/media.py +188 -10
  261. agno/utils/merge_dict.py +22 -1
  262. agno/utils/message.py +60 -0
  263. agno/utils/models/claude.py +40 -11
  264. agno/utils/print_response/agent.py +105 -21
  265. agno/utils/print_response/team.py +103 -38
  266. agno/utils/print_response/workflow.py +251 -34
  267. agno/utils/reasoning.py +22 -1
  268. agno/utils/serialize.py +32 -0
  269. agno/utils/streamlit.py +16 -10
  270. agno/utils/string.py +41 -0
  271. agno/utils/team.py +98 -9
  272. agno/utils/tools.py +1 -1
  273. agno/vectordb/base.py +23 -4
  274. agno/vectordb/cassandra/cassandra.py +65 -9
  275. agno/vectordb/chroma/chromadb.py +182 -38
  276. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  277. agno/vectordb/couchbase/couchbase.py +105 -10
  278. agno/vectordb/lancedb/lance_db.py +124 -133
  279. agno/vectordb/langchaindb/langchaindb.py +25 -7
  280. agno/vectordb/lightrag/lightrag.py +17 -3
  281. agno/vectordb/llamaindex/__init__.py +3 -0
  282. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  283. agno/vectordb/milvus/milvus.py +126 -9
  284. agno/vectordb/mongodb/__init__.py +7 -1
  285. agno/vectordb/mongodb/mongodb.py +112 -7
  286. agno/vectordb/pgvector/pgvector.py +142 -21
  287. agno/vectordb/pineconedb/pineconedb.py +80 -8
  288. agno/vectordb/qdrant/qdrant.py +125 -39
  289. agno/vectordb/redis/__init__.py +9 -0
  290. agno/vectordb/redis/redisdb.py +694 -0
  291. agno/vectordb/singlestore/singlestore.py +111 -25
  292. agno/vectordb/surrealdb/surrealdb.py +31 -5
  293. agno/vectordb/upstashdb/upstashdb.py +76 -8
  294. agno/vectordb/weaviate/weaviate.py +86 -15
  295. agno/workflow/__init__.py +2 -0
  296. agno/workflow/agent.py +299 -0
  297. agno/workflow/condition.py +112 -18
  298. agno/workflow/loop.py +69 -10
  299. agno/workflow/parallel.py +266 -118
  300. agno/workflow/router.py +110 -17
  301. agno/workflow/step.py +638 -129
  302. agno/workflow/steps.py +65 -6
  303. agno/workflow/types.py +61 -23
  304. agno/workflow/workflow.py +2085 -272
  305. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
  306. agno-2.3.0.dist-info/RECORD +577 -0
  307. agno/knowledge/reader/url_reader.py +0 -128
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -610
  310. agno/utils/models/aws_claude.py +0 -170
  311. agno-2.0.1.dist-info/RECORD +0 -515
  312. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  313. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
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: