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
@@ -1,7 +1,7 @@
1
1
  import time
2
2
  import warnings
3
3
  from datetime import date, datetime, timedelta, timezone
4
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union
4
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union, cast
5
5
  from uuid import uuid4
6
6
 
7
7
  if TYPE_CHECKING:
@@ -28,9 +28,10 @@ from agno.db.schemas.knowledge import KnowledgeRow
28
28
  from agno.db.schemas.memory import UserMemory
29
29
  from agno.session import AgentSession, Session, TeamSession, WorkflowSession
30
30
  from agno.utils.log import log_debug, log_error, log_info, log_warning
31
+ from agno.utils.string import sanitize_postgres_string, sanitize_postgres_strings
31
32
 
32
33
  try:
33
- from sqlalchemy import Index, String, Table, UniqueConstraint, and_, case, func, or_, update
34
+ from sqlalchemy import ForeignKey, Index, String, Table, UniqueConstraint, and_, case, func, or_, update
34
35
  from sqlalchemy.dialects import postgresql
35
36
  from sqlalchemy.dialects.postgresql import TIMESTAMP
36
37
  from sqlalchemy.exc import ProgrammingError
@@ -68,6 +69,15 @@ class AsyncPostgresDb(AsyncBaseDb):
68
69
  2. Use the db_url
69
70
  3. Raise an error if neither is provided
70
71
 
72
+ Connection Pool Configuration:
73
+ When creating an engine from db_url, the following settings are applied:
74
+ - pool_pre_ping=True: Validates connections before use to handle terminated
75
+ connections (e.g., "terminating connection due to administrator command")
76
+ - pool_recycle=3600: Recycles connections after 1 hour to prevent stale connections
77
+
78
+ These settings help handle connection terminations gracefully. If you need
79
+ custom pool settings, provide a pre-configured db_engine instead.
80
+
71
81
  Args:
72
82
  id (Optional[str]): The ID of the database.
73
83
  db_url (Optional[str]): The database URL to connect to.
@@ -112,7 +122,11 @@ class AsyncPostgresDb(AsyncBaseDb):
112
122
 
113
123
  _engine: Optional[AsyncEngine] = db_engine
114
124
  if _engine is None and db_url is not None:
115
- _engine = create_async_engine(db_url)
125
+ _engine = create_async_engine(
126
+ db_url,
127
+ pool_pre_ping=True,
128
+ pool_recycle=3600,
129
+ )
116
130
  if _engine is None:
117
131
  raise ValueError("One of db_url or db_engine must be provided")
118
132
 
@@ -178,7 +192,10 @@ class AsyncPostgresDb(AsyncBaseDb):
178
192
  Table: SQLAlchemy Table object
179
193
  """
180
194
  try:
181
- table_schema = get_table_schema_definition(table_type).copy()
195
+ # Pass traces_table_name and db_schema for spans table foreign key resolution
196
+ table_schema = get_table_schema_definition(
197
+ table_type, traces_table_name=self.trace_table_name, db_schema=self.db_schema
198
+ ).copy()
182
199
 
183
200
  columns: List[Column] = []
184
201
  indexes: List[str] = []
@@ -198,6 +215,11 @@ class AsyncPostgresDb(AsyncBaseDb):
198
215
  if col_config.get("unique", False):
199
216
  column_kwargs["unique"] = True
200
217
  unique_constraints.append(col_name)
218
+
219
+ # Handle foreign key constraint
220
+ if "foreign_key" in col_config:
221
+ column_args.append(ForeignKey(col_config["foreign_key"]))
222
+
201
223
  columns.append(Column(*column_args, **column_kwargs)) # type: ignore
202
224
 
203
225
  # Create the table object
@@ -522,6 +544,11 @@ class AsyncPostgresDb(AsyncBaseDb):
522
544
 
523
545
  if user_id is not None:
524
546
  stmt = stmt.where(table.c.user_id == user_id)
547
+
548
+ # Filter by session_type to ensure we get the correct session type
549
+ session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
550
+ stmt = stmt.where(table.c.session_type == session_type_value)
551
+
525
552
  result = await sess.execute(stmt)
526
553
  row = result.fetchone()
527
554
  if row is None:
@@ -604,9 +631,7 @@ class AsyncPostgresDb(AsyncBaseDb):
604
631
  stmt = stmt.where(table.c.created_at <= end_timestamp)
605
632
  if session_name is not None:
606
633
  stmt = stmt.where(
607
- func.coalesce(func.json_extract_path_text(table.c.session_data, "session_name"), "").ilike(
608
- f"%{session_name}%"
609
- )
634
+ func.coalesce(table.c.session_data["session_name"].astext, "").ilike(f"%{session_name}%")
610
635
  )
611
636
  if session_type is not None:
612
637
  session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
@@ -670,6 +695,8 @@ class AsyncPostgresDb(AsyncBaseDb):
670
695
  table = await self._get_table(table_type="sessions")
671
696
 
672
697
  async with self.async_session_factory() as sess, sess.begin():
698
+ # Sanitize session_name to remove null bytes
699
+ sanitized_session_name = sanitize_postgres_string(session_name)
673
700
  stmt = (
674
701
  update(table)
675
702
  .where(table.c.session_id == session_id)
@@ -679,7 +706,7 @@ class AsyncPostgresDb(AsyncBaseDb):
679
706
  func.jsonb_set(
680
707
  func.cast(table.c.session_data, postgresql.JSONB),
681
708
  text("'{session_name}'"),
682
- func.to_jsonb(session_name),
709
+ func.to_jsonb(sanitized_session_name),
683
710
  ),
684
711
  postgresql.JSON,
685
712
  )
@@ -732,6 +759,21 @@ class AsyncPostgresDb(AsyncBaseDb):
732
759
  try:
733
760
  table = await self._get_table(table_type="sessions", create_table_if_not_found=True)
734
761
  session_dict = session.to_dict()
762
+ # Sanitize JSON/dict fields to remove null bytes from nested strings
763
+ if session_dict.get("agent_data"):
764
+ session_dict["agent_data"] = sanitize_postgres_strings(session_dict["agent_data"])
765
+ if session_dict.get("team_data"):
766
+ session_dict["team_data"] = sanitize_postgres_strings(session_dict["team_data"])
767
+ if session_dict.get("workflow_data"):
768
+ session_dict["workflow_data"] = sanitize_postgres_strings(session_dict["workflow_data"])
769
+ if session_dict.get("session_data"):
770
+ session_dict["session_data"] = sanitize_postgres_strings(session_dict["session_data"])
771
+ if session_dict.get("summary"):
772
+ session_dict["summary"] = sanitize_postgres_strings(session_dict["summary"])
773
+ if session_dict.get("metadata"):
774
+ session_dict["metadata"] = sanitize_postgres_strings(session_dict["metadata"])
775
+ if session_dict.get("runs"):
776
+ session_dict["runs"] = sanitize_postgres_strings(session_dict["runs"])
735
777
 
736
778
  if isinstance(session, AgentSession):
737
779
  async with self.async_session_factory() as sess, sess.begin():
@@ -929,22 +971,40 @@ class AsyncPostgresDb(AsyncBaseDb):
929
971
  table = await self._get_table(table_type="memories")
930
972
 
931
973
  async with self.async_session_factory() as sess, sess.begin():
974
+ # Filter out NULL topics and ensure topics is an array before extracting elements
975
+ # jsonb_typeof returns 'array' for JSONB arrays
976
+ conditions = [
977
+ table.c.topics.is_not(None),
978
+ func.jsonb_typeof(table.c.topics) == "array",
979
+ ]
980
+ if user_id is not None:
981
+ conditions.append(table.c.user_id == user_id)
982
+
932
983
  try:
933
- stmt = select(func.jsonb_array_elements_text(table.c.topics))
934
- if user_id is not None:
935
- stmt = stmt.where(table.c.user_id == user_id)
984
+ # jsonb_array_elements_text is a set-returning function that must be used with select_from
985
+ stmt = select(func.jsonb_array_elements_text(table.c.topics).label("topic"))
986
+ stmt = stmt.select_from(table)
987
+ stmt = stmt.where(and_(*conditions))
936
988
  result = await sess.execute(stmt)
937
989
  except ProgrammingError:
938
990
  # Retrying with json_array_elements_text. This works in older versions,
939
991
  # where the topics column was of type JSON instead of JSONB
940
- stmt = select(func.json_array_elements_text(table.c.topics))
992
+ # For JSON (not JSONB), we use json_typeof
993
+ json_conditions = [
994
+ table.c.topics.is_not(None),
995
+ func.json_typeof(table.c.topics) == "array",
996
+ ]
941
997
  if user_id is not None:
942
- stmt = stmt.where(table.c.user_id == user_id)
998
+ json_conditions.append(table.c.user_id == user_id)
999
+ stmt = select(func.json_array_elements_text(table.c.topics).label("topic"))
1000
+ stmt = stmt.select_from(table)
1001
+ stmt = stmt.where(and_(*json_conditions))
943
1002
  result = await sess.execute(stmt)
944
1003
 
945
1004
  records = result.fetchall()
946
-
947
- return list(set([record[0] for record in records]))
1005
+ # Extract topics from records - each record is a Row with a 'topic' attribute
1006
+ topics = [record.topic for record in records if record.topic is not None]
1007
+ return list(set(topics))
948
1008
 
949
1009
  except Exception as e:
950
1010
  log_error(f"Exception reading from memory table: {e}")
@@ -1259,16 +1319,26 @@ class AsyncPostgresDb(AsyncBaseDb):
1259
1319
 
1260
1320
  # Serialize content, categories, and notes into a JSON dict for DB storage
1261
1321
  content_dict = serialize_cultural_knowledge(cultural_knowledge)
1322
+ # Sanitize content_dict to remove null bytes from nested strings
1323
+ if content_dict:
1324
+ content_dict = cast(Dict[str, Any], sanitize_postgres_strings(content_dict))
1325
+
1326
+ # Sanitize string fields to remove null bytes (PostgreSQL doesn't allow them)
1327
+ sanitized_name = sanitize_postgres_string(cultural_knowledge.name)
1328
+ sanitized_summary = sanitize_postgres_string(cultural_knowledge.summary)
1329
+ sanitized_input = sanitize_postgres_string(cultural_knowledge.input)
1262
1330
 
1263
1331
  async with self.async_session_factory() as sess, sess.begin():
1264
1332
  # Use PostgreSQL-specific insert with on_conflict_do_update
1265
1333
  insert_stmt = postgresql.insert(table).values(
1266
1334
  id=cultural_knowledge.id,
1267
- name=cultural_knowledge.name,
1268
- summary=cultural_knowledge.summary,
1335
+ name=sanitized_name,
1336
+ summary=sanitized_summary,
1269
1337
  content=content_dict if content_dict else None,
1270
- metadata=cultural_knowledge.metadata,
1271
- input=cultural_knowledge.input,
1338
+ metadata=sanitize_postgres_strings(cultural_knowledge.metadata)
1339
+ if cultural_knowledge.metadata
1340
+ else None,
1341
+ input=sanitized_input,
1272
1342
  created_at=cultural_knowledge.created_at,
1273
1343
  updated_at=int(time.time()),
1274
1344
  agent_id=cultural_knowledge.agent_id,
@@ -1277,11 +1347,13 @@ class AsyncPostgresDb(AsyncBaseDb):
1277
1347
 
1278
1348
  # Update all fields except id on conflict
1279
1349
  update_dict = {
1280
- "name": cultural_knowledge.name,
1281
- "summary": cultural_knowledge.summary,
1350
+ "name": sanitized_name,
1351
+ "summary": sanitized_summary,
1282
1352
  "content": content_dict if content_dict else None,
1283
- "metadata": cultural_knowledge.metadata,
1284
- "input": cultural_knowledge.input,
1353
+ "metadata": sanitize_postgres_strings(cultural_knowledge.metadata)
1354
+ if cultural_knowledge.metadata
1355
+ else None,
1356
+ "input": sanitized_input,
1285
1357
  "updated_at": int(time.time()),
1286
1358
  "agent_id": cultural_knowledge.agent_id,
1287
1359
  "team_id": cultural_knowledge.team_id,
@@ -1399,6 +1471,13 @@ class AsyncPostgresDb(AsyncBaseDb):
1399
1471
 
1400
1472
  current_time = int(time.time())
1401
1473
 
1474
+ # Sanitize string fields to remove null bytes (PostgreSQL doesn't allow them)
1475
+ sanitized_input = sanitize_postgres_string(memory.input)
1476
+ sanitized_feedback = sanitize_postgres_string(memory.feedback)
1477
+ # Sanitize JSONB fields to remove null bytes from nested strings
1478
+ sanitized_memory = sanitize_postgres_strings(memory.memory) if memory.memory else None
1479
+ sanitized_topics = sanitize_postgres_strings(memory.topics) if memory.topics else None
1480
+
1402
1481
  async with self.async_session_factory() as sess:
1403
1482
  async with sess.begin():
1404
1483
  if memory.memory_id is None:
@@ -1406,25 +1485,27 @@ class AsyncPostgresDb(AsyncBaseDb):
1406
1485
 
1407
1486
  stmt = postgresql.insert(table).values(
1408
1487
  memory_id=memory.memory_id,
1409
- memory=memory.memory,
1410
- input=memory.input,
1488
+ memory=sanitized_memory,
1489
+ input=sanitized_input,
1411
1490
  user_id=memory.user_id,
1412
1491
  agent_id=memory.agent_id,
1413
1492
  team_id=memory.team_id,
1414
- topics=memory.topics,
1415
- feedback=memory.feedback,
1493
+ topics=sanitized_topics,
1494
+ feedback=sanitized_feedback,
1416
1495
  created_at=memory.created_at,
1417
- updated_at=memory.created_at,
1496
+ updated_at=memory.updated_at
1497
+ if memory.updated_at is not None
1498
+ else (memory.created_at if memory.created_at is not None else current_time),
1418
1499
  )
1419
1500
  stmt = stmt.on_conflict_do_update( # type: ignore
1420
1501
  index_elements=["memory_id"],
1421
1502
  set_=dict(
1422
- memory=memory.memory,
1423
- topics=memory.topics,
1424
- input=memory.input,
1503
+ memory=sanitized_memory,
1504
+ topics=sanitized_topics,
1505
+ input=sanitized_input,
1425
1506
  agent_id=memory.agent_id,
1426
1507
  team_id=memory.team_id,
1427
- feedback=memory.feedback,
1508
+ feedback=sanitized_feedback,
1428
1509
  updated_at=current_time,
1429
1510
  # Preserve created_at on update - don't overwrite existing value
1430
1511
  created_at=table.c.created_at,
@@ -1538,7 +1619,7 @@ class AsyncPostgresDb(AsyncBaseDb):
1538
1619
  Exception: If an error occurs during metrics calculation.
1539
1620
  """
1540
1621
  try:
1541
- table = await self._get_table(table_type="metrics")
1622
+ table = await self._get_table(table_type="metrics", create_table_if_not_found=True)
1542
1623
 
1543
1624
  starting_date = await self._get_metrics_calculation_starting_date(table)
1544
1625
 
@@ -1614,7 +1695,7 @@ class AsyncPostgresDb(AsyncBaseDb):
1614
1695
  Exception: If an error occurs during retrieval.
1615
1696
  """
1616
1697
  try:
1617
- table = await self._get_table(table_type="metrics")
1698
+ table = await self._get_table(table_type="metrics", create_table_if_not_found=True)
1618
1699
 
1619
1700
  async with self.async_session_factory() as sess, sess.begin():
1620
1701
  stmt = select(table)
@@ -1664,7 +1745,7 @@ class AsyncPostgresDb(AsyncBaseDb):
1664
1745
  Returns:
1665
1746
  Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
1666
1747
  """
1667
- table = await self._get_table(table_type="knowledge")
1748
+ table = await self._get_table(table_type="knowledge", create_table_if_not_found=True)
1668
1749
 
1669
1750
  try:
1670
1751
  async with self.async_session_factory() as sess, sess.begin():
@@ -1708,8 +1789,7 @@ class AsyncPostgresDb(AsyncBaseDb):
1708
1789
  stmt = select(table)
1709
1790
 
1710
1791
  # Apply sorting
1711
- if sort_by is not None:
1712
- stmt = stmt.order_by(getattr(table.c, sort_by) * (1 if sort_order == "asc" else -1))
1792
+ stmt = apply_sorting(stmt, table, sort_by, sort_order)
1713
1793
 
1714
1794
  # Get total count before applying limit and pagination
1715
1795
  count_stmt = select(func.count()).select_from(stmt.alias())
@@ -1766,10 +1846,19 @@ class AsyncPostgresDb(AsyncBaseDb):
1766
1846
  }
1767
1847
 
1768
1848
  # Build insert and update data only for fields that exist in the table
1849
+ # String fields that need sanitization
1850
+ string_fields = {"name", "description", "type", "status", "status_message", "external_id", "linked_to"}
1851
+
1769
1852
  for model_field, table_column in field_mapping.items():
1770
1853
  if table_column in table_columns:
1771
1854
  value = getattr(knowledge_row, model_field, None)
1772
1855
  if value is not None:
1856
+ # Sanitize string fields to remove null bytes
1857
+ if table_column in string_fields and isinstance(value, str):
1858
+ value = sanitize_postgres_string(value)
1859
+ # Sanitize metadata dict if present
1860
+ elif table_column == "metadata" and isinstance(value, dict):
1861
+ value = sanitize_postgres_strings(value)
1773
1862
  insert_data[table_column] = value
1774
1863
  # Don't include ID in update_fields since it's the primary key
1775
1864
  if table_column != "id":
@@ -1820,12 +1909,26 @@ class AsyncPostgresDb(AsyncBaseDb):
1820
1909
  Exception: If an error occurs during creation.
1821
1910
  """
1822
1911
  try:
1823
- table = await self._get_table(table_type="evals")
1912
+ table = await self._get_table(table_type="evals", create_table_if_not_found=True)
1824
1913
 
1825
1914
  async with self.async_session_factory() as sess, sess.begin():
1826
1915
  current_time = int(time.time())
1916
+ eval_data = eval_run.model_dump()
1917
+ # Sanitize string fields in eval_run
1918
+ if eval_data.get("name"):
1919
+ eval_data["name"] = sanitize_postgres_string(eval_data["name"])
1920
+ if eval_data.get("evaluated_component_name"):
1921
+ eval_data["evaluated_component_name"] = sanitize_postgres_string(
1922
+ eval_data["evaluated_component_name"]
1923
+ )
1924
+ # Sanitize nested dicts/JSON fields
1925
+ if eval_data.get("eval_data"):
1926
+ eval_data["eval_data"] = sanitize_postgres_strings(eval_data["eval_data"])
1927
+ if eval_data.get("eval_input"):
1928
+ eval_data["eval_input"] = sanitize_postgres_strings(eval_data["eval_input"])
1929
+
1827
1930
  stmt = postgresql.insert(table).values(
1828
- {"created_at": current_time, "updated_at": current_time, **eval_run.model_dump()}
1931
+ {"created_at": current_time, "updated_at": current_time, **eval_data}
1829
1932
  )
1830
1933
  await sess.execute(stmt)
1831
1934
 
@@ -2027,8 +2130,12 @@ class AsyncPostgresDb(AsyncBaseDb):
2027
2130
  try:
2028
2131
  table = await self._get_table(table_type="evals")
2029
2132
  async with self.async_session_factory() as sess, sess.begin():
2133
+ # Sanitize string field to remove null bytes
2134
+ sanitized_name = sanitize_postgres_string(name)
2030
2135
  stmt = (
2031
- table.update().where(table.c.run_id == eval_run_id).values(name=name, updated_at=int(time.time()))
2136
+ table.update()
2137
+ .where(table.c.run_id == eval_run_id)
2138
+ .values(name=sanitized_name, updated_at=int(time.time()))
2032
2139
  )
2033
2140
  await sess.execute(stmt)
2034
2141
 
@@ -2176,6 +2283,13 @@ class AsyncPostgresDb(AsyncBaseDb):
2176
2283
  trace_dict = trace.to_dict()
2177
2284
  trace_dict.pop("total_spans", None)
2178
2285
  trace_dict.pop("error_count", None)
2286
+ # Sanitize string fields and nested JSON structures
2287
+ if trace_dict.get("name"):
2288
+ trace_dict["name"] = sanitize_postgres_string(trace_dict["name"])
2289
+ if trace_dict.get("status"):
2290
+ trace_dict["status"] = sanitize_postgres_string(trace_dict["status"])
2291
+ # Sanitize any nested dict/JSON fields
2292
+ trace_dict = cast(Dict[str, Any], sanitize_postgres_strings(trace_dict))
2179
2293
 
2180
2294
  async with self.async_session_factory() as sess, sess.begin():
2181
2295
  # Use upsert to handle concurrent inserts atomically
@@ -2494,7 +2608,15 @@ class AsyncPostgresDb(AsyncBaseDb):
2494
2608
  table = await self._get_table(table_type="spans", create_table_if_not_found=True)
2495
2609
 
2496
2610
  async with self.async_session_factory() as sess, sess.begin():
2497
- stmt = postgresql.insert(table).values(span.to_dict())
2611
+ span_dict = span.to_dict()
2612
+ # Sanitize string fields and nested JSON structures
2613
+ if span_dict.get("name"):
2614
+ span_dict["name"] = sanitize_postgres_string(span_dict["name"])
2615
+ if span_dict.get("status_code"):
2616
+ span_dict["status_code"] = sanitize_postgres_string(span_dict["status_code"])
2617
+ # Sanitize any nested dict/JSON fields
2618
+ span_dict = cast(Dict[str, Any], sanitize_postgres_strings(span_dict))
2619
+ stmt = postgresql.insert(table).values(span_dict)
2498
2620
  await sess.execute(stmt)
2499
2621
 
2500
2622
  except Exception as e:
@@ -2514,7 +2636,15 @@ class AsyncPostgresDb(AsyncBaseDb):
2514
2636
 
2515
2637
  async with self.async_session_factory() as sess, sess.begin():
2516
2638
  for span in spans:
2517
- stmt = postgresql.insert(table).values(span.to_dict())
2639
+ span_dict = span.to_dict()
2640
+ # Sanitize string fields and nested JSON structures
2641
+ if span_dict.get("name"):
2642
+ span_dict["name"] = sanitize_postgres_string(span_dict["name"])
2643
+ if span_dict.get("status_code"):
2644
+ span_dict["status_code"] = sanitize_postgres_string(span_dict["status_code"])
2645
+ # Sanitize any nested dict/JSON fields
2646
+ span_dict = sanitize_postgres_strings(span_dict)
2647
+ stmt = postgresql.insert(table).values(span_dict)
2518
2648
  await sess.execute(stmt)
2519
2649
 
2520
2650
  except Exception as e: