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
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
1268
|
-
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
|
-
|
|
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":
|
|
1281
|
-
"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
|
-
|
|
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=
|
|
1410
|
-
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=
|
|
1415
|
-
feedback=
|
|
1493
|
+
topics=sanitized_topics,
|
|
1494
|
+
feedback=sanitized_feedback,
|
|
1416
1495
|
created_at=memory.created_at,
|
|
1417
|
-
updated_at=memory.
|
|
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=
|
|
1423
|
-
topics=
|
|
1424
|
-
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=
|
|
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
|
-
|
|
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, **
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
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:
|