agno 2.1.3__py3-none-any.whl → 2.1.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 (94) hide show
  1. agno/agent/agent.py +1779 -577
  2. agno/db/async_postgres/__init__.py +3 -0
  3. agno/db/async_postgres/async_postgres.py +1668 -0
  4. agno/db/async_postgres/schemas.py +124 -0
  5. agno/db/async_postgres/utils.py +289 -0
  6. agno/db/base.py +237 -2
  7. agno/db/dynamo/dynamo.py +10 -8
  8. agno/db/dynamo/schemas.py +1 -10
  9. agno/db/dynamo/utils.py +2 -2
  10. agno/db/firestore/firestore.py +2 -2
  11. agno/db/firestore/utils.py +4 -2
  12. agno/db/gcs_json/gcs_json_db.py +2 -2
  13. agno/db/in_memory/in_memory_db.py +2 -2
  14. agno/db/json/json_db.py +2 -2
  15. agno/db/migrations/v1_to_v2.py +30 -13
  16. agno/db/mongo/mongo.py +18 -6
  17. agno/db/mysql/mysql.py +35 -13
  18. agno/db/postgres/postgres.py +29 -6
  19. agno/db/redis/redis.py +2 -2
  20. agno/db/singlestore/singlestore.py +2 -2
  21. agno/db/sqlite/sqlite.py +34 -12
  22. agno/db/sqlite/utils.py +8 -3
  23. agno/eval/accuracy.py +50 -43
  24. agno/eval/performance.py +6 -3
  25. agno/eval/reliability.py +6 -3
  26. agno/eval/utils.py +33 -16
  27. agno/exceptions.py +8 -2
  28. agno/knowledge/embedder/fastembed.py +1 -1
  29. agno/knowledge/knowledge.py +260 -46
  30. agno/knowledge/reader/pdf_reader.py +4 -6
  31. agno/knowledge/reader/reader_factory.py +2 -3
  32. agno/memory/manager.py +241 -33
  33. agno/models/anthropic/claude.py +37 -0
  34. agno/os/app.py +15 -10
  35. agno/os/interfaces/a2a/router.py +3 -5
  36. agno/os/interfaces/agui/router.py +4 -1
  37. agno/os/interfaces/agui/utils.py +33 -6
  38. agno/os/interfaces/slack/router.py +2 -4
  39. agno/os/mcp.py +98 -41
  40. agno/os/router.py +23 -0
  41. agno/os/routers/evals/evals.py +52 -20
  42. agno/os/routers/evals/utils.py +14 -14
  43. agno/os/routers/knowledge/knowledge.py +130 -9
  44. agno/os/routers/knowledge/schemas.py +57 -0
  45. agno/os/routers/memory/memory.py +116 -44
  46. agno/os/routers/metrics/metrics.py +16 -6
  47. agno/os/routers/session/session.py +65 -22
  48. agno/os/schema.py +38 -0
  49. agno/os/utils.py +69 -13
  50. agno/reasoning/anthropic.py +80 -0
  51. agno/reasoning/gemini.py +73 -0
  52. agno/reasoning/openai.py +5 -0
  53. agno/reasoning/vertexai.py +76 -0
  54. agno/session/workflow.py +69 -1
  55. agno/team/team.py +934 -241
  56. agno/tools/function.py +36 -18
  57. agno/tools/google_drive.py +270 -0
  58. agno/tools/googlesheets.py +20 -5
  59. agno/tools/mcp_toolbox.py +3 -3
  60. agno/tools/scrapegraph.py +1 -1
  61. agno/utils/models/claude.py +3 -1
  62. agno/utils/print_response/workflow.py +112 -12
  63. agno/utils/streamlit.py +1 -1
  64. agno/vectordb/base.py +22 -1
  65. agno/vectordb/cassandra/cassandra.py +9 -0
  66. agno/vectordb/chroma/chromadb.py +26 -6
  67. agno/vectordb/clickhouse/clickhousedb.py +9 -1
  68. agno/vectordb/couchbase/couchbase.py +11 -0
  69. agno/vectordb/lancedb/lance_db.py +20 -0
  70. agno/vectordb/langchaindb/langchaindb.py +11 -0
  71. agno/vectordb/lightrag/lightrag.py +9 -0
  72. agno/vectordb/llamaindex/llamaindexdb.py +15 -1
  73. agno/vectordb/milvus/milvus.py +23 -0
  74. agno/vectordb/mongodb/mongodb.py +22 -0
  75. agno/vectordb/pgvector/pgvector.py +19 -0
  76. agno/vectordb/pineconedb/pineconedb.py +35 -4
  77. agno/vectordb/qdrant/qdrant.py +24 -0
  78. agno/vectordb/singlestore/singlestore.py +25 -17
  79. agno/vectordb/surrealdb/surrealdb.py +18 -1
  80. agno/vectordb/upstashdb/upstashdb.py +26 -1
  81. agno/vectordb/weaviate/weaviate.py +18 -0
  82. agno/workflow/condition.py +29 -0
  83. agno/workflow/loop.py +29 -0
  84. agno/workflow/parallel.py +141 -113
  85. agno/workflow/router.py +29 -0
  86. agno/workflow/step.py +146 -25
  87. agno/workflow/steps.py +29 -0
  88. agno/workflow/types.py +26 -1
  89. agno/workflow/workflow.py +507 -22
  90. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/METADATA +100 -41
  91. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/RECORD +94 -86
  92. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/WHEEL +0 -0
  93. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/licenses/LICENSE +0 -0
  94. {agno-2.1.3.dist-info → agno-2.1.5.dist-info}/top_level.txt +0 -0
agno/workflow/workflow.py CHANGED
@@ -24,7 +24,7 @@ from fastapi import WebSocket
24
24
  from pydantic import BaseModel
25
25
 
26
26
  from agno.agent.agent import Agent
27
- from agno.db.base import BaseDb, SessionType
27
+ from agno.db.base import AsyncBaseDb, BaseDb, SessionType
28
28
  from agno.exceptions import InputCheckError, OutputCheckError, RunCancelledException
29
29
  from agno.media import Audio, File, Image, Video
30
30
  from agno.models.message import Message
@@ -129,7 +129,7 @@ class Workflow:
129
129
  steps: Optional[WorkflowSteps] = None
130
130
 
131
131
  # Database to use for this workflow
132
- db: Optional[BaseDb] = None
132
+ db: Optional[Union[BaseDb, AsyncBaseDb]] = None
133
133
 
134
134
  # Default session_id to use for this workflow (autogenerated if not set)
135
135
  session_id: Optional[str] = None
@@ -148,6 +148,8 @@ class Workflow:
148
148
  stream: Optional[bool] = None
149
149
  # Stream the intermediate steps from the Workflow
150
150
  stream_intermediate_steps: bool = False
151
+ # Stream events from executors (agents/teams/functions) within steps
152
+ stream_executor_events: bool = True
151
153
 
152
154
  # Persist the events on the run response
153
155
  store_events: bool = False
@@ -170,12 +172,17 @@ class Workflow:
170
172
  # This helps us improve the Agent and provide better support
171
173
  telemetry: bool = True
172
174
 
175
+ # Add this flag to control if the workflow should add history to the steps
176
+ add_workflow_history_to_steps: bool = False
177
+ # Number of historical runs to include in the messages
178
+ num_history_runs: int = 3
179
+
173
180
  def __init__(
174
181
  self,
175
182
  id: Optional[str] = None,
176
183
  name: Optional[str] = None,
177
184
  description: Optional[str] = None,
178
- db: Optional[BaseDb] = None,
185
+ db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
179
186
  steps: Optional[WorkflowSteps] = None,
180
187
  session_id: Optional[str] = None,
181
188
  session_state: Optional[Dict[str, Any]] = None,
@@ -184,6 +191,7 @@ class Workflow:
184
191
  debug_mode: Optional[bool] = False,
185
192
  stream: Optional[bool] = None,
186
193
  stream_intermediate_steps: bool = False,
194
+ stream_executor_events: bool = True,
187
195
  store_events: bool = False,
188
196
  events_to_skip: Optional[List[Union[WorkflowRunEvent, RunEvent, TeamRunEvent]]] = None,
189
197
  store_executor_outputs: bool = True,
@@ -191,6 +199,8 @@ class Workflow:
191
199
  metadata: Optional[Dict[str, Any]] = None,
192
200
  cache_session: bool = False,
193
201
  telemetry: bool = True,
202
+ add_workflow_history_to_steps: bool = False,
203
+ num_history_runs: int = 3,
194
204
  ):
195
205
  self.id = id
196
206
  self.name = name
@@ -205,13 +215,15 @@ class Workflow:
205
215
  self.events_to_skip = events_to_skip or []
206
216
  self.stream = stream
207
217
  self.stream_intermediate_steps = stream_intermediate_steps
218
+ self.stream_executor_events = stream_executor_events
208
219
  self.store_executor_outputs = store_executor_outputs
209
220
  self.input_schema = input_schema
210
221
  self.metadata = metadata
211
222
  self.cache_session = cache_session
212
223
  self.db = db
213
224
  self.telemetry = telemetry
214
-
225
+ self.add_workflow_history_to_steps = add_workflow_history_to_steps
226
+ self.num_history_runs = num_history_runs
215
227
  self._workflow_session: Optional[WorkflowSession] = None
216
228
 
217
229
  def set_id(self) -> None:
@@ -221,6 +233,9 @@ class Workflow:
221
233
  else:
222
234
  self.id = str(uuid4())
223
235
 
236
+ def _has_async_db(self) -> bool:
237
+ return self.db is not None and isinstance(self.db, AsyncBaseDb)
238
+
224
239
  def _validate_input(
225
240
  self, input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]]
226
241
  ) -> Optional[Union[str, List, Dict, Message, BaseModel]]:
@@ -393,6 +408,33 @@ class Workflow:
393
408
  new_session_name = f"{truncated_desc} - {datetime_str}"
394
409
  return new_session_name
395
410
 
411
+ async def aset_session_name(
412
+ self, session_id: Optional[str] = None, autogenerate: bool = False, session_name: Optional[str] = None
413
+ ) -> WorkflowSession:
414
+ """Set the session name and save to storage, using an async database"""
415
+ session_id = session_id or self.session_id
416
+
417
+ if session_id is None:
418
+ raise Exception("Session ID is not set")
419
+
420
+ # -*- Read from storage
421
+ session = await self.aget_session(session_id=session_id) # type: ignore
422
+
423
+ if autogenerate:
424
+ # -*- Generate name for session
425
+ session_name = self._generate_workflow_session_name()
426
+ log_debug(f"Generated Workflow Session Name: {session_name}")
427
+ elif session_name is None:
428
+ raise Exception("Session name is not set")
429
+
430
+ # -*- Rename session
431
+ session.session_data["session_name"] = session_name # type: ignore
432
+
433
+ # -*- Save to storage
434
+ await self.asave_session(session=session) # type: ignore
435
+
436
+ return session # type: ignore
437
+
396
438
  def set_session_name(
397
439
  self, session_id: Optional[str] = None, autogenerate: bool = False, session_name: Optional[str] = None
398
440
  ) -> WorkflowSession:
@@ -420,6 +462,16 @@ class Workflow:
420
462
 
421
463
  return session # type: ignore
422
464
 
465
+ async def aget_session_name(self, session_id: Optional[str] = None) -> str:
466
+ """Get the session name for the given session ID and user ID."""
467
+ session_id = session_id or self.session_id
468
+ if session_id is None:
469
+ raise Exception("Session ID is not set")
470
+ session = await self.aget_session(session_id=session_id) # type: ignore
471
+ if session is None:
472
+ raise Exception("Session not found")
473
+ return session.session_data.get("session_name", "") if session.session_data else ""
474
+
423
475
  def get_session_name(self, session_id: Optional[str] = None) -> str:
424
476
  """Get the session name for the given session ID and user ID."""
425
477
  session_id = session_id or self.session_id
@@ -430,6 +482,16 @@ class Workflow:
430
482
  raise Exception("Session not found")
431
483
  return session.session_data.get("session_name", "") if session.session_data else ""
432
484
 
485
+ async def aget_session_state(self, session_id: Optional[str] = None) -> Dict[str, Any]:
486
+ """Get the session state for the given session ID and user ID."""
487
+ session_id = session_id or self.session_id
488
+ if session_id is None:
489
+ raise Exception("Session ID is not set")
490
+ session = await self.aget_session(session_id=session_id) # type: ignore
491
+ if session is None:
492
+ raise Exception("Session not found")
493
+ return session.session_data.get("session_state", {}) if session.session_data else {}
494
+
433
495
  def get_session_state(self, session_id: Optional[str] = None) -> Dict[str, Any]:
434
496
  """Get the session state for the given session ID and user ID."""
435
497
  session_id = session_id or self.session_id
@@ -440,6 +502,13 @@ class Workflow:
440
502
  raise Exception("Session not found")
441
503
  return session.session_data.get("session_state", {}) if session.session_data else {}
442
504
 
505
+ async def adelete_session(self, session_id: str):
506
+ """Delete the current session and save to storage"""
507
+ if self.db is None:
508
+ return
509
+ # -*- Delete session
510
+ await self.db.delete_session(session_id=session_id) # type: ignore
511
+
443
512
  def delete_session(self, session_id: str):
444
513
  """Delete the current session and save to storage"""
445
514
  if self.db is None:
@@ -447,6 +516,25 @@ class Workflow:
447
516
  # -*- Delete session
448
517
  self.db.delete_session(session_id=session_id)
449
518
 
519
+ async def aget_run_output(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
520
+ """Get a RunOutput from the database."""
521
+ if self._workflow_session is not None:
522
+ run_response = self._workflow_session.get_run(run_id=run_id)
523
+ if run_response is not None:
524
+ return run_response
525
+ else:
526
+ log_warning(f"RunOutput {run_id} not found in AgentSession {self._workflow_session.session_id}")
527
+ return None
528
+ else:
529
+ workflow_session = await self.aget_session(session_id=session_id) # type: ignore
530
+ if workflow_session is not None:
531
+ run_response = workflow_session.get_run(run_id=run_id)
532
+ if run_response is not None:
533
+ return run_response
534
+ else:
535
+ log_warning(f"RunOutput {run_id} not found in AgentSession {session_id}")
536
+ return None
537
+
450
538
  def get_run_output(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
451
539
  """Get a RunOutput from the database."""
452
540
  if self._workflow_session is not None:
@@ -466,6 +554,26 @@ class Workflow:
466
554
  log_warning(f"RunOutput {run_id} not found in AgentSession {session_id}")
467
555
  return None
468
556
 
557
+ async def aget_last_run_output(self, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
558
+ """Get the last run response from the database."""
559
+ if (
560
+ self._workflow_session is not None
561
+ and self._workflow_session.runs is not None
562
+ and len(self._workflow_session.runs) > 0
563
+ ):
564
+ run_response = self._workflow_session.runs[-1]
565
+ if run_response is not None:
566
+ return run_response
567
+ else:
568
+ workflow_session = await self.aget_session(session_id=session_id) # type: ignore
569
+ if workflow_session is not None and workflow_session.runs is not None and len(workflow_session.runs) > 0:
570
+ run_response = workflow_session.runs[-1]
571
+ if run_response is not None:
572
+ return run_response
573
+ else:
574
+ log_warning(f"No run responses found in WorkflowSession {session_id}")
575
+ return None
576
+
469
577
  def get_last_run_output(self, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
470
578
  """Get the last run response from the database."""
471
579
  if (
@@ -523,6 +631,68 @@ class Workflow:
523
631
 
524
632
  return workflow_session
525
633
 
634
+ async def aread_or_create_session(
635
+ self,
636
+ session_id: str,
637
+ user_id: Optional[str] = None,
638
+ ) -> WorkflowSession:
639
+ from time import time
640
+
641
+ # Returning cached session if we have one
642
+ if self._workflow_session is not None and self._workflow_session.session_id == session_id:
643
+ return self._workflow_session
644
+
645
+ # Try to load from database
646
+ workflow_session = None
647
+ if self.db is not None:
648
+ log_debug(f"Reading WorkflowSession: {session_id}")
649
+
650
+ workflow_session = cast(WorkflowSession, await self._aread_session(session_id=session_id))
651
+
652
+ if workflow_session is None:
653
+ # Creating new session if none found
654
+ log_debug(f"Creating new WorkflowSession: {session_id}")
655
+ workflow_session = WorkflowSession(
656
+ session_id=session_id,
657
+ workflow_id=self.id,
658
+ user_id=user_id,
659
+ workflow_data=self._get_workflow_data(),
660
+ session_data={},
661
+ metadata=self.metadata,
662
+ created_at=int(time()),
663
+ )
664
+
665
+ # Cache the session if relevant
666
+ if workflow_session is not None and self.cache_session:
667
+ self._workflow_session = workflow_session
668
+
669
+ return workflow_session
670
+
671
+ async def aget_session(
672
+ self,
673
+ session_id: Optional[str] = None,
674
+ ) -> Optional[WorkflowSession]:
675
+ """Load an WorkflowSession from database.
676
+
677
+ Args:
678
+ session_id: The session_id to load from storage.
679
+
680
+ Returns:
681
+ WorkflowSession: The WorkflowSession loaded from the database or created if it does not exist.
682
+ """
683
+ if not session_id and not self.session_id:
684
+ raise Exception("No session_id provided")
685
+
686
+ session_id_to_load = session_id or self.session_id
687
+
688
+ # Try to load from database
689
+ if self.db is not None and session_id_to_load is not None:
690
+ workflow_session = cast(WorkflowSession, await self._aread_session(session_id=session_id_to_load))
691
+ return workflow_session
692
+
693
+ log_warning(f"WorkflowSession {session_id_to_load} not found in db")
694
+ return None
695
+
526
696
  def get_session(
527
697
  self,
528
698
  session_id: Optional[str] = None,
@@ -548,6 +718,25 @@ class Workflow:
548
718
  log_warning(f"WorkflowSession {session_id_to_load} not found in db")
549
719
  return None
550
720
 
721
+ async def asave_session(self, session: WorkflowSession) -> None:
722
+ """Save the WorkflowSession to storage, using an async database.
723
+
724
+ Returns:
725
+ Optional[WorkflowSession]: The saved WorkflowSession or None if not saved.
726
+ """
727
+ if self.db is not None and session.session_data is not None:
728
+ if session.session_data.get("session_state") is not None:
729
+ session.session_data["session_state"].pop("current_session_id", None)
730
+ session.session_data["session_state"].pop("current_user_id", None)
731
+ session.session_data["session_state"].pop("current_run_id", None)
732
+ session.session_data["session_state"].pop("workflow_id", None)
733
+ session.session_data["session_state"].pop("run_id", None)
734
+ session.session_data["session_state"].pop("session_id", None)
735
+ session.session_data["session_state"].pop("workflow_name", None)
736
+
737
+ await self._aupsert_session(session=session) # type: ignore
738
+ log_debug(f"Created or updated WorkflowSession record: {session.session_id}")
739
+
551
740
  def save_session(self, session: WorkflowSession) -> None:
552
741
  """Save the WorkflowSession to storage
553
742
 
@@ -568,6 +757,17 @@ class Workflow:
568
757
  log_debug(f"Created or updated WorkflowSession record: {session.session_id}")
569
758
 
570
759
  # -*- Session Database Functions
760
+ async def _aread_session(self, session_id: str) -> Optional[WorkflowSession]:
761
+ """Get a Session from the database."""
762
+ try:
763
+ if not self.db:
764
+ raise ValueError("Db not initialized")
765
+ session = await self.db.get_session(session_id=session_id, session_type=SessionType.WORKFLOW) # type: ignore
766
+ return session if isinstance(session, (WorkflowSession, type(None))) else None
767
+ except Exception as e:
768
+ log_warning(f"Error getting session from db: {e}")
769
+ return None
770
+
571
771
  def _read_session(self, session_id: str) -> Optional[WorkflowSession]:
572
772
  """Get a Session from the database."""
573
773
  try:
@@ -579,9 +779,19 @@ class Workflow:
579
779
  log_warning(f"Error getting session from db: {e}")
580
780
  return None
581
781
 
582
- def _upsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
782
+ async def _aupsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
583
783
  """Upsert a Session into the database."""
784
+ try:
785
+ if not self.db:
786
+ raise ValueError("Db not initialized")
787
+ result = await self.db.upsert_session(session=session) # type: ignore
788
+ return result if isinstance(result, (WorkflowSession, type(None))) else None
789
+ except Exception as e:
790
+ log_warning(f"Error upserting session into db: {e}")
791
+ return None
584
792
 
793
+ def _upsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
794
+ """Upsert a Session into the database."""
585
795
  try:
586
796
  if not self.db:
587
797
  raise ValueError("Db not initialized")
@@ -707,6 +917,34 @@ class Workflow:
707
917
 
708
918
  return event
709
919
 
920
+ def _enrich_event_with_workflow_context(
921
+ self,
922
+ event: Any,
923
+ workflow_run_response: WorkflowRunOutput,
924
+ step_index: Optional[Union[int, tuple]] = None,
925
+ step: Optional[Any] = None,
926
+ ) -> Any:
927
+ """Enrich any event with workflow context information for frontend tracking"""
928
+
929
+ step_id = getattr(step, "step_id", None) if step else None
930
+ step_name = getattr(step, "name", None) if step else None
931
+
932
+ if hasattr(event, "workflow_id"):
933
+ event.workflow_id = workflow_run_response.workflow_id
934
+ if hasattr(event, "workflow_run_id"):
935
+ event.workflow_run_id = workflow_run_response.run_id
936
+ if hasattr(event, "step_id") and step_id:
937
+ event.step_id = step_id
938
+ if hasattr(event, "step_name") and step_name is not None:
939
+ if event.step_name is None:
940
+ event.step_name = step_name
941
+ # Only set step_index if it's not already set (preserve parallel.py's tuples)
942
+ if hasattr(event, "step_index") and step_index is not None:
943
+ if event.step_index is None:
944
+ event.step_index = step_index
945
+
946
+ return event
947
+
710
948
  def _transform_step_output_to_event(
711
949
  self, step_output: StepOutput, workflow_run_response: WorkflowRunOutput, step_index: Optional[int] = None
712
950
  ) -> StepOutputEvent:
@@ -952,6 +1190,11 @@ class Workflow:
952
1190
  workflow_run_response=workflow_run_response,
953
1191
  session_state=session_state,
954
1192
  store_executor_outputs=self.store_executor_outputs,
1193
+ workflow_session=session,
1194
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
1195
+ if self.add_workflow_history_to_steps
1196
+ else None,
1197
+ num_history_runs=self.num_history_runs,
955
1198
  )
956
1199
 
957
1200
  # Check for cancellation after step execution
@@ -1117,10 +1360,16 @@ class Workflow:
1117
1360
  session_id=session.session_id,
1118
1361
  user_id=self.user_id,
1119
1362
  stream_intermediate_steps=stream_intermediate_steps,
1363
+ stream_executor_events=self.stream_executor_events,
1120
1364
  workflow_run_response=workflow_run_response,
1121
1365
  session_state=session_state,
1122
1366
  step_index=i,
1123
1367
  store_executor_outputs=self.store_executor_outputs,
1368
+ workflow_session=session,
1369
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
1370
+ if self.add_workflow_history_to_steps
1371
+ else None,
1372
+ num_history_runs=self.num_history_runs,
1124
1373
  ):
1125
1374
  raise_if_cancelled(workflow_run_response.run_id) # type: ignore
1126
1375
  # Handle events
@@ -1171,11 +1420,19 @@ class Workflow:
1171
1420
  yield step_output_event
1172
1421
 
1173
1422
  elif isinstance(event, WorkflowRunOutputEvent): # type: ignore
1174
- yield self._handle_event(event, workflow_run_response) # type: ignore
1423
+ # Enrich event with workflow context before yielding
1424
+ enriched_event = self._enrich_event_with_workflow_context(
1425
+ event, workflow_run_response, step_index=i, step=step
1426
+ )
1427
+ yield self._handle_event(enriched_event, workflow_run_response) # type: ignore
1175
1428
 
1176
1429
  else:
1177
- # Yield other internal events
1178
- yield self._handle_event(event, workflow_run_response) # type: ignore
1430
+ # Enrich other events with workflow context before yielding
1431
+ enriched_event = self._enrich_event_with_workflow_context(
1432
+ event, workflow_run_response, step_index=i, step=step
1433
+ )
1434
+ if self.stream_executor_events:
1435
+ yield self._handle_event(enriched_event, workflow_run_response) # type: ignore
1179
1436
 
1180
1437
  # Break out of main step loop if early termination was requested
1181
1438
  if "early_termination" in locals() and early_termination:
@@ -1415,6 +1672,11 @@ class Workflow:
1415
1672
  workflow_run_response=workflow_run_response,
1416
1673
  session_state=session_state,
1417
1674
  store_executor_outputs=self.store_executor_outputs,
1675
+ workflow_session=session,
1676
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
1677
+ if self.add_workflow_history_to_steps
1678
+ else None,
1679
+ num_history_runs=self.num_history_runs,
1418
1680
  )
1419
1681
 
1420
1682
  # Check for cancellation after step execution
@@ -1482,7 +1744,10 @@ class Workflow:
1482
1744
 
1483
1745
  self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
1484
1746
  session.upsert_run(run=workflow_run_response)
1485
- self.save_session(session=session)
1747
+ if self._has_async_db():
1748
+ await self.asave_session(session=session)
1749
+ else:
1750
+ self.save_session(session=session)
1486
1751
  # Always clean up the run tracking
1487
1752
  cleanup_run(workflow_run_response.run_id) # type: ignore
1488
1753
 
@@ -1585,10 +1850,16 @@ class Workflow:
1585
1850
  session_id=session.session_id,
1586
1851
  user_id=self.user_id,
1587
1852
  stream_intermediate_steps=stream_intermediate_steps,
1853
+ stream_executor_events=self.stream_executor_events,
1588
1854
  workflow_run_response=workflow_run_response,
1589
1855
  session_state=session_state,
1590
1856
  step_index=i,
1591
1857
  store_executor_outputs=self.store_executor_outputs,
1858
+ workflow_session=session,
1859
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
1860
+ if self.add_workflow_history_to_steps
1861
+ else None,
1862
+ num_history_runs=self.num_history_runs,
1592
1863
  ):
1593
1864
  if workflow_run_response.run_id:
1594
1865
  raise_if_cancelled(workflow_run_response.run_id)
@@ -1638,11 +1909,23 @@ class Workflow:
1638
1909
  yield step_output_event
1639
1910
 
1640
1911
  elif isinstance(event, WorkflowRunOutputEvent): # type: ignore
1641
- yield self._handle_event(event, workflow_run_response, websocket_handler=websocket_handler) # type: ignore
1912
+ # Enrich event with workflow context before yielding
1913
+ enriched_event = self._enrich_event_with_workflow_context(
1914
+ event, workflow_run_response, step_index=i, step=step
1915
+ )
1916
+ yield self._handle_event(
1917
+ enriched_event, workflow_run_response, websocket_handler=websocket_handler
1918
+ ) # type: ignore
1642
1919
 
1643
1920
  else:
1644
- # Yield other internal events
1645
- yield self._handle_event(event, workflow_run_response, websocket_handler=websocket_handler) # type: ignore
1921
+ # Enrich other events with workflow context before yielding
1922
+ enriched_event = self._enrich_event_with_workflow_context(
1923
+ event, workflow_run_response, step_index=i, step=step
1924
+ )
1925
+ if self.stream_executor_events:
1926
+ yield self._handle_event(
1927
+ enriched_event, workflow_run_response, websocket_handler=websocket_handler
1928
+ ) # type: ignore
1646
1929
 
1647
1930
  # Break out of main step loop if early termination was requested
1648
1931
  if "early_termination" in locals() and early_termination:
@@ -1743,7 +2026,10 @@ class Workflow:
1743
2026
  # Store the completed workflow response
1744
2027
  self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
1745
2028
  session.upsert_run(run=workflow_run_response)
1746
- self.save_session(session=session)
2029
+ if self._has_async_db():
2030
+ await self.asave_session(session=session)
2031
+ else:
2032
+ self.save_session(session=session)
1747
2033
 
1748
2034
  # Log Workflow Telemetry
1749
2035
  if self.telemetry:
@@ -1776,7 +2062,10 @@ class Workflow:
1776
2062
  )
1777
2063
 
1778
2064
  # Read existing session from database
1779
- workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
2065
+ if self._has_async_db():
2066
+ workflow_session = await self.aread_or_create_session(session_id=session_id, user_id=user_id)
2067
+ else:
2068
+ workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
1780
2069
  self._update_metadata(session=workflow_session)
1781
2070
 
1782
2071
  # Update session state from DB
@@ -1797,7 +2086,10 @@ class Workflow:
1797
2086
 
1798
2087
  # Store PENDING response immediately
1799
2088
  workflow_session.upsert_run(run=workflow_run_response)
1800
- self.save_session(session=workflow_session)
2089
+ if self._has_async_db():
2090
+ await self.asave_session(session=workflow_session)
2091
+ else:
2092
+ self.save_session(session=workflow_session)
1801
2093
 
1802
2094
  # Prepare execution input
1803
2095
  inputs = WorkflowExecutionInput(
@@ -1816,7 +2108,10 @@ class Workflow:
1816
2108
  try:
1817
2109
  # Update status to RUNNING and save
1818
2110
  workflow_run_response.status = RunStatus.running
1819
- self.save_session(session=workflow_session)
2111
+ if self._has_async_db():
2112
+ await self.asave_session(session=workflow_session)
2113
+ else:
2114
+ self.save_session(session=workflow_session)
1820
2115
 
1821
2116
  await self._aexecute(
1822
2117
  session=workflow_session,
@@ -1832,7 +2127,10 @@ class Workflow:
1832
2127
  logger.error(f"Background workflow execution failed: {e}")
1833
2128
  workflow_run_response.status = RunStatus.error
1834
2129
  workflow_run_response.content = f"Background execution failed: {str(e)}"
1835
- self.save_session(session=workflow_session)
2130
+ if self._has_async_db():
2131
+ await self.asave_session(session=workflow_session)
2132
+ else:
2133
+ self.save_session(session=workflow_session)
1836
2134
 
1837
2135
  # Create and start asyncio task
1838
2136
  loop = asyncio.get_running_loop()
@@ -1867,7 +2165,10 @@ class Workflow:
1867
2165
  )
1868
2166
 
1869
2167
  # Read existing session from database
1870
- workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
2168
+ if self._has_async_db():
2169
+ workflow_session = await self.aread_or_create_session(session_id=session_id, user_id=user_id)
2170
+ else:
2171
+ workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
1871
2172
  self._update_metadata(session=workflow_session)
1872
2173
 
1873
2174
  # Update session state from DB
@@ -1888,7 +2189,10 @@ class Workflow:
1888
2189
 
1889
2190
  # Store PENDING response immediately
1890
2191
  workflow_session.upsert_run(run=workflow_run_response)
1891
- self.save_session(session=workflow_session)
2192
+ if self._has_async_db():
2193
+ await self.asave_session(session=workflow_session)
2194
+ else:
2195
+ self.save_session(session=workflow_session)
1892
2196
 
1893
2197
  # Prepare execution input
1894
2198
  inputs = WorkflowExecutionInput(
@@ -1907,7 +2211,10 @@ class Workflow:
1907
2211
  try:
1908
2212
  # Update status to RUNNING and save
1909
2213
  workflow_run_response.status = RunStatus.running
1910
- self.save_session(session=workflow_session)
2214
+ if self._has_async_db():
2215
+ await self.asave_session(session=workflow_session)
2216
+ else:
2217
+ self.save_session(session=workflow_session)
1911
2218
 
1912
2219
  # Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
1913
2220
  async for event in self._aexecute_stream(
@@ -1929,7 +2236,10 @@ class Workflow:
1929
2236
  logger.error(f"Background streaming workflow execution failed: {e}")
1930
2237
  workflow_run_response.status = RunStatus.error
1931
2238
  workflow_run_response.content = f"Background streaming execution failed: {str(e)}"
1932
- self.save_session(session=workflow_session)
2239
+ if self._has_async_db():
2240
+ await self.asave_session(session=workflow_session)
2241
+ else:
2242
+ self.save_session(session=workflow_session)
1933
2243
 
1934
2244
  # Create and start asyncio task for background streaming execution
1935
2245
  loop = asyncio.get_running_loop()
@@ -1938,6 +2248,18 @@ class Workflow:
1938
2248
  # Return SAME object that will be updated by background execution
1939
2249
  return workflow_run_response
1940
2250
 
2251
+ async def aget_run(self, run_id: str) -> Optional[WorkflowRunOutput]:
2252
+ """Get the status and details of a background workflow run - SIMPLIFIED"""
2253
+ if self.db is not None and self.session_id is not None:
2254
+ session = await self.db.aget_session(session_id=self.session_id, session_type=SessionType.WORKFLOW) # type: ignore
2255
+ if session and isinstance(session, WorkflowSession) and session.runs:
2256
+ # Find the run by ID
2257
+ for run in session.runs:
2258
+ if run.run_id == run_id:
2259
+ return run
2260
+
2261
+ return None
2262
+
1941
2263
  def get_run(self, run_id: str) -> Optional[WorkflowRunOutput]:
1942
2264
  """Get the status and details of a background workflow run - SIMPLIFIED"""
1943
2265
  if self.db is not None and self.session_id is not None:
@@ -2012,6 +2334,8 @@ class Workflow:
2012
2334
  **kwargs: Any,
2013
2335
  ) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
2014
2336
  """Execute the workflow synchronously with optional streaming"""
2337
+ if self._has_async_db():
2338
+ raise Exception("`run()` is not supported with an async DB. Please use `arun()`.")
2015
2339
 
2016
2340
  input = self._validate_input(input)
2017
2341
  if background:
@@ -2199,7 +2523,10 @@ class Workflow:
2199
2523
  )
2200
2524
 
2201
2525
  # Read existing session from database
2202
- workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
2526
+ if self._has_async_db():
2527
+ workflow_session = await self.aread_or_create_session(session_id=session_id, user_id=user_id)
2528
+ else:
2529
+ workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
2203
2530
  self._update_metadata(session=workflow_session)
2204
2531
 
2205
2532
  # Update session state from DB
@@ -2329,6 +2656,8 @@ class Workflow:
2329
2656
  show_step_details: Whether to show individual step outputs
2330
2657
  console: Rich console instance (optional)
2331
2658
  """
2659
+ if self._has_async_db():
2660
+ raise Exception("`print_response()` is not supported with an async DB. Please use `aprint_response()`.")
2332
2661
 
2333
2662
  if stream is None:
2334
2663
  stream = self.stream or False
@@ -2561,6 +2890,18 @@ class Workflow:
2561
2890
  session.session_data = {}
2562
2891
  session.session_data["session_metrics"] = session_metrics.to_dict()
2563
2892
 
2893
+ async def aget_session_metrics(self, session_id: Optional[str] = None) -> Optional[Metrics]:
2894
+ """Get the session metrics for the given session ID and user ID."""
2895
+ session_id = session_id or self.session_id
2896
+ if session_id is None:
2897
+ raise Exception("Session ID is required")
2898
+
2899
+ session = await self.aget_session(session_id=session_id) # type: ignore
2900
+ if session is None:
2901
+ raise Exception("Session not found")
2902
+
2903
+ return self._get_session_metrics(session=session)
2904
+
2564
2905
  def get_session_metrics(self, session_id: Optional[str] = None) -> Optional[Metrics]:
2565
2906
  """Get the session metrics for the given session ID and user ID."""
2566
2907
  session_id = session_id or self.session_id
@@ -2636,3 +2977,147 @@ class Workflow:
2636
2977
  )
2637
2978
  except Exception as e:
2638
2979
  log_debug(f"Could not create Workflow run telemetry event: {e}")
2980
+
2981
+ def cli_app(
2982
+ self,
2983
+ input: Optional[str] = None,
2984
+ session_id: Optional[str] = None,
2985
+ user_id: Optional[str] = None,
2986
+ user: str = "User",
2987
+ emoji: str = ":technologist:",
2988
+ stream: Optional[bool] = None,
2989
+ stream_intermediate_steps: Optional[bool] = None,
2990
+ markdown: bool = True,
2991
+ show_time: bool = True,
2992
+ show_step_details: bool = True,
2993
+ exit_on: Optional[List[str]] = None,
2994
+ **kwargs: Any,
2995
+ ) -> None:
2996
+ """
2997
+ Run an interactive command-line interface to interact with the workflow.
2998
+
2999
+ This method creates a CLI interface that allows users to interact with the workflow
3000
+ either by providing a single input or through continuous interactive prompts.
3001
+
3002
+ Arguments:
3003
+ input: Optional initial input to process before starting interactive mode.
3004
+ session_id: Optional session identifier for maintaining conversation context.
3005
+ user_id: Optional user identifier for tracking user-specific data.
3006
+ user: Display name for the user in the CLI prompt. Defaults to "User".
3007
+ emoji: Emoji to display next to the user name in prompts. Defaults to ":technologist:".
3008
+ stream: Whether to stream the workflow response. If None, uses workflow default.
3009
+ stream_intermediate_steps: Whether to stream intermediate step outputs. If None, uses workflow default.
3010
+ markdown: Whether to render output as markdown. Defaults to True.
3011
+ show_time: Whether to display timestamps in the output. Defaults to True.
3012
+ show_step_details: Whether to show detailed step information. Defaults to True.
3013
+ exit_on: List of commands that will exit the CLI. Defaults to ["exit", "quit", "bye", "stop"].
3014
+ **kwargs: Additional keyword arguments passed to the workflow's print_response method.
3015
+
3016
+ Returns:
3017
+ None: This method runs interactively and does not return a value.
3018
+ """
3019
+
3020
+ from rich.prompt import Prompt
3021
+
3022
+ if input:
3023
+ self.print_response(
3024
+ input=input,
3025
+ stream=stream,
3026
+ stream_intermediate_steps=stream_intermediate_steps,
3027
+ markdown=markdown,
3028
+ show_time=show_time,
3029
+ show_step_details=show_step_details,
3030
+ user_id=user_id,
3031
+ session_id=session_id,
3032
+ **kwargs,
3033
+ )
3034
+
3035
+ _exit_on = exit_on or ["exit", "quit", "bye", "stop"]
3036
+ while True:
3037
+ message = Prompt.ask(f"[bold] {emoji} {user} [/bold]")
3038
+ if message in _exit_on:
3039
+ break
3040
+
3041
+ self.print_response(
3042
+ input=message,
3043
+ stream=stream,
3044
+ stream_intermediate_steps=stream_intermediate_steps,
3045
+ markdown=markdown,
3046
+ show_time=show_time,
3047
+ show_step_details=show_step_details,
3048
+ user_id=user_id,
3049
+ session_id=session_id,
3050
+ **kwargs,
3051
+ )
3052
+
3053
+ async def acli_app(
3054
+ self,
3055
+ input: Optional[str] = None,
3056
+ session_id: Optional[str] = None,
3057
+ user_id: Optional[str] = None,
3058
+ user: str = "User",
3059
+ emoji: str = ":technologist:",
3060
+ stream: Optional[bool] = None,
3061
+ stream_intermediate_steps: Optional[bool] = None,
3062
+ markdown: bool = True,
3063
+ show_time: bool = True,
3064
+ show_step_details: bool = True,
3065
+ exit_on: Optional[List[str]] = None,
3066
+ **kwargs: Any,
3067
+ ) -> None:
3068
+ """
3069
+ Run an interactive command-line interface to interact with the workflow.
3070
+
3071
+ This method creates a CLI interface that allows users to interact with the workflow
3072
+ either by providing a single input or through continuous interactive prompts.
3073
+
3074
+ Arguments:
3075
+ input: Optional initial input to process before starting interactive mode.
3076
+ session_id: Optional session identifier for maintaining conversation context.
3077
+ user_id: Optional user identifier for tracking user-specific data.
3078
+ user: Display name for the user in the CLI prompt. Defaults to "User".
3079
+ emoji: Emoji to display next to the user name in prompts. Defaults to ":technologist:".
3080
+ stream: Whether to stream the workflow response. If None, uses workflow default.
3081
+ stream_intermediate_steps: Whether to stream intermediate step outputs. If None, uses workflow default.
3082
+ markdown: Whether to render output as markdown. Defaults to True.
3083
+ show_time: Whether to display timestamps in the output. Defaults to True.
3084
+ show_step_details: Whether to show detailed step information. Defaults to True.
3085
+ exit_on: List of commands that will exit the CLI. Defaults to ["exit", "quit", "bye", "stop"].
3086
+ **kwargs: Additional keyword arguments passed to the workflow's print_response method.
3087
+
3088
+ Returns:
3089
+ None: This method runs interactively and does not return a value.
3090
+ """
3091
+
3092
+ from rich.prompt import Prompt
3093
+
3094
+ if input:
3095
+ await self.aprint_response(
3096
+ input=input,
3097
+ stream=stream,
3098
+ stream_intermediate_steps=stream_intermediate_steps,
3099
+ markdown=markdown,
3100
+ show_time=show_time,
3101
+ show_step_details=show_step_details,
3102
+ user_id=user_id,
3103
+ session_id=session_id,
3104
+ **kwargs,
3105
+ )
3106
+
3107
+ _exit_on = exit_on or ["exit", "quit", "bye", "stop"]
3108
+ while True:
3109
+ message = Prompt.ask(f"[bold] {emoji} {user} [/bold]")
3110
+ if message in _exit_on:
3111
+ break
3112
+
3113
+ await self.aprint_response(
3114
+ input=message,
3115
+ stream=stream,
3116
+ stream_intermediate_steps=stream_intermediate_steps,
3117
+ markdown=markdown,
3118
+ show_time=show_time,
3119
+ show_step_details=show_step_details,
3120
+ user_id=user_id,
3121
+ session_id=session_id,
3122
+ **kwargs,
3123
+ )