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
@@ -0,0 +1,369 @@
1
+ """Utility functions for mapping between A2A and Agno data structures.
2
+
3
+ This module provides bidirectional mapping between:
4
+ - A2A TaskResult ↔ Agno RunOutput / TeamRunOutput / WorkflowRunOutput
5
+ - A2A StreamEvent ↔ Agno RunOutputEvent / TeamRunOutputEvent / WorkflowRunOutputEvent
6
+ """
7
+
8
+ from typing import AsyncIterator, List, Optional, Union
9
+
10
+ from agno.client.a2a.schemas import Artifact, StreamEvent, TaskResult
11
+ from agno.media import Audio, File, Image, Video
12
+ from agno.run.agent import (
13
+ RunCompletedEvent,
14
+ RunContentEvent,
15
+ RunOutput,
16
+ RunOutputEvent,
17
+ RunStartedEvent,
18
+ )
19
+ from agno.run.team import (
20
+ RunCompletedEvent as TeamRunCompletedEvent,
21
+ )
22
+ from agno.run.team import (
23
+ RunContentEvent as TeamRunContentEvent,
24
+ )
25
+ from agno.run.team import (
26
+ RunStartedEvent as TeamRunStartedEvent,
27
+ )
28
+ from agno.run.team import (
29
+ TeamRunOutput,
30
+ TeamRunOutputEvent,
31
+ )
32
+ from agno.run.workflow import (
33
+ WorkflowCompletedEvent,
34
+ WorkflowRunOutput,
35
+ WorkflowRunOutputEvent,
36
+ WorkflowStartedEvent,
37
+ )
38
+
39
+
40
+ def map_task_result_to_run_output(
41
+ task_result: TaskResult,
42
+ agent_id: str,
43
+ user_id: Optional[str] = None,
44
+ ) -> RunOutput:
45
+ """Convert A2A TaskResult to Agno RunOutput.
46
+
47
+ Maps the A2A protocol response structure to Agno's internal format,
48
+ enabling seamless integration with Agno's agent infrastructure.
49
+
50
+ Args:
51
+ task_result: A2A TaskResult from send_message()
52
+ agent_id: Agent identifier to include in output
53
+ user_id: Optional user identifier to include in output
54
+
55
+ Returns:
56
+ RunOutput: Agno-compatible run output
57
+ """
58
+ # Extract media from artifacts
59
+ images: List[Image] = []
60
+ videos: List[Video] = []
61
+ audio: List[Audio] = []
62
+ files: List[File] = []
63
+
64
+ for artifact in task_result.artifacts:
65
+ _classify_artifact(artifact, images, videos, audio, files)
66
+
67
+ return RunOutput(
68
+ content=task_result.content,
69
+ run_id=task_result.task_id,
70
+ session_id=task_result.context_id,
71
+ agent_id=agent_id,
72
+ user_id=user_id,
73
+ images=images if images else None,
74
+ videos=videos if videos else None,
75
+ audio=audio if audio else None,
76
+ files=files if files else None,
77
+ metadata=task_result.metadata,
78
+ )
79
+
80
+
81
+ def _classify_artifact(
82
+ artifact: Artifact,
83
+ images: List[Image],
84
+ videos: List[Video],
85
+ audio: List[Audio],
86
+ files: List[File],
87
+ ) -> None:
88
+ """Classify an A2A artifact into the appropriate media type list.
89
+
90
+ Args:
91
+ artifact: A2A artifact to classify
92
+ images: List to append images to
93
+ videos: List to append videos to
94
+ audio: List to append audio to
95
+ files: List to append generic files to
96
+ """
97
+ mime_type = artifact.mime_type or ""
98
+ uri = artifact.uri
99
+
100
+ if not uri:
101
+ return
102
+
103
+ if mime_type.startswith("image/"):
104
+ images.append(Image(url=uri, name=artifact.name))
105
+ elif mime_type.startswith("video/"):
106
+ videos.append(Video(url=uri, name=artifact.name))
107
+ elif mime_type.startswith("audio/"):
108
+ audio.append(Audio(url=uri, name=artifact.name))
109
+ else:
110
+ files.append(File(url=uri, name=artifact.name, mime_type=mime_type or None))
111
+
112
+
113
+ async def map_stream_events_to_run_events(
114
+ stream: AsyncIterator[StreamEvent],
115
+ agent_id: str,
116
+ ) -> AsyncIterator[RunOutputEvent]:
117
+ """Convert A2A stream events to Agno run events.
118
+
119
+ Transforms the A2A streaming protocol events into Agno's event system,
120
+ enabling real-time streaming from A2A servers to work with Agno consumers.
121
+
122
+ Args:
123
+ stream: AsyncIterator of A2A StreamEvents
124
+ agent_id: Optional agent identifier to include in events
125
+ user_id: Optional user identifier to include in events
126
+
127
+ Yields:
128
+ RunOutputEvent: Agno-compatible run output events
129
+ """
130
+ run_id: Optional[str] = None
131
+ session_id: Optional[str] = None
132
+ accumulated_content = ""
133
+
134
+ async for event in stream:
135
+ # Capture IDs from events
136
+ if event.task_id:
137
+ run_id = event.task_id
138
+ if event.context_id:
139
+ session_id = event.context_id
140
+
141
+ # Map event types
142
+ if event.event_type == "working":
143
+ yield RunStartedEvent(
144
+ run_id=run_id,
145
+ session_id=session_id,
146
+ agent_id=agent_id,
147
+ )
148
+
149
+ elif event.is_content and event.content:
150
+ accumulated_content += event.content
151
+ yield RunContentEvent(
152
+ content=event.content,
153
+ run_id=run_id,
154
+ session_id=session_id,
155
+ agent_id=agent_id,
156
+ )
157
+
158
+ elif event.is_final:
159
+ # Use content from final event or accumulated content
160
+ final_content = event.content if event.content else accumulated_content
161
+ yield RunCompletedEvent(
162
+ content=final_content,
163
+ run_id=run_id,
164
+ session_id=session_id,
165
+ agent_id=agent_id,
166
+ )
167
+ break # Stream complete
168
+
169
+
170
+ # =============================================================================
171
+ # Team Run Output Mapping Functions
172
+ # =============================================================================
173
+
174
+
175
+ def map_task_result_to_team_run_output(
176
+ task_result: TaskResult,
177
+ team_id: str,
178
+ user_id: Optional[str] = None,
179
+ ) -> TeamRunOutput:
180
+ """Convert A2A TaskResult to Agno TeamRunOutput.
181
+
182
+ Maps the A2A protocol response structure to Agno's team format,
183
+ enabling seamless integration with Agno's team infrastructure.
184
+
185
+ Args:
186
+ task_result: A2A TaskResult from send_message()
187
+ team_id: Optional team identifier to include in output
188
+ user_id: Optional user identifier to include in output
189
+ Returns:
190
+ TeamRunOutput: Agno-compatible team run output
191
+ """
192
+ # Extract media from artifacts
193
+ images: List[Image] = []
194
+ videos: List[Video] = []
195
+ audio: List[Audio] = []
196
+ files: List[File] = []
197
+
198
+ for artifact in task_result.artifacts:
199
+ _classify_artifact(artifact, images, videos, audio, files)
200
+
201
+ return TeamRunOutput(
202
+ content=task_result.content,
203
+ run_id=task_result.task_id,
204
+ session_id=task_result.context_id,
205
+ team_id=team_id,
206
+ user_id=user_id,
207
+ images=images if images else None,
208
+ videos=videos if videos else None,
209
+ audio=audio if audio else None,
210
+ files=files if files else None,
211
+ metadata=task_result.metadata,
212
+ )
213
+
214
+
215
+ async def map_stream_events_to_team_run_events(
216
+ stream: AsyncIterator[StreamEvent],
217
+ team_id: str,
218
+ ) -> AsyncIterator[TeamRunOutputEvent]:
219
+ """Convert A2A stream events to Agno team run events.
220
+
221
+ Transforms the A2A streaming protocol events into Agno's team event system,
222
+ enabling real-time streaming from A2A servers to work with Agno team consumers.
223
+
224
+ Args:
225
+ stream: AsyncIterator of A2A StreamEvents
226
+ team_id: Optional team identifier to include in events
227
+ user_id: Optional user identifier to include in events
228
+ Yields:
229
+ TeamRunOutputEvent: Agno-compatible team run output events
230
+ """
231
+ run_id: Optional[str] = None
232
+ session_id: Optional[str] = None
233
+ accumulated_content = ""
234
+
235
+ async for event in stream:
236
+ # Capture IDs from events
237
+ if event.task_id:
238
+ run_id = event.task_id
239
+ if event.context_id:
240
+ session_id = event.context_id
241
+
242
+ # Map event types
243
+ if event.event_type == "working":
244
+ yield TeamRunStartedEvent(
245
+ run_id=run_id,
246
+ session_id=session_id,
247
+ team_id=team_id,
248
+ )
249
+
250
+ elif event.is_content and event.content:
251
+ accumulated_content += event.content
252
+ yield TeamRunContentEvent(
253
+ content=event.content,
254
+ run_id=run_id,
255
+ session_id=session_id,
256
+ team_id=team_id,
257
+ )
258
+
259
+ elif event.is_final:
260
+ # Use content from final event or accumulated content
261
+ final_content = event.content if event.content else accumulated_content
262
+ yield TeamRunCompletedEvent(
263
+ content=final_content,
264
+ run_id=run_id,
265
+ session_id=session_id,
266
+ team_id=team_id,
267
+ )
268
+ break # Stream complete
269
+
270
+
271
+ # =============================================================================
272
+ # Workflow Run Output Mapping Functions
273
+ # =============================================================================
274
+
275
+
276
+ def map_task_result_to_workflow_run_output(
277
+ task_result: TaskResult,
278
+ workflow_id: str,
279
+ user_id: Optional[str] = None,
280
+ ) -> WorkflowRunOutput:
281
+ """Convert A2A TaskResult to Agno WorkflowRunOutput.
282
+
283
+ Maps the A2A protocol response structure to Agno's workflow format,
284
+ enabling seamless integration with Agno's workflow infrastructure.
285
+
286
+ Args:
287
+ task_result: A2A TaskResult from send_message()
288
+ workflow_id: Optional workflow identifier to include in output
289
+ user_id: Optional user identifier to include in output
290
+ Returns:
291
+ WorkflowRunOutput: Agno-compatible workflow run output
292
+ """
293
+ # Extract media from artifacts
294
+ images: List[Image] = []
295
+ videos: List[Video] = []
296
+ audio: List[Audio] = []
297
+ files: List[File] = []
298
+
299
+ for artifact in task_result.artifacts:
300
+ _classify_artifact(artifact, images, videos, audio, files)
301
+
302
+ return WorkflowRunOutput(
303
+ content=task_result.content,
304
+ run_id=task_result.task_id,
305
+ session_id=task_result.context_id,
306
+ workflow_id=workflow_id,
307
+ user_id=user_id,
308
+ images=images if images else None,
309
+ videos=videos if videos else None,
310
+ audio=audio if audio else None,
311
+ metadata=task_result.metadata,
312
+ )
313
+
314
+
315
+ async def map_stream_events_to_workflow_run_events(
316
+ stream: AsyncIterator[StreamEvent],
317
+ workflow_id: str,
318
+ ) -> AsyncIterator[Union[WorkflowRunOutputEvent, TeamRunOutputEvent, RunOutputEvent]]:
319
+ """Convert A2A stream events to Agno workflow run events.
320
+
321
+ Transforms the A2A streaming protocol events into Agno's workflow event system,
322
+ enabling real-time streaming from A2A servers to work with Agno workflow consumers.
323
+
324
+ Args:
325
+ stream: AsyncIterator of A2A StreamEvents
326
+ workflow_id: Optional workflow identifier to include in events
327
+ user_id: Optional user identifier to include in events
328
+ Yields:
329
+ WorkflowRunOutputEvent: Agno-compatible workflow run output events
330
+ """
331
+ run_id: Optional[str] = None
332
+ session_id: Optional[str] = None
333
+ accumulated_content = ""
334
+
335
+ async for event in stream:
336
+ # Capture IDs from events
337
+ if event.task_id:
338
+ run_id = event.task_id
339
+ if event.context_id:
340
+ session_id = event.context_id
341
+
342
+ # Map event types
343
+ if event.event_type == "working":
344
+ yield WorkflowStartedEvent(
345
+ run_id=run_id,
346
+ session_id=session_id,
347
+ workflow_id=workflow_id,
348
+ )
349
+
350
+ elif event.is_content and event.content:
351
+ accumulated_content += event.content
352
+ # TODO: We don't have workflow content events and we don't know which agent or team created the content, so we're using the workflow_id as the agent_id.
353
+ yield RunContentEvent(
354
+ content=event.content,
355
+ run_id=run_id,
356
+ session_id=session_id,
357
+ agent_id=workflow_id,
358
+ )
359
+
360
+ elif event.is_final:
361
+ # Use content from final event or accumulated content
362
+ final_content = event.content if event.content else accumulated_content
363
+ yield WorkflowCompletedEvent(
364
+ content=final_content,
365
+ run_id=run_id,
366
+ session_id=session_id,
367
+ workflow_id=workflow_id,
368
+ )
369
+ break # Stream complete
@@ -0,0 +1,19 @@
1
+ def quote_db_identifier(db_type: str, identifier: str) -> str:
2
+ """Add the right quotes to the given identifier string (table name, schema name) based on db type.
3
+
4
+ Args:
5
+ db_type: The database type name (e.g., "PostgresDb", "MySQLDb", "SqliteDb")
6
+ identifier: The identifier string to add quotes to
7
+
8
+ Returns:
9
+ The properly quoted identifier string
10
+ """
11
+ if db_type in ("PostgresDb", "AsyncPostgresDb"):
12
+ return f'"{identifier}"'
13
+ elif db_type in ("MySQLDb", "AsyncMySQLDb", "SingleStoreDb"):
14
+ return f"`{identifier}`"
15
+ elif db_type in ("SqliteDb", "AsyncSqliteDb"):
16
+ return f'"{identifier}"'
17
+ else:
18
+ # Default to double quotes for unknown types
19
+ return f'"{identifier}"'
@@ -7,9 +7,11 @@ from typing import Any, Dict, List, Optional, Union, cast
7
7
  from sqlalchemy import text
8
8
 
9
9
  from agno.db.base import BaseDb
10
+ from agno.db.migrations.utils import quote_db_identifier
10
11
  from agno.db.schemas.memory import UserMemory
11
12
  from agno.session import AgentSession, TeamSession, WorkflowSession
12
13
  from agno.utils.log import log_error, log_info, log_warning
14
+ from agno.utils.string import sanitize_postgres_string, sanitize_postgres_strings
13
15
 
14
16
 
15
17
  def convert_v1_metrics_to_v2(metrics_dict: Dict[str, Any]) -> Dict[str, Any]:
@@ -45,7 +47,10 @@ def convert_v1_metrics_to_v2(metrics_dict: Dict[str, Any]) -> Dict[str, Any]:
45
47
 
46
48
 
47
49
  def convert_any_metrics_in_data(data: Any) -> Any:
48
- """Recursively find and convert any metrics dictionaries and handle v1 to v2 field conversion."""
50
+ """Recursively find and convert any metrics dictionaries and handle v1 to v2 field conversion.
51
+
52
+ Also sanitizes all string values to remove null bytes for PostgreSQL compatibility.
53
+ """
49
54
  if isinstance(data, dict):
50
55
  # First apply v1 to v2 field conversion (handles extra_data extraction, thinking/reasoning_content consolidation, etc.)
51
56
  data = convert_v1_fields_to_v2(data)
@@ -62,13 +67,19 @@ def convert_any_metrics_in_data(data: Any) -> Any:
62
67
  converted_dict[key] = convert_v1_metrics_to_v2(value)
63
68
  else:
64
69
  converted_dict[key] = convert_any_metrics_in_data(value)
65
- return converted_dict
70
+
71
+ # Sanitize all strings in the converted dict to remove null bytes
72
+ return sanitize_postgres_strings(converted_dict)
66
73
 
67
74
  elif isinstance(data, list):
68
75
  return [convert_any_metrics_in_data(item) for item in data]
69
76
 
77
+ elif isinstance(data, str):
78
+ # Sanitize string values to remove null bytes
79
+ return sanitize_postgres_string(data)
80
+
70
81
  else:
71
- # Not a dict or list, return as-is
82
+ # Not a dict, list, or string, return as-is
72
83
  return data
73
84
 
74
85
 
@@ -485,15 +496,21 @@ def get_table_content_in_batches(db: BaseDb, db_schema: str, table_name: str, ba
485
496
  else:
486
497
  raise ValueError(f"Invalid database type: {type(db).__name__}")
487
498
 
499
+ db_type = type(db).__name__
500
+ quoted_schema = (
501
+ quote_db_identifier(db_type=db_type, identifier=db_schema) if db_schema and db_schema.strip() else None
502
+ )
503
+ quoted_table = quote_db_identifier(db_type=db_type, identifier=table_name)
504
+
488
505
  offset = 0
489
506
  while True:
490
507
  # Create a new session for each batch to avoid transaction conflicts
491
508
  with db.Session() as sess:
492
509
  # Handle empty schema by omitting the schema prefix (needed for SQLite)
493
- if db_schema and db_schema.strip():
494
- sql_query = f"SELECT * FROM {db_schema}.{table_name} LIMIT {batch_size} OFFSET {offset}"
510
+ if quoted_schema:
511
+ sql_query = f"SELECT * FROM {quoted_schema}.{quoted_table} LIMIT {batch_size} OFFSET {offset}"
495
512
  else:
496
- sql_query = f"SELECT * FROM {table_name} LIMIT {batch_size} OFFSET {offset}"
513
+ sql_query = f"SELECT * FROM {quoted_table} LIMIT {batch_size} OFFSET {offset}"
497
514
 
498
515
  result = sess.execute(text(sql_query))
499
516
  batch = [row._asdict() for row in result]
@@ -530,13 +547,16 @@ def get_all_table_content(db, db_schema: str, table_name: str) -> list[dict[str,
530
547
 
531
548
 
532
549
  def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]:
533
- """Parse v1 Agent sessions into v2 Agent sessions and Memories"""
550
+ """Parse v1 Agent sessions into v2 Agent sessions and Memories
551
+
552
+ Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
553
+ """
534
554
  sessions_v2 = []
535
555
 
536
556
  for item in v1_content:
537
557
  session = {
538
558
  "agent_id": item.get("agent_id"),
539
- "agent_data": item.get("agent_data"),
559
+ "agent_data": sanitize_postgres_strings(item.get("agent_data")) if item.get("agent_data") else None,
540
560
  "session_id": item.get("session_id"),
541
561
  "user_id": item.get("user_id"),
542
562
  "session_data": convert_session_data_comprehensively(item.get("session_data")),
@@ -545,6 +565,7 @@ def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]
545
565
  "created_at": item.get("created_at"),
546
566
  "updated_at": item.get("updated_at"),
547
567
  }
568
+ # Summary field sanitization is handled in convert_session_data_comprehensively
548
569
 
549
570
  try:
550
571
  agent_session = AgentSession.from_dict(session)
@@ -559,13 +580,16 @@ def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]
559
580
 
560
581
 
561
582
  def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
562
- """Parse v1 Team sessions into v2 Team sessions and Memories"""
583
+ """Parse v1 Team sessions into v2 Team sessions and Memories
584
+
585
+ Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
586
+ """
563
587
  sessions_v2 = []
564
588
 
565
589
  for item in v1_content:
566
590
  session = {
567
591
  "team_id": item.get("team_id"),
568
- "team_data": item.get("team_data"),
592
+ "team_data": sanitize_postgres_strings(item.get("team_data")) if item.get("team_data") else None,
569
593
  "session_id": item.get("session_id"),
570
594
  "user_id": item.get("user_id"),
571
595
  "session_data": convert_session_data_comprehensively(item.get("session_data")),
@@ -574,6 +598,8 @@ def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
574
598
  "created_at": item.get("created_at"),
575
599
  "updated_at": item.get("updated_at"),
576
600
  }
601
+ # Summary field sanitization is handled in convert_session_data_comprehensively
602
+
577
603
  try:
578
604
  team_session = TeamSession.from_dict(session)
579
605
  except Exception as e:
@@ -587,13 +613,18 @@ def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
587
613
 
588
614
 
589
615
  def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSession]:
590
- """Parse v1 Workflow sessions into v2 Workflow sessions"""
616
+ """Parse v1 Workflow sessions into v2 Workflow sessions
617
+
618
+ Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
619
+ """
591
620
  sessions_v2 = []
592
621
 
593
622
  for item in v1_content:
594
623
  session = {
595
624
  "workflow_id": item.get("workflow_id"),
596
- "workflow_data": item.get("workflow_data"),
625
+ "workflow_data": sanitize_postgres_strings(item.get("workflow_data"))
626
+ if item.get("workflow_data")
627
+ else None,
597
628
  "session_id": item.get("session_id"),
598
629
  "user_id": item.get("user_id"),
599
630
  "session_data": convert_session_data_comprehensively(item.get("session_data")),
@@ -601,9 +632,11 @@ def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSe
601
632
  "created_at": item.get("created_at"),
602
633
  "updated_at": item.get("updated_at"),
603
634
  # Workflow v2 specific fields
604
- "workflow_name": item.get("workflow_name"),
635
+ "workflow_name": sanitize_postgres_string(item.get("workflow_name")) if item.get("workflow_name") else None,
605
636
  "runs": convert_any_metrics_in_data(item.get("runs")),
606
637
  }
638
+ # Summary field sanitization is handled in convert_session_data_comprehensively
639
+
607
640
  try:
608
641
  workflow_session = WorkflowSession.from_dict(session)
609
642
  except Exception as e:
@@ -617,18 +650,23 @@ def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSe
617
650
 
618
651
 
619
652
  def parse_memories(v1_content: List[Dict[str, Any]]) -> List[UserMemory]:
620
- """Parse v1 Memories into v2 Memories"""
653
+ """Parse v1 Memories into v2 Memories
654
+
655
+ Sanitizes all string fields to remove null bytes for PostgreSQL compatibility.
656
+ """
621
657
  memories_v2 = []
622
658
 
623
659
  for item in v1_content:
624
660
  memory = {
625
661
  "memory_id": item.get("memory_id"),
626
- "memory": item.get("memory"),
627
- "input": item.get("input"),
662
+ "memory": sanitize_postgres_strings(item.get("memory")) if item.get("memory") else None,
663
+ "input": sanitize_postgres_string(item.get("input")) if item.get("input") else None,
628
664
  "updated_at": item.get("updated_at"),
629
665
  "agent_id": item.get("agent_id"),
630
666
  "team_id": item.get("team_id"),
631
667
  "user_id": item.get("user_id"),
668
+ "topics": sanitize_postgres_strings(item.get("topics")) if item.get("topics") else None,
669
+ "feedback": sanitize_postgres_string(item.get("feedback")) if item.get("feedback") else None,
632
670
  }
633
671
  memories_v2.append(UserMemory.from_dict(memory))
634
672