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/db/json/utils.py CHANGED
@@ -5,33 +5,10 @@ from datetime import date, datetime, timedelta, timezone
5
5
  from typing import Any, Dict, List, Optional
6
6
  from uuid import uuid4
7
7
 
8
- from agno.db.base import SessionType
9
- from agno.run.agent import RunOutput
10
- from agno.run.team import TeamRunOutput
11
- from agno.session.summary import SessionSummary
8
+ from agno.db.schemas.culture import CulturalKnowledge
12
9
  from agno.utils.log import log_debug
13
10
 
14
11
 
15
- def hydrate_session(session: dict) -> dict:
16
- """Convert nested dictionaries to their corresponding object types.
17
-
18
- Args:
19
- session (dict): The session dictionary to hydrate.
20
-
21
- Returns:
22
- dict: The hydrated session dictionary.
23
- """
24
- if session.get("summary") is not None:
25
- session["summary"] = SessionSummary.from_dict(session["summary"])
26
- if session.get("runs") is not None:
27
- if session["session_type"] == SessionType.AGENT:
28
- session["runs"] = [RunOutput.from_dict(run) for run in session["runs"]]
29
- elif session["session_type"] == SessionType.TEAM:
30
- session["runs"] = [TeamRunOutput.from_dict(run) for run in session["runs"]]
31
-
32
- return session
33
-
34
-
35
12
  def apply_sorting(
36
13
  data: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
37
14
  ) -> List[Dict[str, Any]]:
@@ -101,7 +78,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
101
78
  all_user_ids = set()
102
79
 
103
80
  for session_type, sessions_count_key, runs_count_key in session_types:
104
- sessions = sessions_data.get(session_type, [])
81
+ sessions = sessions_data.get(session_type, []) or []
105
82
  metrics[sessions_count_key] = len(sessions)
106
83
 
107
84
  for session in sessions:
@@ -122,7 +99,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
122
99
 
123
100
  model_metrics = []
124
101
  for model, count in model_counts.items():
125
- model_id, model_provider = model.split(":")
102
+ model_id, model_provider = model.rsplit(":", 1)
126
103
  model_metrics.append({"model_id": model_id, "model_provider": model_provider, "count": count})
127
104
 
128
105
  metrics["users_count"] = len(all_user_ids)
@@ -194,3 +171,60 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
194
171
  if days_diff <= 0:
195
172
  return []
196
173
  return [starting_date + timedelta(days=x) for x in range(days_diff)]
174
+
175
+
176
+ # -- Cultural Knowledge util methods --
177
+ def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
178
+ """Serialize a CulturalKnowledge object for database storage.
179
+
180
+ Converts the model's separate content, categories, and notes fields
181
+ into a single dict for the database content column.
182
+
183
+ Args:
184
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
185
+
186
+ Returns:
187
+ Dict[str, Any]: A dictionary with the content field as a dict containing content, categories, and notes.
188
+ """
189
+ content_dict: Dict[str, Any] = {}
190
+ if cultural_knowledge.content is not None:
191
+ content_dict["content"] = cultural_knowledge.content
192
+ if cultural_knowledge.categories is not None:
193
+ content_dict["categories"] = cultural_knowledge.categories
194
+ if cultural_knowledge.notes is not None:
195
+ content_dict["notes"] = cultural_knowledge.notes
196
+
197
+ return content_dict if content_dict else {}
198
+
199
+
200
+ def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
201
+ """Deserialize a database row to a CulturalKnowledge object.
202
+
203
+ The database stores content as a dict containing content, categories, and notes.
204
+ This method extracts those fields and converts them back to the model format.
205
+
206
+ Args:
207
+ db_row (Dict[str, Any]): The database row as a dictionary.
208
+
209
+ Returns:
210
+ CulturalKnowledge: The cultural knowledge object.
211
+ """
212
+ # Extract content, categories, and notes from the content field
213
+ content_json = db_row.get("content", {}) or {}
214
+
215
+ return CulturalKnowledge.from_dict(
216
+ {
217
+ "id": db_row.get("id"),
218
+ "name": db_row.get("name"),
219
+ "summary": db_row.get("summary"),
220
+ "content": content_json.get("content"),
221
+ "categories": content_json.get("categories"),
222
+ "notes": content_json.get("notes"),
223
+ "metadata": db_row.get("metadata"),
224
+ "input": db_row.get("input"),
225
+ "created_at": db_row.get("created_at"),
226
+ "updated_at": db_row.get("updated_at"),
227
+ "agent_id": db_row.get("agent_id"),
228
+ "team_id": db_row.get("team_id"),
229
+ }
230
+ )
@@ -0,0 +1,199 @@
1
+ import importlib
2
+ from typing import Optional, Union
3
+
4
+ from packaging import version as packaging_version
5
+ from packaging.version import Version
6
+
7
+ from agno.db.base import AsyncBaseDb, BaseDb
8
+ from agno.utils.log import log_error, log_info, log_warning
9
+
10
+
11
+ class MigrationManager:
12
+ """Manager class to handle database migrations"""
13
+
14
+ available_versions: list[tuple[str, Version]] = [
15
+ ("v2_0_0", packaging_version.parse("2.0.0")),
16
+ ("v2_3_0", packaging_version.parse("2.3.0")),
17
+ ]
18
+
19
+ def __init__(self, db: Union[AsyncBaseDb, BaseDb]):
20
+ self.db = db
21
+
22
+ @property
23
+ def latest_schema_version(self) -> Version:
24
+ return self.available_versions[-1][1]
25
+
26
+ async def up(self, target_version: Optional[str] = None, table_type: Optional[str] = None, force: bool = False):
27
+ """Handle executing an up migration.
28
+
29
+ Args:
30
+ target_version: The version to migrate to, e.g. "v3.0.0". If not provided, the latest available version will be used.
31
+ table_type: The type of table to migrate. If not provided, all table types will be considered.
32
+ """
33
+
34
+ # If not target version is provided, use the latest available version
35
+ if not target_version:
36
+ _target_version = self.latest_schema_version
37
+ log_info(
38
+ f"No target version provided. Will migrate to the latest available version: {str(_target_version)}"
39
+ )
40
+ else:
41
+ _target_version = packaging_version.parse(target_version)
42
+
43
+ # Select tables to migrate
44
+ if table_type:
45
+ if table_type not in ["memory", "session", "metrics", "eval", "knowledge", "culture"]:
46
+ log_warning(
47
+ f"Invalid table type: {table_type}. Use one of: memory, session, metrics, eval, knowledge, culture"
48
+ )
49
+ return
50
+ tables = [(table_type, getattr(self.db, f"{table_type}_table_name"))]
51
+ else:
52
+ tables = [
53
+ ("memories", self.db.memory_table_name),
54
+ ("sessions", self.db.session_table_name),
55
+ ("metrics", self.db.metrics_table_name),
56
+ ("evals", self.db.eval_table_name),
57
+ ("knowledge", self.db.knowledge_table_name),
58
+ ("culture", self.db.culture_table_name),
59
+ ]
60
+
61
+ # Handle migrations for each table separately (extend in future if needed):
62
+ for table_type, table_name in tables:
63
+ if isinstance(self.db, AsyncBaseDb):
64
+ current_version = packaging_version.parse(await self.db.get_latest_schema_version(table_name))
65
+ else:
66
+ current_version = packaging_version.parse(self.db.get_latest_schema_version(table_name))
67
+
68
+ if current_version is None:
69
+ log_warning(f"Skipping up migration: No version found for table {table_name}.")
70
+ continue
71
+
72
+ # If the target version is less or equal to the current version, no migrations needed
73
+ if _target_version <= current_version and not force:
74
+ log_warning(
75
+ f"Skipping up migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
76
+ )
77
+ continue
78
+
79
+ log_info(
80
+ f"Starting database migration for table {table_name}. Current version: {current_version}. Target version: {_target_version}."
81
+ )
82
+
83
+ # Find files after the current version
84
+ latest_version = None
85
+ migration_executed = False
86
+ for version, normalised_version in self.available_versions:
87
+ if normalised_version > current_version:
88
+ if target_version and normalised_version > _target_version:
89
+ break
90
+
91
+ log_info(f"Applying migration {normalised_version} on {table_name}")
92
+ migration_executed = await self._up_migration(version, table_type, table_name)
93
+ if migration_executed:
94
+ log_info(f"Successfully applied migration {normalised_version} on table {table_name}")
95
+ else:
96
+ log_info(f"Skipping application of migration {normalised_version} on table {table_name}")
97
+
98
+ latest_version = normalised_version.public
99
+
100
+ if migration_executed and latest_version:
101
+ log_info(f"Storing version {latest_version} in database for table {table_name}")
102
+ if isinstance(self.db, AsyncBaseDb):
103
+ await self.db.upsert_schema_version(table_name, latest_version)
104
+ else:
105
+ self.db.upsert_schema_version(table_name, latest_version)
106
+ log_info(f"Successfully stored version {latest_version} in database for table {table_name}")
107
+ log_info("----------------------------------------------------------")
108
+
109
+ async def _up_migration(self, version: str, table_type: str, table_name: str) -> bool:
110
+ """Run the database-specific logic to handle an up migration.
111
+
112
+ Args:
113
+ version: The version to migrate to, e.g. "v3.0.0"
114
+ """
115
+ migration_module = importlib.import_module(f"agno.db.migrations.versions.{version}")
116
+
117
+ try:
118
+ if isinstance(self.db, AsyncBaseDb):
119
+ return await migration_module.async_up(self.db, table_type, table_name)
120
+ else:
121
+ return migration_module.up(self.db, table_type, table_name)
122
+ except Exception as e:
123
+ log_error(f"Error running migration to version {version}: {e}")
124
+ raise
125
+
126
+ async def down(self, target_version: str, table_type: Optional[str] = None, force: bool = False):
127
+ """Handle executing a down migration.
128
+
129
+ Args:
130
+ target_version: The version to migrate to. e.g. "v2.3.0"
131
+ table_type: The type of table to migrate. If not provided, all table types will be considered.
132
+ """
133
+ _target_version = packaging_version.parse(target_version)
134
+
135
+ # Select tables to migrate
136
+ if table_type:
137
+ if table_type not in ["memory", "session", "metrics", "eval", "knowledge", "culture"]:
138
+ log_warning(
139
+ f"Invalid table type: {table_type}. Use one of: memory, session, metrics, eval, knowledge, culture"
140
+ )
141
+ return
142
+ tables = [(table_type, getattr(self.db, f"{table_type}_table_name"))]
143
+ else:
144
+ tables = [
145
+ ("memories", self.db.memory_table_name),
146
+ ("sessions", self.db.session_table_name),
147
+ ("metrics", self.db.metrics_table_name),
148
+ ("evals", self.db.eval_table_name),
149
+ ("knowledge", self.db.knowledge_table_name),
150
+ ("culture", self.db.culture_table_name),
151
+ ]
152
+
153
+ for table_type, table_name in tables:
154
+ if isinstance(self.db, AsyncBaseDb):
155
+ current_version = packaging_version.parse(await self.db.get_latest_schema_version(table_name))
156
+ else:
157
+ current_version = packaging_version.parse(self.db.get_latest_schema_version(table_name))
158
+
159
+ if _target_version >= current_version and not force:
160
+ log_warning(
161
+ f"Skipping down migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
162
+ )
163
+ continue
164
+
165
+ migration_executed = False
166
+ # Run down migration for all versions between target and current (include down of current version)
167
+ # Apply down migrations in reverse order to ensure dependencies are met
168
+ for version, normalised_version in reversed(self.available_versions):
169
+ if normalised_version > _target_version:
170
+ log_info(f"Reverting migration {normalised_version} on table {table_name}")
171
+ migration_executed = await self._down_migration(version, table_type, table_name)
172
+ if migration_executed:
173
+ log_info(f"Successfully reverted migration {normalised_version} on table {table_name}")
174
+ else:
175
+ log_info(f"Skipping revert of migration {normalised_version} on table {table_name}")
176
+
177
+ if migration_executed:
178
+ log_info(f"Storing version {_target_version} in database for table {table_name}")
179
+ if isinstance(self.db, AsyncBaseDb):
180
+ await self.db.upsert_schema_version(table_name, _target_version.public)
181
+ else:
182
+ self.db.upsert_schema_version(table_name, _target_version.public)
183
+ log_info(f"Successfully stored version {_target_version} in database for table {table_name}")
184
+
185
+ async def _down_migration(self, version: str, table_type: str, table_name: str) -> bool:
186
+ """Run the database-specific logic to handle a down migration.
187
+
188
+ Args:
189
+ version: The version to migrate to, e.g. "v3.0.0"
190
+ """
191
+ migration_module = importlib.import_module(f"agno.db.migrations.versions.{version}")
192
+ try:
193
+ if isinstance(self.db, AsyncBaseDb):
194
+ return await migration_module.async_down(self.db, table_type, table_name)
195
+ else:
196
+ return migration_module.down(self.db, table_type, table_name)
197
+ except Exception as e:
198
+ log_error(f"Error running migration to version {version}: {e}")
199
+ raise
@@ -1,15 +1,13 @@
1
1
  """Migration utility to migrate your Agno tables from v1 to v2"""
2
2
 
3
+ import gc
3
4
  import json
4
- from typing import Any, Dict, List, Optional, Union
5
+ from typing import Any, Dict, List, Optional, Union, cast
5
6
 
6
7
  from sqlalchemy import text
7
8
 
8
- from agno.db.mongo.mongo import MongoDb
9
- from agno.db.mysql.mysql import MySQLDb
10
- from agno.db.postgres.postgres import PostgresDb
9
+ from agno.db.base import BaseDb
11
10
  from agno.db.schemas.memory import UserMemory
12
- from agno.db.sqlite.sqlite import SqliteDb
13
11
  from agno.session import AgentSession, TeamSession, WorkflowSession
14
12
  from agno.utils.log import log_error, log_info, log_warning
15
13
 
@@ -315,7 +313,7 @@ def convert_v1_fields_to_v2(data: Dict[str, Any]) -> Dict[str, Any]:
315
313
 
316
314
 
317
315
  def migrate(
318
- db: Union[PostgresDb, MySQLDb, SqliteDb, MongoDb],
316
+ db: BaseDb,
319
317
  v1_db_schema: str,
320
318
  agent_sessions_table_name: Optional[str] = None,
321
319
  team_sessions_table_name: Optional[str] = None,
@@ -372,7 +370,7 @@ def migrate(
372
370
 
373
371
 
374
372
  def migrate_table_in_batches(
375
- db: Union[PostgresDb, MySQLDb, SqliteDb, MongoDb],
373
+ db: BaseDb,
376
374
  v1_db_schema: str,
377
375
  v1_table_name: str,
378
376
  v1_table_type: str,
@@ -410,7 +408,7 @@ def migrate_table_in_batches(
410
408
  if hasattr(db, "Session"):
411
409
  db.Session.remove() # type: ignore
412
410
 
413
- db.upsert_sessions(sessions) # type: ignore
411
+ db.upsert_sessions(sessions, preserve_updated_at=True) # type: ignore
414
412
  total_migrated += len(sessions)
415
413
  log_info(f"Bulk upserted {len(sessions)} sessions in batch {batch_count}")
416
414
 
@@ -420,21 +418,35 @@ def migrate_table_in_batches(
420
418
  if hasattr(db, "Session"):
421
419
  db.Session.remove() # type: ignore
422
420
 
423
- db.upsert_memories(memories)
421
+ db.upsert_memories(memories, preserve_updated_at=True)
424
422
  total_migrated += len(memories)
425
423
  log_info(f"Bulk upserted {len(memories)} memories in batch {batch_count}")
426
424
 
427
425
  log_info(f"Completed batch {batch_count}: migrated {batch_size_actual} records")
428
426
 
427
+ # Explicit cleanup to free memory before next batch
428
+ del batch_content
429
+ if v1_table_type in ["agent_sessions", "team_sessions", "workflow_sessions"]:
430
+ del sessions
431
+ elif v1_table_type == "memories":
432
+ del memories
433
+
434
+ # Force garbage collection to return memory to OS
435
+ # This is necessary because Python's memory allocator retains memory after large operations
436
+ # See: https://github.com/sqlalchemy/sqlalchemy/issues/4616
437
+ gc.collect()
438
+
429
439
  log_info(f"✅ Migration completed for table {v1_table_name}: {total_migrated} total records migrated")
430
440
 
431
441
 
432
- def get_table_content_in_batches(
433
- db: Union[PostgresDb, MySQLDb, SqliteDb, MongoDb], db_schema: str, table_name: str, batch_size: int = 5000
434
- ):
442
+ def get_table_content_in_batches(db: BaseDb, db_schema: str, table_name: str, batch_size: int = 5000):
435
443
  """Get table content in batches to avoid memory issues with large tables"""
436
444
  try:
437
- if isinstance(db, MongoDb):
445
+ if type(db).__name__ == "MongoDb":
446
+ from agno.db.mongo.mongo import MongoDb
447
+
448
+ db = cast(MongoDb, db)
449
+
438
450
  # MongoDB implementation with cursor and batching
439
451
  collection = db.database[table_name]
440
452
  cursor = collection.find({}).batch_size(batch_size)
@@ -455,6 +467,24 @@ def get_table_content_in_batches(
455
467
  yield batch
456
468
  else:
457
469
  # SQL database implementations (PostgresDb, MySQLDb, SqliteDb)
470
+ if type(db).__name__ == "PostgresDb":
471
+ from agno.db.postgres.postgres import PostgresDb
472
+
473
+ db = cast(PostgresDb, db)
474
+
475
+ elif type(db).__name__ == "MySQLDb":
476
+ from agno.db.mysql.mysql import MySQLDb
477
+
478
+ db = cast(MySQLDb, db)
479
+
480
+ elif type(db).__name__ == "SqliteDb":
481
+ from agno.db.sqlite.sqlite import SqliteDb
482
+
483
+ db = cast(SqliteDb, db)
484
+
485
+ else:
486
+ raise ValueError(f"Invalid database type: {type(db).__name__}")
487
+
458
488
  offset = 0
459
489
  while True:
460
490
  # Create a new session for each batch to avoid transaction conflicts
File without changes