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.
- agno/agent/agent.py +48 -2
- agno/agent/remote.py +234 -73
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/v2_3_0.py +92 -53
- agno/db/mysql/async_mysql.py +5 -7
- agno/db/mysql/mysql.py +5 -7
- agno/db/mysql/schemas.py +39 -21
- agno/db/postgres/async_postgres.py +172 -42
- agno/db/postgres/postgres.py +186 -38
- agno/db/postgres/schemas.py +39 -21
- agno/db/postgres/utils.py +6 -2
- agno/db/singlestore/schemas.py +41 -21
- agno/db/singlestore/singlestore.py +14 -3
- agno/db/sqlite/async_sqlite.py +7 -2
- agno/db/sqlite/schemas.py +36 -21
- agno/db/sqlite/sqlite.py +3 -7
- agno/knowledge/chunking/document.py +3 -2
- agno/knowledge/chunking/markdown.py +8 -3
- agno/knowledge/chunking/recursive.py +2 -2
- agno/models/base.py +4 -0
- agno/models/google/gemini.py +27 -4
- agno/models/openai/chat.py +1 -1
- agno/models/openai/responses.py +14 -7
- agno/os/middleware/jwt.py +66 -27
- agno/os/routers/agents/router.py +3 -3
- agno/os/routers/evals/evals.py +2 -2
- agno/os/routers/knowledge/knowledge.py +5 -5
- agno/os/routers/knowledge/schemas.py +1 -1
- agno/os/routers/memory/memory.py +4 -4
- agno/os/routers/session/session.py +2 -2
- agno/os/routers/teams/router.py +4 -4
- agno/os/routers/traces/traces.py +3 -3
- agno/os/routers/workflows/router.py +3 -3
- agno/os/schema.py +1 -1
- agno/reasoning/deepseek.py +11 -1
- agno/reasoning/gemini.py +6 -2
- agno/reasoning/groq.py +8 -3
- agno/reasoning/openai.py +2 -0
- agno/remote/base.py +106 -9
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +370 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/team/remote.py +220 -60
- agno/team/team.py +41 -3
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +150 -13
- agno/tools/function.py +6 -1
- agno/tools/mcp/mcp.py +300 -17
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/toolkit.py +89 -21
- agno/utils/mcp.py +49 -8
- agno/utils/string.py +43 -1
- agno/workflow/condition.py +4 -2
- agno/workflow/loop.py +20 -1
- agno/workflow/remote.py +173 -33
- agno/workflow/router.py +4 -1
- agno/workflow/steps.py +4 -0
- agno/workflow/workflow.py +14 -0
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/METADATA +13 -14
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/RECORD +74 -60
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/WHEEL +0 -0
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/top_level.txt +0 -0
agno/db/postgres/postgres.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from datetime import date, datetime, timedelta, timezone
|
|
3
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union, cast
|
|
4
4
|
from uuid import uuid4
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
@@ -27,7 +27,7 @@ from agno.db.schemas.knowledge import KnowledgeRow
|
|
|
27
27
|
from agno.db.schemas.memory import UserMemory
|
|
28
28
|
from agno.session import AgentSession, Session, TeamSession, WorkflowSession
|
|
29
29
|
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
30
|
-
from agno.utils.string import generate_id
|
|
30
|
+
from agno.utils.string import generate_id, sanitize_postgres_string, sanitize_postgres_strings
|
|
31
31
|
|
|
32
32
|
try:
|
|
33
33
|
from sqlalchemy import ForeignKey, Index, String, UniqueConstraint, and_, case, func, or_, select, update
|
|
@@ -91,7 +91,11 @@ class PostgresDb(BaseDb):
|
|
|
91
91
|
"""
|
|
92
92
|
_engine: Optional[Engine] = db_engine
|
|
93
93
|
if _engine is None and db_url is not None:
|
|
94
|
-
_engine = create_engine(
|
|
94
|
+
_engine = create_engine(
|
|
95
|
+
db_url,
|
|
96
|
+
pool_pre_ping=True,
|
|
97
|
+
pool_recycle=3600,
|
|
98
|
+
)
|
|
95
99
|
if _engine is None:
|
|
96
100
|
raise ValueError("One of db_url or db_engine must be provided")
|
|
97
101
|
|
|
@@ -172,7 +176,10 @@ class PostgresDb(BaseDb):
|
|
|
172
176
|
Table: SQLAlchemy Table object
|
|
173
177
|
"""
|
|
174
178
|
try:
|
|
175
|
-
|
|
179
|
+
# Pass traces_table_name and db_schema for spans table foreign key resolution
|
|
180
|
+
table_schema = get_table_schema_definition(
|
|
181
|
+
table_type, traces_table_name=self.trace_table_name, db_schema=self.db_schema
|
|
182
|
+
).copy()
|
|
176
183
|
|
|
177
184
|
columns: List[Column] = []
|
|
178
185
|
indexes: List[str] = []
|
|
@@ -195,12 +202,7 @@ class PostgresDb(BaseDb):
|
|
|
195
202
|
|
|
196
203
|
# Handle foreign key constraint
|
|
197
204
|
if "foreign_key" in col_config:
|
|
198
|
-
|
|
199
|
-
# For spans table, dynamically replace the traces table reference
|
|
200
|
-
# with the actual trace table name configured for this db instance
|
|
201
|
-
if table_type == "spans" and "trace_id" in fk_ref:
|
|
202
|
-
fk_ref = f"{self.db_schema}.{self.trace_table_name}.trace_id"
|
|
203
|
-
column_args.append(ForeignKey(fk_ref))
|
|
205
|
+
column_args.append(ForeignKey(col_config["foreign_key"]))
|
|
204
206
|
|
|
205
207
|
columns.append(Column(*column_args, **column_kwargs)) # type: ignore
|
|
206
208
|
|
|
@@ -512,6 +514,11 @@ class PostgresDb(BaseDb):
|
|
|
512
514
|
|
|
513
515
|
if user_id is not None:
|
|
514
516
|
stmt = stmt.where(table.c.user_id == user_id)
|
|
517
|
+
|
|
518
|
+
# Filter by session_type to ensure we get the correct session type
|
|
519
|
+
session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
|
|
520
|
+
stmt = stmt.where(table.c.session_type == session_type_value)
|
|
521
|
+
|
|
515
522
|
result = sess.execute(stmt).fetchone()
|
|
516
523
|
if result is None:
|
|
517
524
|
return None
|
|
@@ -596,9 +603,7 @@ class PostgresDb(BaseDb):
|
|
|
596
603
|
stmt = stmt.where(table.c.created_at <= end_timestamp)
|
|
597
604
|
if session_name is not None:
|
|
598
605
|
stmt = stmt.where(
|
|
599
|
-
func.coalesce(
|
|
600
|
-
f"%{session_name}%"
|
|
601
|
-
)
|
|
606
|
+
func.coalesce(table.c.session_data["session_name"].astext, "").ilike(f"%{session_name}%")
|
|
602
607
|
)
|
|
603
608
|
if session_type is not None:
|
|
604
609
|
session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
|
|
@@ -663,6 +668,8 @@ class PostgresDb(BaseDb):
|
|
|
663
668
|
return None
|
|
664
669
|
|
|
665
670
|
with self.Session() as sess, sess.begin():
|
|
671
|
+
# Sanitize session_name to remove null bytes
|
|
672
|
+
sanitized_session_name = sanitize_postgres_string(session_name)
|
|
666
673
|
stmt = (
|
|
667
674
|
update(table)
|
|
668
675
|
.where(table.c.session_id == session_id)
|
|
@@ -672,7 +679,7 @@ class PostgresDb(BaseDb):
|
|
|
672
679
|
func.jsonb_set(
|
|
673
680
|
func.cast(table.c.session_data, postgresql.JSONB),
|
|
674
681
|
text("'{session_name}'"),
|
|
675
|
-
func.to_jsonb(
|
|
682
|
+
func.to_jsonb(sanitized_session_name),
|
|
676
683
|
),
|
|
677
684
|
postgresql.JSON,
|
|
678
685
|
)
|
|
@@ -728,6 +735,21 @@ class PostgresDb(BaseDb):
|
|
|
728
735
|
return None
|
|
729
736
|
|
|
730
737
|
session_dict = session.to_dict()
|
|
738
|
+
# Sanitize JSON/dict fields to remove null bytes from nested strings
|
|
739
|
+
if session_dict.get("agent_data"):
|
|
740
|
+
session_dict["agent_data"] = sanitize_postgres_strings(session_dict["agent_data"])
|
|
741
|
+
if session_dict.get("team_data"):
|
|
742
|
+
session_dict["team_data"] = sanitize_postgres_strings(session_dict["team_data"])
|
|
743
|
+
if session_dict.get("workflow_data"):
|
|
744
|
+
session_dict["workflow_data"] = sanitize_postgres_strings(session_dict["workflow_data"])
|
|
745
|
+
if session_dict.get("session_data"):
|
|
746
|
+
session_dict["session_data"] = sanitize_postgres_strings(session_dict["session_data"])
|
|
747
|
+
if session_dict.get("summary"):
|
|
748
|
+
session_dict["summary"] = sanitize_postgres_strings(session_dict["summary"])
|
|
749
|
+
if session_dict.get("metadata"):
|
|
750
|
+
session_dict["metadata"] = sanitize_postgres_strings(session_dict["metadata"])
|
|
751
|
+
if session_dict.get("runs"):
|
|
752
|
+
session_dict["runs"] = sanitize_postgres_strings(session_dict["runs"])
|
|
731
753
|
|
|
732
754
|
if isinstance(session, AgentSession):
|
|
733
755
|
with self.Session() as sess, sess.begin():
|
|
@@ -881,6 +903,18 @@ class PostgresDb(BaseDb):
|
|
|
881
903
|
session_records = []
|
|
882
904
|
for agent_session in agent_sessions:
|
|
883
905
|
session_dict = agent_session.to_dict()
|
|
906
|
+
# Sanitize JSON/dict fields to remove null bytes from nested strings
|
|
907
|
+
if session_dict.get("agent_data"):
|
|
908
|
+
session_dict["agent_data"] = sanitize_postgres_strings(session_dict["agent_data"])
|
|
909
|
+
if session_dict.get("session_data"):
|
|
910
|
+
session_dict["session_data"] = sanitize_postgres_strings(session_dict["session_data"])
|
|
911
|
+
if session_dict.get("summary"):
|
|
912
|
+
session_dict["summary"] = sanitize_postgres_strings(session_dict["summary"])
|
|
913
|
+
if session_dict.get("metadata"):
|
|
914
|
+
session_dict["metadata"] = sanitize_postgres_strings(session_dict["metadata"])
|
|
915
|
+
if session_dict.get("runs"):
|
|
916
|
+
session_dict["runs"] = sanitize_postgres_strings(session_dict["runs"])
|
|
917
|
+
|
|
884
918
|
# Use preserved updated_at if flag is set (even if None), otherwise use current time
|
|
885
919
|
updated_at = session_dict.get("updated_at") if preserve_updated_at else int(time.time())
|
|
886
920
|
session_records.append(
|
|
@@ -926,6 +960,18 @@ class PostgresDb(BaseDb):
|
|
|
926
960
|
session_records = []
|
|
927
961
|
for team_session in team_sessions:
|
|
928
962
|
session_dict = team_session.to_dict()
|
|
963
|
+
# Sanitize JSON/dict fields to remove null bytes from nested strings
|
|
964
|
+
if session_dict.get("team_data"):
|
|
965
|
+
session_dict["team_data"] = sanitize_postgres_strings(session_dict["team_data"])
|
|
966
|
+
if session_dict.get("session_data"):
|
|
967
|
+
session_dict["session_data"] = sanitize_postgres_strings(session_dict["session_data"])
|
|
968
|
+
if session_dict.get("summary"):
|
|
969
|
+
session_dict["summary"] = sanitize_postgres_strings(session_dict["summary"])
|
|
970
|
+
if session_dict.get("metadata"):
|
|
971
|
+
session_dict["metadata"] = sanitize_postgres_strings(session_dict["metadata"])
|
|
972
|
+
if session_dict.get("runs"):
|
|
973
|
+
session_dict["runs"] = sanitize_postgres_strings(session_dict["runs"])
|
|
974
|
+
|
|
929
975
|
# Use preserved updated_at if flag is set (even if None), otherwise use current time
|
|
930
976
|
updated_at = session_dict.get("updated_at") if preserve_updated_at else int(time.time())
|
|
931
977
|
session_records.append(
|
|
@@ -971,6 +1017,18 @@ class PostgresDb(BaseDb):
|
|
|
971
1017
|
session_records = []
|
|
972
1018
|
for workflow_session in workflow_sessions:
|
|
973
1019
|
session_dict = workflow_session.to_dict()
|
|
1020
|
+
# Sanitize JSON/dict fields to remove null bytes from nested strings
|
|
1021
|
+
if session_dict.get("workflow_data"):
|
|
1022
|
+
session_dict["workflow_data"] = sanitize_postgres_strings(session_dict["workflow_data"])
|
|
1023
|
+
if session_dict.get("session_data"):
|
|
1024
|
+
session_dict["session_data"] = sanitize_postgres_strings(session_dict["session_data"])
|
|
1025
|
+
if session_dict.get("summary"):
|
|
1026
|
+
session_dict["summary"] = sanitize_postgres_strings(session_dict["summary"])
|
|
1027
|
+
if session_dict.get("metadata"):
|
|
1028
|
+
session_dict["metadata"] = sanitize_postgres_strings(session_dict["metadata"])
|
|
1029
|
+
if session_dict.get("runs"):
|
|
1030
|
+
session_dict["runs"] = sanitize_postgres_strings(session_dict["runs"])
|
|
1031
|
+
|
|
974
1032
|
# Use preserved updated_at if flag is set (even if None), otherwise use current time
|
|
975
1033
|
updated_at = session_dict.get("updated_at") if preserve_updated_at else int(time.time())
|
|
976
1034
|
session_records.append(
|
|
@@ -1098,16 +1156,35 @@ class PostgresDb(BaseDb):
|
|
|
1098
1156
|
return []
|
|
1099
1157
|
|
|
1100
1158
|
with self.Session() as sess, sess.begin():
|
|
1159
|
+
# Filter out NULL topics and ensure topics is an array before extracting elements
|
|
1160
|
+
# jsonb_typeof returns 'array' for JSONB arrays
|
|
1161
|
+
conditions = [
|
|
1162
|
+
table.c.topics.is_not(None),
|
|
1163
|
+
func.jsonb_typeof(table.c.topics) == "array",
|
|
1164
|
+
]
|
|
1165
|
+
|
|
1101
1166
|
try:
|
|
1102
|
-
|
|
1167
|
+
# jsonb_array_elements_text is a set-returning function that must be used with select_from
|
|
1168
|
+
stmt = select(func.jsonb_array_elements_text(table.c.topics).label("topic"))
|
|
1169
|
+
stmt = stmt.select_from(table)
|
|
1170
|
+
stmt = stmt.where(and_(*conditions))
|
|
1103
1171
|
result = sess.execute(stmt).fetchall()
|
|
1104
1172
|
except ProgrammingError:
|
|
1105
1173
|
# Retrying with json_array_elements_text. This works in older versions,
|
|
1106
1174
|
# where the topics column was of type JSON instead of JSONB
|
|
1107
|
-
|
|
1175
|
+
# For JSON (not JSONB), we use json_typeof
|
|
1176
|
+
json_conditions = [
|
|
1177
|
+
table.c.topics.is_not(None),
|
|
1178
|
+
func.json_typeof(table.c.topics) == "array",
|
|
1179
|
+
]
|
|
1180
|
+
stmt = select(func.json_array_elements_text(table.c.topics).label("topic"))
|
|
1181
|
+
stmt = stmt.select_from(table)
|
|
1182
|
+
stmt = stmt.where(and_(*json_conditions))
|
|
1108
1183
|
result = sess.execute(stmt).fetchall()
|
|
1109
1184
|
|
|
1110
|
-
|
|
1185
|
+
# Extract topics from records - each record is a Row with a 'topic' attribute
|
|
1186
|
+
topics = [record.topic for record in result if record.topic is not None]
|
|
1187
|
+
return list(set(topics))
|
|
1111
1188
|
|
|
1112
1189
|
except Exception as e:
|
|
1113
1190
|
log_error(f"Exception reading from memory table: {e}")
|
|
@@ -1348,6 +1425,10 @@ class PostgresDb(BaseDb):
|
|
|
1348
1425
|
if table is None:
|
|
1349
1426
|
return None
|
|
1350
1427
|
|
|
1428
|
+
# Sanitize string fields to remove null bytes (PostgreSQL doesn't allow them)
|
|
1429
|
+
sanitized_input = sanitize_postgres_string(memory.input)
|
|
1430
|
+
sanitized_feedback = sanitize_postgres_string(memory.feedback)
|
|
1431
|
+
|
|
1351
1432
|
with self.Session() as sess, sess.begin():
|
|
1352
1433
|
if memory.memory_id is None:
|
|
1353
1434
|
memory.memory_id = str(uuid4())
|
|
@@ -1357,24 +1438,26 @@ class PostgresDb(BaseDb):
|
|
|
1357
1438
|
stmt = postgresql.insert(table).values(
|
|
1358
1439
|
memory_id=memory.memory_id,
|
|
1359
1440
|
memory=memory.memory,
|
|
1360
|
-
input=
|
|
1441
|
+
input=sanitized_input,
|
|
1361
1442
|
user_id=memory.user_id,
|
|
1362
1443
|
agent_id=memory.agent_id,
|
|
1363
1444
|
team_id=memory.team_id,
|
|
1364
1445
|
topics=memory.topics,
|
|
1365
|
-
feedback=
|
|
1446
|
+
feedback=sanitized_feedback,
|
|
1366
1447
|
created_at=memory.created_at,
|
|
1367
|
-
updated_at=memory.
|
|
1448
|
+
updated_at=memory.updated_at
|
|
1449
|
+
if memory.updated_at is not None
|
|
1450
|
+
else (memory.created_at if memory.created_at is not None else current_time),
|
|
1368
1451
|
)
|
|
1369
1452
|
stmt = stmt.on_conflict_do_update( # type: ignore
|
|
1370
1453
|
index_elements=["memory_id"],
|
|
1371
1454
|
set_=dict(
|
|
1372
1455
|
memory=memory.memory,
|
|
1373
1456
|
topics=memory.topics,
|
|
1374
|
-
input=
|
|
1457
|
+
input=sanitized_input,
|
|
1375
1458
|
agent_id=memory.agent_id,
|
|
1376
1459
|
team_id=memory.team_id,
|
|
1377
|
-
feedback=
|
|
1460
|
+
feedback=sanitized_feedback,
|
|
1378
1461
|
updated_at=current_time,
|
|
1379
1462
|
# Preserve created_at on update - don't overwrite existing value
|
|
1380
1463
|
created_at=table.c.created_at,
|
|
@@ -1432,16 +1515,20 @@ class PostgresDb(BaseDb):
|
|
|
1432
1515
|
# Use preserved updated_at if flag is set (even if None), otherwise use current time
|
|
1433
1516
|
updated_at = memory.updated_at if preserve_updated_at else current_time
|
|
1434
1517
|
|
|
1518
|
+
# Sanitize string fields to remove null bytes (PostgreSQL doesn't allow them)
|
|
1519
|
+
sanitized_input = sanitize_postgres_string(memory.input)
|
|
1520
|
+
sanitized_feedback = sanitize_postgres_string(memory.feedback)
|
|
1521
|
+
|
|
1435
1522
|
memory_records.append(
|
|
1436
1523
|
{
|
|
1437
1524
|
"memory_id": memory.memory_id,
|
|
1438
1525
|
"memory": memory.memory,
|
|
1439
|
-
"input":
|
|
1526
|
+
"input": sanitized_input,
|
|
1440
1527
|
"user_id": memory.user_id,
|
|
1441
1528
|
"agent_id": memory.agent_id,
|
|
1442
1529
|
"team_id": memory.team_id,
|
|
1443
1530
|
"topics": memory.topics,
|
|
1444
|
-
"feedback":
|
|
1531
|
+
"feedback": sanitized_feedback,
|
|
1445
1532
|
"created_at": memory.created_at,
|
|
1446
1533
|
"updated_at": updated_at,
|
|
1447
1534
|
}
|
|
@@ -1747,8 +1834,7 @@ class PostgresDb(BaseDb):
|
|
|
1747
1834
|
stmt = select(table)
|
|
1748
1835
|
|
|
1749
1836
|
# Apply sorting
|
|
1750
|
-
|
|
1751
|
-
stmt = stmt.order_by(getattr(table.c, sort_by) * (1 if sort_order == "asc" else -1))
|
|
1837
|
+
stmt = apply_sorting(stmt, table, sort_by, sort_order)
|
|
1752
1838
|
|
|
1753
1839
|
# Get total count before applying limit and pagination
|
|
1754
1840
|
count_stmt = select(func.count()).select_from(stmt.alias())
|
|
@@ -1807,10 +1893,19 @@ class PostgresDb(BaseDb):
|
|
|
1807
1893
|
}
|
|
1808
1894
|
|
|
1809
1895
|
# Build insert and update data only for fields that exist in the table
|
|
1896
|
+
# String fields that need sanitization
|
|
1897
|
+
string_fields = {"name", "description", "type", "status", "status_message", "external_id", "linked_to"}
|
|
1898
|
+
|
|
1810
1899
|
for model_field, table_column in field_mapping.items():
|
|
1811
1900
|
if table_column in table_columns:
|
|
1812
1901
|
value = getattr(knowledge_row, model_field, None)
|
|
1813
1902
|
if value is not None:
|
|
1903
|
+
# Sanitize string fields to remove null bytes
|
|
1904
|
+
if table_column in string_fields and isinstance(value, str):
|
|
1905
|
+
value = sanitize_postgres_string(value)
|
|
1906
|
+
# Sanitize metadata dict if present
|
|
1907
|
+
elif table_column == "metadata" and isinstance(value, dict):
|
|
1908
|
+
value = sanitize_postgres_strings(value)
|
|
1814
1909
|
insert_data[table_column] = value
|
|
1815
1910
|
# Don't include ID in update_fields since it's the primary key
|
|
1816
1911
|
if table_column != "id":
|
|
@@ -1865,8 +1960,22 @@ class PostgresDb(BaseDb):
|
|
|
1865
1960
|
|
|
1866
1961
|
with self.Session() as sess, sess.begin():
|
|
1867
1962
|
current_time = int(time.time())
|
|
1963
|
+
eval_data = eval_run.model_dump()
|
|
1964
|
+
# Sanitize string fields in eval_run
|
|
1965
|
+
if eval_data.get("name"):
|
|
1966
|
+
eval_data["name"] = sanitize_postgres_string(eval_data["name"])
|
|
1967
|
+
if eval_data.get("evaluated_component_name"):
|
|
1968
|
+
eval_data["evaluated_component_name"] = sanitize_postgres_string(
|
|
1969
|
+
eval_data["evaluated_component_name"]
|
|
1970
|
+
)
|
|
1971
|
+
# Sanitize nested dicts/JSON fields
|
|
1972
|
+
if eval_data.get("eval_data"):
|
|
1973
|
+
eval_data["eval_data"] = sanitize_postgres_strings(eval_data["eval_data"])
|
|
1974
|
+
if eval_data.get("eval_input"):
|
|
1975
|
+
eval_data["eval_input"] = sanitize_postgres_strings(eval_data["eval_input"])
|
|
1976
|
+
|
|
1868
1977
|
stmt = postgresql.insert(table).values(
|
|
1869
|
-
{"created_at": current_time, "updated_at": current_time, **
|
|
1978
|
+
{"created_at": current_time, "updated_at": current_time, **eval_data}
|
|
1870
1979
|
)
|
|
1871
1980
|
sess.execute(stmt)
|
|
1872
1981
|
|
|
@@ -2080,8 +2189,12 @@ class PostgresDb(BaseDb):
|
|
|
2080
2189
|
return None
|
|
2081
2190
|
|
|
2082
2191
|
with self.Session() as sess, sess.begin():
|
|
2192
|
+
# Sanitize string field to remove null bytes
|
|
2193
|
+
sanitized_name = sanitize_postgres_string(name)
|
|
2083
2194
|
stmt = (
|
|
2084
|
-
table.update()
|
|
2195
|
+
table.update()
|
|
2196
|
+
.where(table.c.run_id == eval_run_id)
|
|
2197
|
+
.values(name=sanitized_name, updated_at=int(time.time()))
|
|
2085
2198
|
)
|
|
2086
2199
|
sess.execute(stmt)
|
|
2087
2200
|
|
|
@@ -2278,15 +2391,25 @@ class PostgresDb(BaseDb):
|
|
|
2278
2391
|
|
|
2279
2392
|
# Serialize content, categories, and notes into a JSON dict for DB storage
|
|
2280
2393
|
content_dict = serialize_cultural_knowledge(cultural_knowledge)
|
|
2394
|
+
# Sanitize content_dict to remove null bytes from nested strings
|
|
2395
|
+
if content_dict:
|
|
2396
|
+
content_dict = cast(Dict[str, Any], sanitize_postgres_strings(content_dict))
|
|
2397
|
+
|
|
2398
|
+
# Sanitize string fields to remove null bytes (PostgreSQL doesn't allow them)
|
|
2399
|
+
sanitized_name = sanitize_postgres_string(cultural_knowledge.name)
|
|
2400
|
+
sanitized_summary = sanitize_postgres_string(cultural_knowledge.summary)
|
|
2401
|
+
sanitized_input = sanitize_postgres_string(cultural_knowledge.input)
|
|
2281
2402
|
|
|
2282
2403
|
with self.Session() as sess, sess.begin():
|
|
2283
2404
|
stmt = postgresql.insert(table).values(
|
|
2284
2405
|
id=cultural_knowledge.id,
|
|
2285
|
-
name=
|
|
2286
|
-
summary=
|
|
2406
|
+
name=sanitized_name,
|
|
2407
|
+
summary=sanitized_summary,
|
|
2287
2408
|
content=content_dict if content_dict else None,
|
|
2288
|
-
metadata=cultural_knowledge.metadata
|
|
2289
|
-
|
|
2409
|
+
metadata=sanitize_postgres_strings(cultural_knowledge.metadata)
|
|
2410
|
+
if cultural_knowledge.metadata
|
|
2411
|
+
else None,
|
|
2412
|
+
input=sanitized_input,
|
|
2290
2413
|
created_at=cultural_knowledge.created_at,
|
|
2291
2414
|
updated_at=int(time.time()),
|
|
2292
2415
|
agent_id=cultural_knowledge.agent_id,
|
|
@@ -2295,11 +2418,13 @@ class PostgresDb(BaseDb):
|
|
|
2295
2418
|
stmt = stmt.on_conflict_do_update( # type: ignore
|
|
2296
2419
|
index_elements=["id"],
|
|
2297
2420
|
set_=dict(
|
|
2298
|
-
name=
|
|
2299
|
-
summary=
|
|
2421
|
+
name=sanitized_name,
|
|
2422
|
+
summary=sanitized_summary,
|
|
2300
2423
|
content=content_dict if content_dict else None,
|
|
2301
|
-
metadata=cultural_knowledge.metadata
|
|
2302
|
-
|
|
2424
|
+
metadata=sanitize_postgres_strings(cultural_knowledge.metadata)
|
|
2425
|
+
if cultural_knowledge.metadata
|
|
2426
|
+
else None,
|
|
2427
|
+
input=sanitized_input,
|
|
2303
2428
|
updated_at=int(time.time()),
|
|
2304
2429
|
agent_id=cultural_knowledge.agent_id,
|
|
2305
2430
|
team_id=cultural_knowledge.team_id,
|
|
@@ -2458,6 +2583,13 @@ class PostgresDb(BaseDb):
|
|
|
2458
2583
|
trace_dict = trace.to_dict()
|
|
2459
2584
|
trace_dict.pop("total_spans", None)
|
|
2460
2585
|
trace_dict.pop("error_count", None)
|
|
2586
|
+
# Sanitize string fields and nested JSON structures
|
|
2587
|
+
if trace_dict.get("name"):
|
|
2588
|
+
trace_dict["name"] = sanitize_postgres_string(trace_dict["name"])
|
|
2589
|
+
if trace_dict.get("status"):
|
|
2590
|
+
trace_dict["status"] = sanitize_postgres_string(trace_dict["status"])
|
|
2591
|
+
# Sanitize any nested dict/JSON fields
|
|
2592
|
+
trace_dict = cast(Dict[str, Any], sanitize_postgres_strings(trace_dict))
|
|
2461
2593
|
|
|
2462
2594
|
with self.Session() as sess, sess.begin():
|
|
2463
2595
|
# Use upsert to handle concurrent inserts atomically
|
|
@@ -2781,7 +2913,15 @@ class PostgresDb(BaseDb):
|
|
|
2781
2913
|
return
|
|
2782
2914
|
|
|
2783
2915
|
with self.Session() as sess, sess.begin():
|
|
2784
|
-
|
|
2916
|
+
span_dict = span.to_dict()
|
|
2917
|
+
# Sanitize string fields and nested JSON structures
|
|
2918
|
+
if span_dict.get("name"):
|
|
2919
|
+
span_dict["name"] = sanitize_postgres_string(span_dict["name"])
|
|
2920
|
+
if span_dict.get("status_code"):
|
|
2921
|
+
span_dict["status_code"] = sanitize_postgres_string(span_dict["status_code"])
|
|
2922
|
+
# Sanitize any nested dict/JSON fields
|
|
2923
|
+
span_dict = cast(Dict[str, Any], sanitize_postgres_strings(span_dict))
|
|
2924
|
+
stmt = postgresql.insert(table).values(span_dict)
|
|
2785
2925
|
sess.execute(stmt)
|
|
2786
2926
|
|
|
2787
2927
|
except Exception as e:
|
|
@@ -2803,7 +2943,15 @@ class PostgresDb(BaseDb):
|
|
|
2803
2943
|
|
|
2804
2944
|
with self.Session() as sess, sess.begin():
|
|
2805
2945
|
for span in spans:
|
|
2806
|
-
|
|
2946
|
+
span_dict = span.to_dict()
|
|
2947
|
+
# Sanitize string fields and nested JSON structures
|
|
2948
|
+
if span_dict.get("name"):
|
|
2949
|
+
span_dict["name"] = sanitize_postgres_string(span_dict["name"])
|
|
2950
|
+
if span_dict.get("status_code"):
|
|
2951
|
+
span_dict["status_code"] = sanitize_postgres_string(span_dict["status_code"])
|
|
2952
|
+
# Sanitize any nested dict/JSON fields
|
|
2953
|
+
span_dict = sanitize_postgres_strings(span_dict)
|
|
2954
|
+
stmt = postgresql.insert(table).values(span_dict)
|
|
2807
2955
|
sess.execute(stmt)
|
|
2808
2956
|
|
|
2809
2957
|
except Exception as e:
|
agno/db/postgres/schemas.py
CHANGED
|
@@ -137,37 +137,56 @@ TRACE_TABLE_SCHEMA = {
|
|
|
137
137
|
"created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
SPAN_TABLE_SCHEMA = {
|
|
141
|
-
"span_id": {"type": String, "primary_key": True, "nullable": False},
|
|
142
|
-
"trace_id": {
|
|
143
|
-
"type": String,
|
|
144
|
-
"nullable": False,
|
|
145
|
-
"index": True,
|
|
146
|
-
"foreign_key": "agno_traces.trace_id", # Foreign key to traces table
|
|
147
|
-
},
|
|
148
|
-
"parent_span_id": {"type": String, "nullable": True, "index": True},
|
|
149
|
-
"name": {"type": String, "nullable": False},
|
|
150
|
-
"span_kind": {"type": String, "nullable": False},
|
|
151
|
-
"status_code": {"type": String, "nullable": False},
|
|
152
|
-
"status_message": {"type": Text, "nullable": True},
|
|
153
|
-
"start_time": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
154
|
-
"end_time": {"type": String, "nullable": False}, # ISO 8601 datetime string
|
|
155
|
-
"duration_ms": {"type": BigInteger, "nullable": False},
|
|
156
|
-
"attributes": {"type": JSONB, "nullable": True},
|
|
157
|
-
"created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
158
|
-
}
|
|
159
140
|
|
|
141
|
+
def _get_span_table_schema(traces_table_name: str = "agno_traces", db_schema: str = "agno") -> dict[str, Any]:
|
|
142
|
+
"""Get the span table schema with the correct foreign key reference.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
traces_table_name: The name of the traces table to reference in the foreign key.
|
|
146
|
+
db_schema: The database schema name.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The span table schema dictionary.
|
|
150
|
+
"""
|
|
151
|
+
return {
|
|
152
|
+
"span_id": {"type": String, "primary_key": True, "nullable": False},
|
|
153
|
+
"trace_id": {
|
|
154
|
+
"type": String,
|
|
155
|
+
"nullable": False,
|
|
156
|
+
"index": True,
|
|
157
|
+
"foreign_key": f"{db_schema}.{traces_table_name}.trace_id",
|
|
158
|
+
},
|
|
159
|
+
"parent_span_id": {"type": String, "nullable": True, "index": True},
|
|
160
|
+
"name": {"type": String, "nullable": False},
|
|
161
|
+
"span_kind": {"type": String, "nullable": False},
|
|
162
|
+
"status_code": {"type": String, "nullable": False},
|
|
163
|
+
"status_message": {"type": Text, "nullable": True},
|
|
164
|
+
"start_time": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
165
|
+
"end_time": {"type": String, "nullable": False}, # ISO 8601 datetime string
|
|
166
|
+
"duration_ms": {"type": BigInteger, "nullable": False},
|
|
167
|
+
"attributes": {"type": JSONB, "nullable": True},
|
|
168
|
+
"created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
169
|
+
}
|
|
160
170
|
|
|
161
|
-
|
|
171
|
+
|
|
172
|
+
def get_table_schema_definition(
|
|
173
|
+
table_type: str, traces_table_name: str = "agno_traces", db_schema: str = "agno"
|
|
174
|
+
) -> dict[str, Any]:
|
|
162
175
|
"""
|
|
163
176
|
Get the expected schema definition for the given table.
|
|
164
177
|
|
|
165
178
|
Args:
|
|
166
179
|
table_type (str): The type of table to get the schema for.
|
|
180
|
+
traces_table_name (str): The name of the traces table (used for spans foreign key).
|
|
181
|
+
db_schema (str): The database schema name (used for spans foreign key).
|
|
167
182
|
|
|
168
183
|
Returns:
|
|
169
184
|
Dict[str, Any]: Dictionary containing column definitions for the table
|
|
170
185
|
"""
|
|
186
|
+
# Handle spans table specially to resolve the foreign key reference
|
|
187
|
+
if table_type == "spans":
|
|
188
|
+
return _get_span_table_schema(traces_table_name, db_schema)
|
|
189
|
+
|
|
171
190
|
schemas = {
|
|
172
191
|
"sessions": SESSION_TABLE_SCHEMA,
|
|
173
192
|
"evals": EVAL_TABLE_SCHEMA,
|
|
@@ -177,7 +196,6 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
|
177
196
|
"culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
|
|
178
197
|
"versions": VERSIONS_TABLE_SCHEMA,
|
|
179
198
|
"traces": TRACE_TABLE_SCHEMA,
|
|
180
|
-
"spans": SPAN_TABLE_SCHEMA,
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
schema = schemas.get(table_type, {})
|
agno/db/postgres/utils.py
CHANGED
|
@@ -15,6 +15,7 @@ from agno.utils.log import log_debug, log_error, log_warning
|
|
|
15
15
|
try:
|
|
16
16
|
from sqlalchemy import Table, func
|
|
17
17
|
from sqlalchemy.dialects import postgresql
|
|
18
|
+
from sqlalchemy.exc import NoSuchTableError
|
|
18
19
|
from sqlalchemy.inspection import inspect
|
|
19
20
|
from sqlalchemy.orm import Session
|
|
20
21
|
from sqlalchemy.sql.expression import text
|
|
@@ -183,6 +184,9 @@ async def ais_valid_table(db_engine: AsyncEngine, table_name: str, table_type: s
|
|
|
183
184
|
return False
|
|
184
185
|
|
|
185
186
|
return True
|
|
187
|
+
except NoSuchTableError:
|
|
188
|
+
log_error(f"Table {db_schema}.{table_name} does not exist")
|
|
189
|
+
return False
|
|
186
190
|
except Exception as e:
|
|
187
191
|
log_error(f"Error validating table schema for {db_schema}.{table_name}: {e}")
|
|
188
192
|
return False
|
|
@@ -317,8 +321,8 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
|
|
|
317
321
|
model_counts[f"{model_id}:{model_provider}"] = (
|
|
318
322
|
model_counts.get(f"{model_id}:{model_provider}", 0) + 1
|
|
319
323
|
)
|
|
320
|
-
|
|
321
|
-
session_metrics =
|
|
324
|
+
session_data = session.get("session_data", {}) or {}
|
|
325
|
+
session_metrics = session_data.get("session_metrics", {}) or {}
|
|
322
326
|
for field in token_metrics:
|
|
323
327
|
token_metrics[field] += session_metrics.get(field, 0)
|
|
324
328
|
|
agno/db/singlestore/schemas.py
CHANGED
|
@@ -131,35 +131,56 @@ TRACE_TABLE_SCHEMA = {
|
|
|
131
131
|
"created_at": {"type": lambda: String(64), "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
SPAN_TABLE_SCHEMA = {
|
|
135
|
-
"span_id": {"type": lambda: String(128), "primary_key": True, "nullable": False},
|
|
136
|
-
"trace_id": {
|
|
137
|
-
"type": lambda: String(128),
|
|
138
|
-
"nullable": False,
|
|
139
|
-
"index": True,
|
|
140
|
-
"foreign_key": "agno_traces.trace_id", # Foreign key to traces table
|
|
141
|
-
},
|
|
142
|
-
"parent_span_id": {"type": lambda: String(128), "nullable": True, "index": True},
|
|
143
|
-
"name": {"type": lambda: String(512), "nullable": False},
|
|
144
|
-
"span_kind": {"type": lambda: String(50), "nullable": False},
|
|
145
|
-
"status_code": {"type": lambda: String(20), "nullable": False},
|
|
146
|
-
"status_message": {"type": Text, "nullable": True},
|
|
147
|
-
"start_time": {"type": lambda: String(64), "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
148
|
-
"end_time": {"type": lambda: String(64), "nullable": False}, # ISO 8601 datetime string
|
|
149
|
-
"duration_ms": {"type": BigInteger, "nullable": False},
|
|
150
|
-
"attributes": {"type": JSON, "nullable": True},
|
|
151
|
-
"created_at": {"type": lambda: String(64), "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
152
|
-
}
|
|
153
134
|
|
|
135
|
+
def _get_span_table_schema(traces_table_name: str = "agno_traces", db_schema: str = "agno") -> dict[str, Any]:
|
|
136
|
+
"""Get the span table schema with the correct foreign key reference.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
traces_table_name: The name of the traces table to reference in the foreign key.
|
|
140
|
+
db_schema: The database schema name.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The span table schema dictionary.
|
|
144
|
+
"""
|
|
145
|
+
return {
|
|
146
|
+
"span_id": {"type": lambda: String(128), "primary_key": True, "nullable": False},
|
|
147
|
+
"trace_id": {
|
|
148
|
+
"type": lambda: String(128),
|
|
149
|
+
"nullable": False,
|
|
150
|
+
"index": True,
|
|
151
|
+
"foreign_key": f"{db_schema}.{traces_table_name}.trace_id",
|
|
152
|
+
},
|
|
153
|
+
"parent_span_id": {"type": lambda: String(128), "nullable": True, "index": True},
|
|
154
|
+
"name": {"type": lambda: String(512), "nullable": False},
|
|
155
|
+
"span_kind": {"type": lambda: String(50), "nullable": False},
|
|
156
|
+
"status_code": {"type": lambda: String(20), "nullable": False},
|
|
157
|
+
"status_message": {"type": Text, "nullable": True},
|
|
158
|
+
"start_time": {"type": lambda: String(64), "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
159
|
+
"end_time": {"type": lambda: String(64), "nullable": False}, # ISO 8601 datetime string
|
|
160
|
+
"duration_ms": {"type": BigInteger, "nullable": False},
|
|
161
|
+
"attributes": {"type": JSON, "nullable": True},
|
|
162
|
+
"created_at": {"type": lambda: String(64), "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
163
|
+
}
|
|
154
164
|
|
|
155
|
-
|
|
165
|
+
|
|
166
|
+
def get_table_schema_definition(
|
|
167
|
+
table_type: str, traces_table_name: str = "agno_traces", db_schema: str = "agno"
|
|
168
|
+
) -> dict[str, Any]:
|
|
156
169
|
"""
|
|
157
170
|
Get the expected schema definition for the given table.
|
|
171
|
+
|
|
158
172
|
Args:
|
|
159
173
|
table_type (str): The type of table to get the schema for.
|
|
174
|
+
traces_table_name (str): The name of the traces table (used for spans foreign key).
|
|
175
|
+
db_schema (str): The database schema name (used for spans foreign key).
|
|
176
|
+
|
|
160
177
|
Returns:
|
|
161
178
|
Dict[str, Any]: Dictionary containing column definitions for the table
|
|
162
179
|
"""
|
|
180
|
+
# Handle spans table specially to resolve the foreign key reference
|
|
181
|
+
if table_type == "spans":
|
|
182
|
+
return _get_span_table_schema(traces_table_name, db_schema)
|
|
183
|
+
|
|
163
184
|
schemas = {
|
|
164
185
|
"sessions": SESSION_TABLE_SCHEMA,
|
|
165
186
|
"evals": EVAL_TABLE_SCHEMA,
|
|
@@ -169,7 +190,6 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
|
169
190
|
"culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
|
|
170
191
|
"versions": VERSIONS_TABLE_SCHEMA,
|
|
171
192
|
"traces": TRACE_TABLE_SCHEMA,
|
|
172
|
-
"spans": SPAN_TABLE_SCHEMA,
|
|
173
193
|
}
|
|
174
194
|
schema = schemas.get(table_type, {})
|
|
175
195
|
|