agno 1.7.3__py3-none-any.whl → 1.7.5__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 (59) hide show
  1. agno/agent/agent.py +113 -31
  2. agno/api/schemas/agent.py +1 -0
  3. agno/api/schemas/team.py +1 -0
  4. agno/app/fastapi/app.py +1 -1
  5. agno/app/fastapi/async_router.py +67 -16
  6. agno/app/fastapi/sync_router.py +80 -14
  7. agno/app/playground/app.py +2 -1
  8. agno/app/playground/async_router.py +97 -28
  9. agno/app/playground/operator.py +25 -19
  10. agno/app/playground/schemas.py +1 -0
  11. agno/app/playground/sync_router.py +93 -26
  12. agno/knowledge/agent.py +39 -2
  13. agno/knowledge/combined.py +1 -1
  14. agno/run/base.py +2 -0
  15. agno/run/response.py +4 -4
  16. agno/run/team.py +6 -6
  17. agno/run/v2/__init__.py +0 -0
  18. agno/run/v2/workflow.py +563 -0
  19. agno/storage/base.py +4 -4
  20. agno/storage/dynamodb.py +74 -10
  21. agno/storage/firestore.py +6 -1
  22. agno/storage/gcs_json.py +8 -2
  23. agno/storage/json.py +20 -5
  24. agno/storage/mongodb.py +14 -5
  25. agno/storage/mysql.py +56 -17
  26. agno/storage/postgres.py +55 -13
  27. agno/storage/redis.py +25 -5
  28. agno/storage/session/__init__.py +3 -1
  29. agno/storage/session/agent.py +3 -0
  30. agno/storage/session/team.py +3 -0
  31. agno/storage/session/v2/__init__.py +5 -0
  32. agno/storage/session/v2/workflow.py +89 -0
  33. agno/storage/singlestore.py +74 -12
  34. agno/storage/sqlite.py +64 -18
  35. agno/storage/yaml.py +26 -6
  36. agno/team/team.py +105 -21
  37. agno/tools/decorator.py +45 -2
  38. agno/tools/function.py +16 -12
  39. agno/utils/log.py +12 -0
  40. agno/utils/message.py +5 -1
  41. agno/utils/openai.py +20 -5
  42. agno/utils/pprint.py +34 -8
  43. agno/vectordb/surrealdb/__init__.py +3 -0
  44. agno/vectordb/surrealdb/surrealdb.py +493 -0
  45. agno/workflow/v2/__init__.py +21 -0
  46. agno/workflow/v2/condition.py +554 -0
  47. agno/workflow/v2/loop.py +602 -0
  48. agno/workflow/v2/parallel.py +659 -0
  49. agno/workflow/v2/router.py +521 -0
  50. agno/workflow/v2/step.py +861 -0
  51. agno/workflow/v2/steps.py +465 -0
  52. agno/workflow/v2/types.py +347 -0
  53. agno/workflow/v2/workflow.py +3132 -0
  54. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/METADATA +4 -1
  55. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/RECORD +59 -44
  56. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/WHEEL +0 -0
  57. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/entry_points.txt +0 -0
  58. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/licenses/LICENSE +0 -0
  59. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/top_level.txt +0 -0
agno/storage/postgres.py CHANGED
@@ -5,6 +5,7 @@ from agno.storage.base import Storage
5
5
  from agno.storage.session import Session
6
6
  from agno.storage.session.agent import AgentSession
7
7
  from agno.storage.session.team import TeamSession
8
+ from agno.storage.session.v2.workflow import WorkflowSession as WorkflowSessionV2
8
9
  from agno.storage.session.workflow import WorkflowSession
9
10
  from agno.utils.log import log_debug, log_info, log_warning, logger
10
11
 
@@ -79,12 +80,12 @@ class PostgresStorage(Storage):
79
80
  log_debug(f"Created PostgresStorage: '{self.schema}.{self.table_name}'")
80
81
 
81
82
  @property
82
- def mode(self) -> Literal["agent", "team", "workflow"]:
83
+ def mode(self) -> Literal["agent", "team", "workflow", "workflow_v2"]:
83
84
  """Get the mode of the storage."""
84
85
  return super().mode
85
86
 
86
87
  @mode.setter
87
- def mode(self, value: Optional[Literal["agent", "team", "workflow"]]) -> None:
88
+ def mode(self, value: Optional[Literal["agent", "team", "workflow", "workflow_v2"]]) -> None:
88
89
  """Set the mode and refresh the table if mode changes."""
89
90
  super(PostgresStorage, type(self)).mode.fset(self, value) # type: ignore
90
91
  if value is not None:
@@ -127,6 +128,11 @@ class PostgresStorage(Storage):
127
128
  Column("workflow_id", String, index=True),
128
129
  Column("workflow_data", postgresql.JSONB),
129
130
  ]
131
+ elif self.mode == "workflow_v2":
132
+ specific_columns = [
133
+ Column("workflow_id", String, index=True),
134
+ Column("workflow_data", postgresql.JSONB),
135
+ ]
130
136
 
131
137
  # Create table with all columns
132
138
  table = Table(
@@ -263,6 +269,8 @@ class PostgresStorage(Storage):
263
269
  return TeamSession.from_dict(result._mapping) if result is not None else None
264
270
  elif self.mode == "workflow":
265
271
  return WorkflowSession.from_dict(result._mapping) if result is not None else None
272
+ elif self.mode == "workflow_v2":
273
+ return WorkflowSessionV2.from_dict(result._mapping) if result is not None else None
266
274
  except Exception as e:
267
275
  if "does not exist" in str(e):
268
276
  log_debug(f"Table does not exist: {self.table.name}")
@@ -296,7 +304,8 @@ class PostgresStorage(Storage):
296
304
  stmt = stmt.where(self.table.c.team_id == entity_id)
297
305
  elif self.mode == "workflow":
298
306
  stmt = stmt.where(self.table.c.workflow_id == entity_id)
299
-
307
+ elif self.mode == "workflow_v2":
308
+ stmt = stmt.where(self.table.c.workflow_id == entity_id)
300
309
  # order by created_at desc
301
310
  stmt = stmt.order_by(self.table.c.created_at.desc())
302
311
  # execute query
@@ -331,7 +340,9 @@ class PostgresStorage(Storage):
331
340
  stmt = stmt.where(self.table.c.agent_id == entity_id)
332
341
  elif self.mode == "team":
333
342
  stmt = stmt.where(self.table.c.team_id == entity_id)
334
- else:
343
+ elif self.mode == "workflow":
344
+ stmt = stmt.where(self.table.c.workflow_id == entity_id)
345
+ elif self.mode == "workflow_v2":
335
346
  stmt = stmt.where(self.table.c.workflow_id == entity_id)
336
347
  # order by created_at desc
337
348
  stmt = stmt.order_by(self.table.c.created_at.desc())
@@ -384,7 +395,8 @@ class PostgresStorage(Storage):
384
395
  stmt = stmt.where(self.table.c.team_id == entity_id)
385
396
  elif self.mode == "workflow":
386
397
  stmt = stmt.where(self.table.c.workflow_id == entity_id)
387
-
398
+ elif self.mode == "workflow_v2":
399
+ stmt = stmt.where(self.table.c.workflow_id == entity_id)
388
400
  # Order by created_at desc and limit results
389
401
  stmt = stmt.order_by(self.table.c.created_at.desc())
390
402
  if limit is not None:
@@ -402,7 +414,8 @@ class PostgresStorage(Storage):
402
414
  session = TeamSession.from_dict(row._mapping) # type: ignore
403
415
  elif self.mode == "workflow":
404
416
  session = WorkflowSession.from_dict(row._mapping) # type: ignore
405
-
417
+ elif self.mode == "workflow_v2":
418
+ session = WorkflowSessionV2.from_dict(row._mapping) # type: ignore
406
419
  if session is not None:
407
420
  sessions.append(session)
408
421
  return sessions
@@ -479,7 +492,7 @@ class PostgresStorage(Storage):
479
492
  agent_id=session.agent_id, # type: ignore
480
493
  team_session_id=session.team_session_id, # type: ignore
481
494
  user_id=session.user_id,
482
- memory=session.memory,
495
+ memory=getattr(session, "memory", None),
483
496
  agent_data=session.agent_data, # type: ignore
484
497
  session_data=session.session_data,
485
498
  extra_data=session.extra_data,
@@ -492,7 +505,7 @@ class PostgresStorage(Storage):
492
505
  agent_id=session.agent_id, # type: ignore
493
506
  team_session_id=session.team_session_id, # type: ignore
494
507
  user_id=session.user_id,
495
- memory=session.memory,
508
+ memory=getattr(session, "memory", None),
496
509
  agent_data=session.agent_data, # type: ignore
497
510
  session_data=session.session_data,
498
511
  extra_data=session.extra_data,
@@ -505,7 +518,7 @@ class PostgresStorage(Storage):
505
518
  team_id=session.team_id, # type: ignore
506
519
  user_id=session.user_id,
507
520
  team_session_id=session.team_session_id, # type: ignore
508
- memory=session.memory,
521
+ memory=getattr(session, "memory", None),
509
522
  team_data=session.team_data, # type: ignore
510
523
  session_data=session.session_data,
511
524
  extra_data=session.extra_data,
@@ -518,19 +531,19 @@ class PostgresStorage(Storage):
518
531
  team_id=session.team_id, # type: ignore
519
532
  user_id=session.user_id,
520
533
  team_session_id=session.team_session_id, # type: ignore
521
- memory=session.memory,
534
+ memory=getattr(session, "memory", None),
522
535
  team_data=session.team_data, # type: ignore
523
536
  session_data=session.session_data,
524
537
  extra_data=session.extra_data,
525
538
  updated_at=int(time.time()),
526
539
  ), # The updated value for each column
527
540
  )
528
- else:
541
+ elif self.mode == "workflow":
529
542
  stmt = postgresql.insert(self.table).values(
530
543
  session_id=session.session_id,
531
544
  workflow_id=session.workflow_id, # type: ignore
532
545
  user_id=session.user_id,
533
- memory=session.memory,
546
+ memory=getattr(session, "memory", None),
534
547
  workflow_data=session.workflow_data, # type: ignore
535
548
  session_data=session.session_data,
536
549
  extra_data=session.extra_data,
@@ -542,13 +555,42 @@ class PostgresStorage(Storage):
542
555
  set_=dict(
543
556
  workflow_id=session.workflow_id, # type: ignore
544
557
  user_id=session.user_id,
545
- memory=session.memory,
558
+ memory=getattr(session, "memory", None),
546
559
  workflow_data=session.workflow_data, # type: ignore
547
560
  session_data=session.session_data,
548
561
  extra_data=session.extra_data,
549
562
  updated_at=int(time.time()),
550
563
  ), # The updated value for each column
551
564
  )
565
+ elif self.mode == "workflow_v2":
566
+ # Convert session to dict to ensure proper serialization
567
+ session_dict = session.to_dict()
568
+
569
+ stmt = postgresql.insert(self.table).values(
570
+ session_id=session.session_id,
571
+ workflow_id=session.workflow_id, # type: ignore
572
+ workflow_name=session.workflow_name, # type: ignore
573
+ user_id=session.user_id,
574
+ runs=session_dict.get("runs"),
575
+ workflow_data=session.workflow_data, # type: ignore
576
+ session_data=session.session_data,
577
+ extra_data=session.extra_data,
578
+ )
579
+ # Define the upsert if the session_id already exists
580
+ # See: https://docs.sqlalchemy.org/en/20/dialects/postgresql.html#postgresql-insert-on-conflict
581
+ stmt = stmt.on_conflict_do_update(
582
+ index_elements=["session_id"],
583
+ set_=dict(
584
+ workflow_id=session.workflow_id, # type: ignore
585
+ workflow_name=session.workflow_name, # type: ignore
586
+ user_id=session.user_id,
587
+ runs=session_dict.get("runs"),
588
+ workflow_data=session.workflow_data, # type: ignore
589
+ session_data=session.session_data,
590
+ extra_data=session.extra_data,
591
+ updated_at=int(time.time()),
592
+ ),
593
+ )
552
594
 
553
595
  sess.execute(stmt)
554
596
  except Exception as e:
agno/storage/redis.py CHANGED
@@ -8,6 +8,7 @@ from agno.storage.base import Storage
8
8
  from agno.storage.session import Session
9
9
  from agno.storage.session.agent import AgentSession
10
10
  from agno.storage.session.team import TeamSession
11
+ from agno.storage.session.v2.workflow import WorkflowSession as WorkflowSessionV2
11
12
  from agno.storage.session.workflow import WorkflowSession
12
13
  from agno.utils.log import log_debug, log_info, logger
13
14
 
@@ -32,7 +33,7 @@ class RedisStorage(Storage):
32
33
  port: int = 6379,
33
34
  db: int = 0,
34
35
  password: Optional[str] = None,
35
- mode: Optional[Literal["agent", "team", "workflow"]] = "agent",
36
+ mode: Optional[Literal["agent", "team", "workflow", "workflow_v2"]] = "agent",
36
37
  ssl: Optional[bool] = False,
37
38
  expire: Optional[int] = None,
38
39
  ):
@@ -45,7 +46,7 @@ class RedisStorage(Storage):
45
46
  port (int): Redis port number
46
47
  db (int): Redis database number
47
48
  password (Optional[str]): Redis password if authentication is required
48
- mode (Optional[Literal["agent", "team", "workflow"]]): Storage mode
49
+ mode (Optional[Literal["agent", "team", "workflow", "workflow_v2"]]): Storage mode
49
50
  ssl (Optional[bool]): Whether to use SSL for Redis connection
50
51
  expire (Optional[int]): TTL (time to live) in seconds for Redis keys. None means no expiration.
51
52
  """
@@ -104,6 +105,8 @@ class RedisStorage(Storage):
104
105
  return TeamSession.from_dict(session_data)
105
106
  elif self.mode == "workflow":
106
107
  return WorkflowSession.from_dict(session_data)
108
+ elif self.mode == "workflow_v2":
109
+ return WorkflowSessionV2.from_dict(session_data)
107
110
 
108
111
  except Exception as e:
109
112
  logger.error(f"Error reading session: {e}")
@@ -128,6 +131,8 @@ class RedisStorage(Storage):
128
131
  self.mode == "workflow" and data["workflow_id"] == entity_id and data["user_id"] == user_id
129
132
  ):
130
133
  session_ids.append(data["session_id"])
134
+ elif self.mode == "workflow_v2" and data["workflow_id"] == entity_id:
135
+ session_ids.append(data["session_id"])
131
136
  elif user_id and data["user_id"] == user_id:
132
137
  session_ids.append(data["session_id"])
133
138
  elif entity_id:
@@ -137,6 +142,8 @@ class RedisStorage(Storage):
137
142
  session_ids.append(data["session_id"])
138
143
  elif self.mode == "workflow" and data["workflow_id"] == entity_id:
139
144
  session_ids.append(data["session_id"])
145
+ elif self.mode == "workflow_v2" and data["workflow_id"] == entity_id:
146
+ session_ids.append(data["session_id"])
140
147
  else:
141
148
  # No filters applied, add all session_ids
142
149
  session_ids.append(data["session_id"])
@@ -166,6 +173,8 @@ class RedisStorage(Storage):
166
173
  self.mode == "workflow" and data["workflow_id"] == entity_id and data["user_id"] == user_id
167
174
  ):
168
175
  _session = WorkflowSession.from_dict(data)
176
+ elif self.mode == "workflow_v2" and data["workflow_id"] == entity_id:
177
+ _session = WorkflowSessionV2.from_dict(data)
169
178
  elif user_id and data["user_id"] == user_id:
170
179
  if self.mode == "agent":
171
180
  _session = AgentSession.from_dict(data)
@@ -173,6 +182,8 @@ class RedisStorage(Storage):
173
182
  _session = TeamSession.from_dict(data)
174
183
  elif self.mode == "workflow":
175
184
  _session = WorkflowSession.from_dict(data)
185
+ elif self.mode == "workflow_v2" and data["workflow_id"] == entity_id:
186
+ _session = WorkflowSessionV2.from_dict(data)
176
187
  elif entity_id:
177
188
  if self.mode == "agent" and data["agent_id"] == entity_id:
178
189
  _session = AgentSession.from_dict(data)
@@ -180,6 +191,8 @@ class RedisStorage(Storage):
180
191
  _session = TeamSession.from_dict(data)
181
192
  elif self.mode == "workflow" and data["workflow_id"] == entity_id:
182
193
  _session = WorkflowSession.from_dict(data)
194
+ elif self.mode == "workflow_v2" and data["workflow_id"] == entity_id:
195
+ _session = WorkflowSessionV2.from_dict(data)
183
196
 
184
197
  if _session:
185
198
  sessions.append(_session)
@@ -192,6 +205,8 @@ class RedisStorage(Storage):
192
205
  _session = TeamSession.from_dict(data)
193
206
  elif self.mode == "workflow":
194
207
  _session = WorkflowSession.from_dict(data)
208
+ elif self.mode == "workflow_v2":
209
+ _session = WorkflowSessionV2.from_dict(data)
195
210
  if _session:
196
211
  sessions.append(_session)
197
212
 
@@ -237,7 +252,8 @@ class RedisStorage(Storage):
237
252
  continue
238
253
  elif self.mode == "workflow" and data["workflow_id"] != entity_id:
239
254
  continue
240
-
255
+ elif self.mode == "workflow_v2" and data["workflow_id"] != entity_id:
256
+ continue
241
257
  # Store with created_at for sorting
242
258
  created_at = data.get("created_at", 0)
243
259
  session_data.append((created_at, data))
@@ -260,7 +276,8 @@ class RedisStorage(Storage):
260
276
  session = TeamSession.from_dict(data)
261
277
  elif self.mode == "workflow":
262
278
  session = WorkflowSession.from_dict(data)
263
-
279
+ elif self.mode == "workflow_v2":
280
+ session = WorkflowSessionV2.from_dict(data)
264
281
  if session is not None:
265
282
  sessions.append(session)
266
283
 
@@ -272,7 +289,10 @@ class RedisStorage(Storage):
272
289
  def upsert(self, session: Session) -> Optional[Session]:
273
290
  """Insert or update a Session in Redis."""
274
291
  try:
275
- data = asdict(session)
292
+ if self.mode == "workflow_v2":
293
+ data = session.to_dict()
294
+ else:
295
+ data = asdict(session)
276
296
  data["updated_at"] = int(time.time())
277
297
  if "created_at" not in data:
278
298
  data["created_at"] = data["updated_at"]
@@ -2,13 +2,15 @@ from typing import Union
2
2
 
3
3
  from agno.storage.session.agent import AgentSession
4
4
  from agno.storage.session.team import TeamSession
5
+ from agno.storage.session.v2.workflow import WorkflowSession as WorkflowSessionV2
5
6
  from agno.storage.session.workflow import WorkflowSession
6
7
 
7
- Session = Union[AgentSession, TeamSession, WorkflowSession]
8
+ Session = Union[AgentSession, TeamSession, WorkflowSession, WorkflowSessionV2]
8
9
 
9
10
  __all__ = [
10
11
  "AgentSession",
11
12
  "TeamSession",
12
13
  "WorkflowSession",
14
+ "WorkflowSessionV2",
13
15
  "Session",
14
16
  ]
@@ -16,6 +16,8 @@ class AgentSession:
16
16
  user_id: Optional[str] = None
17
17
  # ID of the team session this agent session is associated with
18
18
  team_session_id: Optional[str] = None
19
+ # ID of the workflow session this agent session is associated with
20
+ workflow_session_id: Optional[str] = None
19
21
  # Agent Memory
20
22
  memory: Optional[Dict[str, Any]] = None
21
23
  # Session Data: session_name, session_state, images, videos, audio
@@ -51,6 +53,7 @@ class AgentSession:
51
53
  session_id=data.get("session_id"), # type: ignore
52
54
  agent_id=data.get("agent_id"),
53
55
  team_session_id=data.get("team_session_id"),
56
+ workflow_session_id=data.get("workflow_session_id"),
54
57
  user_id=data.get("user_id"),
55
58
  memory=data.get("memory"),
56
59
  agent_data=data.get("agent_data"),
@@ -14,6 +14,8 @@ class TeamSession:
14
14
  session_id: str
15
15
  # ID of the team session this team session is associated with (so for sub-teams)
16
16
  team_session_id: Optional[str] = None
17
+ # ID of the workflow session this team session is associated with
18
+ workflow_session_id: Optional[str] = None
17
19
  # ID of the team that this session is associated with
18
20
  team_id: Optional[str] = None
19
21
  # ID of the user interacting with this team
@@ -50,6 +52,7 @@ class TeamSession:
50
52
  session_id=data.get("session_id"), # type: ignore
51
53
  team_id=data.get("team_id"),
52
54
  team_session_id=data.get("team_session_id"),
55
+ workflow_session_id=data.get("workflow_session_id"),
53
56
  user_id=data.get("user_id"),
54
57
  memory=data.get("memory"),
55
58
  team_data=data.get("team_data"),
@@ -0,0 +1,5 @@
1
+ from agno.storage.session.workflow import WorkflowSession
2
+
3
+ __all__ = [
4
+ "WorkflowSession",
5
+ ]
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, List, Mapping, Optional
5
+
6
+ from agno.run.v2.workflow import WorkflowRunResponse
7
+ from agno.utils.log import logger
8
+
9
+
10
+ @dataclass
11
+ class WorkflowSession:
12
+ """Workflow Session V2 for pipeline-based workflows"""
13
+
14
+ # Session UUID - this is the workflow_session_id that gets set on agents/teams
15
+ session_id: str
16
+ # ID of the user interacting with this workflow
17
+ user_id: Optional[str] = None
18
+
19
+ # ID of the workflow that this session is associated with
20
+ workflow_id: Optional[str] = None
21
+ # Workflow name
22
+ workflow_name: Optional[str] = None
23
+
24
+ # Workflow runs - stores WorkflowRunResponse objects in memory
25
+ runs: Optional[List[WorkflowRunResponse]] = None
26
+
27
+ # Session Data: session_name, session_state, images, videos, audio
28
+ session_data: Optional[Dict[str, Any]] = None
29
+ # Workflow configuration and metadata
30
+ workflow_data: Optional[Dict[str, Any]] = None
31
+ # Extra Data stored with this workflow session
32
+ extra_data: Optional[Dict[str, Any]] = None
33
+
34
+ # The unix timestamp when this session was created
35
+ created_at: Optional[int] = None
36
+ # The unix timestamp when this session was last updated
37
+ updated_at: Optional[int] = None
38
+
39
+ def __post_init__(self):
40
+ if self.runs is None:
41
+ self.runs = []
42
+
43
+ def add_run(self, run_response: WorkflowRunResponse) -> None:
44
+ """Add a workflow run response to this session"""
45
+ if self.runs is None:
46
+ self.runs = []
47
+ # Store the actual WorkflowRunResponse object
48
+ self.runs.append(run_response)
49
+
50
+ def to_dict(self) -> Dict[str, Any]:
51
+ """Convert to dictionary for storage, serializing runs to dicts"""
52
+ return {
53
+ "session_id": self.session_id,
54
+ "user_id": self.user_id,
55
+ "workflow_id": self.workflow_id,
56
+ "workflow_name": self.workflow_name,
57
+ "runs": [run.to_dict() for run in self.runs] if self.runs else None,
58
+ "session_data": self.session_data,
59
+ "workflow_data": self.workflow_data,
60
+ "extra_data": self.extra_data,
61
+ "created_at": self.created_at,
62
+ "updated_at": self.updated_at,
63
+ }
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: Mapping[str, Any]) -> Optional[WorkflowSession]:
67
+ """Create WorkflowSession from dictionary, deserializing runs from dicts"""
68
+ if data is None or data.get("session_id") is None:
69
+ logger.warning("WorkflowSession is missing session_id")
70
+ return None
71
+
72
+ # Deserialize runs from dictionaries back to WorkflowRunResponse objects
73
+ runs_data = data.get("runs")
74
+ runs: Optional[List[WorkflowRunResponse]] = None
75
+ if runs_data is not None:
76
+ runs = [WorkflowRunResponse.from_dict(run_dict) for run_dict in runs_data]
77
+
78
+ return cls(
79
+ session_id=data.get("session_id"), # type: ignore
80
+ user_id=data.get("user_id"),
81
+ workflow_id=data.get("workflow_id"),
82
+ workflow_name=data.get("workflow_name"),
83
+ runs=runs,
84
+ session_data=data.get("session_data"),
85
+ workflow_data=data.get("workflow_data"),
86
+ extra_data=data.get("extra_data"),
87
+ created_at=data.get("created_at"),
88
+ updated_at=data.get("updated_at"),
89
+ )
@@ -5,6 +5,7 @@ from agno.storage.base import Storage
5
5
  from agno.storage.session import Session
6
6
  from agno.storage.session.agent import AgentSession
7
7
  from agno.storage.session.team import TeamSession
8
+ from agno.storage.session.v2.workflow import WorkflowSession as WorkflowSessionV2
8
9
  from agno.storage.session.workflow import WorkflowSession
9
10
  from agno.utils.log import log_debug, log_info, log_warning, logger
10
11
 
@@ -30,7 +31,7 @@ class SingleStoreStorage(Storage):
30
31
  db_engine: Optional[Engine] = None,
31
32
  schema_version: int = 1,
32
33
  auto_upgrade_schema: bool = False,
33
- mode: Optional[Literal["agent", "team", "workflow"]] = "agent",
34
+ mode: Optional[Literal["agent", "team", "workflow", "workflow_v2"]] = "agent",
34
35
  ):
35
36
  """
36
37
  This class provides Agent storage using a singlestore table.
@@ -46,7 +47,7 @@ class SingleStoreStorage(Storage):
46
47
  db_engine (Optional[Engine], optional): The database engine. Defaults to None.
47
48
  schema_version (int, optional): The schema version. Defaults to 1.
48
49
  auto_upgrade_schema (bool, optional): Automatically upgrade the schema. Defaults to False.
49
- mode (Optional[Literal["agent", "team", "workflow"]], optional): The mode of the storage. Defaults to "agent".
50
+ mode (Optional[Literal["agent", "team", "workflow", "workflow_v2"]], optional): The mode of the storage. Defaults to "agent".
50
51
  """
51
52
  super().__init__(mode)
52
53
  _engine: Optional[Engine] = db_engine
@@ -75,12 +76,12 @@ class SingleStoreStorage(Storage):
75
76
  self.table: Table = self.get_table()
76
77
 
77
78
  @property
78
- def mode(self) -> Literal["agent", "team", "workflow"]:
79
+ def mode(self) -> Literal["agent", "team", "workflow", "workflow_v2"]:
79
80
  """Get the mode of the storage."""
80
81
  return super().mode
81
82
 
82
83
  @mode.setter
83
- def mode(self, value: Optional[Literal["agent", "team", "workflow"]]) -> None:
84
+ def mode(self, value: Optional[Literal["agent", "team", "workflow", "workflow_v2"]]) -> None:
84
85
  """Set the mode and refresh the table if mode changes."""
85
86
  super(SingleStoreStorage, type(self)).mode.fset(self, value) # type: ignore
86
87
  if value is not None:
@@ -115,6 +116,11 @@ class SingleStoreStorage(Storage):
115
116
  Column("workflow_id", mysql.TEXT),
116
117
  Column("workflow_data", mysql.JSON),
117
118
  ]
119
+ elif self.mode == "workflow_v2":
120
+ specific_columns = [
121
+ Column("workflow_id", mysql.TEXT),
122
+ Column("workflow_data", mysql.JSON),
123
+ ]
118
124
 
119
125
  # Create table with all columns
120
126
  table = Table(
@@ -171,6 +177,8 @@ class SingleStoreStorage(Storage):
171
177
  return TeamSession.from_dict(existing_row._mapping) # type: ignore
172
178
  elif self.mode == "workflow":
173
179
  return WorkflowSession.from_dict(existing_row._mapping) # type: ignore
180
+ elif self.mode == "workflow_v2":
181
+ return WorkflowSessionV2.from_dict(existing_row._mapping) # type: ignore
174
182
  return None
175
183
 
176
184
  def get_all_session_ids(self, user_id: Optional[str] = None, entity_id: Optional[str] = None) -> List[str]:
@@ -188,6 +196,8 @@ class SingleStoreStorage(Storage):
188
196
  stmt = stmt.where(self.table.c.team_id == entity_id)
189
197
  elif self.mode == "workflow":
190
198
  stmt = stmt.where(self.table.c.workflow_id == entity_id)
199
+ elif self.mode == "workflow_v2":
200
+ stmt = stmt.where(self.table.c.workflow_id == entity_id)
191
201
  # order by created_at desc
192
202
  stmt = stmt.order_by(self.table.c.created_at.desc())
193
203
  # execute query
@@ -214,6 +224,8 @@ class SingleStoreStorage(Storage):
214
224
  stmt = stmt.where(self.table.c.team_id == entity_id)
215
225
  elif self.mode == "workflow":
216
226
  stmt = stmt.where(self.table.c.workflow_id == entity_id)
227
+ elif self.mode == "workflow_v2":
228
+ stmt = stmt.where(self.table.c.workflow_id == entity_id)
217
229
  # order by created_at desc
218
230
  stmt = stmt.order_by(self.table.c.created_at.desc())
219
231
  # execute query
@@ -232,6 +244,10 @@ class SingleStoreStorage(Storage):
232
244
  _workflow_session = WorkflowSession.from_dict(row._mapping) # type: ignore
233
245
  if _workflow_session is not None:
234
246
  sessions.append(_workflow_session)
247
+ elif self.mode == "workflow_v2":
248
+ _workflow_session = WorkflowSessionV2.from_dict(row._mapping) # type: ignore
249
+ if _workflow_session is not None:
250
+ sessions.append(_workflow_session)
235
251
  except Exception:
236
252
  log_debug(f"Table does not exist: {self.table.name}")
237
253
  return sessions
@@ -268,7 +284,8 @@ class SingleStoreStorage(Storage):
268
284
  stmt = stmt.where(self.table.c.team_id == entity_id)
269
285
  elif self.mode == "workflow":
270
286
  stmt = stmt.where(self.table.c.workflow_id == entity_id)
271
-
287
+ elif self.mode == "workflow_v2":
288
+ stmt = stmt.where(self.table.c.workflow_id == entity_id)
272
289
  # Order by created_at desc and limit results
273
290
  stmt = stmt.order_by(self.table.c.created_at.desc())
274
291
  if limit is not None:
@@ -290,7 +307,10 @@ class SingleStoreStorage(Storage):
290
307
  session = WorkflowSession.from_dict(row._mapping) # type: ignore
291
308
  if session is not None:
292
309
  sessions.append(session)
293
-
310
+ elif self.mode == "workflow_v2":
311
+ session = WorkflowSessionV2.from_dict(row._mapping) # type: ignore
312
+ if session is not None:
313
+ sessions.append(session)
294
314
  except Exception as e:
295
315
  if "doesn't exist" in str(e):
296
316
  log_debug(f"Table does not exist: {self.table.name}")
@@ -401,6 +421,25 @@ class SingleStoreStorage(Storage):
401
421
  updated_at = UNIX_TIMESTAMP();
402
422
  """
403
423
  )
424
+ elif self.mode == "workflow_v2":
425
+ # Convert session to dict to ensure proper serialization
426
+ upsert_sql = text(
427
+ f"""
428
+ INSERT INTO {self.schema}.{self.table_name}
429
+ (session_id, workflow_id, user_id, workflow_name, runs, workflow_data, session_data, extra_data, created_at, updated_at)
430
+ VALUES
431
+ (:session_id, :workflow_id, :user_id, :workflow_name, :runs, :workflow_data, :session_data, :extra_data, UNIX_TIMESTAMP(), NULL)
432
+ ON DUPLICATE KEY UPDATE
433
+ workflow_id = VALUES(workflow_id),
434
+ user_id = VALUES(user_id),
435
+ workflow_name = VALUES(workflow_name),
436
+ runs = VALUES(runs),
437
+ workflow_data = VALUES(workflow_data),
438
+ session_data = VALUES(session_data),
439
+ extra_data = VALUES(extra_data),
440
+ updated_at = UNIX_TIMESTAMP();
441
+ """
442
+ )
404
443
 
405
444
  try:
406
445
  if self.mode == "agent":
@@ -411,8 +450,8 @@ class SingleStoreStorage(Storage):
411
450
  "agent_id": session.agent_id, # type: ignore
412
451
  "team_session_id": session.team_session_id, # type: ignore
413
452
  "user_id": session.user_id,
414
- "memory": json.dumps(session.memory, ensure_ascii=False)
415
- if session.memory is not None
453
+ "memory": json.dumps(getattr(session, "memory", None), ensure_ascii=False)
454
+ if getattr(session, "memory", None) is not None
416
455
  else None,
417
456
  "agent_data": json.dumps(session.agent_data, ensure_ascii=False) # type: ignore
418
457
  if session.agent_data is not None # type: ignore
@@ -433,8 +472,8 @@ class SingleStoreStorage(Storage):
433
472
  "team_id": session.team_id, # type: ignore
434
473
  "user_id": session.user_id,
435
474
  "team_session_id": session.team_session_id, # type: ignore
436
- "memory": json.dumps(session.memory, ensure_ascii=False)
437
- if session.memory is not None
475
+ "memory": json.dumps(getattr(session, "memory", None), ensure_ascii=False)
476
+ if getattr(session, "memory", None) is not None
438
477
  else None,
439
478
  "team_data": json.dumps(session.team_data, ensure_ascii=False) # type: ignore
440
479
  if session.team_data is not None # type: ignore
@@ -454,9 +493,32 @@ class SingleStoreStorage(Storage):
454
493
  "session_id": session.session_id,
455
494
  "workflow_id": session.workflow_id, # type: ignore
456
495
  "user_id": session.user_id,
457
- "memory": json.dumps(session.memory, ensure_ascii=False)
458
- if session.memory is not None
496
+ "memory": json.dumps(getattr(session, "memory", None), ensure_ascii=False)
497
+ if getattr(session, "memory", None) is not None
498
+ else None,
499
+ "workflow_data": json.dumps(session.workflow_data, ensure_ascii=False) # type: ignore
500
+ if session.workflow_data is not None # type: ignore
459
501
  else None,
502
+ "session_data": json.dumps(session.session_data, ensure_ascii=False)
503
+ if session.session_data is not None
504
+ else None,
505
+ "extra_data": json.dumps(session.extra_data, ensure_ascii=False)
506
+ if session.extra_data is not None
507
+ else None,
508
+ },
509
+ )
510
+ elif self.mode == "workflow_v2":
511
+ # Convert session to dict to ensure proper serialization
512
+ session_dict = session.to_dict()
513
+
514
+ sess.execute(
515
+ upsert_sql,
516
+ {
517
+ "session_id": session.session_id,
518
+ "workflow_id": session.workflow_id, # type: ignore
519
+ "user_id": session.user_id,
520
+ "workflow_name": session.workflow_name, # type: ignore
521
+ "runs": session_dict.get("runs"),
460
522
  "workflow_data": json.dumps(session.workflow_data, ensure_ascii=False) # type: ignore
461
523
  if session.workflow_data is not None # type: ignore
462
524
  else None,