agno 2.3.21__py3-none-any.whl → 2.3.23__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 (74) hide show
  1. agno/agent/agent.py +48 -2
  2. agno/agent/remote.py +234 -73
  3. agno/client/a2a/__init__.py +10 -0
  4. agno/client/a2a/client.py +554 -0
  5. agno/client/a2a/schemas.py +112 -0
  6. agno/client/a2a/utils.py +369 -0
  7. agno/db/migrations/utils.py +19 -0
  8. agno/db/migrations/v1_to_v2.py +54 -16
  9. agno/db/migrations/versions/v2_3_0.py +92 -53
  10. agno/db/mysql/async_mysql.py +5 -7
  11. agno/db/mysql/mysql.py +5 -7
  12. agno/db/mysql/schemas.py +39 -21
  13. agno/db/postgres/async_postgres.py +172 -42
  14. agno/db/postgres/postgres.py +186 -38
  15. agno/db/postgres/schemas.py +39 -21
  16. agno/db/postgres/utils.py +6 -2
  17. agno/db/singlestore/schemas.py +41 -21
  18. agno/db/singlestore/singlestore.py +14 -3
  19. agno/db/sqlite/async_sqlite.py +7 -2
  20. agno/db/sqlite/schemas.py +36 -21
  21. agno/db/sqlite/sqlite.py +3 -7
  22. agno/knowledge/chunking/document.py +3 -2
  23. agno/knowledge/chunking/markdown.py +8 -3
  24. agno/knowledge/chunking/recursive.py +2 -2
  25. agno/models/base.py +4 -0
  26. agno/models/google/gemini.py +27 -4
  27. agno/models/openai/chat.py +1 -1
  28. agno/models/openai/responses.py +14 -7
  29. agno/os/middleware/jwt.py +66 -27
  30. agno/os/routers/agents/router.py +3 -3
  31. agno/os/routers/evals/evals.py +2 -2
  32. agno/os/routers/knowledge/knowledge.py +5 -5
  33. agno/os/routers/knowledge/schemas.py +1 -1
  34. agno/os/routers/memory/memory.py +4 -4
  35. agno/os/routers/session/session.py +2 -2
  36. agno/os/routers/teams/router.py +4 -4
  37. agno/os/routers/traces/traces.py +3 -3
  38. agno/os/routers/workflows/router.py +3 -3
  39. agno/os/schema.py +1 -1
  40. agno/reasoning/deepseek.py +11 -1
  41. agno/reasoning/gemini.py +6 -2
  42. agno/reasoning/groq.py +8 -3
  43. agno/reasoning/openai.py +2 -0
  44. agno/remote/base.py +106 -9
  45. agno/skills/__init__.py +17 -0
  46. agno/skills/agent_skills.py +370 -0
  47. agno/skills/errors.py +32 -0
  48. agno/skills/loaders/__init__.py +4 -0
  49. agno/skills/loaders/base.py +27 -0
  50. agno/skills/loaders/local.py +216 -0
  51. agno/skills/skill.py +65 -0
  52. agno/skills/utils.py +107 -0
  53. agno/skills/validator.py +277 -0
  54. agno/team/remote.py +220 -60
  55. agno/team/team.py +41 -3
  56. agno/tools/brandfetch.py +27 -18
  57. agno/tools/browserbase.py +150 -13
  58. agno/tools/function.py +6 -1
  59. agno/tools/mcp/mcp.py +300 -17
  60. agno/tools/mcp/multi_mcp.py +269 -14
  61. agno/tools/toolkit.py +89 -21
  62. agno/utils/mcp.py +49 -8
  63. agno/utils/string.py +43 -1
  64. agno/workflow/condition.py +4 -2
  65. agno/workflow/loop.py +20 -1
  66. agno/workflow/remote.py +173 -33
  67. agno/workflow/router.py +4 -1
  68. agno/workflow/steps.py +4 -0
  69. agno/workflow/workflow.py +14 -0
  70. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/METADATA +13 -14
  71. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/RECORD +74 -60
  72. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/WHEEL +0 -0
  73. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/licenses/LICENSE +0 -0
  74. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,7 @@ import time
10
10
  from typing import Any, List, Tuple
11
11
 
12
12
  from agno.db.base import AsyncBaseDb, BaseDb
13
+ from agno.db.migrations.utils import quote_db_identifier
13
14
  from agno.utils.log import log_error, log_info, log_warning
14
15
 
15
16
  try:
@@ -139,6 +140,9 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
139
140
  from sqlalchemy import text
140
141
 
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)
142
146
 
143
147
  with db.Session() as sess, sess.begin(): # type: ignore
144
148
  # Check if table exists
@@ -181,7 +185,7 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
181
185
  sess.execute(
182
186
  text(
183
187
  f"""
184
- ALTER TABLE {db_schema}.{table_name}
188
+ ALTER TABLE {quoted_schema}.{quoted_table}
185
189
  ADD COLUMN created_at BIGINT
186
190
  """
187
191
  ),
@@ -190,7 +194,7 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
190
194
  sess.execute(
191
195
  text(
192
196
  f"""
193
- UPDATE {db_schema}.{table_name}
197
+ UPDATE {quoted_schema}.{quoted_table}
194
198
  SET created_at = COALESCE(updated_at, :default_time)
195
199
  """
196
200
  ),
@@ -200,7 +204,7 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
200
204
  sess.execute(
201
205
  text(
202
206
  f"""
203
- ALTER TABLE {db_schema}.{table_name}
207
+ ALTER TABLE {quoted_schema}.{quoted_table}
204
208
  ALTER COLUMN created_at SET NOT NULL
205
209
  """
206
210
  ),
@@ -210,7 +214,7 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
210
214
  text(
211
215
  f"""
212
216
  CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at
213
- ON {db_schema}.{table_name}(created_at)
217
+ ON {quoted_schema}.{quoted_table}(created_at)
214
218
  """
215
219
  )
216
220
  )
@@ -221,7 +225,7 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
221
225
  sess.execute(
222
226
  text(
223
227
  f"""
224
- ALTER TABLE {db_schema}.{table_name}
228
+ ALTER TABLE {quoted_schema}.{quoted_table}
225
229
  ADD COLUMN feedback TEXT
226
230
  """
227
231
  )
@@ -231,7 +235,7 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
231
235
  ("memory", table_name),
232
236
  ("topics", table_name),
233
237
  ]
234
- _convert_json_to_jsonb(sess, db_schema, json_columns)
238
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
235
239
 
236
240
  if table_type == "sessions":
237
241
  json_columns = [
@@ -243,37 +247,41 @@ def _migrate_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
243
247
  ("runs", table_name),
244
248
  ("summary", table_name),
245
249
  ]
246
- _convert_json_to_jsonb(sess, db_schema, json_columns)
250
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
247
251
  if table_type == "evals":
248
252
  json_columns = [
249
253
  ("eval_data", table_name),
250
254
  ("eval_input", table_name),
251
255
  ]
252
- _convert_json_to_jsonb(sess, db_schema, json_columns)
256
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
253
257
  if table_type == "metrics":
254
258
  json_columns = [
255
259
  ("token_metrics", table_name),
256
260
  ("model_metrics", table_name),
257
261
  ]
258
- _convert_json_to_jsonb(sess, db_schema, json_columns)
262
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
259
263
  if table_type == "knowledge":
260
264
  json_columns = [
261
265
  ("metadata", table_name),
262
266
  ]
263
- _convert_json_to_jsonb(sess, db_schema, json_columns)
267
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
264
268
  if table_type == "culture":
265
269
  json_columns = [
266
270
  ("metadata", table_name),
267
271
  ]
268
- _convert_json_to_jsonb(sess, db_schema, json_columns)
272
+ _convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
269
273
 
270
274
  sess.commit()
271
275
  return True
272
276
 
273
277
 
274
- def _convert_json_to_jsonb(sess: Any, db_schema: str, json_columns: List[Tuple[str, str]]) -> None:
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
275
282
  for column_name, table_name in json_columns:
276
- table_full_name = f"{db_schema}.{table_name}" if db_schema else table_name
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
277
285
  # Check current type
278
286
  col_type = sess.execute(
279
287
  text(
@@ -305,6 +313,9 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
305
313
  from sqlalchemy import text
306
314
 
307
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)
308
319
 
309
320
  async with db.async_session_factory() as sess, sess.begin(): # type: ignore
310
321
  # Check if table exists
@@ -349,7 +360,7 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
349
360
  await sess.execute(
350
361
  text(
351
362
  f"""
352
- ALTER TABLE {db_schema}.{table_name}
363
+ ALTER TABLE {quoted_schema}.{quoted_table}
353
364
  ADD COLUMN created_at BIGINT
354
365
  """
355
366
  ),
@@ -358,7 +369,7 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
358
369
  await sess.execute(
359
370
  text(
360
371
  f"""
361
- UPDATE {db_schema}.{table_name}
372
+ UPDATE {quoted_schema}.{quoted_table}
362
373
  SET created_at = COALESCE(updated_at, :default_time)
363
374
  """
364
375
  ),
@@ -368,7 +379,7 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
368
379
  await sess.execute(
369
380
  text(
370
381
  f"""
371
- ALTER TABLE {db_schema}.{table_name}
382
+ ALTER TABLE {quoted_schema}.{quoted_table}
372
383
  ALTER COLUMN created_at SET NOT NULL
373
384
  """
374
385
  ),
@@ -378,7 +389,7 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
378
389
  text(
379
390
  f"""
380
391
  CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at
381
- ON {db_schema}.{table_name}(created_at)
392
+ ON {quoted_schema}.{quoted_table}(created_at)
382
393
  """
383
394
  )
384
395
  )
@@ -389,7 +400,7 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
389
400
  await sess.execute(
390
401
  text(
391
402
  f"""
392
- ALTER TABLE {db_schema}.{table_name}
403
+ ALTER TABLE {quoted_schema}.{quoted_table}
393
404
  ADD COLUMN feedback TEXT
394
405
  """
395
406
  )
@@ -399,7 +410,7 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
399
410
  ("memory", table_name),
400
411
  ("topics", table_name),
401
412
  ]
402
- await _async_convert_json_to_jsonb(sess, db_schema, json_columns)
413
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
403
414
  if table_type == "sessions":
404
415
  json_columns = [
405
416
  ("session_data", table_name),
@@ -410,39 +421,43 @@ async def _migrate_async_postgres(db: AsyncBaseDb, table_type: str, table_name:
410
421
  ("runs", table_name),
411
422
  ("summary", table_name),
412
423
  ]
413
- await _async_convert_json_to_jsonb(sess, db_schema, json_columns)
424
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
414
425
 
415
426
  if table_type == "evals":
416
427
  json_columns = [
417
428
  ("eval_data", table_name),
418
429
  ("eval_input", table_name),
419
430
  ]
420
- await _async_convert_json_to_jsonb(sess, db_schema, json_columns)
431
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
421
432
  if table_type == "metrics":
422
433
  json_columns = [
423
434
  ("token_metrics", table_name),
424
435
  ("model_metrics", table_name),
425
436
  ]
426
- await _async_convert_json_to_jsonb(sess, db_schema, json_columns)
437
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
427
438
  if table_type == "knowledge":
428
439
  json_columns = [
429
440
  ("metadata", table_name),
430
441
  ]
431
- await _async_convert_json_to_jsonb(sess, db_schema, json_columns)
442
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
432
443
 
433
444
  if table_type == "culture":
434
445
  json_columns = [
435
446
  ("metadata", table_name),
436
447
  ]
437
- await _async_convert_json_to_jsonb(sess, db_schema, json_columns)
448
+ await _async_convert_json_to_jsonb(sess, db_schema, json_columns, db_type)
438
449
 
439
450
  await sess.commit()
440
451
  return True
441
452
 
442
453
 
443
- async def _async_convert_json_to_jsonb(sess: Any, db_schema: str, json_columns: List[Tuple[str, str]]) -> None:
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
444
458
  for column_name, table_name in json_columns:
445
- table_full_name = f"{db_schema}.{table_name}" if db_schema else table_name
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
446
461
  # Check current type
447
462
  result = await sess.execute(
448
463
  text(
@@ -475,6 +490,9 @@ def _migrate_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
475
490
  from sqlalchemy import text
476
491
 
477
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)
478
496
 
479
497
  with db.Session() as sess, sess.begin(): # type: ignore
480
498
  # Check if table exists
@@ -517,7 +535,7 @@ def _migrate_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
517
535
  sess.execute(
518
536
  text(
519
537
  f"""
520
- ALTER TABLE `{db_schema}`.`{table_name}`
538
+ ALTER TABLE {quoted_schema}.{quoted_table}
521
539
  ADD COLUMN `created_at` BIGINT,
522
540
  ADD INDEX `idx_{table_name}_created_at` (`created_at`)
523
541
  """
@@ -527,7 +545,7 @@ def _migrate_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
527
545
  sess.execute(
528
546
  text(
529
547
  f"""
530
- UPDATE `{db_schema}`.`{table_name}`
548
+ UPDATE {quoted_schema}.{quoted_table}
531
549
  SET `created_at` = COALESCE(`updated_at`, :default_time)
532
550
  """
533
551
  ),
@@ -537,7 +555,7 @@ def _migrate_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
537
555
  sess.execute(
538
556
  text(
539
557
  f"""
540
- ALTER TABLE `{db_schema}`.`{table_name}`
558
+ ALTER TABLE {quoted_schema}.{quoted_table}
541
559
  MODIFY COLUMN `created_at` BIGINT NOT NULL
542
560
  """
543
561
  )
@@ -549,7 +567,7 @@ def _migrate_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
549
567
  sess.execute(
550
568
  text(
551
569
  f"""
552
- ALTER TABLE `{db_schema}`.`{table_name}`
570
+ ALTER TABLE {quoted_schema}.{quoted_table}
553
571
  ADD COLUMN `feedback` TEXT
554
572
  """
555
573
  )
@@ -561,6 +579,8 @@ def _migrate_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
561
579
 
562
580
  def _migrate_sqlite(db: BaseDb, table_type: str, table_name: str) -> bool:
563
581
  """Migrate SQLite database."""
582
+ db_type = type(db).__name__
583
+ quoted_table = quote_db_identifier(db_type, table_name)
564
584
 
565
585
  with db.Session() as sess, sess.begin(): # type: ignore
566
586
  # Check if table exists
@@ -581,7 +601,7 @@ def _migrate_sqlite(db: BaseDb, table_type: str, table_name: str) -> bool:
581
601
  # SQLite doesn't support ALTER TABLE ADD COLUMN with constraints easily
582
602
  # We'll use a simpler approach
583
603
  # Check if columns already exist using PRAGMA
584
- result = sess.execute(text(f"PRAGMA table_info({table_name})"))
604
+ result = sess.execute(text(f"PRAGMA table_info({quoted_table})"))
585
605
  columns_info = result.fetchall()
586
606
  existing_columns = {row[1] for row in columns_info} # row[1] contains column name
587
607
 
@@ -592,13 +612,13 @@ def _migrate_sqlite(db: BaseDb, table_type: str, table_name: str) -> bool:
592
612
  # Add created_at column with NOT NULL constraint and default value
593
613
  # SQLite doesn't support ALTER COLUMN, so we add NOT NULL directly
594
614
  sess.execute(
595
- text(f"ALTER TABLE {table_name} ADD COLUMN created_at BIGINT NOT NULL DEFAULT {current_time}"),
615
+ text(f"ALTER TABLE {quoted_table} ADD COLUMN created_at BIGINT NOT NULL DEFAULT {current_time}"),
596
616
  )
597
617
  # Populate created_at for existing rows
598
618
  sess.execute(
599
619
  text(
600
620
  f"""
601
- UPDATE {table_name}
621
+ UPDATE {quoted_table}
602
622
  SET created_at = COALESCE(updated_at, :default_time)
603
623
  WHERE created_at = :default_time
604
624
  """
@@ -607,13 +627,13 @@ def _migrate_sqlite(db: BaseDb, table_type: str, table_name: str) -> bool:
607
627
  )
608
628
  # Add index
609
629
  sess.execute(
610
- text(f"CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON {table_name}(created_at)")
630
+ text(f"CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON {quoted_table}(created_at)")
611
631
  )
612
632
 
613
633
  # Add feedback if it doesn't exist
614
634
  if "feedback" not in existing_columns:
615
635
  log_info(f"-- Adding feedback column to {table_name}")
616
- sess.execute(text(f"ALTER TABLE {table_name} ADD COLUMN feedback VARCHAR"))
636
+ sess.execute(text(f"ALTER TABLE {quoted_table} ADD COLUMN feedback VARCHAR"))
617
637
 
618
638
  sess.commit()
619
639
  return True
@@ -621,6 +641,8 @@ def _migrate_sqlite(db: BaseDb, table_type: str, table_name: str) -> bool:
621
641
 
622
642
  async def _migrate_async_sqlite(db: AsyncBaseDb, table_type: str, table_name: str) -> bool:
623
643
  """Migrate SQLite database."""
644
+ db_type = type(db).__name__
645
+ quoted_table = quote_db_identifier(db_type, table_name)
624
646
 
625
647
  async with db.async_session_factory() as sess, sess.begin(): # type: ignore
626
648
  # Check if table exists
@@ -642,7 +664,7 @@ async def _migrate_async_sqlite(db: AsyncBaseDb, table_type: str, table_name: st
642
664
  # SQLite doesn't support ALTER TABLE ADD COLUMN with constraints easily
643
665
  # We'll use a simpler approach
644
666
  # Check if columns already exist using PRAGMA
645
- result = await sess.execute(text(f"PRAGMA table_info({table_name})"))
667
+ result = await sess.execute(text(f"PRAGMA table_info({quoted_table})"))
646
668
  columns_info = result.fetchall()
647
669
  existing_columns = {row[1] for row in columns_info} # row[1] contains column name
648
670
 
@@ -653,13 +675,13 @@ async def _migrate_async_sqlite(db: AsyncBaseDb, table_type: str, table_name: st
653
675
  # Add created_at column with NOT NULL constraint and default value
654
676
  # SQLite doesn't support ALTER COLUMN, so we add NOT NULL directly
655
677
  await sess.execute(
656
- text(f"ALTER TABLE {table_name} ADD COLUMN created_at BIGINT NOT NULL DEFAULT {current_time}"),
678
+ text(f"ALTER TABLE {quoted_table} ADD COLUMN created_at BIGINT NOT NULL DEFAULT {current_time}"),
657
679
  )
658
680
  # Populate created_at for existing rows
659
681
  await sess.execute(
660
682
  text(
661
683
  f"""
662
- UPDATE {table_name}
684
+ UPDATE {quoted_table}
663
685
  SET created_at = COALESCE(updated_at, :default_time)
664
686
  WHERE created_at = :default_time
665
687
  """
@@ -668,13 +690,13 @@ async def _migrate_async_sqlite(db: AsyncBaseDb, table_type: str, table_name: st
668
690
  )
669
691
  # Add index
670
692
  await sess.execute(
671
- text(f"CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON {table_name}(created_at)")
693
+ text(f"CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON {quoted_table}(created_at)")
672
694
  )
673
695
 
674
696
  # Add feedback if it doesn't exist
675
697
  if "feedback" not in existing_columns:
676
698
  log_info(f"-- Adding feedback column to {table_name}")
677
- await sess.execute(text(f"ALTER TABLE {table_name} ADD COLUMN feedback VARCHAR"))
699
+ await sess.execute(text(f"ALTER TABLE {quoted_table} ADD COLUMN feedback VARCHAR"))
678
700
 
679
701
  await sess.commit()
680
702
  return True
@@ -685,6 +707,9 @@ def _migrate_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
685
707
  from sqlalchemy import text
686
708
 
687
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)
688
713
 
689
714
  with db.Session() as sess, sess.begin(): # type: ignore
690
715
  # Check if table exists
@@ -727,7 +752,7 @@ def _migrate_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
727
752
  sess.execute(
728
753
  text(
729
754
  f"""
730
- ALTER TABLE `{db_schema}`.`{table_name}`
755
+ ALTER TABLE {quoted_schema}.{quoted_table}
731
756
  ADD COLUMN `created_at` BIGINT,
732
757
  ADD INDEX `idx_{table_name}_created_at` (`created_at`)
733
758
  """
@@ -737,7 +762,7 @@ def _migrate_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
737
762
  sess.execute(
738
763
  text(
739
764
  f"""
740
- UPDATE `{db_schema}`.`{table_name}`
765
+ UPDATE {quoted_schema}.{quoted_table}
741
766
  SET `created_at` = COALESCE(`updated_at`, :default_time)
742
767
  """
743
768
  ),
@@ -750,7 +775,7 @@ def _migrate_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
750
775
  sess.execute(
751
776
  text(
752
777
  f"""
753
- ALTER TABLE `{db_schema}`.`{table_name}`
778
+ ALTER TABLE {quoted_schema}.{quoted_table}
754
779
  ADD COLUMN `feedback` TEXT
755
780
  """
756
781
  )
@@ -765,6 +790,9 @@ def _revert_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
765
790
  from sqlalchemy import text
766
791
 
767
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)
768
796
 
769
797
  with db.Session() as sess, sess.begin(): # type: ignore
770
798
  # Check if table exists
@@ -786,9 +814,9 @@ def _revert_postgres(db: BaseDb, table_type: str, table_name: str) -> bool:
786
814
  return False
787
815
  if table_type == "memories":
788
816
  # Remove columns (in reverse order)
789
- sess.execute(text(f"ALTER TABLE {db_schema}.{table_name} DROP COLUMN IF EXISTS feedback"))
817
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS feedback"))
790
818
  sess.execute(text(f"DROP INDEX IF EXISTS idx_{table_name}_created_at"))
791
- sess.execute(text(f"ALTER TABLE {db_schema}.{table_name} DROP COLUMN IF EXISTS created_at"))
819
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS created_at"))
792
820
  sess.commit()
793
821
  return True
794
822
 
@@ -798,6 +826,9 @@ async def _revert_async_postgres(db: AsyncBaseDb, table_type: str, table_name: s
798
826
  from sqlalchemy import text
799
827
 
800
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)
801
832
 
802
833
  async with db.async_session_factory() as sess, sess.begin(): # type: ignore
803
834
  # Check if table exists
@@ -820,9 +851,9 @@ async def _revert_async_postgres(db: AsyncBaseDb, table_type: str, table_name: s
820
851
  return False
821
852
  if table_type == "memories":
822
853
  # Remove columns (in reverse order)
823
- await sess.execute(text(f"ALTER TABLE {db_schema}.{table_name} DROP COLUMN IF EXISTS feedback"))
854
+ await sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS feedback"))
824
855
  await sess.execute(text(f"DROP INDEX IF EXISTS idx_{table_name}_created_at"))
825
- await sess.execute(text(f"ALTER TABLE {db_schema}.{table_name} DROP COLUMN IF EXISTS created_at"))
856
+ await sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS created_at"))
826
857
  await sess.commit()
827
858
  return True
828
859
 
@@ -832,6 +863,9 @@ def _revert_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
832
863
  from sqlalchemy import text
833
864
 
834
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)
835
869
 
836
870
  with db.Session() as sess, sess.begin(): # type: ignore
837
871
  # Check if table exists
@@ -867,7 +901,7 @@ def _revert_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
867
901
  }
868
902
  # Drop feedback column if it exists
869
903
  if "feedback" in existing_columns:
870
- sess.execute(text(f"ALTER TABLE `{db_schema}`.`{table_name}` DROP COLUMN `feedback`"))
904
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN `feedback`"))
871
905
  # Drop created_at index if it exists
872
906
  index_exists = sess.execute(
873
907
  text(
@@ -881,10 +915,12 @@ def _revert_mysql(db: BaseDb, table_type: str, table_name: str) -> bool:
881
915
  {"schema": db_schema, "table_name": table_name, "index_name": f"idx_{table_name}_created_at"},
882
916
  ).scalar()
883
917
  if index_exists:
884
- sess.execute(text(f"ALTER TABLE `{db_schema}`.`{table_name}` DROP INDEX `idx_{table_name}_created_at`"))
918
+ sess.execute(
919
+ text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP INDEX `idx_{table_name}_created_at`")
920
+ )
885
921
  # Drop created_at column if it exists
886
922
  if "created_at" in existing_columns:
887
- sess.execute(text(f"ALTER TABLE `{db_schema}`.`{table_name}` DROP COLUMN `created_at`"))
923
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN `created_at`"))
888
924
 
889
925
  sess.commit()
890
926
  return True
@@ -909,6 +945,9 @@ def _revert_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
909
945
  from sqlalchemy import text
910
946
 
911
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)
912
951
 
913
952
  with db.Session() as sess, sess.begin(): # type: ignore
914
953
  # Check if table exists
@@ -929,10 +968,10 @@ def _revert_singlestore(db: BaseDb, table_type: str, table_name: str) -> bool:
929
968
  log_info(f"Table {table_name} does not exist, skipping revert")
930
969
  return False
931
970
  if table_type == "memories":
932
- sess.execute(text(f"ALTER TABLE `{db_schema}`.`{table_name}` DROP COLUMN IF EXISTS `feedback`"))
971
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS `feedback`"))
933
972
  sess.execute(
934
- text(f"ALTER TABLE `{db_schema}`.`{table_name}` DROP INDEX IF EXISTS `idx_{table_name}_created_at`")
973
+ text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP INDEX IF EXISTS `idx_{table_name}_created_at`")
935
974
  )
936
- sess.execute(text(f"ALTER TABLE `{db_schema}`.`{table_name}` DROP COLUMN IF EXISTS `created_at`"))
975
+ sess.execute(text(f"ALTER TABLE {quoted_schema}.{quoted_table} DROP COLUMN IF EXISTS `created_at`"))
937
976
  sess.commit()
938
977
  return True
@@ -158,7 +158,10 @@ class AsyncMySQLDb(AsyncBaseDb):
158
158
  Table: SQLAlchemy Table object
159
159
  """
160
160
  try:
161
- table_schema = get_table_schema_definition(table_type).copy()
161
+ # Pass traces_table_name and db_schema for spans table foreign key resolution
162
+ table_schema = get_table_schema_definition(
163
+ table_type, traces_table_name=self.trace_table_name, db_schema=self.db_schema
164
+ ).copy()
162
165
 
163
166
  log_debug(f"Creating table {self.db_schema}.{table_name} with schema: {table_schema}")
164
167
 
@@ -183,12 +186,7 @@ class AsyncMySQLDb(AsyncBaseDb):
183
186
 
184
187
  # Handle foreign key constraint
185
188
  if "foreign_key" in col_config:
186
- fk_ref = col_config["foreign_key"]
187
- # For spans table, dynamically replace the traces table reference
188
- # with the actual trace table name configured for this db instance
189
- if table_type == "spans" and "trace_id" in fk_ref:
190
- fk_ref = f"{self.db_schema}.{self.trace_table_name}.trace_id"
191
- column_args.append(ForeignKey(fk_ref))
189
+ column_args.append(ForeignKey(col_config["foreign_key"]))
192
190
 
193
191
  columns.append(Column(*column_args, **column_kwargs)) # type: ignore
194
192
 
agno/db/mysql/mysql.py CHANGED
@@ -155,7 +155,10 @@ class MySQLDb(BaseDb):
155
155
  Table: SQLAlchemy Table object
156
156
  """
157
157
  try:
158
- table_schema = get_table_schema_definition(table_type).copy()
158
+ # Pass traces_table_name and db_schema for spans table foreign key resolution
159
+ table_schema = get_table_schema_definition(
160
+ table_type, traces_table_name=self.trace_table_name, db_schema=self.db_schema
161
+ ).copy()
159
162
 
160
163
  columns: List[Column] = []
161
164
  indexes: List[str] = []
@@ -178,12 +181,7 @@ class MySQLDb(BaseDb):
178
181
 
179
182
  # Handle foreign key constraint
180
183
  if "foreign_key" in col_config:
181
- fk_ref = col_config["foreign_key"]
182
- # For spans table, dynamically replace the traces table reference
183
- # with the actual trace table name configured for this db instance
184
- if table_type == "spans" and "trace_id" in fk_ref:
185
- fk_ref = f"{self.db_schema}.{self.trace_table_name}.trace_id"
186
- column_args.append(ForeignKey(fk_ref))
184
+ column_args.append(ForeignKey(col_config["foreign_key"]))
187
185
 
188
186
  columns.append(Column(*column_args, **column_kwargs)) # type: ignore
189
187
 
agno/db/mysql/schemas.py CHANGED
@@ -136,37 +136,56 @@ TRACE_TABLE_SCHEMA = {
136
136
  "created_at": {"type": lambda: String(128), "nullable": False, "index": True}, # ISO 8601 datetime string
137
137
  }
138
138
 
139
- SPAN_TABLE_SCHEMA = {
140
- "span_id": {"type": lambda: String(128), "primary_key": True, "nullable": False},
141
- "trace_id": {
142
- "type": lambda: String(128),
143
- "nullable": False,
144
- "index": True,
145
- "foreign_key": "agno_traces.trace_id", # Foreign key to traces table
146
- },
147
- "parent_span_id": {"type": lambda: String(128), "nullable": True, "index": True},
148
- "name": {"type": lambda: String(255), "nullable": False},
149
- "span_kind": {"type": lambda: String(50), "nullable": False},
150
- "status_code": {"type": lambda: String(50), "nullable": False},
151
- "status_message": {"type": Text, "nullable": True},
152
- "start_time": {"type": lambda: String(128), "nullable": False, "index": True}, # ISO 8601 datetime string
153
- "end_time": {"type": lambda: String(128), "nullable": False}, # ISO 8601 datetime string
154
- "duration_ms": {"type": BigInteger, "nullable": False},
155
- "attributes": {"type": JSON, "nullable": True},
156
- "created_at": {"type": lambda: String(128), "nullable": False, "index": True}, # ISO 8601 datetime string
157
- }
158
139
 
140
+ def _get_span_table_schema(traces_table_name: str = "agno_traces", db_schema: str = "agno") -> dict[str, Any]:
141
+ """Get the span table schema with the correct foreign key reference.
142
+
143
+ Args:
144
+ traces_table_name: The name of the traces table to reference in the foreign key.
145
+ db_schema: The database schema name.
146
+
147
+ Returns:
148
+ The span table schema dictionary.
149
+ """
150
+ return {
151
+ "span_id": {"type": lambda: String(128), "primary_key": True, "nullable": False},
152
+ "trace_id": {
153
+ "type": lambda: String(128),
154
+ "nullable": False,
155
+ "index": True,
156
+ "foreign_key": f"{db_schema}.{traces_table_name}.trace_id",
157
+ },
158
+ "parent_span_id": {"type": lambda: String(128), "nullable": True, "index": True},
159
+ "name": {"type": lambda: String(255), "nullable": False},
160
+ "span_kind": {"type": lambda: String(50), "nullable": False},
161
+ "status_code": {"type": lambda: String(50), "nullable": False},
162
+ "status_message": {"type": Text, "nullable": True},
163
+ "start_time": {"type": lambda: String(128), "nullable": False, "index": True}, # ISO 8601 datetime string
164
+ "end_time": {"type": lambda: String(128), "nullable": False}, # ISO 8601 datetime string
165
+ "duration_ms": {"type": BigInteger, "nullable": False},
166
+ "attributes": {"type": JSON, "nullable": True},
167
+ "created_at": {"type": lambda: String(128), "nullable": False, "index": True}, # ISO 8601 datetime string
168
+ }
159
169
 
160
- def get_table_schema_definition(table_type: str) -> dict[str, Any]:
170
+
171
+ def get_table_schema_definition(
172
+ table_type: str, traces_table_name: str = "agno_traces", db_schema: str = "agno"
173
+ ) -> dict[str, Any]:
161
174
  """
162
175
  Get the expected schema definition for the given table.
163
176
 
164
177
  Args:
165
178
  table_type (str): The type of table to get the schema for.
179
+ traces_table_name (str): The name of the traces table (used for spans foreign key).
180
+ db_schema (str): The database schema name (used for spans foreign key).
166
181
 
167
182
  Returns:
168
183
  Dict[str, Any]: Dictionary containing column definitions for the table
169
184
  """
185
+ # Handle spans table specially to resolve the foreign key reference
186
+ if table_type == "spans":
187
+ return _get_span_table_schema(traces_table_name, db_schema)
188
+
170
189
  schemas = {
171
190
  "sessions": SESSION_TABLE_SCHEMA,
172
191
  "evals": EVAL_TABLE_SCHEMA,
@@ -176,7 +195,6 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
176
195
  "culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
177
196
  "versions": VERSIONS_TABLE_SCHEMA,
178
197
  "traces": TRACE_TABLE_SCHEMA,
179
- "spans": SPAN_TABLE_SCHEMA,
180
198
  }
181
199
 
182
200
  schema = schemas.get(table_type, {})