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,977 @@
1
+ """Migration v2.3.0: Schema updates for memories and PostgreSQL JSONB
2
+
3
+ Changes:
4
+ - Add created_at column to memories table (all databases)
5
+ - Add feedback column to memories table (all databases)
6
+ - Change JSON to JSONB for PostgreSQL
7
+ """
8
+
9
+ import time
10
+ from typing import Any, List, Tuple
11
+
12
+ from agno.db.base import AsyncBaseDb, BaseDb
13
+ from agno.db.migrations.utils import quote_db_identifier
14
+ from agno.utils.log import log_error, log_info, log_warning
15
+
16
+ try:
17
+ from sqlalchemy import text
18
+ except ImportError:
19
+ raise ImportError("`sqlalchemy` not installed. Please install it using `pip install sqlalchemy`")
20
+
21
+
22
+ def up(db: BaseDb, table_type: str, table_name: str) -> bool:
23
+ """
24
+ Apply the following changes to the database:
25
+ - Add created_at, feedback columns to memories table
26
+ - Convert JSON to JSONB for PostgreSQL
27
+ - Change String to Text for long fields (PostgreSQL)
28
+ - Add default values to metrics table (MySQL)
29
+
30
+ Notice only the changes related to the given table_type are applied.
31
+
32
+ Returns:
33
+ bool: True if any migration was applied, False otherwise.
34
+ """
35
+ db_type = type(db).__name__
36
+
37
+ try:
38
+ if db_type == "PostgresDb":
39
+ return _migrate_postgres(db, table_type, table_name)
40
+ elif db_type == "MySQLDb":
41
+ return _migrate_mysql(db, table_type, table_name)
42
+ elif db_type == "SqliteDb":
43
+ return _migrate_sqlite(db, table_type, table_name)
44
+ elif db_type == "SingleStoreDb":
45
+ return _migrate_singlestore(db, table_type, table_name)
46
+ else:
47
+ log_info(f"{db_type} does not require schema migrations (NoSQL/document store)")
48
+ return False
49
+ except Exception as e:
50
+ log_error(f"Error running migration v2.3.0 for {db_type} on table {table_name}: {e}")
51
+ raise
52
+
53
+
54
+ async def async_up(db: AsyncBaseDb, table_type: str, table_name: str) -> bool:
55
+ """
56
+ Apply the following changes to the database:
57
+ - Add created_at, feedback columns to memories table
58
+ - Convert JSON to JSONB for PostgreSQL
59
+ - Change String to Text for long fields (PostgreSQL)
60
+ - Add default values to metrics table (MySQL)
61
+
62
+ Notice only the changes related to the given table_type are applied.
63
+
64
+ Returns:
65
+ bool: True if any migration was applied, False otherwise.
66
+ """
67
+ db_type = type(db).__name__
68
+
69
+ try:
70
+ if db_type == "AsyncPostgresDb":
71
+ return await _migrate_async_postgres(db, table_type, table_name)
72
+ elif db_type == "AsyncSqliteDb":
73
+ return await _migrate_async_sqlite(db, table_type, table_name)
74
+ else:
75
+ log_info(f"{db_type} does not require schema migrations (NoSQL/document store)")
76
+ return False
77
+ except Exception as e:
78
+ log_error(f"Error running migration v2.3.0 for {db_type} on table {table_name}: {e}")
79
+ raise
80
+
81
+
82
+ def down(db: BaseDb, table_type: str, table_name: str) -> bool:
83
+ """
84
+ Revert the following changes to the database:
85
+ - Remove created_at, feedback columns from memories table
86
+ - Revert JSONB to JSON for PostgreSQL (if needed)
87
+
88
+ Notice only the changes related to the given table_type are reverted.
89
+
90
+ Returns:
91
+ bool: True if any migration was reverted, False otherwise.
92
+ """
93
+ db_type = type(db).__name__
94
+
95
+ try:
96
+ if db_type == "PostgresDb":
97
+ return _revert_postgres(db, table_type, table_name)
98
+ elif db_type == "MySQLDb":
99
+ return _revert_mysql(db, table_type, table_name)
100
+ elif db_type == "SqliteDb":
101
+ return _revert_sqlite(db, table_type, table_name)
102
+ elif db_type == "SingleStoreDb":
103
+ return _revert_singlestore(db, table_type, table_name)
104
+ else:
105
+ log_info(f"Revert not implemented for {db_type}")
106
+ return False
107
+ except Exception as e:
108
+ log_error(f"Error reverting migration v2.3.0 for {db_type} on table {table_name}: {e}")
109
+ raise
110
+
111
+
112
+ async def async_down(db: AsyncBaseDb, table_type: str, table_name: str) -> bool:
113
+ """
114
+ Revert the following changes to the database:
115
+ - Remove created_at, feedback columns from memories table
116
+ - Revert JSONB to JSON for PostgreSQL (if needed)
117
+
118
+ Notice only the changes related to the given table_type are reverted.
119
+
120
+ Returns:
121
+ bool: True if any migration was reverted, False otherwise.
122
+ """
123
+ db_type = type(db).__name__
124
+
125
+ try:
126
+ if db_type == "AsyncPostgresDb":
127
+ return await _revert_async_postgres(db, table_type, table_name)
128
+ elif db_type == "AsyncSqliteDb":
129
+ return await _revert_async_sqlite(db, table_type, table_name)
130
+ else:
131
+ log_info(f"Revert not implemented for {db_type}")
132
+ return False
133
+ except Exception as e:
134
+ log_error(f"Error reverting migration v2.3.0 for {db_type} on table {table_name} asynchronously: {e}")
135
+ raise
136
+
137
+
138
+ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
139
+ """Migrate PostgreSQL database."""
140
+ from sqlalchemy import text
141
+
142
+ db_schema = db.db_schema or "public" # type: ignore
143
+ db_type = type(db).__name__
144
+ quoted_schema = quote_db_identifier(db_type, db_schema)
145
+ quoted_table = quote_db_identifier(db_type, table_name)
146
+
147
+ with db.Session() as sess, sess.begin(): # type: ignore
148
+ # Check if table exists
149
+ table_exists = sess.execute(
150
+ text(
151
+ """
152
+ SELECT EXISTS (
153
+ SELECT FROM information_schema.tables
154
+ WHERE table_schema = :schema
155
+ AND table_name = :table_name
156
+ )
157
+ """
158
+ ),
159
+ {"schema": db_schema, "table_name": table_name},
160
+ ).scalar()
161
+
162
+ if not table_exists:
163
+ log_info(f"Table {table_name} does not exist, skipping migration")
164
+ return False
165
+ if table_type == "memories":
166
+ # Check if columns already exist
167
+ check_columns = sess.execute(
168
+ text(
169
+ """
170
+ SELECT column_name
171
+ FROM information_schema.columns
172
+ WHERE table_schema = :schema
173
+ AND table_name = :table_name
174
+ """
175
+ ),
176
+ {"schema": db_schema, "table_name": table_name},
177
+ ).fetchall()
178
+ existing_columns = {row[0] for row in check_columns}
179
+
180
+ # Add created_at if it doesn't exist
181
+ if "created_at" not in existing_columns:
182
+ log_info(f"-- Adding created_at column to {table_name}")
183
+ current_time = int(time.time())
184
+ # Add created_at column
185
+ sess.execute(
186
+ text(
187
+ f"""
188
+ ALTER TABLE {quoted_schema}.{quoted_table}
189
+ ADD COLUMN created_at BIGINT
190
+ """
191
+ ),
192
+ )
193
+ # Populate created_at
194
+ sess.execute(
195
+ text(
196
+ f"""
197
+ UPDATE {quoted_schema}.{quoted_table}
198
+ SET created_at = COALESCE(updated_at, :default_time)
199
+ """
200
+ ),
201
+ {"default_time": current_time},
202
+ )
203
+ # Set created_at as non nullable
204
+ sess.execute(
205
+ text(
206
+ f"""
207
+ ALTER TABLE {quoted_schema}.{quoted_table}
208
+ ALTER COLUMN created_at SET NOT NULL
209
+ """
210
+ ),
211
+ )
212
+ # Add index
213
+ sess.execute(
214
+ text(
215
+ f"""
216
+ CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at
217
+ ON {quoted_schema}.{quoted_table}(created_at)
218
+ """
219
+ )
220
+ )
221
+
222
+ # Add feedback if it doesn't exist
223
+ if "feedback" not in existing_columns:
224
+ log_info(f"Adding feedback column to {table_name}")
225
+ sess.execute(
226
+ text(
227
+ f"""
228
+ ALTER TABLE {quoted_schema}.{quoted_table}
229
+ ADD COLUMN feedback TEXT
230
+ """
231
+ )
232
+ )
233
+
234
+ json_columns = [
235
+ ("memory", table_name),
236
+ ("topics", table_name),
237
+ ]
238
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
239
+
240
+ if table_type == "sessions":
241
+ json_columns = [
242
+ ("session_data", table_name),
243
+ ("agent_data", table_name),
244
+ ("team_data", table_name),
245
+ ("workflow_data", table_name),
246
+ ("metadata", table_name),
247
+ ("runs", table_name),
248
+ ("summary", table_name),
249
+ ]
250
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
251
+ if table_type == "evals":
252
+ json_columns = [
253
+ ("eval_data", table_name),
254
+ ("eval_input", table_name),
255
+ ]
256
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
257
+ if table_type == "metrics":
258
+ json_columns = [
259
+ ("token_metrics", table_name),
260
+ ("model_metrics", table_name),
261
+ ]
262
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
263
+ if table_type == "knowledge":
264
+ json_columns = [
265
+ ("metadata", table_name),
266
+ ]
267
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
268
+ if table_type == "culture":
269
+ json_columns = [
270
+ ("metadata", table_name),
271
+ ]
272
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
273
+
274
+ sess.commit()
275
+ return True
276
+
277
+
278
+ def _convert_json_to_jsonb(
279
+ sess: Any, db_schema: str, json_columns: List[Tuple[str, str]], db_type: str = "PostgresDb"
280
+ ) -> None:
281
+ quoted_schema = quote_db_identifier(db_type, db_schema) if db_schema else None
282
+ for column_name, table_name in json_columns:
283
+ quoted_table = quote_db_identifier(db_type, table_name)
284
+ table_full_name = f"{quoted_schema}.{quoted_table}" if quoted_schema else quoted_table
285
+ # Check current type
286
+ col_type = sess.execute(
287
+ text(
288
+ """
289
+ SELECT data_type
290
+ FROM information_schema.columns
291
+ WHERE table_schema = :schema
292
+ AND table_name = :table_name
293
+ AND column_name = :column_name
294
+ """
295
+ ),
296
+ {"schema": db_schema, "table_name": table_name, "column_name": column_name},
297
+ ).scalar()
298
+
299
+ if col_type == "json":
300
+ log_info(f"-- Converting {table_name}.{column_name} from JSON to JSONB")
301
+ sess.execute(
302
+ text(
303
+ f"""
304
+ ALTER TABLE {table_full_name}
305
+ ALTER COLUMN {column_name} TYPE JSONB USING {column_name}::jsonb
306
+ """
307
+ )
308
+ )
309
+
310
+
311
+ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name: str) -> bool:
312
+ """Migrate PostgreSQL database."""
313
+ from sqlalchemy import text
314
+
315
+ db_schema = db.db_schema or "public" # type: ignore
316
+ db_type = type(db).__name__
317
+ quoted_schema = quote_db_identifier(db_type, db_schema)
318
+ quoted_table = quote_db_identifier(db_type, table_name)
319
+
320
+ async with db.async_session_factory() as sess, sess.begin(): # type: ignore
321
+ # Check if table exists
322
+ result = await sess.execute(
323
+ text(
324
+ """
325
+ SELECT EXISTS (
326
+ SELECT FROM information_schema.tables
327
+ WHERE table_schema = :schema
328
+ AND table_name = :table_name
329
+ )
330
+ """
331
+ ),
332
+ {"schema": db_schema, "table_name": table_name},
333
+ )
334
+ table_exists = result.scalar()
335
+
336
+ if not table_exists:
337
+ log_info(f"Table {table_name} does not exist, skipping migration")
338
+ return False
339
+ if table_type == "memories":
340
+ # Check if columns already exist
341
+ result = await sess.execute(
342
+ text(
343
+ """
344
+ SELECT column_name
345
+ FROM information_schema.columns
346
+ WHERE table_schema = :schema
347
+ AND table_name = :table_name
348
+ """
349
+ ),
350
+ {"schema": db_schema, "table_name": table_name},
351
+ )
352
+ check_columns = result.fetchall()
353
+ existing_columns = {row[0] for row in check_columns}
354
+
355
+ # Add created_at if it doesn't exist
356
+ if "created_at" not in existing_columns:
357
+ log_info(f"-- Adding created_at column to {table_name}")
358
+ current_time = int(time.time())
359
+ # Add created_at column
360
+ await sess.execute(
361
+ text(
362
+ f"""
363
+ ALTER TABLE {quoted_schema}.{quoted_table}
364
+ ADD COLUMN created_at BIGINT
365
+ """
366
+ ),
367
+ )
368
+ # Populate created_at
369
+ await sess.execute(
370
+ text(
371
+ f"""
372
+ UPDATE {quoted_schema}.{quoted_table}
373
+ SET created_at = COALESCE(updated_at, :default_time)
374
+ """
375
+ ),
376
+ {"default_time": current_time},
377
+ )
378
+ # Set created_at as non nullable
379
+ await sess.execute(
380
+ text(
381
+ f"""
382
+ ALTER TABLE {quoted_schema}.{quoted_table}
383
+ ALTER COLUMN created_at SET NOT NULL
384
+ """
385
+ ),
386
+ )
387
+ # Add index
388
+ await sess.execute(
389
+ text(
390
+ f"""
391
+ CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at
392
+ ON {quoted_schema}.{quoted_table}(created_at)
393
+ """
394
+ )
395
+ )
396
+
397
+ # Add feedback if it doesn't exist
398
+ if "feedback" not in existing_columns:
399
+ log_info(f"Adding feedback column to {table_name}")
400
+ await sess.execute(
401
+ text(
402
+ f"""
403
+ ALTER TABLE {quoted_schema}.{quoted_table}
404
+ ADD COLUMN feedback TEXT
405
+ """
406
+ )
407
+ )
408
+
409
+ json_columns = [
410
+ ("memory", table_name),
411
+ ("topics", table_name),
412
+ ]
413
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
414
+ if table_type == "sessions":
415
+ json_columns = [
416
+ ("session_data", table_name),
417
+ ("agent_data", table_name),
418
+ ("team_data", table_name),
419
+ ("workflow_data", table_name),
420
+ ("metadata", table_name),
421
+ ("runs", table_name),
422
+ ("summary", table_name),
423
+ ]
424
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
425
+
426
+ if table_type == "evals":
427
+ json_columns = [
428
+ ("eval_data", table_name),
429
+ ("eval_input", table_name),
430
+ ]
431
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
432
+ if table_type == "metrics":
433
+ json_columns = [
434
+ ("token_metrics", table_name),
435
+ ("model_metrics", table_name),
436
+ ]
437
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
438
+ if table_type == "knowledge":
439
+ json_columns = [
440
+ ("metadata", table_name),
441
+ ]
442
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
443
+
444
+ if table_type == "culture":
445
+ json_columns = [
446
+ ("metadata", table_name),
447
+ ]
448
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
449
+
450
+ await sess.commit()
451
+ return True
452
+
453
+
454
+ async def _async_convert_json_to_jsonb(
455
+ sess: Any, db_schema: str, json_columns: List[Tuple[str, str]], db_type: str = "AsyncPostgresDb"
456
+ ) -> None:
457
+ quoted_schema = quote_db_identifier(db_type, db_schema) if db_schema else None
458
+ for column_name, table_name in json_columns:
459
+ quoted_table = quote_db_identifier(db_type, table_name)
460
+ table_full_name = f"{quoted_schema}.{quoted_table}" if quoted_schema else quoted_table
461
+ # Check current type
462
+ result = await sess.execute(
463
+ text(
464
+ """
465
+ SELECT data_type
466
+ FROM information_schema.columns
467
+ WHERE table_schema = :schema
468
+ AND table_name = :table_name
469
+ AND column_name = :column_name
470
+ """
471
+ ),
472
+ {"schema": db_schema, "table_name": table_name, "column_name": column_name},
473
+ )
474
+ col_type = result.scalar()
475
+
476
+ if col_type == "json":
477
+ log_info(f"-- Converting {table_name}.{column_name} from JSON to JSONB")
478
+ await sess.execute(
479
+ text(
480
+ f"""
481
+ ALTER TABLE {table_full_name}
482
+ ALTER COLUMN {column_name} TYPE JSONB USING {column_name}::jsonb
483
+ """
484
+ )
485
+ )
486
+
487
+
488
+ def _migrate_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
489
+ """Migrate MySQL database."""
490
+ from sqlalchemy import text
491
+
492
+ db_schema = db.db_schema or "agno" # type: ignore
493
+ db_type = type(db).__name__
494
+ quoted_schema = quote_db_identifier(db_type, db_schema)
495
+ quoted_table = quote_db_identifier(db_type, table_name)
496
+
497
+ with db.Session() as sess, sess.begin(): # type: ignore
498
+ # Check if table exists
499
+ table_exists = sess.execute(
500
+ text(
501
+ """
502
+ SELECT EXISTS (
503
+ SELECT 1 FROM INFORMATION_SCHEMA.TABLES
504
+ WHERE TABLE_SCHEMA = :schema
505
+ AND TABLE_NAME = :table_name
506
+ )
507
+ """
508
+ ),
509
+ {"schema": db_schema, "table_name": table_name},
510
+ ).scalar()
511
+
512
+ if not table_exists:
513
+ log_info(f"Table {table_name} does not exist, skipping migration")
514
+ return False
515
+ if table_type == "memories":
516
+ # Check if columns already exist
517
+ check_columns = sess.execute(
518
+ text(
519
+ """
520
+ SELECT COLUMN_NAME
521
+ FROM INFORMATION_SCHEMA.COLUMNS
522
+ WHERE TABLE_SCHEMA = :schema
523
+ AND TABLE_NAME = :table_name
524
+ """
525
+ ),
526
+ {"schema": db_schema, "table_name": table_name},
527
+ ).fetchall()
528
+ existing_columns = {row[0] for row in check_columns}
529
+
530
+ # Add created_at if it doesn't exist
531
+ if "created_at" not in existing_columns:
532
+ log_info(f"-- Adding created_at column to {table_name}")
533
+ current_time = int(time.time())
534
+ # Add created_at column
535
+ sess.execute(
536
+ text(
537
+ f"""
538
+ ALTER TABLE {quoted_schema}.{quoted_table}
539
+ ADD COLUMN `created_at` BIGINT,
540
+ ADD INDEX `idx_{table_name}_created_at` (`created_at`)
541
+ """
542
+ ),
543
+ )
544
+ # Populate created_at
545
+ sess.execute(
546
+ text(
547
+ f"""
548
+ UPDATE {quoted_schema}.{quoted_table}
549
+ SET `created_at` = COALESCE(`updated_at`, :default_time)
550
+ """
551
+ ),
552
+ {"default_time": current_time},
553
+ )
554
+ # Set created_at as non nullable
555
+ sess.execute(
556
+ text(
557
+ f"""
558
+ ALTER TABLE {quoted_schema}.{quoted_table}
559
+ MODIFY COLUMN `created_at` BIGINT NOT NULL
560
+ """
561
+ )
562
+ )
563
+
564
+ # Add feedback if it doesn't exist
565
+ if "feedback" not in existing_columns:
566
+ log_info(f"-- Adding feedback column to {table_name}")
567
+ sess.execute(
568
+ text(
569
+ f"""
570
+ ALTER TABLE {quoted_schema}.{quoted_table}
571
+ ADD COLUMN `feedback` TEXT
572
+ """
573
+ )
574
+ )
575
+
576
+ sess.commit()
577
+ return True
578
+
579
+
580
+ def _migrate_sqlite(db: BaseDb, table_type: str, table_name: str) -> bool:
581
+ """Migrate SQLite database."""
582
+ db_type = type(db).__name__
583
+ quoted_table = quote_db_identifier(db_type, table_name)
584
+
585
+ with db.Session() as sess, sess.begin(): # type: ignore
586
+ # Check if table exists
587
+ table_exists = sess.execute(
588
+ text(
589
+ """
590
+ SELECT COUNT(*) FROM sqlite_master
591
+ WHERE type='table' AND name=:table_name
592
+ """
593
+ ),
594
+ {"table_name": table_name},
595
+ ).scalar()
596
+
597
+ if not table_exists:
598
+ log_info(f"Table {table_name} does not exist, skipping migration")
599
+ return False
600
+ if table_type == "memories":
601
+ # SQLite doesn't support ALTER TABLE ADD COLUMN with constraints easily
602
+ # We'll use a simpler approach
603
+ # Check if columns already exist using PRAGMA
604
+ result = sess.execute(text(f"PRAGMA table_info({quoted_table})"))
605
+ columns_info = result.fetchall()
606
+ existing_columns = {row[1] for row in columns_info} # row[1] contains column name
607
+
608
+ # Add created_at if it doesn't exist
609
+ if "created_at" not in existing_columns:
610
+ log_info(f"-- Adding created_at column to {table_name}")
611
+ current_time = int(time.time())
612
+ # Add created_at column with NOT NULL constraint and default value
613
+ # SQLite doesn't support ALTER COLUMN, so we add NOT NULL directly
614
+ sess.execute(
615
+ text(f"ALTER TABLE {quoted_table} ADD COLUMN created_at BIGINT NOT NULL DEFAULT {current_time}"),
616
+ )
617
+ # Populate created_at for existing rows
618
+ sess.execute(
619
+ text(
620
+ f"""
621
+ UPDATE {quoted_table}
622
+ SET created_at = COALESCE(updated_at, :default_time)
623
+ WHERE created_at = :default_time
624
+ """
625
+ ),
626
+ {"default_time": current_time},
627
+ )
628
+ # Add index
629
+ sess.execute(
630
+ text(f"CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON {quoted_table}(created_at)")
631
+ )
632
+
633
+ # Add feedback if it doesn't exist
634
+ if "feedback" not in existing_columns:
635
+ log_info(f"-- Adding feedback column to {table_name}")
636
+ sess.execute(text(f"ALTER TABLE {quoted_table} ADD COLUMN feedback VARCHAR"))
637
+
638
+ sess.commit()
639
+ return True
640
+
641
+
642
+ async def _migrate_async_sqlite(db: AsyncBaseDb, table_type: str, table_name: str) -> bool:
643
+ """Migrate SQLite database."""
644
+ db_type = type(db).__name__
645
+ quoted_table = quote_db_identifier(db_type, table_name)
646
+
647
+ async with db.async_session_factory() as sess, sess.begin(): # type: ignore
648
+ # Check if table exists
649
+ result = await sess.execute(
650
+ text(
651
+ """
652
+ SELECT COUNT(*) FROM sqlite_master
653
+ WHERE type='table' AND name=:table_name
654
+ """
655
+ ),
656
+ {"table_name": table_name},
657
+ )
658
+ table_exists = result.scalar()
659
+
660
+ if not table_exists:
661
+ log_info(f"Table {table_name} does not exist, skipping migration")
662
+ return False
663
+ if table_type == "memories":
664
+ # SQLite doesn't support ALTER TABLE ADD COLUMN with constraints easily
665
+ # We'll use a simpler approach
666
+ # Check if columns already exist using PRAGMA
667
+ result = await sess.execute(text(f"PRAGMA table_info({quoted_table})"))
668
+ columns_info = result.fetchall()
669
+ existing_columns = {row[1] for row in columns_info} # row[1] contains column name
670
+
671
+ # Add created_at if it doesn't exist
672
+ if "created_at" not in existing_columns:
673
+ log_info(f"-- Adding created_at column to {table_name}")
674
+ current_time = int(time.time())
675
+ # Add created_at column with NOT NULL constraint and default value
676
+ # SQLite doesn't support ALTER COLUMN, so we add NOT NULL directly
677
+ await sess.execute(
678
+ text(f"ALTER TABLE {quoted_table} ADD COLUMN created_at BIGINT NOT NULL DEFAULT {current_time}"),
679
+ )
680
+ # Populate created_at for existing rows
681
+ await sess.execute(
682
+ text(
683
+ f"""
684
+ UPDATE {quoted_table}
685
+ SET created_at = COALESCE(updated_at, :default_time)
686
+ WHERE created_at = :default_time
687
+ """
688
+ ),
689
+ {"default_time": current_time},
690
+ )
691
+ # Add index
692
+ await sess.execute(
693
+ text(f"CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON {quoted_table}(created_at)")
694
+ )
695
+
696
+ # Add feedback if it doesn't exist
697
+ if "feedback" not in existing_columns:
698
+ log_info(f"-- Adding feedback column to {table_name}")
699
+ await sess.execute(text(f"ALTER TABLE {quoted_table} ADD COLUMN feedback VARCHAR"))
700
+
701
+ await sess.commit()
702
+ return True
703
+
704
+
705
+ def _migrate_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
706
+ """Migrate SingleStore database."""
707
+ from sqlalchemy import text
708
+
709
+ db_schema = db.db_schema or "agno" # type: ignore
710
+ db_type = type(db).__name__
711
+ quoted_schema = quote_db_identifier(db_type, db_schema)
712
+ quoted_table = quote_db_identifier(db_type, table_name)
713
+
714
+ with db.Session() as sess, sess.begin(): # type: ignore
715
+ # Check if table exists
716
+ table_exists = sess.execute(
717
+ text(
718
+ """
719
+ SELECT EXISTS (
720
+ SELECT 1 FROM INFORMATION_SCHEMA.TABLES
721
+ WHERE TABLE_SCHEMA = :schema
722
+ AND TABLE_NAME = :table_name
723
+ )
724
+ """
725
+ ),
726
+ {"schema": db_schema, "table_name": table_name},
727
+ ).scalar()
728
+
729
+ if not table_exists:
730
+ log_info(f"Table {table_name} does not exist, skipping migration")
731
+ return False
732
+ if table_type == "memories":
733
+ # Check if columns already exist
734
+ check_columns = sess.execute(
735
+ text(
736
+ """
737
+ SELECT COLUMN_NAME
738
+ FROM INFORMATION_SCHEMA.COLUMNS
739
+ WHERE TABLE_SCHEMA = :schema
740
+ AND TABLE_NAME = :table_name
741
+ """
742
+ ),
743
+ {"schema": db_schema, "table_name": table_name},
744
+ ).fetchall()
745
+ existing_columns = {row[0] for row in check_columns}
746
+
747
+ # Add created_at if it doesn't exist
748
+ if "created_at" not in existing_columns:
749
+ log_info(f"-- Adding created_at column to {table_name}")
750
+ current_time = int(time.time())
751
+ # Add created_at column
752
+ sess.execute(
753
+ text(
754
+ f"""
755
+ ALTER TABLE {quoted_schema}.{quoted_table}
756
+ ADD COLUMN `created_at` BIGINT,
757
+ ADD INDEX `idx_{table_name}_created_at` (`created_at`)
758
+ """
759
+ ),
760
+ )
761
+ # Populate created_at
762
+ sess.execute(
763
+ text(
764
+ f"""
765
+ UPDATE {quoted_schema}.{quoted_table}
766
+ SET `created_at` = COALESCE(`updated_at`, :default_time)
767
+ """
768
+ ),
769
+ {"default_time": current_time},
770
+ )
771
+
772
+ # Add feedback if it doesn't exist
773
+ if "feedback" not in existing_columns:
774
+ log_info(f"-- Adding feedback column to {table_name}")
775
+ sess.execute(
776
+ text(
777
+ f"""
778
+ ALTER TABLE {quoted_schema}.{quoted_table}
779
+ ADD COLUMN `feedback` TEXT
780
+ """
781
+ )
782
+ )
783
+
784
+ sess.commit()
785
+ return True
786
+
787
+
788
+ def _revert_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
789
+ """Revert PostgreSQL migration."""
790
+ from sqlalchemy import text
791
+
792
+ db_schema = db.db_schema or "agno" # type: ignore
793
+ db_type = type(db).__name__
794
+ quoted_schema = quote_db_identifier(db_type, db_schema)
795
+ quoted_table = quote_db_identifier(db_type, table_name)
796
+
797
+ with db.Session() as sess, sess.begin(): # type: ignore
798
+ # Check if table exists
799
+ table_exists = sess.execute(
800
+ text(
801
+ """
802
+ SELECT EXISTS (
803
+ SELECT FROM information_schema.tables
804
+ WHERE table_schema = :schema
805
+ AND table_name = :table_name
806
+ )
807
+ """
808
+ ),
809
+ {"schema": db_schema, "table_name": table_name},
810
+ ).scalar()
811
+
812
+ if not table_exists:
813
+ log_info(f"Table {table_name} does not exist, skipping revert")
814
+ return False
815
+ if table_type == "memories":
816
+ # Remove columns (in reverse order)
817
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS feedback"))
818
+ sess.execute(text(f"DROP INDEX IF EXISTS idx_{table_name}_created_at"))
819
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS created_at"))
820
+ sess.commit()
821
+ return True
822
+
823
+
824
+ async def _revert_async_postgres(db: AsyncBaseDb, table_type: str, table_name: str) -> bool:
825
+ """Revert PostgreSQL migration."""
826
+ from sqlalchemy import text
827
+
828
+ db_schema = db.db_schema or "agno" # type: ignore
829
+ db_type = type(db).__name__
830
+ quoted_schema = quote_db_identifier(db_type, db_schema)
831
+ quoted_table = quote_db_identifier(db_type, table_name)
832
+
833
+ async with db.async_session_factory() as sess, sess.begin(): # type: ignore
834
+ # Check if table exists
835
+ result = await sess.execute(
836
+ text(
837
+ """
838
+ SELECT EXISTS (
839
+ SELECT FROM information_schema.tables
840
+ WHERE table_schema = :schema
841
+ AND table_name = :table_name
842
+ )
843
+ """
844
+ ),
845
+ {"schema": db_schema, "table_name": table_name},
846
+ )
847
+ table_exists = result.scalar()
848
+
849
+ if not table_exists:
850
+ log_info(f"Table {table_name} does not exist, skipping revert")
851
+ return False
852
+ if table_type == "memories":
853
+ # Remove columns (in reverse order)
854
+ await sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS feedback"))
855
+ await sess.execute(text(f"DROP INDEX IF EXISTS idx_{table_name}_created_at"))
856
+ await sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS created_at"))
857
+ await sess.commit()
858
+ return True
859
+
860
+
861
+ def _revert_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
862
+ """Revert MySQL migration."""
863
+ from sqlalchemy import text
864
+
865
+ db_schema = db.db_schema or "agno" # type: ignore
866
+ db_type = type(db).__name__
867
+ quoted_schema = quote_db_identifier(db_type, db_schema)
868
+ quoted_table = quote_db_identifier(db_type, table_name)
869
+
870
+ with db.Session() as sess, sess.begin(): # type: ignore
871
+ # Check if table exists
872
+ table_exists = sess.execute(
873
+ text(
874
+ """
875
+ SELECT EXISTS (
876
+ SELECT 1 FROM INFORMATION_SCHEMA.TABLES
877
+ WHERE TABLE_SCHEMA = :schema
878
+ AND TABLE_NAME = :table_name
879
+ )
880
+ """
881
+ ),
882
+ {"schema": db_schema, "table_name": table_name},
883
+ ).scalar()
884
+
885
+ if not table_exists:
886
+ log_info(f"Table {table_name} does not exist, skipping revert")
887
+ return False
888
+ if table_type == "memories":
889
+ # Get existing columns
890
+ existing_columns = {
891
+ row[0]
892
+ for row in sess.execute(
893
+ text(
894
+ """
895
+ SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
896
+ WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table_name
897
+ """
898
+ ),
899
+ {"schema": db_schema, "table_name": table_name},
900
+ )
901
+ }
902
+ # Drop feedback column if it exists
903
+ if "feedback" in existing_columns:
904
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN `feedback`"))
905
+ # Drop created_at index if it exists
906
+ index_exists = sess.execute(
907
+ text(
908
+ """
909
+ SELECT COUNT(1) FROM INFORMATION_SCHEMA.STATISTICS
910
+ WHERE TABLE_SCHEMA = :schema
911
+ AND TABLE_NAME = :table_name
912
+ AND INDEX_NAME = :index_name
913
+ """
914
+ ),
915
+ {"schema": db_schema, "table_name": table_name, "index_name": f"idx_{table_name}_created_at"},
916
+ ).scalar()
917
+ if index_exists:
918
+ sess.execute(
919
+ text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP INDEX `idx_{table_name}_created_at`")
920
+ )
921
+ # Drop created_at column if it exists
922
+ if "created_at" in existing_columns:
923
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN `created_at`"))
924
+
925
+ sess.commit()
926
+ return True
927
+
928
+
929
+ def _revert_sqlite(db: BaseDb, table_type: str, table_name: str) -> bool:
930
+ """Revert SQLite migration."""
931
+ log_warning(f"-- SQLite does not support DROP COLUMN easily. Manual migration may be required for {table_name}.")
932
+
933
+ return False
934
+
935
+
936
+ async def _revert_async_sqlite(db: AsyncBaseDb, table_type: str, table_name: str) -> bool:
937
+ """Revert SQLite migration."""
938
+ log_warning(f"-- SQLite does not support DROP COLUMN easily. Manual migration may be required for {table_name}.")
939
+
940
+ return False
941
+
942
+
943
+ def _revert_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
944
+ """Revert SingleStore migration."""
945
+ from sqlalchemy import text
946
+
947
+ db_schema = db.db_schema or "agno" # type: ignore
948
+ db_type = type(db).__name__
949
+ quoted_schema = quote_db_identifier(db_type, db_schema)
950
+ quoted_table = quote_db_identifier(db_type, table_name)
951
+
952
+ with db.Session() as sess, sess.begin(): # type: ignore
953
+ # Check if table exists
954
+ table_exists = sess.execute(
955
+ text(
956
+ """
957
+ SELECT EXISTS (
958
+ SELECT 1 FROM INFORMATION_SCHEMA.TABLES
959
+ WHERE TABLE_SCHEMA = :schema
960
+ AND TABLE_NAME = :table_name
961
+ )
962
+ """
963
+ ),
964
+ {"schema": db_schema, "table_name": table_name},
965
+ ).scalar()
966
+
967
+ if not table_exists:
968
+ log_info(f"Table {table_name} does not exist, skipping revert")
969
+ return False
970
+ if table_type == "memories":
971
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS `feedback`"))
972
+ sess.execute(
973
+ text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP INDEX IF EXISTS `idx_{table_name}_created_at`")
974
+ )
975
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS `created_at`"))
976
+ sess.commit()
977
+ return True