agno 2.2.13__py3-none-any.whl → 2.4.3__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 (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
@@ -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_info(f"Skipping 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_info(
75
+ f"Skipping 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
@@ -0,0 +1,19 @@
1
+ def quote_db_identifier(db_type: str, identifier: str) -> str:
2
+ """Add the right quotes to the given identifier string (table name, schema name) based on db type.
3
+
4
+ Args:
5
+ db_type: The database type name (e.g., "PostgresDb", "MySQLDb", "SqliteDb")
6
+ identifier: The identifier string to add quotes to
7
+
8
+ Returns:
9
+ The properly quoted identifier string
10
+ """
11
+ if db_type in ("PostgresDb", "AsyncPostgresDb"):
12
+ return f'"{identifier}"'
13
+ elif db_type in ("MySQLDb", "AsyncMySQLDb", "SingleStoreDb"):
14
+ return f"`{identifier}`"
15
+ elif db_type in ("SqliteDb", "AsyncSqliteDb"):
16
+ return f'"{identifier}"'
17
+ else:
18
+ # Default to double quotes for unknown types
19
+ return f'"{identifier}"'
@@ -7,9 +7,11 @@ from typing import Any, Dict, List, Optional, Union, cast
7
7
  from sqlalchemy import text
8
8
 
9
9
  from agno.db.base import BaseDb
10
+ from agno.db.migrations.utils import quote_db_identifier
10
11
  from agno.db.schemas.memory import UserMemory
11
12
  from agno.session import AgentSession, TeamSession, WorkflowSession
12
13
  from agno.utils.log import log_error, log_info, log_warning
14
+ from agno.utils.string import sanitize_postgres_string, sanitize_postgres_strings
13
15
 
14
16
 
15
17
  def convert_v1_metrics_to_v2(metrics_dict: Dict[str, Any]) -> Dict[str, Any]:
@@ -45,7 +47,10 @@ def convert_v1_metrics_to_v2(metrics_dict: Dict[str, Any]) -> Dict[str, Any]:
45
47
 
46
48
 
47
49
  def convert_any_metrics_in_data(data: Any) -> Any:
48
- """Recursively find and convert any metrics dictionaries and handle v1 to v2 field conversion."""
50
+ """Recursively find and convert any metrics dictionaries and handle v1 to v2 field conversion.
51
+
52
+ Also sanitizes all string values to remove null bytes for PostgreSQL compatibility.
53
+ """
49
54
  if isinstance(data, dict):
50
55
  # First apply v1 to v2 field conversion (handles extra_data extraction, thinking/reasoning_content consolidation, etc.)
51
56
  data = convert_v1_fields_to_v2(data)
@@ -62,13 +67,19 @@ def convert_any_metrics_in_data(data: Any) -> Any:
62
67
  converted_dict[key] = convert_v1_metrics_to_v2(value)
63
68
  else:
64
69
  converted_dict[key] = convert_any_metrics_in_data(value)
65
- return converted_dict
70
+
71
+ # Sanitize all strings in the converted dict to remove null bytes
72
+ return sanitize_postgres_strings(converted_dict)
66
73
 
67
74
  elif isinstance(data, list):
68
75
  return [convert_any_metrics_in_data(item) for item in data]
69
76
 
77
+ elif isinstance(data, str):
78
+ # Sanitize string values to remove null bytes
79
+ return sanitize_postgres_string(data)
80
+
70
81
  else:
71
- # Not a dict or list, return as-is
82
+ # Not a dict, list, or string, return as-is
72
83
  return data
73
84
 
74
85
 
@@ -485,15 +496,21 @@ def get_table_content_in_batches(db: BaseDb, db_schema: str, table_name: str, ba
485
496
  else:
486
497
  raise ValueError(f"Invalid database type: {type(db).__name__}")
487
498
 
499
+ db_type = type(db).__name__
500
+ quoted_schema = (
501
+ quote_db_identifier(db_type=db_type, identifier=db_schema) if db_schema and db_schema.strip() else None
502
+ )
503
+ quoted_table = quote_db_identifier(db_type=db_type, identifier=table_name)
504
+
488
505
  offset = 0
489
506
  while True:
490
507
  # Create a new session for each batch to avoid transaction conflicts
491
508
  with db.Session() as sess:
492
509
  # Handle empty schema by omitting the schema prefix (needed for SQLite)
493
- if db_schema and db_schema.strip():
494
- sql_query = f"SELECT * FROM {db_schema}.{table_name} LIMIT {batch_size} OFFSET {offset}"
510
+ if quoted_schema:
511
+ sql_query = f"SELECT * FROM {quoted_schema}.{quoted_table} LIMIT {batch_size} OFFSET {offset}"
495
512
  else:
496
- sql_query = f"SELECT * FROM {table_name} LIMIT {batch_size} OFFSET {offset}"
513
+ sql_query = f"SELECT * FROM {quoted_table} LIMIT {batch_size} OFFSET {offset}"
497
514
 
498
515
  result = sess.execute(text(sql_query))
499
516
  batch = [row._asdict() for row in result]
@@ -530,13 +547,16 @@ def get_all_table_content(db, db_schema: str, table_name: str) -> list[dict[str,
530
547
 
531
548
 
532
549
  def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]:
533
- """Parse v1 Agent sessions into v2 Agent sessions and Memories"""
550
+ """Parse v1 Agent sessions into v2 Agent sessions and Memories
551
+
552
+ Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
553
+ """
534
554
  sessions_v2 = []
535
555
 
536
556
  for item in v1_content:
537
557
  session = {
538
558
  "agent_id": item.get("agent_id"),
539
- "agent_data": item.get("agent_data"),
559
+ "agent_data": sanitize_postgres_strings(item.get("agent_data")) if item.get("agent_data") else None,
540
560
  "session_id": item.get("session_id"),
541
561
  "user_id": item.get("user_id"),
542
562
  "session_data": convert_session_data_comprehensively(item.get("session_data")),
@@ -545,6 +565,7 @@ def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]
545
565
  "created_at": item.get("created_at"),
546
566
  "updated_at": item.get("updated_at"),
547
567
  }
568
+ # Summary field sanitization is handled in convert_session_data_comprehensively
548
569
 
549
570
  try:
550
571
  agent_session = AgentSession.from_dict(session)
@@ -559,13 +580,16 @@ def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]
559
580
 
560
581
 
561
582
  def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
562
- """Parse v1 Team sessions into v2 Team sessions and Memories"""
583
+ """Parse v1 Team sessions into v2 Team sessions and Memories
584
+
585
+ Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
586
+ """
563
587
  sessions_v2 = []
564
588
 
565
589
  for item in v1_content:
566
590
  session = {
567
591
  "team_id": item.get("team_id"),
568
- "team_data": item.get("team_data"),
592
+ "team_data": sanitize_postgres_strings(item.get("team_data")) if item.get("team_data") else None,
569
593
  "session_id": item.get("session_id"),
570
594
  "user_id": item.get("user_id"),
571
595
  "session_data": convert_session_data_comprehensively(item.get("session_data")),
@@ -574,6 +598,8 @@ def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
574
598
  "created_at": item.get("created_at"),
575
599
  "updated_at": item.get("updated_at"),
576
600
  }
601
+ # Summary field sanitization is handled in convert_session_data_comprehensively
602
+
577
603
  try:
578
604
  team_session = TeamSession.from_dict(session)
579
605
  except Exception as e:
@@ -587,13 +613,18 @@ def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
587
613
 
588
614
 
589
615
  def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSession]:
590
- """Parse v1 Workflow sessions into v2 Workflow sessions"""
616
+ """Parse v1 Workflow sessions into v2 Workflow sessions
617
+
618
+ Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
619
+ """
591
620
  sessions_v2 = []
592
621
 
593
622
  for item in v1_content:
594
623
  session = {
595
624
  "workflow_id": item.get("workflow_id"),
596
- "workflow_data": item.get("workflow_data"),
625
+ "workflow_data": sanitize_postgres_strings(item.get("workflow_data"))
626
+ if item.get("workflow_data")
627
+ else None,
597
628
  "session_id": item.get("session_id"),
598
629
  "user_id": item.get("user_id"),
599
630
  "session_data": convert_session_data_comprehensively(item.get("session_data")),
@@ -601,9 +632,11 @@ def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSe
601
632
  "created_at": item.get("created_at"),
602
633
  "updated_at": item.get("updated_at"),
603
634
  # Workflow v2 specific fields
604
- "workflow_name": item.get("workflow_name"),
635
+ "workflow_name": sanitize_postgres_string(item.get("workflow_name")) if item.get("workflow_name") else None,
605
636
  "runs": convert_any_metrics_in_data(item.get("runs")),
606
637
  }
638
+ # Summary field sanitization is handled in convert_session_data_comprehensively
639
+
607
640
  try:
608
641
  workflow_session = WorkflowSession.from_dict(session)
609
642
  except Exception as e:
@@ -617,18 +650,23 @@ def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSe
617
650
 
618
651
 
619
652
  def parse_memories(v1_content: List[Dict[str, Any]]) -> List[UserMemory]:
620
- """Parse v1 Memories into v2 Memories"""
653
+ """Parse v1 Memories into v2 Memories
654
+
655
+ Sanitizes all string fields to remove null bytes for PostgreSQL compatibility.
656
+ """
621
657
  memories_v2 = []
622
658
 
623
659
  for item in v1_content:
624
660
  memory = {
625
661
  "memory_id": item.get("memory_id"),
626
- "memory": item.get("memory"),
627
- "input": item.get("input"),
662
+ "memory": sanitize_postgres_strings(item.get("memory")) if item.get("memory") else None,
663
+ "input": sanitize_postgres_string(item.get("input")) if item.get("input") else None,
628
664
  "updated_at": item.get("updated_at"),
629
665
  "agent_id": item.get("agent_id"),
630
666
  "team_id": item.get("team_id"),
631
667
  "user_id": item.get("user_id"),
668
+ "topics": sanitize_postgres_strings(item.get("topics")) if item.get("topics") else None,
669
+ "feedback": sanitize_postgres_string(item.get("feedback")) if item.get("feedback") else None,
632
670
  }
633
671
  memories_v2.append(UserMemory.from_dict(memory))
634
672
 
File without changes