agno 2.2.13__py3-none-any.whl → 2.3.1__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 (92) hide show
  1. agno/agent/agent.py +197 -110
  2. agno/api/api.py +2 -0
  3. agno/db/base.py +26 -0
  4. agno/db/dynamo/dynamo.py +8 -0
  5. agno/db/dynamo/schemas.py +1 -0
  6. agno/db/firestore/firestore.py +8 -0
  7. agno/db/firestore/schemas.py +1 -0
  8. agno/db/gcs_json/gcs_json_db.py +8 -0
  9. agno/db/in_memory/in_memory_db.py +8 -1
  10. agno/db/json/json_db.py +8 -0
  11. agno/db/migrations/manager.py +199 -0
  12. agno/db/migrations/versions/__init__.py +0 -0
  13. agno/db/migrations/versions/v2_3_0.py +938 -0
  14. agno/db/mongo/async_mongo.py +16 -6
  15. agno/db/mongo/mongo.py +11 -0
  16. agno/db/mongo/schemas.py +3 -0
  17. agno/db/mongo/utils.py +17 -0
  18. agno/db/mysql/mysql.py +76 -3
  19. agno/db/mysql/schemas.py +20 -10
  20. agno/db/postgres/async_postgres.py +99 -25
  21. agno/db/postgres/postgres.py +75 -6
  22. agno/db/postgres/schemas.py +30 -20
  23. agno/db/redis/redis.py +15 -2
  24. agno/db/redis/schemas.py +4 -0
  25. agno/db/schemas/memory.py +13 -0
  26. agno/db/singlestore/schemas.py +11 -0
  27. agno/db/singlestore/singlestore.py +79 -5
  28. agno/db/sqlite/async_sqlite.py +97 -19
  29. agno/db/sqlite/schemas.py +10 -0
  30. agno/db/sqlite/sqlite.py +79 -2
  31. agno/db/surrealdb/surrealdb.py +8 -0
  32. agno/knowledge/chunking/semantic.py +7 -2
  33. agno/knowledge/embedder/nebius.py +1 -1
  34. agno/knowledge/knowledge.py +57 -86
  35. agno/knowledge/reader/csv_reader.py +7 -9
  36. agno/knowledge/reader/docx_reader.py +5 -5
  37. agno/knowledge/reader/field_labeled_csv_reader.py +16 -18
  38. agno/knowledge/reader/json_reader.py +5 -4
  39. agno/knowledge/reader/markdown_reader.py +8 -8
  40. agno/knowledge/reader/pdf_reader.py +11 -11
  41. agno/knowledge/reader/pptx_reader.py +5 -5
  42. agno/knowledge/reader/s3_reader.py +3 -3
  43. agno/knowledge/reader/text_reader.py +8 -8
  44. agno/knowledge/reader/web_search_reader.py +1 -48
  45. agno/knowledge/reader/website_reader.py +10 -10
  46. agno/models/anthropic/claude.py +319 -28
  47. agno/models/aws/claude.py +32 -0
  48. agno/models/azure/openai_chat.py +19 -10
  49. agno/models/base.py +612 -545
  50. agno/models/cerebras/cerebras.py +8 -11
  51. agno/models/cohere/chat.py +27 -1
  52. agno/models/google/gemini.py +39 -7
  53. agno/models/groq/groq.py +25 -11
  54. agno/models/meta/llama.py +20 -9
  55. agno/models/meta/llama_openai.py +3 -19
  56. agno/models/nebius/nebius.py +4 -4
  57. agno/models/openai/chat.py +30 -14
  58. agno/models/openai/responses.py +10 -13
  59. agno/models/response.py +1 -0
  60. agno/models/vertexai/claude.py +26 -0
  61. agno/os/app.py +8 -19
  62. agno/os/router.py +54 -0
  63. agno/os/routers/knowledge/knowledge.py +2 -2
  64. agno/os/schema.py +2 -2
  65. agno/session/agent.py +57 -92
  66. agno/session/summary.py +1 -1
  67. agno/session/team.py +62 -112
  68. agno/session/workflow.py +353 -57
  69. agno/team/team.py +227 -125
  70. agno/tools/models/nebius.py +5 -5
  71. agno/tools/models_labs.py +20 -10
  72. agno/tools/nano_banana.py +151 -0
  73. agno/tools/yfinance.py +12 -11
  74. agno/utils/http.py +111 -0
  75. agno/utils/media.py +11 -0
  76. agno/utils/models/claude.py +8 -0
  77. agno/utils/print_response/agent.py +33 -12
  78. agno/utils/print_response/team.py +22 -12
  79. agno/vectordb/couchbase/couchbase.py +6 -2
  80. agno/workflow/condition.py +13 -0
  81. agno/workflow/loop.py +13 -0
  82. agno/workflow/parallel.py +13 -0
  83. agno/workflow/router.py +13 -0
  84. agno/workflow/step.py +120 -20
  85. agno/workflow/steps.py +13 -0
  86. agno/workflow/workflow.py +76 -63
  87. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/METADATA +6 -2
  88. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/RECORD +91 -88
  89. agno/tools/googlesearch.py +0 -98
  90. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/WHEEL +0 -0
  91. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/licenses/LICENSE +0 -0
  92. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,7 @@ from agno.db.mongo.utils import (
9
9
  apply_sorting,
10
10
  bulk_upsert_metrics,
11
11
  calculate_date_metrics,
12
- create_collection_indexes,
12
+ create_collection_indexes_async,
13
13
  deserialize_cultural_knowledge_from_db,
14
14
  fetch_all_sessions_data,
15
15
  get_dates_to_calculate_metrics_for,
@@ -167,9 +167,9 @@ class AsyncMongoDb(AsyncBaseDb):
167
167
 
168
168
  self._event_loop = current_loop
169
169
  self._database = None # Reset database reference
170
- # Clear collection caches when switching event loops
170
+ # Clear collection caches and initialization flags when switching event loops
171
171
  for attr in list(vars(self).keys()):
172
- if attr.endswith("_collection"):
172
+ if attr.endswith("_collection") or attr.endswith("_initialized"):
173
173
  delattr(self, attr)
174
174
 
175
175
  return self._client # type: ignore
@@ -307,9 +307,8 @@ class AsyncMongoDb(AsyncBaseDb):
307
307
  if not hasattr(self, f"_{collection_name}_initialized"):
308
308
  if not create_collection_if_not_found:
309
309
  return None
310
- # Note: Motor doesn't have sync create_index, so we use it as-is
311
- # The indexes are created in the background
312
- create_collection_indexes(collection, collection_type) # type: ignore
310
+ # Create indexes asynchronously for Motor collections
311
+ await create_collection_indexes_async(collection, collection_type)
313
312
  setattr(self, f"_{collection_name}_initialized", True)
314
313
  log_debug(f"Initialized collection '{collection_name}'")
315
314
  else:
@@ -321,6 +320,14 @@ class AsyncMongoDb(AsyncBaseDb):
321
320
  log_error(f"Error getting collection {collection_name}: {e}")
322
321
  raise
323
322
 
323
+ def get_latest_schema_version(self):
324
+ """Get the latest version of the database schema."""
325
+ pass
326
+
327
+ def upsert_schema_version(self, version: str) -> None:
328
+ """Upsert the schema version into the database."""
329
+ pass
330
+
324
331
  # -- Session methods --
325
332
 
326
333
  async def delete_session(self, session_id: str) -> bool:
@@ -1241,6 +1248,9 @@ class AsyncMongoDb(AsyncBaseDb):
1241
1248
  "memory_id": memory.memory_id,
1242
1249
  "memory": memory.memory,
1243
1250
  "topics": memory.topics,
1251
+ "input": memory.input,
1252
+ "feedback": memory.feedback,
1253
+ "created_at": memory.created_at,
1244
1254
  "updated_at": updated_at,
1245
1255
  }
1246
1256
 
agno/db/mongo/mongo.py CHANGED
@@ -236,6 +236,14 @@ class MongoDb(BaseDb):
236
236
  log_error(f"Error getting collection {collection_name}: {e}")
237
237
  raise
238
238
 
239
+ def get_latest_schema_version(self):
240
+ """Get the latest version of the database schema."""
241
+ pass
242
+
243
+ def upsert_schema_version(self, version: str) -> None:
244
+ """Upsert the schema version into the database."""
245
+ pass
246
+
239
247
  # -- Session methods --
240
248
 
241
249
  def delete_session(self, session_id: str) -> bool:
@@ -1140,7 +1148,10 @@ class MongoDb(BaseDb):
1140
1148
  "team_id": memory.team_id,
1141
1149
  "memory_id": memory.memory_id,
1142
1150
  "memory": memory.memory,
1151
+ "input": memory.input,
1152
+ "feedback": memory.feedback,
1143
1153
  "topics": memory.topics,
1154
+ "created_at": memory.created_at,
1144
1155
  "updated_at": updated_at,
1145
1156
  }
1146
1157
 
agno/db/mongo/schemas.py CHANGED
@@ -19,6 +19,9 @@ MEMORY_COLLECTION_SCHEMA = [
19
19
  {"key": "agent_id"},
20
20
  {"key": "team_id"},
21
21
  {"key": "topics"},
22
+ {"key": "input"},
23
+ {"key": "feedback"},
24
+ {"key": "created_at"},
22
25
  {"key": "updated_at"},
23
26
  ]
24
27
 
agno/db/mongo/utils.py CHANGED
@@ -34,6 +34,23 @@ def create_collection_indexes(collection: Collection, collection_type: str) -> N
34
34
  log_warning(f"Error creating indexes for {collection_type} collection: {e}")
35
35
 
36
36
 
37
+ async def create_collection_indexes_async(collection: Any, collection_type: str) -> None:
38
+ """Create all required indexes for a collection (async version for Motor)"""
39
+ try:
40
+ indexes = get_collection_indexes(collection_type)
41
+ for index_spec in indexes:
42
+ key = index_spec["key"]
43
+ unique = index_spec.get("unique", False)
44
+
45
+ if isinstance(key, list):
46
+ await collection.create_index(key, unique=unique)
47
+ else:
48
+ await collection.create_index([(key, 1)], unique=unique)
49
+
50
+ except Exception as e:
51
+ log_warning(f"Error creating indexes for {collection_type} collection: {e}")
52
+
53
+
37
54
  def apply_sorting(
38
55
  query_args: Dict[str, Any], sort_by: Optional[str] = None, sort_order: Optional[str] = None
39
56
  ) -> List[tuple]:
agno/db/mysql/mysql.py CHANGED
@@ -6,6 +6,7 @@ from uuid import uuid4
6
6
  from sqlalchemy import Index, UniqueConstraint
7
7
 
8
8
  from agno.db.base import BaseDb, SessionType
9
+ from agno.db.migrations.manager import MigrationManager
9
10
  from agno.db.mysql.schemas import get_table_schema_definition
10
11
  from agno.db.mysql.utils import (
11
12
  apply_sorting,
@@ -50,6 +51,7 @@ class MySQLDb(BaseDb):
50
51
  metrics_table: Optional[str] = None,
51
52
  eval_table: Optional[str] = None,
52
53
  knowledge_table: Optional[str] = None,
54
+ versions_table: Optional[str] = None,
53
55
  id: Optional[str] = None,
54
56
  ):
55
57
  """
@@ -70,6 +72,7 @@ class MySQLDb(BaseDb):
70
72
  metrics_table (Optional[str]): Name of the table to store metrics.
71
73
  eval_table (Optional[str]): Name of the table to store evaluation runs data.
72
74
  knowledge_table (Optional[str]): Name of the table to store knowledge content.
75
+ versions_table (Optional[str]): Name of the table to store schema versions.
73
76
  id (Optional[str]): ID of the database.
74
77
 
75
78
  Raises:
@@ -90,6 +93,7 @@ class MySQLDb(BaseDb):
90
93
  metrics_table=metrics_table,
91
94
  eval_table=eval_table,
92
95
  knowledge_table=knowledge_table,
96
+ versions_table=versions_table,
93
97
  )
94
98
 
95
99
  _engine: Optional[Engine] = db_engine
@@ -218,9 +222,15 @@ class MySQLDb(BaseDb):
218
222
  (self.metrics_table_name, "metrics"),
219
223
  (self.eval_table_name, "evals"),
220
224
  (self.knowledge_table_name, "knowledge"),
225
+ (self.versions_table_name, "versions"),
221
226
  ]
222
227
 
223
228
  for table_name, table_type in tables_to_create:
229
+ if table_name != self.versions_table_name:
230
+ # Also store the schema version for the created table
231
+ latest_schema_version = MigrationManager(self).latest_schema_version
232
+ self.upsert_schema_version(table_name=table_name, version=latest_schema_version.public)
233
+
224
234
  self._create_table(table_name=table_name, table_type=table_type, db_schema=self.db_schema)
225
235
 
226
236
  def _get_table(self, table_type: str, create_table_if_not_found: Optional[bool] = False) -> Optional[Table]:
@@ -278,6 +288,15 @@ class MySQLDb(BaseDb):
278
288
  )
279
289
  return self.culture_table
280
290
 
291
+ if table_type == "versions":
292
+ self.versions_table = self._get_or_create_table(
293
+ table_name=self.versions_table_name,
294
+ table_type="versions",
295
+ db_schema=self.db_schema,
296
+ create_table_if_not_found=create_table_if_not_found,
297
+ )
298
+ return self.versions_table
299
+
281
300
  raise ValueError(f"Unknown table type: {table_type}")
282
301
 
283
302
  def _get_or_create_table(
@@ -302,7 +321,14 @@ class MySQLDb(BaseDb):
302
321
  if not create_table_if_not_found:
303
322
  return None
304
323
 
305
- return self._create_table(table_name=table_name, table_type=table_type, db_schema=db_schema)
324
+ created_table = self._create_table(table_name=table_name, table_type=table_type, db_schema=db_schema)
325
+
326
+ if table_name != self.versions_table_name:
327
+ # Also store the schema version for the created table
328
+ latest_schema_version = MigrationManager(self).latest_schema_version
329
+ self.upsert_schema_version(table_name=table_name, version=latest_schema_version.public)
330
+
331
+ return created_table
306
332
 
307
333
  if not is_valid_table(
308
334
  db_engine=self.db_engine,
@@ -321,6 +347,39 @@ class MySQLDb(BaseDb):
321
347
  log_error(f"Error loading existing table {db_schema}.{table_name}: {e}")
322
348
  raise
323
349
 
350
+ def get_latest_schema_version(self, table_name: str) -> str:
351
+ """Get the latest version of the database schema."""
352
+ table = self._get_table(table_type="versions", create_table_if_not_found=True)
353
+ with self.Session() as sess:
354
+ # Latest version for the given table
355
+ stmt = select(table).where(table.c.table_name == table_name).order_by(table.c.version.desc()).limit(1) # type: ignore
356
+ result = sess.execute(stmt).fetchone()
357
+ if result is None:
358
+ return "2.0.0"
359
+ version_dict = dict(result._mapping)
360
+ return version_dict.get("version") or "2.0.0"
361
+
362
+ def upsert_schema_version(self, table_name: str, version: str) -> None:
363
+ """Upsert the schema version into the database."""
364
+ table = self._get_table(table_type="versions", create_table_if_not_found=True)
365
+ if table is None:
366
+ return
367
+ current_datetime = datetime.now().isoformat()
368
+ with self.Session() as sess, sess.begin():
369
+ stmt = mysql.insert(table).values( # type: ignore
370
+ table_name=table_name,
371
+ version=version,
372
+ created_at=current_datetime, # Store as ISO format string
373
+ updated_at=current_datetime,
374
+ )
375
+ # Update version if table_name already exists
376
+ stmt = stmt.on_duplicate_key_update(
377
+ version=version,
378
+ created_at=current_datetime,
379
+ updated_at=current_datetime,
380
+ )
381
+ sess.execute(stmt)
382
+
324
383
  # -- Session methods --
325
384
  def delete_session(self, session_id: str) -> bool:
326
385
  """
@@ -1287,6 +1346,8 @@ class MySQLDb(BaseDb):
1287
1346
  if memory.memory_id is None:
1288
1347
  memory.memory_id = str(uuid4())
1289
1348
 
1349
+ current_time = int(time.time())
1350
+
1290
1351
  stmt = mysql.insert(table).values(
1291
1352
  memory_id=memory.memory_id,
1292
1353
  memory=memory.memory,
@@ -1295,7 +1356,9 @@ class MySQLDb(BaseDb):
1295
1356
  agent_id=memory.agent_id,
1296
1357
  team_id=memory.team_id,
1297
1358
  topics=memory.topics,
1298
- updated_at=int(time.time()),
1359
+ feedback=memory.feedback,
1360
+ created_at=memory.created_at,
1361
+ updated_at=memory.created_at,
1299
1362
  )
1300
1363
  stmt = stmt.on_duplicate_key_update(
1301
1364
  memory=memory.memory,
@@ -1303,7 +1366,10 @@ class MySQLDb(BaseDb):
1303
1366
  input=memory.input,
1304
1367
  agent_id=memory.agent_id,
1305
1368
  team_id=memory.team_id,
1306
- updated_at=int(time.time()),
1369
+ feedback=memory.feedback,
1370
+ updated_at=current_time,
1371
+ # Preserve created_at on update - don't overwrite existing value
1372
+ created_at=table.c.created_at,
1307
1373
  )
1308
1374
  sess.execute(stmt)
1309
1375
 
@@ -1358,12 +1424,14 @@ class MySQLDb(BaseDb):
1358
1424
  # Prepare bulk data
1359
1425
  bulk_data = []
1360
1426
  current_time = int(time.time())
1427
+
1361
1428
  for memory in memories:
1362
1429
  if memory.memory_id is None:
1363
1430
  memory.memory_id = str(uuid4())
1364
1431
 
1365
1432
  # Use preserved updated_at if flag is set and value exists, otherwise use current time
1366
1433
  updated_at = memory.updated_at if preserve_updated_at else current_time
1434
+
1367
1435
  bulk_data.append(
1368
1436
  {
1369
1437
  "memory_id": memory.memory_id,
@@ -1373,6 +1441,8 @@ class MySQLDb(BaseDb):
1373
1441
  "agent_id": memory.agent_id,
1374
1442
  "team_id": memory.team_id,
1375
1443
  "topics": memory.topics,
1444
+ "feedback": memory.feedback,
1445
+ "created_at": memory.created_at,
1376
1446
  "updated_at": updated_at,
1377
1447
  }
1378
1448
  )
@@ -1388,7 +1458,10 @@ class MySQLDb(BaseDb):
1388
1458
  input=stmt.inserted.input,
1389
1459
  agent_id=stmt.inserted.agent_id,
1390
1460
  team_id=stmt.inserted.team_id,
1461
+ feedback=stmt.inserted.feedback,
1391
1462
  updated_at=stmt.inserted.updated_at,
1463
+ # Preserve created_at on update
1464
+ created_at=table.c.created_at,
1392
1465
  )
1393
1466
  sess.execute(stmt, bulk_data)
1394
1467
 
agno/db/mysql/schemas.py CHANGED
@@ -39,6 +39,8 @@ USER_MEMORY_TABLE_SCHEMA = {
39
39
  "team_id": {"type": lambda: String(128), "nullable": True},
40
40
  "user_id": {"type": lambda: String(128), "nullable": True, "index": True},
41
41
  "topics": {"type": JSON, "nullable": True},
42
+ "feedback": {"type": Text, "nullable": True},
43
+ "created_at": {"type": BigInteger, "nullable": False, "index": True},
42
44
  "updated_at": {"type": BigInteger, "nullable": True, "index": True},
43
45
  }
44
46
 
@@ -76,20 +78,20 @@ KNOWLEDGE_TABLE_SCHEMA = {
76
78
 
77
79
  METRICS_TABLE_SCHEMA = {
78
80
  "id": {"type": lambda: String(128), "primary_key": True, "nullable": False},
79
- "agent_runs_count": {"type": BigInteger, "nullable": False},
80
- "team_runs_count": {"type": BigInteger, "nullable": False},
81
- "workflow_runs_count": {"type": BigInteger, "nullable": False},
82
- "agent_sessions_count": {"type": BigInteger, "nullable": False},
83
- "team_sessions_count": {"type": BigInteger, "nullable": False},
84
- "workflow_sessions_count": {"type": BigInteger, "nullable": False},
85
- "users_count": {"type": BigInteger, "nullable": False},
86
- "token_metrics": {"type": JSON, "nullable": False},
87
- "model_metrics": {"type": JSON, "nullable": False},
81
+ "agent_runs_count": {"type": BigInteger, "nullable": False, "default": 0},
82
+ "team_runs_count": {"type": BigInteger, "nullable": False, "default": 0},
83
+ "workflow_runs_count": {"type": BigInteger, "nullable": False, "default": 0},
84
+ "agent_sessions_count": {"type": BigInteger, "nullable": False, "default": 0},
85
+ "team_sessions_count": {"type": BigInteger, "nullable": False, "default": 0},
86
+ "workflow_sessions_count": {"type": BigInteger, "nullable": False, "default": 0},
87
+ "users_count": {"type": BigInteger, "nullable": False, "default": 0},
88
+ "token_metrics": {"type": JSON, "nullable": False, "default": {}},
89
+ "model_metrics": {"type": JSON, "nullable": False, "default": {}},
88
90
  "date": {"type": Date, "nullable": False, "index": True},
89
91
  "aggregation_period": {"type": lambda: String(20), "nullable": False},
90
92
  "created_at": {"type": BigInteger, "nullable": False},
91
93
  "updated_at": {"type": BigInteger, "nullable": True},
92
- "completed": {"type": Boolean, "nullable": False},
94
+ "completed": {"type": Boolean, "nullable": False, "default": False},
93
95
  "_unique_constraints": [
94
96
  {
95
97
  "name": "uq_metrics_date_period",
@@ -111,6 +113,13 @@ CULTURAL_KNOWLEDGE_TABLE_SCHEMA = {
111
113
  "team_id": {"type": lambda: String(128), "nullable": True},
112
114
  }
113
115
 
116
+ VERSIONS_TABLE_SCHEMA = {
117
+ "table_name": {"type": lambda: String(128), "nullable": False, "primary_key": True},
118
+ "version": {"type": lambda: String(10), "nullable": False},
119
+ "created_at": {"type": lambda: String(128), "nullable": False, "index": True},
120
+ "updated_at": {"type": lambda: String(128), "nullable": True},
121
+ }
122
+
114
123
 
115
124
  def get_table_schema_definition(table_type: str) -> dict[str, Any]:
116
125
  """
@@ -129,6 +138,7 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
129
138
  "memories": USER_MEMORY_TABLE_SCHEMA,
130
139
  "knowledge": KNOWLEDGE_TABLE_SCHEMA,
131
140
  "culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
141
+ "versions": VERSIONS_TABLE_SCHEMA,
132
142
  }
133
143
 
134
144
  schema = schemas.get(table_type, {})
@@ -1,9 +1,11 @@
1
1
  import time
2
+ import warnings
2
3
  from datetime import date, datetime, timedelta, timezone
3
4
  from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
4
5
  from uuid import uuid4
5
6
 
6
7
  from agno.db.base import AsyncBaseDb, SessionType
8
+ from agno.db.migrations.manager import MigrationManager
7
9
  from agno.db.postgres.schemas import get_table_schema_definition
8
10
  from agno.db.postgres.utils import (
9
11
  abulk_upsert_metrics,
@@ -47,6 +49,7 @@ class AsyncPostgresDb(AsyncBaseDb):
47
49
  eval_table: Optional[str] = None,
48
50
  knowledge_table: Optional[str] = None,
49
51
  culture_table: Optional[str] = None,
52
+ versions_table: Optional[str] = None,
50
53
  db_id: Optional[str] = None, # Deprecated, use id instead.
51
54
  ):
52
55
  """
@@ -68,6 +71,7 @@ class AsyncPostgresDb(AsyncBaseDb):
68
71
  eval_table (Optional[str]): Name of the table to store evaluation runs data.
69
72
  knowledge_table (Optional[str]): Name of the table to store knowledge content.
70
73
  culture_table (Optional[str]): Name of the table to store cultural knowledge.
74
+ versions_table (Optional[str]): Name of the table to store schema versions.
71
75
  db_id: Deprecated, use id instead.
72
76
 
73
77
  Raises:
@@ -75,7 +79,11 @@ class AsyncPostgresDb(AsyncBaseDb):
75
79
  ValueError: If none of the tables are provided.
76
80
  """
77
81
  if db_id is not None:
78
- log_warning("db_id is deprecated and will be removed in a future version, use id instead.")
82
+ warnings.warn(
83
+ "The 'db_id' parameter is deprecated and will be removed in future versions. Use 'id' instead.",
84
+ DeprecationWarning,
85
+ stacklevel=2,
86
+ )
79
87
 
80
88
  super().__init__(
81
89
  id=id or db_id,
@@ -85,6 +93,7 @@ class AsyncPostgresDb(AsyncBaseDb):
85
93
  eval_table=eval_table,
86
94
  knowledge_table=knowledge_table,
87
95
  culture_table=culture_table,
96
+ versions_table=versions_table,
88
97
  )
89
98
 
90
99
  _engine: Optional[AsyncEngine] = db_engine
@@ -122,9 +131,14 @@ class AsyncPostgresDb(AsyncBaseDb):
122
131
  (self.metrics_table_name, "metrics"),
123
132
  (self.eval_table_name, "evals"),
124
133
  (self.knowledge_table_name, "knowledge"),
134
+ (self.versions_table_name, "versions"),
125
135
  ]
126
136
 
127
137
  for table_name, table_type in tables_to_create:
138
+ # Also store the schema version for the created table
139
+ latest_schema_version = MigrationManager(self).latest_schema_version
140
+ await self.upsert_schema_version(table_name=table_name, version=latest_schema_version.public)
141
+
128
142
  await self._create_table(table_name=table_name, table_type=table_type, db_schema=self.db_schema)
129
143
 
130
144
  async def _create_table(self, table_name: str, table_type: str, db_schema: str) -> Table:
@@ -255,6 +269,13 @@ class AsyncPostgresDb(AsyncBaseDb):
255
269
  )
256
270
  return self.culture_table
257
271
 
272
+ if table_type == "versions":
273
+ if not hasattr(self, "versions_table"):
274
+ self.versions_table = await self._get_or_create_table(
275
+ table_name=self.versions_table_name, table_type="versions", db_schema=self.db_schema
276
+ )
277
+ return self.versions_table
278
+
258
279
  raise ValueError(f"Unknown table type: {table_type}")
259
280
 
260
281
  async def _get_or_create_table(self, table_name: str, table_type: str, db_schema: str) -> Table:
@@ -274,6 +295,11 @@ class AsyncPostgresDb(AsyncBaseDb):
274
295
  table_is_available = await ais_table_available(session=sess, table_name=table_name, db_schema=db_schema)
275
296
 
276
297
  if not table_is_available:
298
+ if table_name != self.versions_table_name:
299
+ # Also store the schema version for the created table
300
+ latest_schema_version = MigrationManager(self).latest_schema_version
301
+ await self.upsert_schema_version(table_name=table_name, version=latest_schema_version.public)
302
+
277
303
  return await self._create_table(table_name=table_name, table_type=table_type, db_schema=db_schema)
278
304
 
279
305
  if not await ais_valid_table(
@@ -291,12 +317,52 @@ class AsyncPostgresDb(AsyncBaseDb):
291
317
  return Table(table_name, self.metadata, schema=db_schema, autoload_with=connection)
292
318
 
293
319
  table = await conn.run_sync(create_table)
320
+
294
321
  return table
295
322
 
296
323
  except Exception as e:
297
324
  log_error(f"Error loading existing table {db_schema}.{table_name}: {e}")
298
325
  raise
299
326
 
327
+ async def get_latest_schema_version(self, table_name: str) -> str:
328
+ """Get the latest version of the database schema."""
329
+ table = await self._get_table(table_type="versions")
330
+ if table is None:
331
+ return "2.0.0"
332
+
333
+ async with self.async_session_factory() as sess:
334
+ stmt = select(table)
335
+ # Latest version for the given table
336
+ stmt = stmt.where(table.c.table_name == table_name)
337
+ stmt = stmt.order_by(table.c.version.desc()).limit(1)
338
+ result = await sess.execute(stmt)
339
+ row = result.fetchone()
340
+ if row is None:
341
+ return "2.0.0"
342
+
343
+ version_dict = dict(row._mapping)
344
+ return version_dict.get("version") or "2.0.0"
345
+
346
+ async def upsert_schema_version(self, table_name: str, version: str) -> None:
347
+ """Upsert the schema version into the database."""
348
+ table = await self._get_table(table_type="versions")
349
+ if table is None:
350
+ return
351
+ current_datetime = datetime.now().isoformat()
352
+ async with self.async_session_factory() as sess, sess.begin():
353
+ stmt = postgresql.insert(table).values(
354
+ table_name=table_name,
355
+ version=version,
356
+ created_at=current_datetime, # Store as ISO format string
357
+ updated_at=current_datetime,
358
+ )
359
+ # Update version if table_name already exists
360
+ stmt = stmt.on_conflict_do_update(
361
+ index_elements=["table_name"],
362
+ set_=dict(version=version, updated_at=current_datetime),
363
+ )
364
+ await sess.execute(stmt)
365
+
300
366
  # -- Session methods --
301
367
  async def delete_session(self, session_id: str) -> bool:
302
368
  """
@@ -1231,36 +1297,44 @@ class AsyncPostgresDb(AsyncBaseDb):
1231
1297
  try:
1232
1298
  table = await self._get_table(table_type="memories")
1233
1299
 
1234
- async with self.async_session_factory() as sess, sess.begin():
1235
- if memory.memory_id is None:
1236
- memory.memory_id = str(uuid4())
1300
+ current_time = int(time.time())
1237
1301
 
1238
- stmt = postgresql.insert(table).values(
1239
- memory_id=memory.memory_id,
1240
- memory=memory.memory,
1241
- input=memory.input,
1242
- user_id=memory.user_id,
1243
- agent_id=memory.agent_id,
1244
- team_id=memory.team_id,
1245
- topics=memory.topics,
1246
- updated_at=int(time.time()),
1247
- )
1248
- stmt = stmt.on_conflict_do_update( # type: ignore
1249
- index_elements=["memory_id"],
1250
- set_=dict(
1302
+ async with self.async_session_factory() as sess:
1303
+ async with sess.begin():
1304
+ if memory.memory_id is None:
1305
+ memory.memory_id = str(uuid4())
1306
+
1307
+ stmt = postgresql.insert(table).values(
1308
+ memory_id=memory.memory_id,
1251
1309
  memory=memory.memory,
1252
- topics=memory.topics,
1253
1310
  input=memory.input,
1311
+ user_id=memory.user_id,
1254
1312
  agent_id=memory.agent_id,
1255
1313
  team_id=memory.team_id,
1256
- updated_at=int(time.time()),
1257
- ),
1258
- ).returning(table)
1314
+ topics=memory.topics,
1315
+ feedback=memory.feedback,
1316
+ created_at=memory.created_at,
1317
+ updated_at=memory.created_at,
1318
+ )
1319
+ stmt = stmt.on_conflict_do_update( # type: ignore
1320
+ index_elements=["memory_id"],
1321
+ set_=dict(
1322
+ memory=memory.memory,
1323
+ topics=memory.topics,
1324
+ input=memory.input,
1325
+ agent_id=memory.agent_id,
1326
+ team_id=memory.team_id,
1327
+ feedback=memory.feedback,
1328
+ updated_at=current_time,
1329
+ # Preserve created_at on update - don't overwrite existing value
1330
+ created_at=table.c.created_at,
1331
+ ),
1332
+ ).returning(table)
1259
1333
 
1260
- result = await sess.execute(stmt)
1261
- row = result.fetchone()
1262
- if row is None:
1263
- return None
1334
+ result = await sess.execute(stmt)
1335
+ row = result.fetchone()
1336
+ if row is None:
1337
+ return None
1264
1338
 
1265
1339
  memory_raw = dict(row._mapping)
1266
1340