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/client/a2a/utils.py
ADDED
|
@@ -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}"'
|
agno/db/migrations/v1_to_v2.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
494
|
-
sql_query = f"SELECT * FROM {
|
|
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 {
|
|
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
|
|